gflow-cli 0.18.0__tar.gz → 0.19.0__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.
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/.env.template +9 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/CHANGELOG.md +24 -1
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/PKG-INFO +3 -1
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/docs/CONFIGURATION.md +10 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/docs/INDEX.md +1 -1
- gflow_cli-0.19.0/docs/LIVE_VERIFICATION_v0.19.0.md +60 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/docs/SECURITY.md +20 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/docs/USAGE.md +1 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/pyproject.toml +10 -1
- gflow_cli-0.19.0/scripts/dev/spike_patchright.py +357 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/src/gflow_cli/__init__.py +1 -1
- gflow_cli-0.19.0/src/gflow_cli/api/_engine.py +113 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/src/gflow_cli/api/_retry.py +5 -3
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/src/gflow_cli/api/client.py +22 -3
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/src/gflow_cli/api/recaptcha.py +11 -3
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/src/gflow_cli/api/transports/ui_automation.py +25 -5
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/src/gflow_cli/cli.py +21 -1
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/src/gflow_cli/config.py +25 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/src/gflow_cli/errors.py +24 -0
- gflow_cli-0.19.0/tests/api/test_engine.py +95 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/tests/api/test_retry.py +27 -0
- gflow_cli-0.19.0/tests/cli/test_settings_validation.py +35 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/tests/test_config.py +27 -1
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/tests/test_errors.py +26 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/uv.lock +25 -2
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/.claude/README.md +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/.claude/commands/gflow/active.md +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/.claude/commands/gflow/branch-review.md +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/.claude/commands/gflow/changelog.md +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/.claude/commands/gflow/check.md +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/.claude/commands/gflow/doc-review.md +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/.claude/commands/gflow/known-issues.md +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/.claude/commands/gflow/next.md +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/.claude/commands/gflow/plan.md +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/.claude/commands/gflow/pr-council-review.md +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/.claude/commands/gflow/predict.md +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/.claude/commands/gflow/release.md +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/.claude/commands/gflow/scenario.md +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/.claude/commands/gflow/status.md +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/.gitattributes +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/.github/CODEOWNERS +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/.github/copilot-instructions.md +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/.github/dependabot.yml +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/.github/workflows/ci.yml +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/.github/workflows/external-pr-triage.yml +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/.github/workflows/governance-advisory.yml +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/.github/workflows/governance-benchmark.yml +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/.github/workflows/release.yml +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/.gitignore +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/.gitleaks.toml +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/.pre-commit-config.yaml +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/.secrets.baseline +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/AGENTS.md +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/CLAUDE.md +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/CONFIGURATION.md +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/CONTRIBUTING.md +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/DISCLAIMER.md +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/GEMINI.md +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/KNOWN_ISSUES.md +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/LICENSE +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/PLAN.md +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/README.md +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/RELEASE.md +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/ROADMAP.md +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/conftest.py +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/docker-compose.yml +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/docs/AGENT_GUIDE.md +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/docs/ARCHITECTURE.md +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/docs/AUTHENTICATION.md +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/docs/CHARACTER.md +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/docs/CHARACTER_RECON.md +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/docs/DATA_LAYER.md +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/docs/DEBUGGING.md +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/docs/DEMOS.md +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/docs/DEVELOPMENT.md +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/docs/E2E_TESTING.md +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/docs/EXTERNAL_STORAGE.md +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/docs/GITHUB.md +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/docs/GOVERNANCE_BENCHMARK.md +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/docs/IMAGE_UPSCALE_RECON.md +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/docs/LIVE_VERIFICATION_data_layer.md +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/docs/LIVE_VERIFICATION_image_batch.md +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/docs/LIVE_VERIFICATION_v0.10.0.md +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/docs/LIVE_VERIFICATION_v0.11.0.md +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/docs/LIVE_VERIFICATION_v0.12.0.md +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/docs/LIVE_VERIFICATION_v0.13.0.md +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/docs/LIVE_VERIFICATION_v0.16.0.md +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/docs/LIVE_VERIFICATION_v0.17.0.md +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/docs/LIVE_VERIFICATION_v0.18.0.md +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/docs/LIVE_VERIFICATION_v0.7.0.md +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/docs/LIVE_VERIFICATION_v0.8.1.md +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/docs/LIVE_VERIFICATION_v0.9.0.md +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/docs/LIVE_VERIFICATION_v0.9.1.md +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/docs/LIVE_VERIFICATION_video_download.md +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/docs/MOVIE.md +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/docs/PROJECT_STATUS.md +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/docs/USER_GUIDE.md +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/docs/assets/demo-split-pf.gif +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/docs/assets/example-run.gif +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/docs/schemas/movie-handoff.schema.json +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/docs/superpowers/plans/2026-06-12-issue-174-library-ui-attach/PLAN.md +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/examples/README.md +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/examples/batch_from_config.py +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/examples/multi_prompt_t2i.py +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/examples/sample_config.json +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/examples/sample_prompts.txt +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/examples/single_image_t2i.py +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/examples/workflow_chain.py +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/llms.txt +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/samples/README.md +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/samples/captured/01_upload_image.json +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/samples/captured/02_batchAsyncGenerateVideoText.json +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/samples/captured/03_batchCheckAsyncVideoGenerationStatus.json +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/samples/captured/04_archive_workflow.json +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/samples/captured/05_createProject.json +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/samples/captured/06_batchGenerateImages.json +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/samples/captured/07_batchGenerateImages_seeded.json +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/samples/captured/08_batchAsyncGenerateVideoStartAndEndImage.json +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/samples/captured/09_batchAsyncGenerateVideoReferenceImages.json +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/samples/captured/10_batchCheckAsyncVideoGenerationStatus_successful.json +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/samples/captured/11_batchCheckAsyncVideoGenerationStatus_failed.json +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/samples/captured/12_create_scene.json +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/samples/captured/13_sceneWorkflows_update.json +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/samples/captured/14_get_scene_workflows.json +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/samples/captured/15_commit_flowWorkflow.json +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/scripts/ci/check_doc_links.py +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/scripts/ci/check_materiality.py +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/scripts/ci/check_repo_hygiene.py +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/scripts/debug_editor.py +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/scripts/debug_gen_settings.py +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/scripts/debug_settings.py +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/scripts/dev/_recording_client.py +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/scripts/dev/_spike_common.py +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/scripts/dev/active_plan.py +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/scripts/dev/capture_i2v_frame_slots_dom.py +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/scripts/dev/capture_i2v_intercept_submit.py +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/scripts/dev/capture_i2v_model_select_repro.py +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/scripts/dev/capture_i2v_post_bind_state.py +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/scripts/dev/capture_image_add_media_dom.py +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/scripts/dev/capture_locale_invariants.py +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/scripts/dev/cdp_drive_and_probe.py +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/scripts/dev/character_create_spike.py +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/scripts/dev/character_create_spike_v2.py +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/scripts/dev/dump_character_selectors.js +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/scripts/dev/make_project.py +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/scripts/dev/materiality_backtest.py +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/scripts/dev/monitor_pr_38.py +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/scripts/dev/patch_character.py +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/scripts/dev/record_flow_capture.py +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/scripts/dev/skillopt/README.md +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/scripts/dev/skillopt/harness.py +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/scripts/dev/skillopt/tasks.json +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/scripts/dev/spike_char_editor_dom.py +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/scripts/dev/spike_char_gen_capture.py +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/scripts/dev/spike_image_upscale_capture.py +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/scripts/dev/spike_image_upscale_drive.py +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/scripts/dev/spike_image_upscale_recaptcha_action.py +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/scripts/dev/spike_image_upscale_rest_probe.py +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/scripts/dev/spike_issue170_picker_locale_recon.py +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/scripts/dev/spike_issue174_library_ui_recon.py +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/scripts/dev/spike_movie_attach_payload.py +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/scripts/dev/spike_movie_entity_recon.py +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/scripts/dev/spike_movie_gen_capture.py +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/scripts/dev/spike_movie_picker_select.py +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/scripts/dev/spike_movie_voice_list.py +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/scripts/dev/spike_patch_entity.py +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/scripts/diag/README.md +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/scripts/diag/capture_flow_traffic.py +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/scripts/diag/memory_profile.py +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/scripts/diag/recaptcha_mint.py +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/scripts/record_demo.ps1 +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/scripts/smoke_image.py +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/scripts/smoke_real_chrome_image.py +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/scripts/smoke_video_editor.py +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/scripts/smoke_worker_style.py +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/scripts/verify_chrome_auth_viability.py +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/skills/README.md +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/skills/gflow-cli/SKILL.md +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/skills/plan/SKILL.md +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/skills/pr-council-review/SKILL.md +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/skills/predict/SKILL.md +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/skills/scenario/SKILL.md +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/skills/status/SKILL.md +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/sonar-project.properties +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/src/gflow_cli/__main__.py +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/src/gflow_cli/_cli_helpers.py +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/src/gflow_cli/api/__init__.py +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/src/gflow_cli/api/_sapisidhash.py +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/src/gflow_cli/api/character.py +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/src/gflow_cli/api/dto.py +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/src/gflow_cli/api/image.py +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/src/gflow_cli/api/image_upscale.py +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/src/gflow_cli/api/routes.py +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/src/gflow_cli/api/scene.py +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/src/gflow_cli/api/transports/__init__.py +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/src/gflow_cli/api/transports/_common.py +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/src/gflow_cli/api/transports/_fingerprint.py +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/src/gflow_cli/api/transports/base.py +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/src/gflow_cli/api/transports/experimental/__init__.py +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/src/gflow_cli/api/transports/experimental/bearer.py +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/src/gflow_cli/api/transports/experimental/evaluate_fetch.py +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/src/gflow_cli/api/transports/experimental/sapisidhash.py +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/src/gflow_cli/api/transports/ui_automation_video.py +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/src/gflow_cli/api/video.py +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/src/gflow_cli/auth/__init__.py +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/src/gflow_cli/auth/base.py +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/src/gflow_cli/auth/cookies.py +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/src/gflow_cli/auth/factory.py +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/src/gflow_cli/auth/internal_chromium.py +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/src/gflow_cli/auth/real_chrome.py +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/src/gflow_cli/auth/strategies.py +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/src/gflow_cli/auth/verification.py +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/src/gflow_cli/browser_manager.py +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/src/gflow_cli/chain.py +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/src/gflow_cli/chain_manifest.py +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/src/gflow_cli/cli_character.py +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/src/gflow_cli/cli_data.py +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/src/gflow_cli/cli_image.py +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/src/gflow_cli/cli_models.py +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/src/gflow_cli/cli_movie.py +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/src/gflow_cli/cli_run.py +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/src/gflow_cli/cli_scene.py +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/src/gflow_cli/cli_video.py +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/src/gflow_cli/composition.py +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/src/gflow_cli/data/__init__.py +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/src/gflow_cli/data/chain_repo.py +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/src/gflow_cli/data/migrations/0001_initial.sql +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/src/gflow_cli/data/migrations/0002_add_cloud_storage.sql +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/src/gflow_cli/data/migrations/0003_add_scene_tables.sql +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/src/gflow_cli/data/migrations/0004_add_scene_output_path.sql +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/src/gflow_cli/data/migrations/0005_add_chain_links.sql +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/src/gflow_cli/data/migrations/0006_add_operations_metadata.sql +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/src/gflow_cli/data/migrations/__init__.py +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/src/gflow_cli/data/models.py +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/src/gflow_cli/data/queries.py +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/src/gflow_cli/data/recorder.py +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/src/gflow_cli/data/redaction.py +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/src/gflow_cli/data/repository.py +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/src/gflow_cli/data/store.py +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/src/gflow_cli/exceptions.py +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/src/gflow_cli/image_batch.py +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/src/gflow_cli/json_output.py +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/src/gflow_cli/manifest.py +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/src/gflow_cli/media.py +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/src/gflow_cli/movie_manifest.py +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/src/gflow_cli/observability.py +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/src/gflow_cli/paths.py +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/src/gflow_cli/profile_store.py +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/src/gflow_cli/services/__init__.py +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/src/gflow_cli/services/character_create.py +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/src/gflow_cli/storage.py +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/tasks/lessons.md +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/test_assets/sample_batch.json +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/test_assets/sample_batch.tsv +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/test_assets/sample_batch_invalid.tsv +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/tests/__init__.py +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/tests/api/__init__.py +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/tests/api/fixtures/character_gen_response.json +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/tests/api/fixtures/patch_entity_response.json +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/tests/api/test_aisandbox_auth_error.py +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/tests/api/test_aisandbox_auth_headers.py +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/tests/api/test_bearer_redaction.py +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/tests/api/test_character.py +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/tests/api/test_client.py +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/tests/api/test_client_character.py +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/tests/api/test_client_delete_characters.py +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/tests/api/test_client_generate_character.py +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/tests/api/test_client_image.py +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/tests/api/test_client_launch_kwargs.py +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/tests/api/test_client_patch_entity.py +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/tests/api/test_client_scene.py +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/tests/api/test_client_upscale.py +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/tests/api/test_concurrency.py +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/tests/api/test_dto.py +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/tests/api/test_image.py +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/tests/api/test_image_dto.py +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/tests/api/test_image_upscale.py +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/tests/api/test_post_json_aisandbox_auth.py +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/tests/api/test_recaptcha.py +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/tests/api/test_routes.py +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/tests/api/test_routes_character.py +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/tests/api/test_routes_scene.py +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/tests/api/test_sapisidhash_helper.py +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/tests/api/test_scene_models.py +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/tests/api/test_video.py +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/tests/api/test_video_request.py +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/tests/api/transports/__init__.py +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/tests/api/transports/test_base.py +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/tests/api/transports/test_bearer.py +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/tests/api/transports/test_common.py +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/tests/api/transports/test_evaluate_fetch.py +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/tests/api/transports/test_factory.py +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/tests/api/transports/test_fingerprint.py +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/tests/api/transports/test_sapisidhash.py +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/tests/api/transports/test_transport_timeout.py +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/tests/api/transports/test_ui_automation.py +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/tests/api/transports/test_ui_automation_batch.py +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/tests/api/transports/test_ui_automation_image_mode.py +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/tests/api/transports/test_ui_automation_video.py +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/tests/api/transports/test_ui_character_editor.py +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/tests/auth/strategies/test_factory.py +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/tests/auth/strategies/test_strategies.py +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/tests/auth/test_verification.py +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/tests/cli/__init__.py +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/tests/cli/test_cli_auth_list.py +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/tests/cli/test_cli_character.py +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/tests/cli/test_cli_character_create.py +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/tests/cli/test_cli_data.py +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/tests/cli/test_cli_image.py +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/tests/cli/test_cli_image_seed_removed.py +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/tests/cli/test_cli_image_selector_drift.py +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/tests/cli/test_cli_image_upscale.py +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/tests/cli/test_cli_models.py +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/tests/cli/test_cli_movie.py +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/tests/cli/test_cli_run.py +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/tests/cli/test_cli_scene.py +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/tests/cli/test_cli_video.py +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/tests/cli/test_cli_video_chain.py +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/tests/cli/test_error_handling.py +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/tests/cli/test_helpers.py +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/tests/cli/test_movie_manifest.py +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/tests/cli/test_t2i_multi_prompt.py +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/tests/composition/test_character.py +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/tests/composition/test_compose_prompt.py +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/tests/composition/test_handoff.py +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/tests/conftest.py +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/tests/data/__init__.py +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/tests/data/test_chain_repo.py +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/tests/data/test_find_incomplete_character.py +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/tests/data/test_models.py +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/tests/data/test_packaging.py +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/tests/data/test_recorder.py +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/tests/data/test_recorder_character.py +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/tests/data/test_redaction.py +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/tests/data/test_repository.py +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/tests/data/test_scene_persistence.py +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/tests/data/test_settings_and_errors.py +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/tests/data/test_store_migrations.py +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/tests/dev/test_record_flow_capture.py +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/tests/dev/test_recording_client.py +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/tests/e2e/__init__.py +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/tests/e2e/conftest.py +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/tests/e2e/test_aisandbox_auth_live.py +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/tests/e2e/test_auth_verification_e2e.py +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/tests/e2e/test_chain_e2e.py +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/tests/e2e/test_character_create_e2e.py +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/tests/e2e/test_data_layer_e2e.py +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/tests/e2e/test_i2v_flags_e2e.py +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/tests/e2e/test_image_batch_e2e.py +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/tests/e2e/test_image_i2i_ref_cap_e2e.py +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/tests/e2e/test_json_output_e2e.py +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/tests/e2e/test_locale_selectors_e2e.py +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/tests/e2e/test_scene_compose_live.py +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/tests/e2e/test_transports_e2e.py +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/tests/e2e/test_video_r2v_ref_cap_e2e.py +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/tests/e2e/test_video_t2v_e2e.py +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/tests/features/__init__.py +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/tests/features/auth.feature +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/tests/features/auth_login.feature +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/tests/features/character_create.feature +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/tests/features/character_read.feature +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/tests/features/conftest.py +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/tests/features/image.feature +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/tests/features/image_upscale.feature +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/tests/features/locale_picker_include.feature +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/tests/features/test_auth_login_steps.py +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/tests/features/test_auth_steps.py +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/tests/features/test_character_create_steps.py +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/tests/features/test_character_read_steps.py +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/tests/features/test_image_steps.py +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/tests/features/test_image_upscale_steps.py +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/tests/features/test_locale_picker_include_steps.py +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/tests/features/test_step_collision_guard.py +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/tests/features/test_video_chain_steps.py +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/tests/features/video_chain.feature +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/tests/fixtures/__init__.py +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/tests/fixtures/seeded_catalog.py +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/tests/image_batch/__init__.py +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/tests/image_batch/test_image_manifest.py +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/tests/image_batch/test_observability_events.py +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/tests/integration/__init__.py +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/tests/integration/conftest.py +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/tests/integration/constants.py +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/tests/integration/test_storage_gcs.py +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/tests/integration/test_storage_s3.py +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/tests/scripts/test_capture_locale_invariants.py +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/tests/scripts/test_check_materiality.py +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/tests/scripts/test_check_repo_hygiene.py +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/tests/scripts/test_materiality_backtest.py +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/tests/services/__init__.py +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/tests/services/test_character_create_redaction.py +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/tests/services/test_character_create_saga.py +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/tests/services/test_character_gen_no_direct_post.py +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/tests/smoke/__init__.py +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/tests/smoke/test_profile_account_smoke.py +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/tests/smoke/test_real_flow.py +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/tests/test_auth.py +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/tests/test_browser_manager.py +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/tests/test_chain.py +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/tests/test_chain_manifest.py +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/tests/test_cli_data.py +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/tests/test_conftest_isolation.py +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/tests/test_data_queries.py +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/tests/test_documentation_gate.py +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/tests/test_errors_403.py +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/tests/test_json_output.py +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/tests/test_manifest.py +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/tests/test_marker_registry.py +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/tests/test_media.py +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/tests/test_observability.py +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/tests/test_paths.py +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/tests/test_profile_store.py +0 -0
- {gflow_cli-0.18.0 → gflow_cli-0.19.0}/tests/test_smoke.py +0 -0
|
@@ -106,6 +106,15 @@
|
|
|
106
106
|
# visible window). The session is still reused from the persistent profile.
|
|
107
107
|
# GFLOW_CLI_HEADLESS=true
|
|
108
108
|
|
|
109
|
+
# Browser automation engine — playwright (default) | patchright
|
|
110
|
+
# patchright is an OPT-IN, drop-in patched Playwright (Chromium) that avoids the
|
|
111
|
+
# Runtime.enable CDP leak for stronger reCAPTCHA-Enterprise evasion on the HEADED
|
|
112
|
+
# path. It is NOT a headless unlock. Requires a separate install:
|
|
113
|
+
# pip install patchright (or: pip install 'gflow-cli[patchright]')
|
|
114
|
+
# When using system Chrome (channel=chrome, the gflow default) you do NOT need
|
|
115
|
+
# `patchright install chromium`. Unset to revert to the default playwright engine.
|
|
116
|
+
# GFLOW_CLI_BROWSER_ENGINE=playwright
|
|
117
|
+
|
|
109
118
|
# BCP-47 locale tag passed to Playwright's launch `locale=` parameter
|
|
110
119
|
# (controls Accept-Language only — Chrome's UI language is forced to
|
|
111
120
|
# en-US via the `--lang=en-US` launch arg regardless of this setting, so
|
|
@@ -7,6 +7,28 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.19.0] — 2026-06-12
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- **Opt-in Patchright browser engine** via `GFLOW_CLI_BROWSER_ENGINE=patchright`
|
|
15
|
+
(default `playwright`, unchanged). Patchright is a drop-in patched Playwright
|
|
16
|
+
(Chromium) that runs page evaluations in an isolated execution context to
|
|
17
|
+
avoid the `Runtime.enable` CDP leak, for stronger reCAPTCHA-Enterprise evasion
|
|
18
|
+
on the **headed** path. It is **not** a headless unlock and must be installed
|
|
19
|
+
separately: `pip install 'gflow-cli[patchright]'`. The default engine is
|
|
20
|
+
byte-identical to before. `gflow auth status` now reports the active engine.
|
|
21
|
+
- `BrowserEngineUnavailableError` (exit code 24): selecting `patchright` without
|
|
22
|
+
the package installed now fails with a clear `pip install patchright`
|
|
23
|
+
remediation hint instead of a raw `ImportError` hashed to a generic exit 1.
|
|
24
|
+
|
|
25
|
+
### Fixed
|
|
26
|
+
|
|
27
|
+
- An invalid `GFLOW_CLI_*` enum value (e.g. a typo'd `GFLOW_CLI_BROWSER_ENGINE`,
|
|
28
|
+
`GFLOW_CLI_PROVIDER`, or `GFLOW_CLI_LOG_LEVEL`) now fails with a clean
|
|
29
|
+
configuration error and **exit code 11** naming the offending variable, instead
|
|
30
|
+
of leaking a raw pydantic `ValidationError` traceback and exiting 1.
|
|
31
|
+
|
|
10
32
|
## [0.18.0] — 2026-06-12
|
|
11
33
|
|
|
12
34
|
### Added
|
|
@@ -1527,7 +1549,8 @@ shell-script template that branches on these codes.
|
|
|
1527
1549
|
|
|
1528
1550
|
First skeleton. Not functional end-to-end yet.
|
|
1529
1551
|
|
|
1530
|
-
[Unreleased]: https://github.com/ffroliva/gflow-cli/compare/v0.
|
|
1552
|
+
[Unreleased]: https://github.com/ffroliva/gflow-cli/compare/v0.19.0...HEAD
|
|
1553
|
+
[0.19.0]: https://github.com/ffroliva/gflow-cli/compare/v0.18.0...v0.19.0
|
|
1531
1554
|
[0.18.0]: https://github.com/ffroliva/gflow-cli/compare/v0.17.0...v0.18.0
|
|
1532
1555
|
[0.17.0]: https://github.com/ffroliva/gflow-cli/compare/v0.16.0...v0.17.0
|
|
1533
1556
|
[0.16.0]: https://github.com/ffroliva/gflow-cli/compare/v0.15.1...v0.16.0
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: gflow-cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.19.0
|
|
4
4
|
Summary: Unofficial CLI for Google Flow — drive Veo image-to-video generations from the terminal.
|
|
5
5
|
Project-URL: Homepage, https://github.com/ffroliva/gflow-cli
|
|
6
6
|
Project-URL: Issues, https://github.com/ffroliva/gflow-cli/issues
|
|
@@ -58,6 +58,8 @@ Requires-Dist: ruff>=0.15.16; extra == 'dev'
|
|
|
58
58
|
Provides-Extra: gcs
|
|
59
59
|
Requires-Dist: gcsfs>=2024.2.0; extra == 'gcs'
|
|
60
60
|
Requires-Dist: universal-pathlib>=0.2.5; extra == 'gcs'
|
|
61
|
+
Provides-Extra: patchright
|
|
62
|
+
Requires-Dist: patchright==1.60.1; extra == 'patchright'
|
|
61
63
|
Provides-Extra: s3
|
|
62
64
|
Requires-Dist: s3fs>=2024.2.0; extra == 's3'
|
|
63
65
|
Requires-Dist: universal-pathlib>=0.2.5; extra == 's3'
|
|
@@ -190,6 +190,16 @@ GFLOW_CLI_HISTORY_PROMPTS=redacted gflow image t2i "confidential brief"
|
|
|
190
190
|
**Default:** `true`
|
|
191
191
|
**When to flip to `false`:** if reCAPTCHA Enterprise refuses to mint tokens (Google's bot-detection sometimes refuses headless Chromium but accepts a visible window). Set to `false` and re-run; the browser will appear during generation but the session is still reused from the persistent profile.
|
|
192
192
|
|
|
193
|
+
### `GFLOW_CLI_BROWSER_ENGINE`
|
|
194
|
+
|
|
195
|
+
**What:** Selects the browser-automation engine backing the Playwright API.
|
|
196
|
+
**Values:** `playwright` | `patchright`
|
|
197
|
+
**Default:** `playwright`
|
|
198
|
+
**What `patchright` is:** an opt-in, drop-in patched Playwright (Chromium) that runs page evaluations in an isolated execution context to avoid the `Runtime.enable` CDP leak, for stronger reCAPTCHA-Enterprise evasion on the **headed** path. It is **not** a headless unlock — Google still detects headless Chromium regardless of engine.
|
|
199
|
+
**Install:** `pip install 'gflow-cli[patchright]'` (or `pip install patchright`). Selecting `patchright` without it installed fails fast with exit code 24 and a pip remediation hint. When using system Chrome (the gflow default, `channel=chrome`) you do **not** need `patchright install chromium`.
|
|
200
|
+
**Reverting:** unset the variable (or set `playwright`) — the default path is byte-identical to a build without this feature, with no profile migration.
|
|
201
|
+
**Security note:** the `patchright` extra ships a *patched Chromium driver* that handles your live Google session cookies; it is exact-pinned and treated as a security-review-required dependency. See [SECURITY.md § Dependencies](SECURITY.md).
|
|
202
|
+
|
|
193
203
|
### `GFLOW_CLI_LOCALE`
|
|
194
204
|
|
|
195
205
|
**What:** BCP-47 locale tag passed to Playwright's `launch_persistent_context(locale=...)` — controls the `Accept-Language` HTTP header only.
|
|
@@ -111,7 +111,7 @@ Slash commands for Claude Code, stored in `.claude/commands/gflow/`. All prefixe
|
|
|
111
111
|
**"A gflow command hangs / fails — where do I start?"** → [DEBUGGING § Quick reference](DEBUGGING.md#quick-reference)
|
|
112
112
|
**"Flow's UI broke a selector — how do I diagnose it?"** → [DEBUGGING § Inspecting Flow's live UI](DEBUGGING.md#inspecting-flows-live-ui)
|
|
113
113
|
**"What does each `ui_automation.*` log event mean?"** → [DEBUGGING § Listener & HTTP-layer debugging](DEBUGGING.md#listener--http-layer-debugging)
|
|
114
|
-
**"What was actually live-verified for the latest release?"** → latest: [LIVE_VERIFICATION_v0.
|
|
114
|
+
**"What was actually live-verified for the latest release?"** → latest: [LIVE_VERIFICATION_v0.19.0](LIVE_VERIFICATION_v0.19.0.md) (opt-in patchright engine: credit-free `image t2i` end-to-end under `GFLOW_CLI_BROWSER_ENGINE=patchright`, 5-layer ledger + missing-dep/invalid-enum/default-path checks; 403→200 premise documented as pending a hot profile). v0.14.0 / v0.15.0 / v0.15.1 shipped without a dedicated doc (CI + the v0.13.0 baseline). Prior: [v0.18.0](LIVE_VERIFICATION_v0.18.0.md) · [v0.17.0](LIVE_VERIFICATION_v0.17.0.md) · [v0.16.0](LIVE_VERIFICATION_v0.16.0.md) · [v0.13.0](LIVE_VERIFICATION_v0.13.0.md) · [v0.12.0](LIVE_VERIFICATION_v0.12.0.md) · [v0.11.0](LIVE_VERIFICATION_v0.11.0.md) · [v0.10.0](LIVE_VERIFICATION_v0.10.0.md) · [v0.9.1](LIVE_VERIFICATION_v0.9.1.md) · [v0.9.0](LIVE_VERIFICATION_v0.9.0.md) · [v0.8.1](LIVE_VERIFICATION_v0.8.1.md) · [v0.7.0](LIVE_VERIFICATION_v0.7.0.md)
|
|
115
115
|
**"Where is the reverse-engineered wire protocol for a feature?"** → the `*_RECON.md` design docs: [CHARACTER_RECON](CHARACTER_RECON.md) (Flow character entity protocol), [IMAGE_UPSCALE_RECON](IMAGE_UPSCALE_RECON.md) (`/v1/flow/upsampleImage` wire). Naming convention: one `<FEATURE>_RECON.md` per reverse-engineered surface, kept as the durable spec after the feature ships.
|
|
116
116
|
**"What was live-verified for the data layer (PR #58)?"** → [LIVE_VERIFICATION_data_layer](LIVE_VERIFICATION_data_layer.md) — 1 Imagen + 1 Veo credit on denon82, 6-layer ledger (file + magic + Pillow + DB rows + CLI round-trip + structlog)
|
|
117
117
|
**"What was live-verified for the video-download feature (#29)?"** → [LIVE_VERIFICATION_video_download](LIVE_VERIFICATION_video_download.md)
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# Live Verification — v0.19.0
|
|
2
|
+
|
|
3
|
+
Feature under test: **opt-in Patchright browser engine** (`GFLOW_CLI_BROWSER_ENGINE=patchright`).
|
|
4
|
+
Verified 2026-06-12 against live Google Flow on a real authenticated profile. Image
|
|
5
|
+
generation is credit-free, so the end-to-end run cost **$0**.
|
|
6
|
+
|
|
7
|
+
## Layer 1 — file count
|
|
8
|
+
|
|
9
|
+
One image produced by `gflow image t2i` through the integrated patchright path:
|
|
10
|
+
|
|
11
|
+
```
|
|
12
|
+
scripts/dev/_spike_out/patch_verify/images/2026-06-12/6cb21d9e-e4f9-40ba-897c-eb25c6333892_1.jpg
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
(written to a throwaway `GFLOW_CLI_OUTPUT_DIR`, since cleaned up).
|
|
16
|
+
|
|
17
|
+
## Layer 2 — magic bytes
|
|
18
|
+
|
|
19
|
+
`b[:3] == FF D8 FF` → **valid JPEG**. Size 723,911 bytes.
|
|
20
|
+
|
|
21
|
+
## Layer 3 — dimensions / shape
|
|
22
|
+
|
|
23
|
+
Pillow: `format=JPEG dims=(768, 1376)` → correct **9:16** portrait (the default aspect).
|
|
24
|
+
|
|
25
|
+
## Layer 4 — structlog invariants
|
|
26
|
+
|
|
27
|
+
From the live run (`correlation_id=40877345-0af4-472f-966a-94f88112f0d9`):
|
|
28
|
+
|
|
29
|
+
- `browser.engine_selected engine=patchright` — the **settings resolver** selected
|
|
30
|
+
patchright (the real wired path, not a test monkeypatch).
|
|
31
|
+
- `ui_automation.batch_request_body` — prompt submitted (`bytes=5054`).
|
|
32
|
+
- `ui_automation.batch_response_seen status=200` and
|
|
33
|
+
`ui_automation.batch_response_captured status=200` — `batchGenerateImages`
|
|
34
|
+
returned 200 and the response body parsed under patchright.
|
|
35
|
+
|
|
36
|
+
## Layer 5 — user-confirmable artifact
|
|
37
|
+
|
|
38
|
+
A studio-lit image of "a small green cactus in a terracotta pot" — visually
|
|
39
|
+
inspectable, matches the prompt.
|
|
40
|
+
|
|
41
|
+
## Supporting checks (same release)
|
|
42
|
+
|
|
43
|
+
| Check | Evidence | Result |
|
|
44
|
+
|---|---|---|
|
|
45
|
+
| Default path byte-identical | client-level `navigator.webdriver` read through real `FlowApiClient`: `playwright → None` (masked), `patchright → False` (both non-bot) | ✅ default still stealthed |
|
|
46
|
+
| Missing-dependency UX | `GFLOW_CLI_BROWSER_ENGINE=patchright` with patchright uninstalled → `image t2i` | ✅ **exit 24**, `BrowserEngineUnavailableError` + `pip install patchright` hint, no traceback, no browser launched |
|
|
47
|
+
| Invalid enum setting | `GFLOW_CLI_BROWSER_ENGINE=patchwright` (and `GFLOW_CLI_PROVIDER=bogus`) | ✅ **exit 11**, clean "Configuration error" naming the variable, no raw pydantic traceback |
|
|
48
|
+
| reCAPTCHA mint under patchright | `TokenMinter.mint` with `isolated_context=False` | ✅ returns a real token (the predicted isolated-world break, fixed) |
|
|
49
|
+
| Retry across engine boundary | patchright's distinct `TimeoutError` through the real retry policy | ✅ retried (unit test) |
|
|
50
|
+
|
|
51
|
+
## Premise still pending (documented, not a blocker)
|
|
52
|
+
|
|
53
|
+
The *value* premise — does patchright reduce real 403s vs Playwright — could **not**
|
|
54
|
+
be measured this cycle: the documented hot profile (`denon82`) had cooled, so plain
|
|
55
|
+
Playwright also returned 200 (no 403 to flip). A faithful 403 cannot be emulated
|
|
56
|
+
locally (a route-injected 403 is engine-blind; inflating real WAF heat risks the
|
|
57
|
+
account). The feature ships as a **proven non-breaking, opt-in escape-hatch** with
|
|
58
|
+
the default unchanged. Re-fire trigger: the instant any profile actually 403s, run
|
|
59
|
+
`scripts/dev/spike_patchright.py --profile <hot> --engines playwright,patchright`
|
|
60
|
+
to capture the `WAF_403 → OK_200` differential.
|
|
@@ -201,6 +201,26 @@ Audited with `pip-audit` in CI on every push. Major dependency surface:
|
|
|
201
201
|
|
|
202
202
|
No transitive dep with known CVEs at the time of v0.1.0 scaffold.
|
|
203
203
|
|
|
204
|
+
### Optional `patchright` engine — elevated supply-chain trust
|
|
205
|
+
|
|
206
|
+
The `gflow-cli[patchright]` extra (opt-in via `GFLOW_CLI_BROWSER_ENGINE=patchright`)
|
|
207
|
+
installs **Patchright**, a single-maintainer package that ships a **patched
|
|
208
|
+
Chromium driver**. Because that driver *is* the browser process that loads your
|
|
209
|
+
real Google session — it has direct access to session cookies, the OAuth Bearer
|
|
210
|
+
token, and SAPISID — it carries a higher blast radius than an ordinary Python
|
|
211
|
+
dependency: a compromised release could exfiltrate the full Google session.
|
|
212
|
+
|
|
213
|
+
Controls:
|
|
214
|
+
|
|
215
|
+
- **Optional-only** — never in the base `dependencies`; the default install pulls
|
|
216
|
+
neither the package nor its driver.
|
|
217
|
+
- **Exact-pinned** (`patchright==1.60.1`) so a release cannot silently move the
|
|
218
|
+
browser binary underneath you.
|
|
219
|
+
- **Security-review-required on bump** — a Patchright version change is treated
|
|
220
|
+
like a browser-binary change, NOT a routine dependabot auto-merge.
|
|
221
|
+
- Default engine is `playwright` (Microsoft, security-reviewed); patchright is an
|
|
222
|
+
experiment you opt into per the trade-off above.
|
|
223
|
+
|
|
204
224
|
## Reporting
|
|
205
225
|
|
|
206
226
|
| Issue type | How |
|
|
@@ -1031,6 +1031,7 @@ shell scripts can branch on the failure mode without parsing stderr.
|
|
|
1031
1031
|
| `21` | `ChainPartialError` | A video chain stopped mid-way; earlier links completed | Resume from the last completed link shown in the error |
|
|
1032
1032
|
| `22` | `UpscaleUnavailableError` | 4K upscale is gated to Flow **Ultra** accounts (HTTP 403) | Use `--scale 2k`, or upgrade the Flow plan |
|
|
1033
1033
|
| `23` | `UiSelectorDriftError` | A Flow editor control could not be located — Google changed the frontend (issue #183) | Update gflow-cli; file a bug with the probe name + debug screenshot from the error message |
|
|
1034
|
+
| `24` | `BrowserEngineUnavailableError` | `GFLOW_CLI_BROWSER_ENGINE=patchright` but the engine is not installed | `pip install 'gflow-cli[patchright]'`, or unset `GFLOW_CLI_BROWSER_ENGINE` |
|
|
1034
1035
|
| `130`| SIGINT | User-interrupted (Ctrl-C) | — |
|
|
1035
1036
|
|
|
1036
1037
|
**Exit code 16 — data store / migration error.** Fires when:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "gflow-cli"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.19.0"
|
|
4
4
|
description = "Unofficial CLI for Google Flow — drive Veo image-to-video generations from the terminal."
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
license = { file = "LICENSE" }
|
|
@@ -58,6 +58,15 @@ s3 = [
|
|
|
58
58
|
chain = [
|
|
59
59
|
"av>=12",
|
|
60
60
|
]
|
|
61
|
+
# Patchright engine extra — opt-in, drop-in patched Playwright (Chromium) that
|
|
62
|
+
# avoids the Runtime.enable CDP leak on the headed path. Selected via
|
|
63
|
+
# GFLOW_CLI_BROWSER_ENGINE=patchright. Ships a PATCHED Chromium driver that
|
|
64
|
+
# handles live Google session cookies/Bearer/SAPISID, so it is EXACT-pinned and
|
|
65
|
+
# treated as a security-review-required bump (NOT a dependabot auto-merge) —
|
|
66
|
+
# see SECURITY.md. Not a headless unlock.
|
|
67
|
+
patchright = [
|
|
68
|
+
"patchright==1.60.1",
|
|
69
|
+
]
|
|
61
70
|
|
|
62
71
|
[project.scripts]
|
|
63
72
|
gflow = "gflow_cli.cli:main"
|
|
@@ -0,0 +1,357 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
r"""Patchright engine spike — the Phase-0 kill-gate for the opt-in browser engine.
|
|
3
|
+
|
|
4
|
+
Plan: docs/superpowers/plans/2026-06-12-patchright-engine/{PLAN.md,SCENARIO.md}
|
|
5
|
+
Predict verdict: CAUTION 6/10 — this spike must prove the premise before any
|
|
6
|
+
production file is touched.
|
|
7
|
+
|
|
8
|
+
Touches ZERO production files. It swaps the browser engine at RUNTIME (monkeypatch
|
|
9
|
+
of the module-level ``async_playwright`` that ``client.py`` / ``ui_automation.py``
|
|
10
|
+
import) and reuses the real ``FlowApiClient.generate_image`` path so the 403→200
|
|
11
|
+
comparison is faithful — not a hand-rolled request that could 403 for the wrong
|
|
12
|
+
reason. Image generation is credit-free (memory: flow-credits-videos-only), so the
|
|
13
|
+
decisive hot-profile leg costs $0.
|
|
14
|
+
|
|
15
|
+
Two probe phases per engine (``playwright`` baseline vs ``patchright``):
|
|
16
|
+
|
|
17
|
+
A. PRIMITIVE PROBE (owns its own page; full control)
|
|
18
|
+
- launch the real profile with the EXACT production launch kwargs
|
|
19
|
+
(replicates client._persistent_context_kwargs + the navigator.webdriver mask)
|
|
20
|
+
- record channel (scenario 7), navigator.webdriver (scenario 3 baseline)
|
|
21
|
+
- discover_site_key (scenario 8 / Leg 2)
|
|
22
|
+
- MINT A/B (scenario 1 / Leg 1, the make-or-break): under patchright, call the
|
|
23
|
+
reCAPTCHA execute JS with the default (isolated) context AND with
|
|
24
|
+
isolated_context=False — record which returns a token. This directly
|
|
25
|
+
validates/invalidates the "isolated_context default breaks grecaptcha"
|
|
26
|
+
hypothesis from predict.
|
|
27
|
+
- listener smoke (Legs 3/4 lite): page.on("response")/page.on("request") fire.
|
|
28
|
+
|
|
29
|
+
B. FAITHFUL GENERATION (scenario 2 / Leg 6 — DECISIVE, + real Leg 3)
|
|
30
|
+
- swap engine on client.py + ui_automation.py
|
|
31
|
+
- patch recaptcha.TokenMinter.mint to inject isolated_context=False (patchright)
|
|
32
|
+
- attach a context-page response listener capturing the batchGenerateImages
|
|
33
|
+
HTTP status + whether response.json() parses
|
|
34
|
+
- run the real generate_image and classify the outcome (200 / 403-WAF /
|
|
35
|
+
mint-broken / other)
|
|
36
|
+
|
|
37
|
+
Run it on a HOT profile (one currently 403ing under playwright) for the decisive
|
|
38
|
+
comparison. Pass-bar = patchright measurably flips 403→200 AND mint works AND
|
|
39
|
+
listeners parse. Any red leg → STOP (write SPIKE-RESULTS.md, recommend
|
|
40
|
+
cadence-shaping). See PLAN.md Phase 0.
|
|
41
|
+
|
|
42
|
+
Usage (headed, supervised; PYTHONUTF8=1 on Windows):
|
|
43
|
+
|
|
44
|
+
.venv\Scripts\python.exe scripts\dev\spike_patchright.py ^
|
|
45
|
+
--profile denon82 --engines playwright,patchright ^
|
|
46
|
+
--prompt "a red ceramic mug on a wooden table, studio light"
|
|
47
|
+
|
|
48
|
+
# primitives only (no generation), e.g. on a clean profile:
|
|
49
|
+
.venv\Scripts\python.exe scripts\dev\spike_patchright.py ^
|
|
50
|
+
--profile ffroliva --skip-generation
|
|
51
|
+
|
|
52
|
+
Outputs (gitignored): scripts/dev/_spike_out/spike_patchright_<ts>.json
|
|
53
|
+
"""
|
|
54
|
+
|
|
55
|
+
from __future__ import annotations
|
|
56
|
+
|
|
57
|
+
import argparse
|
|
58
|
+
import asyncio
|
|
59
|
+
import json
|
|
60
|
+
import sys
|
|
61
|
+
import time
|
|
62
|
+
import traceback
|
|
63
|
+
from pathlib import Path
|
|
64
|
+
from typing import Any
|
|
65
|
+
|
|
66
|
+
_ROOT = Path(__file__).resolve().parents[2]
|
|
67
|
+
_SRC = _ROOT / "src"
|
|
68
|
+
if _SRC.exists() and str(_SRC) not in sys.path:
|
|
69
|
+
sys.path.insert(0, str(_SRC))
|
|
70
|
+
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
|
71
|
+
|
|
72
|
+
from _spike_common import default_out_path, resolve_profile_dir, step # noqa: E402
|
|
73
|
+
|
|
74
|
+
# Production surfaces under test (worktree src/ via the bootstrap above).
|
|
75
|
+
import gflow_cli.api.client as client_mod # noqa: E402
|
|
76
|
+
import gflow_cli.api.transports.ui_automation as ui_mod # noqa: E402
|
|
77
|
+
from gflow_cli.api import recaptcha as recaptcha_mod # noqa: E402
|
|
78
|
+
from gflow_cli.api import routes # noqa: E402
|
|
79
|
+
from gflow_cli.api.client import FlowApiClient # noqa: E402
|
|
80
|
+
from gflow_cli.api.image import Aspect, GenerateImageRequest, Model # noqa: E402
|
|
81
|
+
from gflow_cli.api.recaptcha import RecaptchaError, discover_site_key # noqa: E402
|
|
82
|
+
from gflow_cli.browser_manager import channel_for_profile # noqa: E402
|
|
83
|
+
|
|
84
|
+
_EXECUTE_JS = recaptcha_mod._EXECUTE_JS # noqa: SLF001 — spike reuses the production mint JS
|
|
85
|
+
|
|
86
|
+
# Original references so each engine run restores a clean module state.
|
|
87
|
+
_PW_ASYNC = client_mod.async_playwright
|
|
88
|
+
_ORIG_MINT = recaptcha_mod.TokenMinter.mint
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def _engine_async_playwright(engine: str) -> Any:
|
|
92
|
+
"""Return the async_playwright factory for *engine* (imports patchright lazily)."""
|
|
93
|
+
if engine == "playwright":
|
|
94
|
+
return _PW_ASYNC
|
|
95
|
+
if engine == "patchright":
|
|
96
|
+
from patchright.async_api import async_playwright as patch_async
|
|
97
|
+
|
|
98
|
+
return patch_async
|
|
99
|
+
msg = f"unknown engine {engine!r}"
|
|
100
|
+
raise ValueError(msg)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def _launch_kwargs(profile_dir: Path) -> dict[str, Any]:
|
|
104
|
+
"""Replicate client._persistent_context_kwargs verbatim (headed, identical for
|
|
105
|
+
both engines so ENGINE is the only variable in the comparison)."""
|
|
106
|
+
return {
|
|
107
|
+
"user_data_dir": str(profile_dir),
|
|
108
|
+
"headless": False,
|
|
109
|
+
"viewport": {"width": 1280, "height": 720},
|
|
110
|
+
"locale": "en-US",
|
|
111
|
+
"extra_http_headers": {"Accept-Language": "en-US,en;q=0.9"},
|
|
112
|
+
"channel": channel_for_profile(profile_dir),
|
|
113
|
+
"ignore_default_args": ["--enable-automation", "--no-sandbox", "--password-store=basic"],
|
|
114
|
+
"args": ["--disable-blink-features=AutomationControlled", "--disable-dev-shm-usage"],
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
async def _eval_token(page: Any, site_key: str, action: str, *, isolated: bool | None) -> Any:
|
|
119
|
+
"""Run the production reCAPTCHA execute JS. ``isolated=None`` → engine default
|
|
120
|
+
(no kwarg, valid for both engines); ``isolated=False`` → patchright main-world."""
|
|
121
|
+
if isolated is None:
|
|
122
|
+
return await page.evaluate(_EXECUTE_JS, [site_key, action])
|
|
123
|
+
return await page.evaluate(_EXECUTE_JS, [site_key, action], isolated_context=isolated)
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
async def probe_primitives(engine: str, profile_dir: Path, action: str) -> dict[str, Any]:
|
|
127
|
+
"""Phase A — primitives on a self-owned page. No FlowApiClient, full control."""
|
|
128
|
+
out: dict[str, Any] = {"engine": engine, "channel": channel_for_profile(profile_dir)}
|
|
129
|
+
apw = _engine_async_playwright(engine)
|
|
130
|
+
req_count = {"request": 0, "response": 0}
|
|
131
|
+
try:
|
|
132
|
+
async with apw() as pw:
|
|
133
|
+
try:
|
|
134
|
+
ctx = await pw.chromium.launch_persistent_context(**_launch_kwargs(profile_dir))
|
|
135
|
+
except Exception as exc: # noqa: BLE001
|
|
136
|
+
out["launch_error"] = f"{type(exc).__name__}: {exc}"
|
|
137
|
+
out["hint"] = (
|
|
138
|
+
"patchright may need `patchright install chromium` if channel='chrome' "
|
|
139
|
+
"is not honoured — scenario 7."
|
|
140
|
+
)
|
|
141
|
+
return out
|
|
142
|
+
out["launch_ok"] = True
|
|
143
|
+
await ctx.add_init_script(
|
|
144
|
+
"Object.defineProperty(navigator,'webdriver',{get:()=>undefined})",
|
|
145
|
+
)
|
|
146
|
+
page = ctx.pages[0] if ctx.pages else await ctx.new_page()
|
|
147
|
+
page.on("request", lambda *_: req_count.__setitem__("request", req_count["request"] + 1))
|
|
148
|
+
page.on(
|
|
149
|
+
"response", lambda *_: req_count.__setitem__("response", req_count["response"] + 1)
|
|
150
|
+
)
|
|
151
|
+
await page.goto(routes.EDITOR_BOOTSTRAP_URL, wait_until="domcontentloaded", timeout=60_000)
|
|
152
|
+
await page.wait_for_timeout(4000)
|
|
153
|
+
|
|
154
|
+
out["navigator_webdriver"] = await page.evaluate("() => navigator.webdriver")
|
|
155
|
+
try:
|
|
156
|
+
site_key = await discover_site_key(page)
|
|
157
|
+
out["site_key_prefix"] = site_key[:12]
|
|
158
|
+
out["discover_site_key"] = "ok"
|
|
159
|
+
except RecaptchaError as exc:
|
|
160
|
+
out["discover_site_key"] = f"FAIL: {exc}"
|
|
161
|
+
site_key = ""
|
|
162
|
+
|
|
163
|
+
# MINT A/B — the make-or-break (Leg 1 / scenario 1).
|
|
164
|
+
if site_key:
|
|
165
|
+
# Engine default (playwright: main world; patchright: isolated world).
|
|
166
|
+
try:
|
|
167
|
+
tok = await _eval_token(page, site_key, action, isolated=None)
|
|
168
|
+
out["mint_default"] = "ok" if (isinstance(tok, str) and tok) else "empty"
|
|
169
|
+
except Exception as exc: # noqa: BLE001
|
|
170
|
+
out["mint_default"] = f"FAIL: {type(exc).__name__}: {str(exc)[:80]}"
|
|
171
|
+
# Patchright-only: force main world.
|
|
172
|
+
if engine == "patchright":
|
|
173
|
+
try:
|
|
174
|
+
tok2 = await _eval_token(page, site_key, action, isolated=False)
|
|
175
|
+
out["mint_isolated_false"] = (
|
|
176
|
+
"ok" if (isinstance(tok2, str) and tok2) else "empty"
|
|
177
|
+
)
|
|
178
|
+
except Exception as exc: # noqa: BLE001
|
|
179
|
+
out["mint_isolated_false"] = f"FAIL: {type(exc).__name__}: {str(exc)[:80]}"
|
|
180
|
+
|
|
181
|
+
out["listener_counts"] = dict(req_count)
|
|
182
|
+
await ctx.close()
|
|
183
|
+
except Exception as exc: # noqa: BLE001
|
|
184
|
+
out["error"] = f"{type(exc).__name__}: {exc}"
|
|
185
|
+
out["traceback"] = traceback.format_exc(limit=4)
|
|
186
|
+
return out
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def _install_engine(engine: str) -> None:
|
|
190
|
+
"""Runtime engine swap on the production modules + mint fix for patchright."""
|
|
191
|
+
apw = _engine_async_playwright(engine)
|
|
192
|
+
client_mod.async_playwright = apw
|
|
193
|
+
ui_mod.async_playwright = apw
|
|
194
|
+
if engine == "patchright":
|
|
195
|
+
|
|
196
|
+
async def _patched_mint(self: Any, action: str) -> str:
|
|
197
|
+
site_key = await self.site_key()
|
|
198
|
+
token = await self._page.evaluate( # noqa: SLF001
|
|
199
|
+
_EXECUTE_JS, [site_key, action], isolated_context=False
|
|
200
|
+
)
|
|
201
|
+
if not isinstance(token, str) or not token:
|
|
202
|
+
msg = f"empty token (patched main-world mint) for action={action!r}"
|
|
203
|
+
raise RecaptchaError(msg)
|
|
204
|
+
return token
|
|
205
|
+
|
|
206
|
+
recaptcha_mod.TokenMinter.mint = _patched_mint # type: ignore[method-assign]
|
|
207
|
+
else:
|
|
208
|
+
recaptcha_mod.TokenMinter.mint = _ORIG_MINT # type: ignore[method-assign]
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def _restore_engine() -> None:
|
|
212
|
+
client_mod.async_playwright = _PW_ASYNC
|
|
213
|
+
ui_mod.async_playwright = _PW_ASYNC
|
|
214
|
+
recaptcha_mod.TokenMinter.mint = _ORIG_MINT # type: ignore[method-assign]
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
async def probe_generation(
|
|
218
|
+
engine: str, profile_dir: Path, *, prompt: str, aspect: str, model: str, action: str
|
|
219
|
+
) -> dict[str, Any]:
|
|
220
|
+
"""Phase B — faithful generate_image under *engine*; classify 200 vs 403 (Leg 6)."""
|
|
221
|
+
out: dict[str, Any] = {"engine": engine}
|
|
222
|
+
batch_hits: list[dict[str, Any]] = []
|
|
223
|
+
_install_engine(engine)
|
|
224
|
+
try:
|
|
225
|
+
req = GenerateImageRequest(
|
|
226
|
+
prompt=prompt, aspect=Aspect.from_cli(aspect), model=Model.from_cli(model)
|
|
227
|
+
)
|
|
228
|
+
async with FlowApiClient(profile_dir=profile_dir, headless=False) as client:
|
|
229
|
+
# Attach a response listener to every pooled page (scenario 4 — assert
|
|
230
|
+
# the batchGenerateImages body parses, not just that a callback fires).
|
|
231
|
+
def _on_response(resp: Any) -> None:
|
|
232
|
+
if "batchGenerateImages" in resp.url:
|
|
233
|
+
asyncio.create_task(_record(resp))
|
|
234
|
+
|
|
235
|
+
async def _record(resp: Any) -> None:
|
|
236
|
+
entry: dict[str, Any] = {"status": resp.status}
|
|
237
|
+
try:
|
|
238
|
+
body = await resp.json()
|
|
239
|
+
entry["json_ok"] = True
|
|
240
|
+
entry["has_media"] = bool(
|
|
241
|
+
isinstance(body, dict) and (body.get("media") or body.get("results"))
|
|
242
|
+
)
|
|
243
|
+
except Exception as exc: # noqa: BLE001
|
|
244
|
+
entry["json_ok"] = False
|
|
245
|
+
entry["json_err"] = type(exc).__name__
|
|
246
|
+
batch_hits.append(entry)
|
|
247
|
+
|
|
248
|
+
for p in getattr(client, "_pages", []):
|
|
249
|
+
p.on("response", _on_response)
|
|
250
|
+
|
|
251
|
+
t0 = time.time()
|
|
252
|
+
try:
|
|
253
|
+
img = await client.generate_image(req=req, recaptcha_action=action)
|
|
254
|
+
out["result"] = "OK_200"
|
|
255
|
+
out["media_url_present"] = bool(getattr(img, "url", None) or getattr(img, "name", None))
|
|
256
|
+
except RecaptchaError as exc:
|
|
257
|
+
out["result"] = "MINT_BROKEN"
|
|
258
|
+
out["detail"] = str(exc)[:160]
|
|
259
|
+
except client_mod.WafRejectionError as exc:
|
|
260
|
+
out["result"] = "WAF_403"
|
|
261
|
+
out["detail"] = str(exc)[:160]
|
|
262
|
+
except Exception as exc: # noqa: BLE001
|
|
263
|
+
out["result"] = f"OTHER:{type(exc).__name__}"
|
|
264
|
+
out["detail"] = str(exc)[:160]
|
|
265
|
+
out["elapsed_s"] = round(time.time() - t0, 1)
|
|
266
|
+
await asyncio.sleep(0.5) # let any in-flight _record tasks settle
|
|
267
|
+
out["batch_responses"] = batch_hits
|
|
268
|
+
except Exception as exc: # noqa: BLE001
|
|
269
|
+
out["error"] = f"{type(exc).__name__}: {exc}"
|
|
270
|
+
out["traceback"] = traceback.format_exc(limit=4)
|
|
271
|
+
finally:
|
|
272
|
+
_restore_engine()
|
|
273
|
+
return out
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
async def _run(args: argparse.Namespace, profile_dir: Path, out_path: Path) -> int:
|
|
277
|
+
engines = [e.strip() for e in args.engines.split(",") if e.strip()]
|
|
278
|
+
results: dict[str, Any] = {
|
|
279
|
+
"spike": "patchright-engine-phase0",
|
|
280
|
+
"capturedAt": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
|
|
281
|
+
"profile": args.profile,
|
|
282
|
+
"engines": engines,
|
|
283
|
+
"action": args.action,
|
|
284
|
+
"primitives": [],
|
|
285
|
+
"generation": [],
|
|
286
|
+
}
|
|
287
|
+
for engine in engines:
|
|
288
|
+
step("A", f"[{engine}] primitive probe", prefix="patch")
|
|
289
|
+
results["primitives"].append(await probe_primitives(engine, profile_dir, args.action))
|
|
290
|
+
if not args.skip_generation:
|
|
291
|
+
step("B", f"[{engine}] faithful generation (credit-free image)", prefix="patch")
|
|
292
|
+
results["generation"].append(
|
|
293
|
+
await probe_generation(
|
|
294
|
+
engine,
|
|
295
|
+
profile_dir,
|
|
296
|
+
prompt=args.prompt,
|
|
297
|
+
aspect=args.aspect,
|
|
298
|
+
model=args.model,
|
|
299
|
+
action=args.action,
|
|
300
|
+
)
|
|
301
|
+
)
|
|
302
|
+
|
|
303
|
+
out_path.parent.mkdir(parents=True, exist_ok=True)
|
|
304
|
+
out_path.write_text(json.dumps(results, indent=2, ensure_ascii=False), encoding="utf-8")
|
|
305
|
+
|
|
306
|
+
# Console summary — the at-a-glance gate readout.
|
|
307
|
+
print("\n================ PATCHRIGHT SPIKE SUMMARY ================", flush=True)
|
|
308
|
+
for p in results["primitives"]:
|
|
309
|
+
eng = p["engine"]
|
|
310
|
+
print(
|
|
311
|
+
f"[A {eng:10}] launch={p.get('launch_ok', p.get('launch_error', '?'))} "
|
|
312
|
+
f"channel={p.get('channel')} webdriver={p.get('navigator_webdriver')} "
|
|
313
|
+
f"discover={p.get('discover_site_key')} mint_default={p.get('mint_default')} "
|
|
314
|
+
f"mint_iso_false={p.get('mint_isolated_false', 'n/a')} "
|
|
315
|
+
f"listeners={p.get('listener_counts')}",
|
|
316
|
+
flush=True,
|
|
317
|
+
)
|
|
318
|
+
for g in results["generation"]:
|
|
319
|
+
eng = g["engine"]
|
|
320
|
+
print(
|
|
321
|
+
f"[B {eng:10}] result={g.get('result', g.get('error'))} "
|
|
322
|
+
f"elapsed={g.get('elapsed_s')}s batch={g.get('batch_responses')}",
|
|
323
|
+
flush=True,
|
|
324
|
+
)
|
|
325
|
+
print(f"\n[patch] full results -> {out_path}", flush=True)
|
|
326
|
+
print(
|
|
327
|
+
"[patch] GATE: pass = patchright mint works (mint_iso_false=ok) AND "
|
|
328
|
+
"(on a hot profile) patchright result=OK_200 while playwright result=WAF_403.",
|
|
329
|
+
flush=True,
|
|
330
|
+
)
|
|
331
|
+
return 0
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
def main(argv: list[str] | None = None) -> int:
|
|
335
|
+
p = argparse.ArgumentParser(description="Patchright engine Phase-0 spike (credit-free).")
|
|
336
|
+
p.add_argument("--profile", default="ffroliva", help="gflow profile name (use a HOT one for the decisive leg)")
|
|
337
|
+
p.add_argument("--engines", default="playwright,patchright", help="comma list")
|
|
338
|
+
p.add_argument("--prompt", default="a red ceramic mug on a wooden table, soft studio light")
|
|
339
|
+
p.add_argument("--aspect", default="9:16")
|
|
340
|
+
p.add_argument("--model", default="nano2")
|
|
341
|
+
p.add_argument("--action", default="imageGeneration", help="reCAPTCHA action (memory: action mismatch → silent 403)")
|
|
342
|
+
p.add_argument("--skip-generation", action="store_true", help="primitives only (no credit-free gen)")
|
|
343
|
+
p.add_argument("--out", default=None)
|
|
344
|
+
args = p.parse_args(argv)
|
|
345
|
+
|
|
346
|
+
profile_dir = resolve_profile_dir(args.profile)
|
|
347
|
+
out_path = Path(args.out) if args.out else default_out_path("spike_patchright", ".json")
|
|
348
|
+
step("--", f"profile={args.profile} engines={args.engines} gen={not args.skip_generation}", prefix="patch")
|
|
349
|
+
try:
|
|
350
|
+
return asyncio.run(_run(args, profile_dir, out_path))
|
|
351
|
+
except KeyboardInterrupt:
|
|
352
|
+
print("[patch] aborted", file=sys.stderr)
|
|
353
|
+
return 130
|
|
354
|
+
|
|
355
|
+
|
|
356
|
+
if __name__ == "__main__":
|
|
357
|
+
raise SystemExit(main())
|