fruxon 2.0.1__tar.gz → 2.0.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {fruxon-2.0.1 → fruxon-2.0.2}/HISTORY.md +77 -0
- {fruxon-2.0.1 → fruxon-2.0.2}/PKG-INFO +3 -1
- {fruxon-2.0.1 → fruxon-2.0.2}/README.md +2 -0
- {fruxon-2.0.1 → fruxon-2.0.2}/src/fruxon/_version.py +2 -2
- {fruxon-2.0.1 → fruxon-2.0.2}/src/fruxon/cli/examples.py +12 -0
- {fruxon-2.0.1 → fruxon-2.0.2}/src/fruxon/cli/integrations.py +298 -1
- {fruxon-2.0.1 → fruxon-2.0.2}/src/fruxon/cli/llm_providers.py +354 -0
- {fruxon-2.0.1 → fruxon-2.0.2}/src/fruxon/cli/messages.py +46 -7
- {fruxon-2.0.1 → fruxon-2.0.2}/src/fruxon/fruxon.py +185 -3
- {fruxon-2.0.1 → fruxon-2.0.2}/src/fruxon/models.py +5 -2
- {fruxon-2.0.1 → fruxon-2.0.2}/src/fruxon/skills/fruxon-build-agent/SKILL.md +19 -3
- {fruxon-2.0.1 → fruxon-2.0.2}/src/fruxon/skills/fruxon-create-integration/SKILL.md +22 -11
- {fruxon-2.0.1 → fruxon-2.0.2}/src/fruxon/skills/fruxon-meet/SKILL.md +2 -0
- fruxon-2.0.2/tests/test_integrations_connect_cli.py +338 -0
- fruxon-2.0.2/tests/test_llm_providers_connect_cli.py +314 -0
- {fruxon-2.0.1 → fruxon-2.0.2}/tests/test_messages.py +20 -0
- {fruxon-2.0.1 → fruxon-2.0.2}/.gitignore +0 -0
- {fruxon-2.0.1 → fruxon-2.0.2}/LICENSE +0 -0
- {fruxon-2.0.1 → fruxon-2.0.2}/pyproject.toml +0 -0
- {fruxon-2.0.1 → fruxon-2.0.2}/src/fruxon/__init__.py +0 -0
- {fruxon-2.0.1 → fruxon-2.0.2}/src/fruxon/__main__.py +0 -0
- {fruxon-2.0.1 → fruxon-2.0.2}/src/fruxon/_ssl.py +0 -0
- {fruxon-2.0.1 → fruxon-2.0.2}/src/fruxon/checks/__init__.py +0 -0
- {fruxon-2.0.1 → fruxon-2.0.2}/src/fruxon/checks/agent.py +0 -0
- {fruxon-2.0.1 → fruxon-2.0.2}/src/fruxon/checks/ledger.py +0 -0
- {fruxon-2.0.1 → fruxon-2.0.2}/src/fruxon/checks/references.py +0 -0
- {fruxon-2.0.1 → fruxon-2.0.2}/src/fruxon/cli/__init__.py +0 -0
- {fruxon-2.0.1 → fruxon-2.0.2}/src/fruxon/cli/_crash.py +0 -0
- {fruxon-2.0.1 → fruxon-2.0.2}/src/fruxon/cli/_schema.py +0 -0
- {fruxon-2.0.1 → fruxon-2.0.2}/src/fruxon/cli/_shared.py +0 -0
- {fruxon-2.0.1 → fruxon-2.0.2}/src/fruxon/cli/_stream.py +0 -0
- {fruxon-2.0.1 → fruxon-2.0.2}/src/fruxon/cli/agents.py +0 -0
- {fruxon-2.0.1 → fruxon-2.0.2}/src/fruxon/cli/agents_approvals.py +0 -0
- {fruxon-2.0.1 → fruxon-2.0.2}/src/fruxon/cli/agents_budget.py +0 -0
- {fruxon-2.0.1 → fruxon-2.0.2}/src/fruxon/cli/agents_channels.py +0 -0
- {fruxon-2.0.1 → fruxon-2.0.2}/src/fruxon/cli/agents_draft.py +0 -0
- {fruxon-2.0.1 → fruxon-2.0.2}/src/fruxon/cli/agents_evaluations.py +0 -0
- {fruxon-2.0.1 → fruxon-2.0.2}/src/fruxon/cli/agents_executions.py +0 -0
- {fruxon-2.0.1 → fruxon-2.0.2}/src/fruxon/cli/agents_improvements.py +0 -0
- {fruxon-2.0.1 → fruxon-2.0.2}/src/fruxon/cli/agents_lifecycle.py +0 -0
- {fruxon-2.0.1 → fruxon-2.0.2}/src/fruxon/cli/agents_memory.py +0 -0
- {fruxon-2.0.1 → fruxon-2.0.2}/src/fruxon/cli/agents_reply_style.py +0 -0
- {fruxon-2.0.1 → fruxon-2.0.2}/src/fruxon/cli/agents_revisions.py +0 -0
- {fruxon-2.0.1 → fruxon-2.0.2}/src/fruxon/cli/agents_sandbox.py +0 -0
- {fruxon-2.0.1 → fruxon-2.0.2}/src/fruxon/cli/agents_sandbox_test.py +0 -0
- {fruxon-2.0.1 → fruxon-2.0.2}/src/fruxon/cli/agents_slots.py +0 -0
- {fruxon-2.0.1 → fruxon-2.0.2}/src/fruxon/cli/agents_system_blocks.py +0 -0
- {fruxon-2.0.1 → fruxon-2.0.2}/src/fruxon/cli/agents_tests.py +0 -0
- {fruxon-2.0.1 → fruxon-2.0.2}/src/fruxon/cli/agents_topics.py +0 -0
- {fruxon-2.0.1 → fruxon-2.0.2}/src/fruxon/cli/applications.py +0 -0
- {fruxon-2.0.1 → fruxon-2.0.2}/src/fruxon/cli/applications_entry_points.py +0 -0
- {fruxon-2.0.1 → fruxon-2.0.2}/src/fruxon/cli/assets.py +0 -0
- {fruxon-2.0.1 → fruxon-2.0.2}/src/fruxon/cli/auth.py +0 -0
- {fruxon-2.0.1 → fruxon-2.0.2}/src/fruxon/cli/capabilities.py +0 -0
- {fruxon-2.0.1 → fruxon-2.0.2}/src/fruxon/cli/completion.py +0 -0
- {fruxon-2.0.1 → fruxon-2.0.2}/src/fruxon/cli/config.py +0 -0
- {fruxon-2.0.1 → fruxon-2.0.2}/src/fruxon/cli/consult_pins.py +0 -0
- {fruxon-2.0.1 → fruxon-2.0.2}/src/fruxon/cli/conversation_imports.py +0 -0
- {fruxon-2.0.1 → fruxon-2.0.2}/src/fruxon/cli/dash.py +0 -0
- {fruxon-2.0.1 → fruxon-2.0.2}/src/fruxon/cli/describe.py +0 -0
- {fruxon-2.0.1 → fruxon-2.0.2}/src/fruxon/cli/doctor.py +0 -0
- {fruxon-2.0.1 → fruxon-2.0.2}/src/fruxon/cli/environments.py +0 -0
- {fruxon-2.0.1 → fruxon-2.0.2}/src/fruxon/cli/escalations.py +0 -0
- {fruxon-2.0.1 → fruxon-2.0.2}/src/fruxon/cli/guides.py +0 -0
- {fruxon-2.0.1 → fruxon-2.0.2}/src/fruxon/cli/keys.py +0 -0
- {fruxon-2.0.1 → fruxon-2.0.2}/src/fruxon/cli/knowledge_bases.py +0 -0
- {fruxon-2.0.1 → fruxon-2.0.2}/src/fruxon/cli/knowledge_documents.py +0 -0
- {fruxon-2.0.1 → fruxon-2.0.2}/src/fruxon/cli/knowledge_notices.py +0 -0
- {fruxon-2.0.1 → fruxon-2.0.2}/src/fruxon/cli/knowledge_sources.py +0 -0
- {fruxon-2.0.1 → fruxon-2.0.2}/src/fruxon/cli/metrics.py +0 -0
- {fruxon-2.0.1 → fruxon-2.0.2}/src/fruxon/cli/participants.py +0 -0
- {fruxon-2.0.1 → fruxon-2.0.2}/src/fruxon/cli/pipelines.py +0 -0
- {fruxon-2.0.1 → fruxon-2.0.2}/src/fruxon/cli/secrets.py +0 -0
- {fruxon-2.0.1 → fruxon-2.0.2}/src/fruxon/cli/skills.py +0 -0
- {fruxon-2.0.1 → fruxon-2.0.2}/src/fruxon/cli/storage.py +0 -0
- {fruxon-2.0.1 → fruxon-2.0.2}/src/fruxon/cli/tools.py +0 -0
- {fruxon-2.0.1 → fruxon-2.0.2}/src/fruxon/cli/triggers.py +0 -0
- {fruxon-2.0.1 → fruxon-2.0.2}/src/fruxon/cli/triggers_budget.py +0 -0
- {fruxon-2.0.1 → fruxon-2.0.2}/src/fruxon/cli/triggers_check.py +0 -0
- {fruxon-2.0.1 → fruxon-2.0.2}/src/fruxon/cli/triggers_ledger.py +0 -0
- {fruxon-2.0.1 → fruxon-2.0.2}/src/fruxon/cli/triggers_ledger_stage_tests.py +0 -0
- {fruxon-2.0.1 → fruxon-2.0.2}/src/fruxon/cli/triggers_questions.py +0 -0
- {fruxon-2.0.1 → fruxon-2.0.2}/src/fruxon/cli/triggers_shape.py +0 -0
- {fruxon-2.0.1 → fruxon-2.0.2}/src/fruxon/cli/workspaces.py +0 -0
- {fruxon-2.0.1 → fruxon-2.0.2}/src/fruxon/credentials.py +0 -0
- {fruxon-2.0.1 → fruxon-2.0.2}/src/fruxon/device_auth.py +0 -0
- {fruxon-2.0.1 → fruxon-2.0.2}/src/fruxon/doctor.py +0 -0
- {fruxon-2.0.1 → fruxon-2.0.2}/src/fruxon/exceptions.py +0 -0
- {fruxon-2.0.1 → fruxon-2.0.2}/src/fruxon/flow_validation.py +0 -0
- {fruxon-2.0.1 → fruxon-2.0.2}/src/fruxon/output.py +0 -0
- {fruxon-2.0.1 → fruxon-2.0.2}/src/fruxon/params.py +0 -0
- {fruxon-2.0.1 → fruxon-2.0.2}/src/fruxon/sandbox_scenario.py +0 -0
- {fruxon-2.0.1 → fruxon-2.0.2}/src/fruxon/skills/__init__.py +0 -0
- {fruxon-2.0.1 → fruxon-2.0.2}/src/fruxon/skills/fruxon-agent-mode/SKILL.md +0 -0
- {fruxon-2.0.1 → fruxon-2.0.2}/src/fruxon/skills/fruxon-debug-trace/SKILL.md +0 -0
- {fruxon-2.0.1 → fruxon-2.0.2}/src/fruxon/skills/fruxon-use-integrations/SKILL.md +0 -0
- {fruxon-2.0.1 → fruxon-2.0.2}/src/fruxon/telemetry.py +0 -0
- {fruxon-2.0.1 → fruxon-2.0.2}/src/fruxon/tui/__init__.py +0 -0
- {fruxon-2.0.1 → fruxon-2.0.2}/src/fruxon/tui/app.py +0 -0
- {fruxon-2.0.1 → fruxon-2.0.2}/src/fruxon/tui/demo.py +0 -0
- {fruxon-2.0.1 → fruxon-2.0.2}/src/fruxon/tui/links.py +0 -0
- {fruxon-2.0.1 → fruxon-2.0.2}/src/fruxon/tui/screens/__init__.py +0 -0
- {fruxon-2.0.1 → fruxon-2.0.2}/src/fruxon/tui/screens/agent.py +0 -0
- {fruxon-2.0.1 → fruxon-2.0.2}/src/fruxon/tui/screens/base.py +0 -0
- {fruxon-2.0.1 → fruxon-2.0.2}/src/fruxon/tui/screens/home.py +0 -0
- {fruxon-2.0.1 → fruxon-2.0.2}/src/fruxon/tui/screens/ledger.py +0 -0
- {fruxon-2.0.1 → fruxon-2.0.2}/src/fruxon/tui/screens/loader.py +0 -0
- {fruxon-2.0.1 → fruxon-2.0.2}/src/fruxon/tui/screens/pickers.py +0 -0
- {fruxon-2.0.1 → fruxon-2.0.2}/src/fruxon/tui/screens/trace.py +0 -0
- {fruxon-2.0.1 → fruxon-2.0.2}/src/fruxon/tui/source.py +0 -0
- {fruxon-2.0.1 → fruxon-2.0.2}/src/fruxon/tui/theme.py +0 -0
- {fruxon-2.0.1 → fruxon-2.0.2}/src/fruxon/tui/views.py +0 -0
- {fruxon-2.0.1 → fruxon-2.0.2}/src/fruxon/tui/widgets.py +0 -0
- {fruxon-2.0.1 → fruxon-2.0.2}/src/fruxon/ui.py +0 -0
- {fruxon-2.0.1 → fruxon-2.0.2}/src/fruxon/update_check.py +0 -0
- {fruxon-2.0.1 → fruxon-2.0.2}/src/fruxon/validation.py +0 -0
- {fruxon-2.0.1 → fruxon-2.0.2}/tests/.test_durations.json +0 -0
- {fruxon-2.0.1 → fruxon-2.0.2}/tests/__init__.py +0 -0
- {fruxon-2.0.1 → fruxon-2.0.2}/tests/conftest.py +0 -0
- {fruxon-2.0.1 → fruxon-2.0.2}/tests/test_actor.py +0 -0
- {fruxon-2.0.1 → fruxon-2.0.2}/tests/test_agent_lifecycle.py +0 -0
- {fruxon-2.0.1 → fruxon-2.0.2}/tests/test_agent_revisions_list.py +0 -0
- {fruxon-2.0.1 → fruxon-2.0.2}/tests/test_agent_slots.py +0 -0
- {fruxon-2.0.1 → fruxon-2.0.2}/tests/test_agents_check_cli.py +0 -0
- {fruxon-2.0.1 → fruxon-2.0.2}/tests/test_agents_revisions_cli.py +0 -0
- {fruxon-2.0.1 → fruxon-2.0.2}/tests/test_agents_sandbox.py +0 -0
- {fruxon-2.0.1 → fruxon-2.0.2}/tests/test_applications.py +0 -0
- {fruxon-2.0.1 → fruxon-2.0.2}/tests/test_approvals.py +0 -0
- {fruxon-2.0.1 → fruxon-2.0.2}/tests/test_assets.py +0 -0
- {fruxon-2.0.1 → fruxon-2.0.2}/tests/test_budgets.py +0 -0
- {fruxon-2.0.1 → fruxon-2.0.2}/tests/test_capabilities.py +0 -0
- {fruxon-2.0.1 → fruxon-2.0.2}/tests/test_channels.py +0 -0
- {fruxon-2.0.1 → fruxon-2.0.2}/tests/test_checks_agent.py +0 -0
- {fruxon-2.0.1 → fruxon-2.0.2}/tests/test_checks_ledger.py +0 -0
- {fruxon-2.0.1 → fruxon-2.0.2}/tests/test_cli.py +0 -0
- {fruxon-2.0.1 → fruxon-2.0.2}/tests/test_cli_base_url_routing.py +0 -0
- {fruxon-2.0.1 → fruxon-2.0.2}/tests/test_cli_crash.py +0 -0
- {fruxon-2.0.1 → fruxon-2.0.2}/tests/test_cli_root_flags.py +0 -0
- {fruxon-2.0.1 → fruxon-2.0.2}/tests/test_client.py +0 -0
- {fruxon-2.0.1 → fruxon-2.0.2}/tests/test_connect_nudge.py +0 -0
- {fruxon-2.0.1 → fruxon-2.0.2}/tests/test_consult.py +0 -0
- {fruxon-2.0.1 → fruxon-2.0.2}/tests/test_conversation_imports.py +0 -0
- {fruxon-2.0.1 → fruxon-2.0.2}/tests/test_credentials.py +0 -0
- {fruxon-2.0.1 → fruxon-2.0.2}/tests/test_dash_app.py +0 -0
- {fruxon-2.0.1 → fruxon-2.0.2}/tests/test_dash_cli.py +0 -0
- {fruxon-2.0.1 → fruxon-2.0.2}/tests/test_dash_client.py +0 -0
- {fruxon-2.0.1 → fruxon-2.0.2}/tests/test_dash_links.py +0 -0
- {fruxon-2.0.1 → fruxon-2.0.2}/tests/test_dash_source.py +0 -0
- {fruxon-2.0.1 → fruxon-2.0.2}/tests/test_describe.py +0 -0
- {fruxon-2.0.1 → fruxon-2.0.2}/tests/test_device_auth.py +0 -0
- {fruxon-2.0.1 → fruxon-2.0.2}/tests/test_docs_commands_exist.py +0 -0
- {fruxon-2.0.1 → fruxon-2.0.2}/tests/test_doctor.py +0 -0
- {fruxon-2.0.1 → fruxon-2.0.2}/tests/test_draft_bootstrap.py +0 -0
- {fruxon-2.0.1 → fruxon-2.0.2}/tests/test_draft_evaluate_cli.py +0 -0
- {fruxon-2.0.1 → fruxon-2.0.2}/tests/test_draft_pull_shape.py +0 -0
- {fruxon-2.0.1 → fruxon-2.0.2}/tests/test_draft_run_replay.py +0 -0
- {fruxon-2.0.1 → fruxon-2.0.2}/tests/test_draft_validate_cli.py +0 -0
- {fruxon-2.0.1 → fruxon-2.0.2}/tests/test_drafts.py +0 -0
- {fruxon-2.0.1 → fruxon-2.0.2}/tests/test_entry_points.py +0 -0
- {fruxon-2.0.1 → fruxon-2.0.2}/tests/test_environments.py +0 -0
- {fruxon-2.0.1 → fruxon-2.0.2}/tests/test_escalations.py +0 -0
- {fruxon-2.0.1 → fruxon-2.0.2}/tests/test_evaluations.py +0 -0
- {fruxon-2.0.1 → fruxon-2.0.2}/tests/test_execution_records.py +0 -0
- {fruxon-2.0.1 → fruxon-2.0.2}/tests/test_flow_validation.py +0 -0
- {fruxon-2.0.1 → fruxon-2.0.2}/tests/test_fruxon.py +0 -0
- {fruxon-2.0.1 → fruxon-2.0.2}/tests/test_guides.py +0 -0
- {fruxon-2.0.1 → fruxon-2.0.2}/tests/test_improvements.py +0 -0
- {fruxon-2.0.1 → fruxon-2.0.2}/tests/test_integration_configs_write_cli.py +0 -0
- {fruxon-2.0.1 → fruxon-2.0.2}/tests/test_integration_triggers_cli.py +0 -0
- {fruxon-2.0.1 → fruxon-2.0.2}/tests/test_integrations_authorize_cli.py +0 -0
- {fruxon-2.0.1 → fruxon-2.0.2}/tests/test_kb_trial_cli.py +0 -0
- {fruxon-2.0.1 → fruxon-2.0.2}/tests/test_knowledge_bases.py +0 -0
- {fruxon-2.0.1 → fruxon-2.0.2}/tests/test_knowledge_documents.py +0 -0
- {fruxon-2.0.1 → fruxon-2.0.2}/tests/test_knowledge_notices.py +0 -0
- {fruxon-2.0.1 → fruxon-2.0.2}/tests/test_knowledge_sources.py +0 -0
- {fruxon-2.0.1 → fruxon-2.0.2}/tests/test_llm_provider_configs_cli.py +0 -0
- {fruxon-2.0.1 → fruxon-2.0.2}/tests/test_managed_by.py +0 -0
- {fruxon-2.0.1 → fruxon-2.0.2}/tests/test_memory.py +0 -0
- {fruxon-2.0.1 → fruxon-2.0.2}/tests/test_metrics.py +0 -0
- {fruxon-2.0.1 → fruxon-2.0.2}/tests/test_online_llm_config_refs.py +0 -0
- {fruxon-2.0.1 → fruxon-2.0.2}/tests/test_online_prefetch_refs.py +0 -0
- {fruxon-2.0.1 → fruxon-2.0.2}/tests/test_online_secret_refs.py +0 -0
- {fruxon-2.0.1 → fruxon-2.0.2}/tests/test_output.py +0 -0
- {fruxon-2.0.1 → fruxon-2.0.2}/tests/test_params.py +0 -0
- {fruxon-2.0.1 → fruxon-2.0.2}/tests/test_participants.py +0 -0
- {fruxon-2.0.1 → fruxon-2.0.2}/tests/test_participants_write.py +0 -0
- {fruxon-2.0.1 → fruxon-2.0.2}/tests/test_pipelines.py +0 -0
- {fruxon-2.0.1 → fruxon-2.0.2}/tests/test_reply_style.py +0 -0
- {fruxon-2.0.1 → fruxon-2.0.2}/tests/test_sandbox_scenario.py +0 -0
- {fruxon-2.0.1 → fruxon-2.0.2}/tests/test_schema.py +0 -0
- {fruxon-2.0.1 → fruxon-2.0.2}/tests/test_schema_fetch.py +0 -0
- {fruxon-2.0.1 → fruxon-2.0.2}/tests/test_secrets.py +0 -0
- {fruxon-2.0.1 → fruxon-2.0.2}/tests/test_skills.py +0 -0
- {fruxon-2.0.1 → fruxon-2.0.2}/tests/test_ssl.py +0 -0
- {fruxon-2.0.1 → fruxon-2.0.2}/tests/test_storage.py +0 -0
- {fruxon-2.0.1 → fruxon-2.0.2}/tests/test_system_blocks.py +0 -0
- {fruxon-2.0.1 → fruxon-2.0.2}/tests/test_system_blocks_cli.py +0 -0
- {fruxon-2.0.1 → fruxon-2.0.2}/tests/test_telemetry.py +0 -0
- {fruxon-2.0.1 → fruxon-2.0.2}/tests/test_test_chats.py +0 -0
- {fruxon-2.0.1 → fruxon-2.0.2}/tests/test_topics.py +0 -0
- {fruxon-2.0.1 → fruxon-2.0.2}/tests/test_trigger_budgets.py +0 -0
- {fruxon-2.0.1 → fruxon-2.0.2}/tests/test_triggers.py +0 -0
- {fruxon-2.0.1 → fruxon-2.0.2}/tests/test_triggers_check_cli.py +0 -0
- {fruxon-2.0.1 → fruxon-2.0.2}/tests/test_triggers_ledger.py +0 -0
- {fruxon-2.0.1 → fruxon-2.0.2}/tests/test_triggers_ledger_batch_views.py +0 -0
- {fruxon-2.0.1 → fruxon-2.0.2}/tests/test_triggers_ledger_stage_tests.py +0 -0
- {fruxon-2.0.1 → fruxon-2.0.2}/tests/test_triggers_questions.py +0 -0
- {fruxon-2.0.1 → fruxon-2.0.2}/tests/test_triggers_shape.py +0 -0
- {fruxon-2.0.1 → fruxon-2.0.2}/tests/test_triggers_write.py +0 -0
- {fruxon-2.0.1 → fruxon-2.0.2}/tests/test_tui_boundary.py +0 -0
- {fruxon-2.0.1 → fruxon-2.0.2}/tests/test_ui.py +0 -0
- {fruxon-2.0.1 → fruxon-2.0.2}/tests/test_update_check.py +0 -0
- {fruxon-2.0.1 → fruxon-2.0.2}/tests/test_validation.py +0 -0
- {fruxon-2.0.1 → fruxon-2.0.2}/tests/test_workspaces_cli.py +0 -0
|
@@ -1,5 +1,82 @@
|
|
|
1
1
|
# History
|
|
2
2
|
|
|
3
|
+
## Unreleased — `fruxon messages list --reason EMAIL_THREAD_UNAVAILABLE`
|
|
4
|
+
|
|
5
|
+
- `--reason` takes the server's new `EMAIL_THREAD_UNAVAILABLE` (fruxon-backend
|
|
6
|
+
#1494): an email thread's pinned conversation could not take the message (its
|
|
7
|
+
topic is missing, abandoned or mismatched, or its session belongs to another
|
|
8
|
+
thread), so it was withheld, the Gmail thread labelled `Fruxon/Needs human`,
|
|
9
|
+
and it is never replayed. It was refused as a typo before reaching the server.
|
|
10
|
+
A server without it answers 400 for it.
|
|
11
|
+
|
|
12
|
+
## Unreleased — a link to fill in an LLM provider's API key
|
|
13
|
+
|
|
14
|
+
- **`fruxon llm-providers connect <provider>`** creates a config for an LLM
|
|
15
|
+
provider and prints a credential link. A human opens it, signs in to Fruxon
|
|
16
|
+
and types the API key (Bedrock's access key pair, Vertex's service account
|
|
17
|
+
key); the page tests it with the provider and publishes the config, which an
|
|
18
|
+
agent pins as `provider.tenantConfigId`. So an agent building an agent through
|
|
19
|
+
the CLI hands the user a link instead of asking for the key, and never sees
|
|
20
|
+
it. `--config <id>` fills an existing config instead; `--name` and
|
|
21
|
+
`--param name=value` (a non-secret setting such as `region`; required ones
|
|
22
|
+
are checked, and a secret is refused, before anything is created) shape the
|
|
23
|
+
new one. A key the provider turns down saves nothing and five lock the link;
|
|
24
|
+
one it could not check is saved with a warning. The link opens only for a
|
|
25
|
+
workspace member whose role may set up model providers (`llm_providers:write`,
|
|
26
|
+
which the Developer preset carries); `--no-sign-in` mints one whoever holds it
|
|
27
|
+
can fill (Admin or Owner). `--for`, `--note` and `--expires-in-hours` go on
|
|
28
|
+
the link. A config the command created is deleted again when no link could be
|
|
29
|
+
minted for it. A provider sharing another's credential is pointed at its
|
|
30
|
+
owner.
|
|
31
|
+
- **`fruxon llm-providers link-status <provider> <config> <request>`** says
|
|
32
|
+
where that link stands — `OPEN`, `FILLED`, `REFUSED`, `EXPIRED`, `LOCKED` or
|
|
33
|
+
`WITHDRAWN` — and `--wait` waits until it is filled or stops working. Exits 0
|
|
34
|
+
for open or filled, 13 once it stopped working, 14 when `--wait` runs out.
|
|
35
|
+
- Client methods `create_llm_provider_config`, `delete_llm_provider_config`,
|
|
36
|
+
`create_llm_credential_link` and `get_llm_credential_link`.
|
|
37
|
+
|
|
38
|
+
## Unreleased — a link to fill in an integration's credentials
|
|
39
|
+
|
|
40
|
+
- **`fruxon integrations connect <integration>`** creates a config for an
|
|
41
|
+
integration that takes an API key, a bearer token or a username and password,
|
|
42
|
+
and prints a credential link. A human opens it, signs in to Fruxon and types
|
|
43
|
+
the credential; the page tests it against the service and publishes the
|
|
44
|
+
config. So an agent building an agent through the CLI hands the user a link
|
|
45
|
+
instead of asking for the secret, and never sees it. `--config <id>` fills an
|
|
46
|
+
existing config instead; `--name`, `--auth-method` (when the integration
|
|
47
|
+
declares several methods a link can fill) and `--param name=value` (a
|
|
48
|
+
connection setting such as a subdomain; required ones are checked before
|
|
49
|
+
anything is created) shape the new one. The link opens only for a workspace
|
|
50
|
+
member whose role may set up integrations; `--no-sign-in` mints one whoever
|
|
51
|
+
holds it can fill. `--for`, `--note` and `--expires-in-hours` (1–720, default
|
|
52
|
+
168) go on the link. A config the command created is deleted again when no
|
|
53
|
+
link could be minted for it. OAuth integrations are pointed at
|
|
54
|
+
`integrations authorize`. Needs `integrations:write`, which the default
|
|
55
|
+
login preset carries (fruxon-backend #1487); a `--no-sign-in` link also needs
|
|
56
|
+
`workspace:write` (Admin or Owner).
|
|
57
|
+
- Client methods `create_integration_config`, `delete_integration_config` and
|
|
58
|
+
`create_credential_link`.
|
|
59
|
+
|
|
60
|
+
## Unreleased — `fruxon messages list --reason` takes every withhold reason
|
|
61
|
+
|
|
62
|
+
- `--reason` accepted only `HUMAN_OWNED_CONVERSATION` and `ADMISSION_REFUSED`, so
|
|
63
|
+
`--reason OVER_BUDGET` and the other email-channel reasons were refused as typos
|
|
64
|
+
before reaching the server. It now takes every server `InboundWithholdReason`:
|
|
65
|
+
`NOT_ADDRESSED`, the email channel's `AUTO_REPLY`, `BULK_OR_LIST`, `BOUNCE`,
|
|
66
|
+
`OWN_ADDRESS`, `NOT_FOR_RECIPIENT`, `UNVERIFIED_SENDER`, `OVER_BUDGET`,
|
|
67
|
+
`THREAD_HAS_ANOTHER_SENDER`, `DELIVERY_DEFERRED_TOO_LONG`, and the new
|
|
68
|
+
`DRAFT_EDITED` (a customer wrote while a person was editing the agent's Gmail
|
|
69
|
+
draft; fruxon-backend #1488). A server without `DRAFT_EDITED` answers 400 for it.
|
|
70
|
+
|
|
71
|
+
## Unreleased — a quality check that could not run is no failed reply
|
|
72
|
+
|
|
73
|
+
- `AgentJudgeActivity.pass_rate` is `passed / (passed + failed)`, as the server
|
|
74
|
+
now computes it (fruxon-backend #1482). A check that could not run (a model
|
|
75
|
+
error or a timeout) gave no verdict: it is counted in `errors` only, where it
|
|
76
|
+
used to count against the rate. A log-only agent whose checks errored gets
|
|
77
|
+
`None`, not 0, so the `fruxon dash` Quality check tile shows its average score.
|
|
78
|
+
The docstring said "errors included"; it now says what the field holds.
|
|
79
|
+
|
|
3
80
|
## Unreleased — `o` in `fruxon dash` opens the same page in the web app
|
|
4
81
|
|
|
5
82
|
- `o` opens the web app on the exact tab or record behind what the console
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: fruxon
|
|
3
|
-
Version: 2.0.
|
|
3
|
+
Version: 2.0.2
|
|
4
4
|
Summary: The Fruxon SDK is a lightweight Python client for integrating with the Fruxon platform.
|
|
5
5
|
Project-URL: bugs, https://github.com/fruxon-ai/fruxon-sdk/issues
|
|
6
6
|
Project-URL: changelog, https://github.com/fruxon-ai/fruxon-sdk/blob/main/HISTORY.md
|
|
@@ -143,11 +143,13 @@ Integrations + tools:
|
|
|
143
143
|
| `fruxon integrations list/get/create/update/verify/open` | Manage external integrations — the connections agents draw tools from. `create`/`update`/`verify` take a `--file` JSON body; `open` opens the dashboard. |
|
|
144
144
|
| `fruxon integrations configs list/get/update/publish` | The per-integration auth/config records. `update --file` patches a config's draft (only the fields sent; `{"egressCountry": ""}` clears an egress pin) and prints it with secrets masked; `publish` makes the draft the revision tool calls resolve, asks first (`--yes` in agent mode), and prints the new revision's `id` and `version`, never the credentials. |
|
|
145
145
|
| `fruxon integrations authorize <integration>` | Mint an application-level OAuth authorization URL to **connect** an integration — auto-detects its OAuth2 method (`--auth-method` to pick, `--scope`/`--config-param` to tune). OAuth needs a browser consent step, so the CLI hands you the link; a human clicks it, and the connection is saved as a tenant config a slot can pin. |
|
|
146
|
+
| `fruxon integrations connect <integration>` | Create a config for an API-key, token or password integration (Stripe, most REST APIs) and print a **credential link**: a human opens it, signs in to Fruxon, types the credential, and the page tests it and publishes the config. The secret never passes through the terminal or an agent driving the CLI. `--config <id>` fills an existing config, `--param name=value` sets a connection setting, `--no-sign-in` mints a link anyone holding it can fill (for a client without a Fruxon login), `--expires-in-hours` 1–720. Needs `integrations:write` (the default login preset); `--no-sign-in` also needs `workspace:write` (Admin or Owner). |
|
|
146
147
|
| `fruxon integrations triggers <integration>` | List the event types an integration can fire an agent on — each descriptor's `id` is the `eventType` a trigger listens for, plus the `payloadFields` (dotted paths) a binding's `parameterMappings` can read. Discovery for wiring an inbound-event trigger without guessing. |
|
|
147
148
|
| `fruxon integrations mcp …` | Inspect and enable the MCP server for an integration. |
|
|
148
149
|
| `fruxon tools list/get/create/update/delete/run` | Manage the tools inside an integration. Integration-scoped. `get` and `run` select the tool with `--tool`; `run` executes it outside any agent — real credentials, real result — with `-p key=value`, and `-c` naming the config that supplies credentials. The tenant list API hides internal `knowledge_base` / `assets` tools; `tools list` recovers them by ID and lists them like any other (`--no-include-internal` for the raw server answer). |
|
|
149
150
|
| `fruxon keys list/mint/revoke/delete/history/scopes` | Audit and revoke scoped tokens. Minting opens the dashboard so the secret never enters the CLI process. |
|
|
150
151
|
| `fruxon llm-providers list/get/models` | Browse LLM providers and models supported at the tenant level. |
|
|
152
|
+
| `fruxon llm-providers connect <provider>` / `link-status` | Create a config for an LLM provider and print a **credential link**: a human opens it, signs in to Fruxon, types the API key (or access keys, or service account key), and the page tests it with the provider and publishes the config an agent pins as `provider.tenantConfigId`. The key never passes through the terminal or an agent driving the CLI. `--config <id>` fills an existing config, `--param name=value` sets a non-secret setting such as Bedrock's `region`, `--no-sign-in` mints a link anyone holding it can fill (Admin or Owner). `link-status <provider> <config> <request> [--wait]` says when it was filled. |
|
|
151
153
|
| `fruxon llm-providers configs list/get/test/publish` | The workspace's saved LLM credentials — the `provider.tenantConfigId` an agent pins. `get` shows the draft (secrets masked), the published revision and last known health. `test` probes the draft on every provider surface that shares the credential (or just one, given that surface's provider id), one line per failing surface, exit 12 on failure (`llm_providers:test`). `publish` ships the draft as a new revision that every agent on the config switches to — it confirms, needs `--yes` in agent mode, and does not wait on a passing test (`llm_providers:write`). |
|
|
152
154
|
| `fruxon assets create/list/get/wait/operations/delete` | Manage knowledge-base (RAG) assets an agent can query — upload local files, wait for async ingestion, inspect operations, and use the ids for `assetConfig.assetIds`. `delete` confirms + refuses without `--yes` in agent mode. |
|
|
153
155
|
| `fruxon knowledge-bases list/get/create/update/publish/archive/delete` | The authored, reviewable corpus behind a `manual_knowledge` asset — **not** the asset itself, which is what gets searched (`get` prints the asset id as `backingAssetId`). **`publish` is the step that makes any of it findable**: only `PUBLISHED` documents travel in a snapshot, and only publishing the *base* moves one, so a base full of finished articles answers nothing until it runs. It returns on *acceptance*, not searchability — RAG ingests afterwards, so a pending publish state is normal. `list` hides archived bases, which keep answering for agents already attached to them; `delete` is refused while an agent revision still attaches one, and `archive` is the reversible path. |
|
|
@@ -108,11 +108,13 @@ Integrations + tools:
|
|
|
108
108
|
| `fruxon integrations list/get/create/update/verify/open` | Manage external integrations — the connections agents draw tools from. `create`/`update`/`verify` take a `--file` JSON body; `open` opens the dashboard. |
|
|
109
109
|
| `fruxon integrations configs list/get/update/publish` | The per-integration auth/config records. `update --file` patches a config's draft (only the fields sent; `{"egressCountry": ""}` clears an egress pin) and prints it with secrets masked; `publish` makes the draft the revision tool calls resolve, asks first (`--yes` in agent mode), and prints the new revision's `id` and `version`, never the credentials. |
|
|
110
110
|
| `fruxon integrations authorize <integration>` | Mint an application-level OAuth authorization URL to **connect** an integration — auto-detects its OAuth2 method (`--auth-method` to pick, `--scope`/`--config-param` to tune). OAuth needs a browser consent step, so the CLI hands you the link; a human clicks it, and the connection is saved as a tenant config a slot can pin. |
|
|
111
|
+
| `fruxon integrations connect <integration>` | Create a config for an API-key, token or password integration (Stripe, most REST APIs) and print a **credential link**: a human opens it, signs in to Fruxon, types the credential, and the page tests it and publishes the config. The secret never passes through the terminal or an agent driving the CLI. `--config <id>` fills an existing config, `--param name=value` sets a connection setting, `--no-sign-in` mints a link anyone holding it can fill (for a client without a Fruxon login), `--expires-in-hours` 1–720. Needs `integrations:write` (the default login preset); `--no-sign-in` also needs `workspace:write` (Admin or Owner). |
|
|
111
112
|
| `fruxon integrations triggers <integration>` | List the event types an integration can fire an agent on — each descriptor's `id` is the `eventType` a trigger listens for, plus the `payloadFields` (dotted paths) a binding's `parameterMappings` can read. Discovery for wiring an inbound-event trigger without guessing. |
|
|
112
113
|
| `fruxon integrations mcp …` | Inspect and enable the MCP server for an integration. |
|
|
113
114
|
| `fruxon tools list/get/create/update/delete/run` | Manage the tools inside an integration. Integration-scoped. `get` and `run` select the tool with `--tool`; `run` executes it outside any agent — real credentials, real result — with `-p key=value`, and `-c` naming the config that supplies credentials. The tenant list API hides internal `knowledge_base` / `assets` tools; `tools list` recovers them by ID and lists them like any other (`--no-include-internal` for the raw server answer). |
|
|
114
115
|
| `fruxon keys list/mint/revoke/delete/history/scopes` | Audit and revoke scoped tokens. Minting opens the dashboard so the secret never enters the CLI process. |
|
|
115
116
|
| `fruxon llm-providers list/get/models` | Browse LLM providers and models supported at the tenant level. |
|
|
117
|
+
| `fruxon llm-providers connect <provider>` / `link-status` | Create a config for an LLM provider and print a **credential link**: a human opens it, signs in to Fruxon, types the API key (or access keys, or service account key), and the page tests it with the provider and publishes the config an agent pins as `provider.tenantConfigId`. The key never passes through the terminal or an agent driving the CLI. `--config <id>` fills an existing config, `--param name=value` sets a non-secret setting such as Bedrock's `region`, `--no-sign-in` mints a link anyone holding it can fill (Admin or Owner). `link-status <provider> <config> <request> [--wait]` says when it was filled. |
|
|
116
118
|
| `fruxon llm-providers configs list/get/test/publish` | The workspace's saved LLM credentials — the `provider.tenantConfigId` an agent pins. `get` shows the draft (secrets masked), the published revision and last known health. `test` probes the draft on every provider surface that shares the credential (or just one, given that surface's provider id), one line per failing surface, exit 12 on failure (`llm_providers:test`). `publish` ships the draft as a new revision that every agent on the config switches to — it confirms, needs `--yes` in agent mode, and does not wait on a passing test (`llm_providers:write`). |
|
|
117
119
|
| `fruxon assets create/list/get/wait/operations/delete` | Manage knowledge-base (RAG) assets an agent can query — upload local files, wait for async ingestion, inspect operations, and use the ids for `assetConfig.assetIds`. `delete` confirms + refuses without `--yes` in agent mode. |
|
|
118
120
|
| `fruxon knowledge-bases list/get/create/update/publish/archive/delete` | The authored, reviewable corpus behind a `manual_knowledge` asset — **not** the asset itself, which is what gets searched (`get` prints the asset id as `backingAssetId`). **`publish` is the step that makes any of it findable**: only `PUBLISHED` documents travel in a snapshot, and only publishing the *base* moves one, so a base full of finished articles answers nothing until it runs. It returns on *acceptance*, not searchability — RAG ingests afterwards, so a pending publish state is normal. `list` hides archived bases, which keep answering for agents already attached to them; `delete` is refused while an agent revision still attaches one, and `archive` is the reversible path. |
|
|
@@ -18,7 +18,7 @@ version_tuple: tuple[int | str, ...]
|
|
|
18
18
|
commit_id: str | None
|
|
19
19
|
__commit_id__: str | None
|
|
20
20
|
|
|
21
|
-
__version__ = version = '2.0.
|
|
22
|
-
__version_tuple__ = version_tuple = (2, 0,
|
|
21
|
+
__version__ = version = '2.0.2'
|
|
22
|
+
__version_tuple__ = version_tuple = (2, 0, 2)
|
|
23
23
|
|
|
24
24
|
__commit_id__ = commit_id = None
|
|
@@ -696,6 +696,10 @@ _EXAMPLES: dict[str, list[tuple[str, str]]] = {
|
|
|
696
696
|
"fruxon integrations authorize google_sheets",
|
|
697
697
|
"Mint an OAuth connect link to hand a human (auto-detects the method)",
|
|
698
698
|
),
|
|
699
|
+
(
|
|
700
|
+
"fruxon integrations connect stripe",
|
|
701
|
+
"Create a config and mint a link a human fills its API key in; the secret never reaches the CLI",
|
|
702
|
+
),
|
|
699
703
|
("fruxon integrations triggers gmail", "Discover the event types + payload paths a trigger can wire to"),
|
|
700
704
|
("fruxon integrations create -f config.json", "Wire up a new external service"),
|
|
701
705
|
("fruxon integrations open my-integration", "Open the dashboard config page"),
|
|
@@ -731,6 +735,14 @@ _EXAMPLES: dict[str, list[tuple[str, str]]] = {
|
|
|
731
735
|
("fruxon llm-providers configs test vertex_ai 4950…", "Probe every surface sharing the credential"),
|
|
732
736
|
("fruxon llm-providers configs test vertex_ai_maas 4950…", "…or just that one surface"),
|
|
733
737
|
("fruxon llm-providers configs publish vertex_ai 4950…", "Ship the draft; every agent on it switches"),
|
|
738
|
+
(
|
|
739
|
+
"fruxon llm-providers connect openai",
|
|
740
|
+
"Create a config and mint a link a human fills its API key in; the key never reaches the CLI",
|
|
741
|
+
),
|
|
742
|
+
(
|
|
743
|
+
"fruxon llm-providers link-status openai <config-id> <request-id> --wait",
|
|
744
|
+
"Wait until the human filled the link (or it stopped working)",
|
|
745
|
+
),
|
|
734
746
|
],
|
|
735
747
|
"keys": [
|
|
736
748
|
("fruxon keys list", "Audit existing API keys"),
|
|
@@ -26,7 +26,7 @@ import typer
|
|
|
26
26
|
from rich.markup import escape
|
|
27
27
|
|
|
28
28
|
from fruxon.cli import app
|
|
29
|
-
from fruxon.cli._shared import build_client, confirm_or_die, load_json_body, print_connect_nudge
|
|
29
|
+
from fruxon.cli._shared import build_client, confirm_or_die, format_ts, load_json_body, print_connect_nudge
|
|
30
30
|
from fruxon.exceptions import FruxonError
|
|
31
31
|
from fruxon.fruxon import Integration
|
|
32
32
|
from fruxon.ui import (
|
|
@@ -676,6 +676,303 @@ def integrations_authorize(
|
|
|
676
676
|
)
|
|
677
677
|
|
|
678
678
|
|
|
679
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
680
|
+
# connect — a credential link for an API key, token or password
|
|
681
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
682
|
+
|
|
683
|
+
# What a credential link can take: the page renders a form for these, never an OAuth consent.
|
|
684
|
+
_LINKABLE_CREDENTIAL_TYPES = {"API_KEY", "BEARER_TOKEN", "BASIC_AUTH"}
|
|
685
|
+
|
|
686
|
+
_MAX_LINK_HOURS = 720
|
|
687
|
+
|
|
688
|
+
|
|
689
|
+
@integrations_app.command("connect")
|
|
690
|
+
def integrations_connect(
|
|
691
|
+
integration: Annotated[str, typer.Argument(help="Integration to connect (e.g. stripe).")],
|
|
692
|
+
config_id: Annotated[
|
|
693
|
+
str | None,
|
|
694
|
+
typer.Option("--config", help="Fill this existing config's credentials instead of creating a config."),
|
|
695
|
+
] = None,
|
|
696
|
+
name: Annotated[
|
|
697
|
+
str | None,
|
|
698
|
+
typer.Option("--name", help="Display name of the new config. Defaults to the integration's name."),
|
|
699
|
+
] = None,
|
|
700
|
+
auth_method: Annotated[
|
|
701
|
+
str | None,
|
|
702
|
+
typer.Option(
|
|
703
|
+
"--auth-method",
|
|
704
|
+
help="The auth-metadata id the new config uses. Omit when the integration declares one it can link.",
|
|
705
|
+
),
|
|
706
|
+
] = None,
|
|
707
|
+
param: Annotated[
|
|
708
|
+
list[str] | None,
|
|
709
|
+
typer.Option(
|
|
710
|
+
"--param",
|
|
711
|
+
help="key=value non-secret connection setting for the new config (repeatable, e.g. subdomain=acme).",
|
|
712
|
+
),
|
|
713
|
+
] = None,
|
|
714
|
+
sign_in: Annotated[
|
|
715
|
+
bool,
|
|
716
|
+
typer.Option(
|
|
717
|
+
"--sign-in/--no-sign-in",
|
|
718
|
+
help=(
|
|
719
|
+
"Open only for a signed-in workspace member who may set up integrations (default). "
|
|
720
|
+
"--no-sign-in: whoever holds the link can fill it."
|
|
721
|
+
),
|
|
722
|
+
),
|
|
723
|
+
] = True,
|
|
724
|
+
for_email: Annotated[
|
|
725
|
+
str | None,
|
|
726
|
+
typer.Option("--for", help="Who the link is for, shown in the config's open-links list. Never checked."),
|
|
727
|
+
] = None,
|
|
728
|
+
note: Annotated[str | None, typer.Option("--note", help="A note shown above the form.")] = None,
|
|
729
|
+
expires_in_hours: Annotated[
|
|
730
|
+
int | None,
|
|
731
|
+
typer.Option(
|
|
732
|
+
"--expires-in-hours",
|
|
733
|
+
min=1,
|
|
734
|
+
max=_MAX_LINK_HOURS,
|
|
735
|
+
help=f"How long the link works: 1 to {_MAX_LINK_HOURS} hours. Default 168 (7 days).",
|
|
736
|
+
),
|
|
737
|
+
] = None,
|
|
738
|
+
workspace: Annotated[str | None, typer.Option("--workspace", help="Workspace identifier override.")] = None,
|
|
739
|
+
base_url: Annotated[str | None, typer.Option("--base-url", help="API base URL override.")] = None,
|
|
740
|
+
output: Annotated[
|
|
741
|
+
str | None,
|
|
742
|
+
typer.Option("--output", "-o", help="Output format: text (default for humans), json (default in agent mode)."),
|
|
743
|
+
] = None,
|
|
744
|
+
):
|
|
745
|
+
"""Mint a link a human opens to fill in an integration's credentials.
|
|
746
|
+
|
|
747
|
+
For an API key, a bearer token or a username and password. Creates a
|
|
748
|
+
config for the integration (or takes [bold]--config <id>[/bold]) and prints
|
|
749
|
+
a link to a Fruxon page. Whoever opens it types the credential there; the
|
|
750
|
+
page tests it against the service and publishes the config, which a slot's
|
|
751
|
+
`tenantConfigId` can then pin. The values never pass through this
|
|
752
|
+
terminal, nor through an AI agent driving the CLI: hand the link to the
|
|
753
|
+
human, never ask them for the secret.
|
|
754
|
+
|
|
755
|
+
The link opens only for a member of this workspace signed in to Fruxon
|
|
756
|
+
whose role may set up integrations, so holding it proves nothing and it is
|
|
757
|
+
safe to show in a chat. [bold]--no-sign-in[/bold] mints one that whoever
|
|
758
|
+
holds it can fill, for a client or partner without a Fruxon login: send
|
|
759
|
+
that one privately. A new link for a config replaces the one sent before.
|
|
760
|
+
|
|
761
|
+
OAuth integrations connect through [bold]fruxon integrations authorize[/bold]
|
|
762
|
+
instead. Needs the `integrations:write` scope (the default login preset
|
|
763
|
+
has it); a [bold]--no-sign-in[/bold] link also needs `workspace:write`, which
|
|
764
|
+
only Admin and Owner hold.
|
|
765
|
+
|
|
766
|
+
Examples:
|
|
767
|
+
fruxon integrations connect stripe
|
|
768
|
+
fruxon integrations connect zendesk --param subdomain=acme
|
|
769
|
+
fruxon integrations connect stripe --config <config-id>
|
|
770
|
+
fruxon integrations connect stripe --no-sign-in --for cfo@client.com --expires-in-hours 48
|
|
771
|
+
"""
|
|
772
|
+
output = _resolve_write_output(output)
|
|
773
|
+
parameters = _parse_key_values(param, flag="--param", example="subdomain=acme")
|
|
774
|
+
if config_id is not None and (name is not None or auth_method is not None or parameters):
|
|
775
|
+
fail(
|
|
776
|
+
"--name, --auth-method and --param set up a new config; --config fills one that exists.",
|
|
777
|
+
hint="Drop [bold]--config[/bold] to create a config, or drop the others to fill the existing one.",
|
|
778
|
+
code=EXIT_VALIDATION,
|
|
779
|
+
)
|
|
780
|
+
|
|
781
|
+
client = build_client(workspace, base_url)
|
|
782
|
+
created = config_id is None
|
|
783
|
+
if config_id is None:
|
|
784
|
+
config_id = _create_linkable_config(client, integration, name, auth_method, parameters)
|
|
785
|
+
|
|
786
|
+
try:
|
|
787
|
+
link = client.create_credential_link(
|
|
788
|
+
integration,
|
|
789
|
+
config_id,
|
|
790
|
+
require_sign_in=sign_in,
|
|
791
|
+
intended_for_email=for_email,
|
|
792
|
+
note=note,
|
|
793
|
+
ttl_hours=expires_in_hours,
|
|
794
|
+
)
|
|
795
|
+
except FruxonError as e:
|
|
796
|
+
leftover = _delete_new_config(client, integration, config_id) if created else None
|
|
797
|
+
fail_from_api_error(e, hint=_hint_for_link_error(e, integration, leftover))
|
|
798
|
+
|
|
799
|
+
url = link.get("url") or ""
|
|
800
|
+
if output == "json":
|
|
801
|
+
record = {
|
|
802
|
+
"integration": integration,
|
|
803
|
+
"configId": config_id,
|
|
804
|
+
"createdConfig": created,
|
|
805
|
+
"requestId": link.get("id"),
|
|
806
|
+
"url": url,
|
|
807
|
+
"mode": link.get("mode"),
|
|
808
|
+
"expiresAt": link.get("expiresAt"),
|
|
809
|
+
}
|
|
810
|
+
print(json.dumps(record, indent=2))
|
|
811
|
+
return
|
|
812
|
+
|
|
813
|
+
if stderr.is_terminal:
|
|
814
|
+
print_banner(subtitle=f"connect · {integration}")
|
|
815
|
+
stderr.print()
|
|
816
|
+
what = f"a new config [dim]{config_id}[/dim]" if created else f"config [dim]{config_id}[/dim]"
|
|
817
|
+
say_ok(f"Open this link to fill in the [bold]{escape(integration)}[/bold] credentials for {what}:")
|
|
818
|
+
stderr.print()
|
|
819
|
+
print(url)
|
|
820
|
+
stderr.print()
|
|
821
|
+
who = (
|
|
822
|
+
"Opens for a workspace member signed in to Fruxon who may set up integrations."
|
|
823
|
+
if str(link.get("mode") or "").upper() == "SIGN_IN"
|
|
824
|
+
else "Whoever holds it can fill it in: send it privately."
|
|
825
|
+
)
|
|
826
|
+
stderr.print(f" [dim]{who} Works until {format_ts(link.get('expiresAt'))} UTC.[/dim]")
|
|
827
|
+
stderr.print(
|
|
828
|
+
" [dim]Once filled, the config is published: pin it with a slot's tenantConfigId "
|
|
829
|
+
f"(fruxon integrations configs get {integration} {config_id}).[/dim]"
|
|
830
|
+
)
|
|
831
|
+
|
|
832
|
+
|
|
833
|
+
def _create_linkable_config(
|
|
834
|
+
client, integration: str, name: str | None, auth_method: str | None, parameters: dict[str, str]
|
|
835
|
+
) -> str:
|
|
836
|
+
"""Create the unpublished config a credential link fills; return its id. Exits on anything it can't link."""
|
|
837
|
+
try:
|
|
838
|
+
meta = client.get_integration(integration)
|
|
839
|
+
except FruxonError as e:
|
|
840
|
+
fail_from_api_error(e, hint=_hint_for_error(e))
|
|
841
|
+
|
|
842
|
+
config_metadata = meta.config_metadata if isinstance(meta.config_metadata, dict) else {}
|
|
843
|
+
methods = [m for m in config_metadata.get("authMetadata") or [] if isinstance(m, dict)]
|
|
844
|
+
method = _linkable_method(integration, methods, auth_method)
|
|
845
|
+
_check_connection_parameters(integration, config_metadata.get("parametersMetadata"), parameters)
|
|
846
|
+
|
|
847
|
+
body = {
|
|
848
|
+
"displayName": name or meta.display_name or integration,
|
|
849
|
+
"parameters": parameters,
|
|
850
|
+
"auth": {"integrationAuthMetadataId": method["id"], "level": "APPLICATION", "parameters": {}},
|
|
851
|
+
}
|
|
852
|
+
try:
|
|
853
|
+
config = client.create_integration_config(integration, body)
|
|
854
|
+
except FruxonError as e:
|
|
855
|
+
fail_from_api_error(e, hint=_hint_for_config_error(e, integration))
|
|
856
|
+
return str(config.get("id"))
|
|
857
|
+
|
|
858
|
+
|
|
859
|
+
def _linkable_method(integration: str, methods: list[dict], auth_method: str | None) -> dict:
|
|
860
|
+
"""The application-level API key / token / password method the new config uses, or exit saying why not."""
|
|
861
|
+
|
|
862
|
+
def credential_type(m: dict) -> str:
|
|
863
|
+
# A custom method names the standard shape its credential is entered in, as the backend's
|
|
864
|
+
# EffectiveCredentialType does (e.g. an email + password it exchanges for a session token).
|
|
865
|
+
return str(m.get("userCredentialAuthType") or m.get("type") or "").upper()
|
|
866
|
+
|
|
867
|
+
def application_level(m: dict) -> bool:
|
|
868
|
+
return "APPLICATION" in {str(level).upper() for level in (m.get("supportedLevels") or [])}
|
|
869
|
+
|
|
870
|
+
def oauth_hint() -> str | None:
|
|
871
|
+
if any(credential_type(m) == "O_AUTH2" and application_level(m) for m in methods):
|
|
872
|
+
return f"It signs in through OAuth: run [bold]fruxon integrations authorize {integration}[/bold]."
|
|
873
|
+
return None
|
|
874
|
+
|
|
875
|
+
if auth_method is not None:
|
|
876
|
+
chosen = next((m for m in methods if str(m.get("id")) == auth_method), None)
|
|
877
|
+
if chosen is None:
|
|
878
|
+
declared = ", ".join(str(m.get("id")) for m in methods) or "(none)"
|
|
879
|
+
fail(
|
|
880
|
+
f"[bold]{integration}[/bold] declares no auth method [bold]{auth_method}[/bold].",
|
|
881
|
+
hint=f"Declared methods: {declared}.",
|
|
882
|
+
code=EXIT_VALIDATION,
|
|
883
|
+
)
|
|
884
|
+
if credential_type(chosen) not in _LINKABLE_CREDENTIAL_TYPES or not application_level(chosen):
|
|
885
|
+
fail(
|
|
886
|
+
f"Auth method [bold]{auth_method}[/bold] is not an API key, token or password the workspace shares, "
|
|
887
|
+
"so a link can't fill it.",
|
|
888
|
+
hint=oauth_hint() or "Connect it from the dashboard.",
|
|
889
|
+
code=EXIT_VALIDATION,
|
|
890
|
+
)
|
|
891
|
+
return chosen
|
|
892
|
+
|
|
893
|
+
linkable = [m for m in methods if credential_type(m) in _LINKABLE_CREDENTIAL_TYPES and application_level(m)]
|
|
894
|
+
if not linkable:
|
|
895
|
+
fail(
|
|
896
|
+
f"[bold]{integration}[/bold] takes no API key, token or password a link can fill.",
|
|
897
|
+
hint=oauth_hint() or "Connect it from the dashboard.",
|
|
898
|
+
code=EXIT_VALIDATION,
|
|
899
|
+
)
|
|
900
|
+
if len(linkable) > 1:
|
|
901
|
+
listed = ", ".join(f"{m.get('id')} ({m.get('displayName') or m.get('type')})" for m in linkable)
|
|
902
|
+
fail(
|
|
903
|
+
f"[bold]{integration}[/bold] declares more than one method a link can fill — pick one with --auth-method.",
|
|
904
|
+
hint=f"Methods: {listed}.",
|
|
905
|
+
code=EXIT_VALIDATION,
|
|
906
|
+
)
|
|
907
|
+
return linkable[0]
|
|
908
|
+
|
|
909
|
+
|
|
910
|
+
def _check_connection_parameters(integration: str, declared: object, parameters: dict[str, str]) -> None:
|
|
911
|
+
"""Refuse --param values the config would not take, and a required setting left out, before creating anything."""
|
|
912
|
+
fields = {str(p.get("name")): p for p in declared or [] if isinstance(p, dict) and p.get("name")}
|
|
913
|
+
unknown = sorted(set(parameters) - set(fields))
|
|
914
|
+
if unknown:
|
|
915
|
+
fail(
|
|
916
|
+
f"[bold]{integration}[/bold] has no connection setting {', '.join(unknown)}.",
|
|
917
|
+
hint=f"Its settings: {', '.join(sorted(fields)) or '(none)'}.",
|
|
918
|
+
code=EXIT_VALIDATION,
|
|
919
|
+
)
|
|
920
|
+
secret = sorted(n for n in parameters if fields[n].get("secret"))
|
|
921
|
+
if secret:
|
|
922
|
+
fail(
|
|
923
|
+
f"{', '.join(secret)} is secret, so it doesn't go on the command line.",
|
|
924
|
+
hint="Connect this integration from the dashboard.",
|
|
925
|
+
code=EXIT_VALIDATION,
|
|
926
|
+
)
|
|
927
|
+
missing = sorted(
|
|
928
|
+
n for n, p in fields.items() if p.get("required") and p.get("defaultValue") is None and n not in parameters
|
|
929
|
+
)
|
|
930
|
+
if missing:
|
|
931
|
+
fail(
|
|
932
|
+
f"[bold]{integration}[/bold] needs {', '.join(missing)} to connect.",
|
|
933
|
+
hint="Pass each with [bold]--param name=value[/bold].",
|
|
934
|
+
code=EXIT_VALIDATION,
|
|
935
|
+
)
|
|
936
|
+
|
|
937
|
+
|
|
938
|
+
def _delete_new_config(client, integration: str, config_id: str) -> str | None:
|
|
939
|
+
"""Delete the config this command created when no link could be minted for it; its id when it is still there."""
|
|
940
|
+
try:
|
|
941
|
+
client.delete_integration_config(integration, config_id)
|
|
942
|
+
except FruxonError:
|
|
943
|
+
return config_id
|
|
944
|
+
return None
|
|
945
|
+
|
|
946
|
+
|
|
947
|
+
def _hint_for_link_error(error: FruxonError, integration: str, leftover: str | None) -> str | None:
|
|
948
|
+
from fruxon.exceptions import FruxonAPIError
|
|
949
|
+
|
|
950
|
+
hint = _hint_for_config_error(error, integration)
|
|
951
|
+
if isinstance(error, FruxonAPIError) and error.error_code == "not_linkable":
|
|
952
|
+
hint = (
|
|
953
|
+
"Connect it from the dashboard, or with "
|
|
954
|
+
f"[bold]fruxon integrations authorize {integration}[/bold] for OAuth."
|
|
955
|
+
)
|
|
956
|
+
if leftover is not None:
|
|
957
|
+
cleanup = f"The config it created, {leftover}, is still there: delete it from the dashboard."
|
|
958
|
+
hint = f"{hint} {cleanup}" if hint else cleanup
|
|
959
|
+
return hint
|
|
960
|
+
|
|
961
|
+
|
|
962
|
+
def _parse_key_values(pairs: list[str] | None, *, flag: str, example: str) -> dict[str, str]:
|
|
963
|
+
parsed: dict[str, str] = {}
|
|
964
|
+
for pair in pairs or []:
|
|
965
|
+
key, sep, value = pair.partition("=")
|
|
966
|
+
if not sep or not key:
|
|
967
|
+
fail(
|
|
968
|
+
f"Invalid {flag} [bold]{escape(pair)}[/bold] — expected key=value.",
|
|
969
|
+
hint=f"e.g. {flag} {example}",
|
|
970
|
+
code=EXIT_VALIDATION,
|
|
971
|
+
)
|
|
972
|
+
parsed[key] = value
|
|
973
|
+
return parsed
|
|
974
|
+
|
|
975
|
+
|
|
679
976
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
680
977
|
# configs — tenant-level integration configs
|
|
681
978
|
# ─────────────────────────────────────────────────────────────────────────────
|