fruxon 1.0.0__tar.gz → 2.0.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (212) hide show
  1. {fruxon-1.0.0 → fruxon-2.0.1}/HISTORY.md +117 -0
  2. {fruxon-1.0.0 → fruxon-2.0.1}/PKG-INFO +41 -12
  3. {fruxon-1.0.0 → fruxon-2.0.1}/README.md +38 -11
  4. {fruxon-1.0.0 → fruxon-2.0.1}/pyproject.toml +6 -0
  5. {fruxon-1.0.0 → fruxon-2.0.1}/src/fruxon/_ssl.py +2 -0
  6. {fruxon-1.0.0 → fruxon-2.0.1}/src/fruxon/_version.py +2 -2
  7. fruxon-2.0.1/src/fruxon/checks/__init__.py +101 -0
  8. fruxon-2.0.1/src/fruxon/checks/agent.py +983 -0
  9. fruxon-2.0.1/src/fruxon/checks/ledger.py +456 -0
  10. fruxon-2.0.1/src/fruxon/checks/references.py +224 -0
  11. {fruxon-1.0.0 → fruxon-2.0.1}/src/fruxon/cli/__init__.py +12 -1
  12. {fruxon-1.0.0 → fruxon-2.0.1}/src/fruxon/cli/_stream.py +12 -5
  13. {fruxon-1.0.0 → fruxon-2.0.1}/src/fruxon/cli/agents.py +9 -635
  14. {fruxon-1.0.0 → fruxon-2.0.1}/src/fruxon/cli/agents_approvals.py +26 -8
  15. {fruxon-1.0.0 → fruxon-2.0.1}/src/fruxon/cli/agents_draft.py +19 -228
  16. {fruxon-1.0.0 → fruxon-2.0.1}/src/fruxon/cli/agents_executions.py +17 -10
  17. {fruxon-1.0.0 → fruxon-2.0.1}/src/fruxon/cli/agents_revisions.py +6 -6
  18. {fruxon-1.0.0 → fruxon-2.0.1}/src/fruxon/cli/assets.py +6 -6
  19. {fruxon-1.0.0 → fruxon-2.0.1}/src/fruxon/cli/auth.py +134 -29
  20. fruxon-2.0.1/src/fruxon/cli/dash.py +120 -0
  21. {fruxon-1.0.0 → fruxon-2.0.1}/src/fruxon/cli/examples.py +2 -2
  22. {fruxon-1.0.0 → fruxon-2.0.1}/src/fruxon/cli/integrations.py +1 -1
  23. {fruxon-1.0.0 → fruxon-2.0.1}/src/fruxon/cli/llm_providers.py +12 -10
  24. {fruxon-1.0.0 → fruxon-2.0.1}/src/fruxon/cli/metrics.py +4 -4
  25. {fruxon-1.0.0 → fruxon-2.0.1}/src/fruxon/cli/participants.py +4 -4
  26. {fruxon-1.0.0 → fruxon-2.0.1}/src/fruxon/cli/secrets.py +5 -5
  27. {fruxon-1.0.0 → fruxon-2.0.1}/src/fruxon/cli/storage.py +3 -3
  28. {fruxon-1.0.0 → fruxon-2.0.1}/src/fruxon/cli/tools.py +1 -1
  29. {fruxon-1.0.0 → fruxon-2.0.1}/src/fruxon/cli/triggers.py +2 -14
  30. fruxon-2.0.1/src/fruxon/cli/triggers_check.py +123 -0
  31. {fruxon-1.0.0 → fruxon-2.0.1}/src/fruxon/cli/triggers_ledger.py +42 -3
  32. {fruxon-1.0.0 → fruxon-2.0.1}/src/fruxon/cli/triggers_shape.py +15 -0
  33. {fruxon-1.0.0 → fruxon-2.0.1}/src/fruxon/credentials.py +4 -2
  34. {fruxon-1.0.0 → fruxon-2.0.1}/src/fruxon/device_auth.py +10 -0
  35. {fruxon-1.0.0 → fruxon-2.0.1}/src/fruxon/flow_validation.py +11 -1
  36. {fruxon-1.0.0 → fruxon-2.0.1}/src/fruxon/fruxon.py +407 -8
  37. {fruxon-1.0.0 → fruxon-2.0.1}/src/fruxon/models.py +266 -6
  38. {fruxon-1.0.0 → fruxon-2.0.1}/src/fruxon/skills/__init__.py +1 -0
  39. {fruxon-1.0.0 → fruxon-2.0.1}/src/fruxon/skills/fruxon-agent-mode/SKILL.md +9 -5
  40. {fruxon-1.0.0 → fruxon-2.0.1}/src/fruxon/skills/fruxon-build-agent/SKILL.md +23 -22
  41. {fruxon-1.0.0 → fruxon-2.0.1}/src/fruxon/skills/fruxon-debug-trace/SKILL.md +5 -5
  42. {fruxon-1.0.0 → fruxon-2.0.1}/src/fruxon/skills/fruxon-meet/SKILL.md +4 -3
  43. fruxon-2.0.1/src/fruxon/tui/__init__.py +10 -0
  44. fruxon-2.0.1/src/fruxon/tui/app.py +351 -0
  45. fruxon-2.0.1/src/fruxon/tui/demo.py +927 -0
  46. fruxon-2.0.1/src/fruxon/tui/links.py +163 -0
  47. fruxon-2.0.1/src/fruxon/tui/screens/__init__.py +1 -0
  48. fruxon-2.0.1/src/fruxon/tui/screens/agent.py +428 -0
  49. fruxon-2.0.1/src/fruxon/tui/screens/base.py +269 -0
  50. fruxon-2.0.1/src/fruxon/tui/screens/home.py +268 -0
  51. fruxon-2.0.1/src/fruxon/tui/screens/ledger.py +562 -0
  52. fruxon-2.0.1/src/fruxon/tui/screens/loader.py +257 -0
  53. fruxon-2.0.1/src/fruxon/tui/screens/pickers.py +112 -0
  54. fruxon-2.0.1/src/fruxon/tui/screens/trace.py +319 -0
  55. fruxon-2.0.1/src/fruxon/tui/source.py +1031 -0
  56. fruxon-2.0.1/src/fruxon/tui/theme.py +203 -0
  57. fruxon-2.0.1/src/fruxon/tui/views.py +308 -0
  58. fruxon-2.0.1/src/fruxon/tui/widgets.py +777 -0
  59. {fruxon-1.0.0 → fruxon-2.0.1}/tests/.test_durations.json +8 -0
  60. {fruxon-1.0.0 → fruxon-2.0.1}/tests/test_approvals.py +37 -0
  61. fruxon-2.0.1/tests/test_checks_agent.py +237 -0
  62. fruxon-2.0.1/tests/test_checks_ledger.py +256 -0
  63. {fruxon-1.0.0 → fruxon-2.0.1}/tests/test_cli.py +261 -27
  64. fruxon-2.0.1/tests/test_dash_app.py +562 -0
  65. fruxon-2.0.1/tests/test_dash_cli.py +106 -0
  66. fruxon-2.0.1/tests/test_dash_client.py +355 -0
  67. fruxon-2.0.1/tests/test_dash_links.py +218 -0
  68. fruxon-2.0.1/tests/test_dash_source.py +750 -0
  69. {fruxon-1.0.0 → fruxon-2.0.1}/tests/test_device_auth.py +35 -0
  70. {fruxon-1.0.0 → fruxon-2.0.1}/tests/test_flow_validation.py +8 -0
  71. fruxon-2.0.1/tests/test_triggers_check_cli.py +88 -0
  72. {fruxon-1.0.0 → fruxon-2.0.1}/tests/test_triggers_ledger.py +41 -2
  73. {fruxon-1.0.0 → fruxon-2.0.1}/tests/test_triggers_shape.py +114 -0
  74. fruxon-2.0.1/tests/test_tui_boundary.py +101 -0
  75. {fruxon-1.0.0 → fruxon-2.0.1}/.gitignore +0 -0
  76. {fruxon-1.0.0 → fruxon-2.0.1}/LICENSE +0 -0
  77. {fruxon-1.0.0 → fruxon-2.0.1}/src/fruxon/__init__.py +0 -0
  78. {fruxon-1.0.0 → fruxon-2.0.1}/src/fruxon/__main__.py +0 -0
  79. {fruxon-1.0.0 → fruxon-2.0.1}/src/fruxon/cli/_crash.py +0 -0
  80. {fruxon-1.0.0 → fruxon-2.0.1}/src/fruxon/cli/_schema.py +0 -0
  81. {fruxon-1.0.0 → fruxon-2.0.1}/src/fruxon/cli/_shared.py +0 -0
  82. {fruxon-1.0.0 → fruxon-2.0.1}/src/fruxon/cli/agents_budget.py +0 -0
  83. {fruxon-1.0.0 → fruxon-2.0.1}/src/fruxon/cli/agents_channels.py +0 -0
  84. {fruxon-1.0.0 → fruxon-2.0.1}/src/fruxon/cli/agents_evaluations.py +0 -0
  85. {fruxon-1.0.0 → fruxon-2.0.1}/src/fruxon/cli/agents_improvements.py +0 -0
  86. {fruxon-1.0.0 → fruxon-2.0.1}/src/fruxon/cli/agents_lifecycle.py +0 -0
  87. {fruxon-1.0.0 → fruxon-2.0.1}/src/fruxon/cli/agents_memory.py +0 -0
  88. {fruxon-1.0.0 → fruxon-2.0.1}/src/fruxon/cli/agents_reply_style.py +0 -0
  89. {fruxon-1.0.0 → fruxon-2.0.1}/src/fruxon/cli/agents_sandbox.py +0 -0
  90. {fruxon-1.0.0 → fruxon-2.0.1}/src/fruxon/cli/agents_sandbox_test.py +0 -0
  91. {fruxon-1.0.0 → fruxon-2.0.1}/src/fruxon/cli/agents_slots.py +0 -0
  92. {fruxon-1.0.0 → fruxon-2.0.1}/src/fruxon/cli/agents_system_blocks.py +0 -0
  93. {fruxon-1.0.0 → fruxon-2.0.1}/src/fruxon/cli/agents_tests.py +0 -0
  94. {fruxon-1.0.0 → fruxon-2.0.1}/src/fruxon/cli/agents_topics.py +0 -0
  95. {fruxon-1.0.0 → fruxon-2.0.1}/src/fruxon/cli/applications.py +0 -0
  96. {fruxon-1.0.0 → fruxon-2.0.1}/src/fruxon/cli/applications_entry_points.py +0 -0
  97. {fruxon-1.0.0 → fruxon-2.0.1}/src/fruxon/cli/capabilities.py +0 -0
  98. {fruxon-1.0.0 → fruxon-2.0.1}/src/fruxon/cli/completion.py +0 -0
  99. {fruxon-1.0.0 → fruxon-2.0.1}/src/fruxon/cli/config.py +0 -0
  100. {fruxon-1.0.0 → fruxon-2.0.1}/src/fruxon/cli/consult_pins.py +0 -0
  101. {fruxon-1.0.0 → fruxon-2.0.1}/src/fruxon/cli/conversation_imports.py +0 -0
  102. {fruxon-1.0.0 → fruxon-2.0.1}/src/fruxon/cli/describe.py +0 -0
  103. {fruxon-1.0.0 → fruxon-2.0.1}/src/fruxon/cli/doctor.py +0 -0
  104. {fruxon-1.0.0 → fruxon-2.0.1}/src/fruxon/cli/environments.py +0 -0
  105. {fruxon-1.0.0 → fruxon-2.0.1}/src/fruxon/cli/escalations.py +0 -0
  106. {fruxon-1.0.0 → fruxon-2.0.1}/src/fruxon/cli/guides.py +0 -0
  107. {fruxon-1.0.0 → fruxon-2.0.1}/src/fruxon/cli/keys.py +0 -0
  108. {fruxon-1.0.0 → fruxon-2.0.1}/src/fruxon/cli/knowledge_bases.py +0 -0
  109. {fruxon-1.0.0 → fruxon-2.0.1}/src/fruxon/cli/knowledge_documents.py +0 -0
  110. {fruxon-1.0.0 → fruxon-2.0.1}/src/fruxon/cli/knowledge_notices.py +0 -0
  111. {fruxon-1.0.0 → fruxon-2.0.1}/src/fruxon/cli/knowledge_sources.py +0 -0
  112. {fruxon-1.0.0 → fruxon-2.0.1}/src/fruxon/cli/messages.py +0 -0
  113. {fruxon-1.0.0 → fruxon-2.0.1}/src/fruxon/cli/pipelines.py +0 -0
  114. {fruxon-1.0.0 → fruxon-2.0.1}/src/fruxon/cli/skills.py +0 -0
  115. {fruxon-1.0.0 → fruxon-2.0.1}/src/fruxon/cli/triggers_budget.py +0 -0
  116. {fruxon-1.0.0 → fruxon-2.0.1}/src/fruxon/cli/triggers_ledger_stage_tests.py +0 -0
  117. {fruxon-1.0.0 → fruxon-2.0.1}/src/fruxon/cli/triggers_questions.py +0 -0
  118. {fruxon-1.0.0 → fruxon-2.0.1}/src/fruxon/cli/workspaces.py +0 -0
  119. {fruxon-1.0.0 → fruxon-2.0.1}/src/fruxon/doctor.py +0 -0
  120. {fruxon-1.0.0 → fruxon-2.0.1}/src/fruxon/exceptions.py +0 -0
  121. {fruxon-1.0.0 → fruxon-2.0.1}/src/fruxon/output.py +0 -0
  122. {fruxon-1.0.0 → fruxon-2.0.1}/src/fruxon/params.py +0 -0
  123. {fruxon-1.0.0 → fruxon-2.0.1}/src/fruxon/sandbox_scenario.py +0 -0
  124. {fruxon-1.0.0 → fruxon-2.0.1}/src/fruxon/skills/fruxon-create-integration/SKILL.md +0 -0
  125. {fruxon-1.0.0 → fruxon-2.0.1}/src/fruxon/skills/fruxon-use-integrations/SKILL.md +0 -0
  126. {fruxon-1.0.0 → fruxon-2.0.1}/src/fruxon/telemetry.py +0 -0
  127. {fruxon-1.0.0 → fruxon-2.0.1}/src/fruxon/ui.py +0 -0
  128. {fruxon-1.0.0 → fruxon-2.0.1}/src/fruxon/update_check.py +0 -0
  129. {fruxon-1.0.0 → fruxon-2.0.1}/src/fruxon/validation.py +0 -0
  130. {fruxon-1.0.0 → fruxon-2.0.1}/tests/__init__.py +0 -0
  131. {fruxon-1.0.0 → fruxon-2.0.1}/tests/conftest.py +0 -0
  132. {fruxon-1.0.0 → fruxon-2.0.1}/tests/test_actor.py +0 -0
  133. {fruxon-1.0.0 → fruxon-2.0.1}/tests/test_agent_lifecycle.py +0 -0
  134. {fruxon-1.0.0 → fruxon-2.0.1}/tests/test_agent_revisions_list.py +0 -0
  135. {fruxon-1.0.0 → fruxon-2.0.1}/tests/test_agent_slots.py +0 -0
  136. {fruxon-1.0.0 → fruxon-2.0.1}/tests/test_agents_check_cli.py +0 -0
  137. {fruxon-1.0.0 → fruxon-2.0.1}/tests/test_agents_revisions_cli.py +0 -0
  138. {fruxon-1.0.0 → fruxon-2.0.1}/tests/test_agents_sandbox.py +0 -0
  139. {fruxon-1.0.0 → fruxon-2.0.1}/tests/test_applications.py +0 -0
  140. {fruxon-1.0.0 → fruxon-2.0.1}/tests/test_assets.py +0 -0
  141. {fruxon-1.0.0 → fruxon-2.0.1}/tests/test_budgets.py +0 -0
  142. {fruxon-1.0.0 → fruxon-2.0.1}/tests/test_capabilities.py +0 -0
  143. {fruxon-1.0.0 → fruxon-2.0.1}/tests/test_channels.py +0 -0
  144. {fruxon-1.0.0 → fruxon-2.0.1}/tests/test_cli_base_url_routing.py +0 -0
  145. {fruxon-1.0.0 → fruxon-2.0.1}/tests/test_cli_crash.py +0 -0
  146. {fruxon-1.0.0 → fruxon-2.0.1}/tests/test_cli_root_flags.py +0 -0
  147. {fruxon-1.0.0 → fruxon-2.0.1}/tests/test_client.py +0 -0
  148. {fruxon-1.0.0 → fruxon-2.0.1}/tests/test_connect_nudge.py +0 -0
  149. {fruxon-1.0.0 → fruxon-2.0.1}/tests/test_consult.py +0 -0
  150. {fruxon-1.0.0 → fruxon-2.0.1}/tests/test_conversation_imports.py +0 -0
  151. {fruxon-1.0.0 → fruxon-2.0.1}/tests/test_credentials.py +0 -0
  152. {fruxon-1.0.0 → fruxon-2.0.1}/tests/test_describe.py +0 -0
  153. {fruxon-1.0.0 → fruxon-2.0.1}/tests/test_docs_commands_exist.py +0 -0
  154. {fruxon-1.0.0 → fruxon-2.0.1}/tests/test_doctor.py +0 -0
  155. {fruxon-1.0.0 → fruxon-2.0.1}/tests/test_draft_bootstrap.py +0 -0
  156. {fruxon-1.0.0 → fruxon-2.0.1}/tests/test_draft_evaluate_cli.py +0 -0
  157. {fruxon-1.0.0 → fruxon-2.0.1}/tests/test_draft_pull_shape.py +0 -0
  158. {fruxon-1.0.0 → fruxon-2.0.1}/tests/test_draft_run_replay.py +0 -0
  159. {fruxon-1.0.0 → fruxon-2.0.1}/tests/test_draft_validate_cli.py +0 -0
  160. {fruxon-1.0.0 → fruxon-2.0.1}/tests/test_drafts.py +0 -0
  161. {fruxon-1.0.0 → fruxon-2.0.1}/tests/test_entry_points.py +0 -0
  162. {fruxon-1.0.0 → fruxon-2.0.1}/tests/test_environments.py +0 -0
  163. {fruxon-1.0.0 → fruxon-2.0.1}/tests/test_escalations.py +0 -0
  164. {fruxon-1.0.0 → fruxon-2.0.1}/tests/test_evaluations.py +0 -0
  165. {fruxon-1.0.0 → fruxon-2.0.1}/tests/test_execution_records.py +0 -0
  166. {fruxon-1.0.0 → fruxon-2.0.1}/tests/test_fruxon.py +0 -0
  167. {fruxon-1.0.0 → fruxon-2.0.1}/tests/test_guides.py +0 -0
  168. {fruxon-1.0.0 → fruxon-2.0.1}/tests/test_improvements.py +0 -0
  169. {fruxon-1.0.0 → fruxon-2.0.1}/tests/test_integration_configs_write_cli.py +0 -0
  170. {fruxon-1.0.0 → fruxon-2.0.1}/tests/test_integration_triggers_cli.py +0 -0
  171. {fruxon-1.0.0 → fruxon-2.0.1}/tests/test_integrations_authorize_cli.py +0 -0
  172. {fruxon-1.0.0 → fruxon-2.0.1}/tests/test_kb_trial_cli.py +0 -0
  173. {fruxon-1.0.0 → fruxon-2.0.1}/tests/test_knowledge_bases.py +0 -0
  174. {fruxon-1.0.0 → fruxon-2.0.1}/tests/test_knowledge_documents.py +0 -0
  175. {fruxon-1.0.0 → fruxon-2.0.1}/tests/test_knowledge_notices.py +0 -0
  176. {fruxon-1.0.0 → fruxon-2.0.1}/tests/test_knowledge_sources.py +0 -0
  177. {fruxon-1.0.0 → fruxon-2.0.1}/tests/test_llm_provider_configs_cli.py +0 -0
  178. {fruxon-1.0.0 → fruxon-2.0.1}/tests/test_managed_by.py +0 -0
  179. {fruxon-1.0.0 → fruxon-2.0.1}/tests/test_memory.py +0 -0
  180. {fruxon-1.0.0 → fruxon-2.0.1}/tests/test_messages.py +0 -0
  181. {fruxon-1.0.0 → fruxon-2.0.1}/tests/test_metrics.py +0 -0
  182. {fruxon-1.0.0 → fruxon-2.0.1}/tests/test_online_llm_config_refs.py +0 -0
  183. {fruxon-1.0.0 → fruxon-2.0.1}/tests/test_online_prefetch_refs.py +0 -0
  184. {fruxon-1.0.0 → fruxon-2.0.1}/tests/test_online_secret_refs.py +0 -0
  185. {fruxon-1.0.0 → fruxon-2.0.1}/tests/test_output.py +0 -0
  186. {fruxon-1.0.0 → fruxon-2.0.1}/tests/test_params.py +0 -0
  187. {fruxon-1.0.0 → fruxon-2.0.1}/tests/test_participants.py +0 -0
  188. {fruxon-1.0.0 → fruxon-2.0.1}/tests/test_participants_write.py +0 -0
  189. {fruxon-1.0.0 → fruxon-2.0.1}/tests/test_pipelines.py +0 -0
  190. {fruxon-1.0.0 → fruxon-2.0.1}/tests/test_reply_style.py +0 -0
  191. {fruxon-1.0.0 → fruxon-2.0.1}/tests/test_sandbox_scenario.py +0 -0
  192. {fruxon-1.0.0 → fruxon-2.0.1}/tests/test_schema.py +0 -0
  193. {fruxon-1.0.0 → fruxon-2.0.1}/tests/test_schema_fetch.py +0 -0
  194. {fruxon-1.0.0 → fruxon-2.0.1}/tests/test_secrets.py +0 -0
  195. {fruxon-1.0.0 → fruxon-2.0.1}/tests/test_skills.py +0 -0
  196. {fruxon-1.0.0 → fruxon-2.0.1}/tests/test_ssl.py +0 -0
  197. {fruxon-1.0.0 → fruxon-2.0.1}/tests/test_storage.py +0 -0
  198. {fruxon-1.0.0 → fruxon-2.0.1}/tests/test_system_blocks.py +0 -0
  199. {fruxon-1.0.0 → fruxon-2.0.1}/tests/test_system_blocks_cli.py +0 -0
  200. {fruxon-1.0.0 → fruxon-2.0.1}/tests/test_telemetry.py +0 -0
  201. {fruxon-1.0.0 → fruxon-2.0.1}/tests/test_test_chats.py +0 -0
  202. {fruxon-1.0.0 → fruxon-2.0.1}/tests/test_topics.py +0 -0
  203. {fruxon-1.0.0 → fruxon-2.0.1}/tests/test_trigger_budgets.py +0 -0
  204. {fruxon-1.0.0 → fruxon-2.0.1}/tests/test_triggers.py +0 -0
  205. {fruxon-1.0.0 → fruxon-2.0.1}/tests/test_triggers_ledger_batch_views.py +0 -0
  206. {fruxon-1.0.0 → fruxon-2.0.1}/tests/test_triggers_ledger_stage_tests.py +0 -0
  207. {fruxon-1.0.0 → fruxon-2.0.1}/tests/test_triggers_questions.py +0 -0
  208. {fruxon-1.0.0 → fruxon-2.0.1}/tests/test_triggers_write.py +0 -0
  209. {fruxon-1.0.0 → fruxon-2.0.1}/tests/test_ui.py +0 -0
  210. {fruxon-1.0.0 → fruxon-2.0.1}/tests/test_update_check.py +0 -0
  211. {fruxon-1.0.0 → fruxon-2.0.1}/tests/test_validation.py +0 -0
  212. {fruxon-1.0.0 → fruxon-2.0.1}/tests/test_workspaces_cli.py +0 -0
@@ -1,5 +1,122 @@
1
1
  # History
2
2
 
3
+ ## Unreleased — `o` in `fruxon dash` opens the same page in the web app
4
+
5
+ - `o` opens the web app on the exact tab or record behind what the console
6
+ shows: Home → the Application's Overview over the same window, an agent → its
7
+ page, a run → that execution (`?tab=executions&id=`), an automation → its
8
+ Overview, Settings or Work tab, an item → that item (`?tab=work&item=`), the
9
+ Items filter → `&status=`. On Configuration it opens where the selected
10
+ finding is put right (the agent editor, an integration's configs, an LLM
11
+ provider, the automation's settings or edit page, its work filtered to a
12
+ stage). The selected row wins; `O` opens the screen's own page.
13
+ - Every link carries `?workspace=<slug>`, so the browser switches to the
14
+ console's workspace before it opens the page.
15
+ - The automation link was broken (`/applications/{id}/…`); it is
16
+ `/app/{slug}/automations/triggers/{id}` now.
17
+ - The URL stays up as a notification after it opens — over SSH, that is the
18
+ link. `--demo` shows the URL it would open and opens nothing.
19
+ - `CheckResult.fix` names where a finding is put right (`editor`,
20
+ `integration:<id>`, `llm-provider:<id>`, `trigger-settings`,
21
+ `work:<status>:<stage>`, …); `to_dict()` and `-o json` are unchanged. The
22
+ link map is one pure function, `fruxon.tui.links.web_link`.
23
+
24
+ ## Unreleased — an agent reads as one agent, not a flow
25
+
26
+ An agent runs as one definition. These surfaces still drew it as steps.
27
+
28
+ - **`fruxon dash`**: an agent's Configuration tab shows an **Agent · rev N**
29
+ panel: one box with the agent's model, its tools, and each tool behind a
30
+ human-approval gate (`⧗ issue_refund · approval by …`). A gate with no
31
+ approver is named in red. It was a **Flow** panel drawing boxes joined by
32
+ arrows. The `--demo` agents no longer appear as a five-step pipeline. The
33
+ `consults this agent` line under the panel is gone; the Consult roster row
34
+ covers consults.
35
+ - **`fruxon agents executions trace`**: the text tree's first line for an
36
+ agent run reads `1 Agent` instead of `1 FLOW_STEP · Agent`. Paths and
37
+ `-o json` are unchanged: the JSON is still the server's payload.
38
+ - **`fruxon agents draft run`**: the NDJSON stream no longer relays the
39
+ server's `step_trace` for the `FLOW_STEP` frame around a run. The steps
40
+ inside it still come through.
41
+ - **`fruxon agents approvals get` / `list`** no longer print a `step` for an
42
+ agent's gate. It was always `agent`, because the gate sits on a tool the
43
+ agent called. A workflow's tool step (`TOOL_FLOW_STEP`) still names its
44
+ step. `-o json` keeps `stepIdentifier` either way.
45
+ - **`fruxon agents check`** and the dash's check rows say `consult is off
46
+ (network.enabled)`, `consult is on (network.enabled) but the roster is
47
+ empty`, and `no tool needs human approval`. They used to say "no step
48
+ enables consult", list step names, or say "on any step".
49
+ - **Help text, guides and docs** talk about an agent and its definition,
50
+ not a flow or a step. This covers `draft validate` (its list of checks now
51
+ matches what it checks), `draft evaluate` (and its confirmation prompt),
52
+ `secrets`, `assets`, `metrics`, `participants`, `llm-providers configs`,
53
+ `storage`, `agents approvals`, and the Python client's docstrings. The
54
+ README and `docs/python-client.md` "Test a draft" examples now use the real
55
+ `put_draft` → `test(agent, base_revision, request)` calls; the old ones
56
+ posted a `flow` body to a signature that doesn't exist.
57
+
58
+ ## Unreleased — a form stage's own wording, checked before saving
59
+
60
+ Follows the backend's operator-worded form messages (BE #1415) and a form
61
+ batch's drafted rows (BE #1335).
62
+
63
+ - **`fruxon triggers preview-shape`** shows a FORM stage's `join notice` under
64
+ its question: the line sent when more items join an open form, with its
65
+ counts filled in. A stage whose `formJoinNotice` is blank says a join sends
66
+ nothing. `-o json` carries it as `formJoinNotice`
67
+ (`WorkShapeStagePreview.form_join_notice`), null on every other stage.
68
+ - **`fruxon triggers set-stages --help`** documents `formClosing` (the line
69
+ under the header in the form message) and `formJoinNotice`, the
70
+ `{{item_count}}` / `{{added_count}}` they fill in, unset vs blank, and that
71
+ the server refuses both on a stage that is not a form.
72
+ - **`fruxon triggers ledger batches`** prints `N waiting, M drafted` for an open
73
+ FORM batch: rows saved on the page and not yet submitted, which no item
74
+ shows. `-o json` carries `draftCount` (`LedgerBatch.draft_count`).
75
+
76
+ ## Unreleased — `fruxon triggers check`, and an automation's Configuration tab in `fruxon dash`
77
+
78
+ - **`fruxon triggers check <trigger> [-o json]`** audits a work-shape
79
+ automation part by part: the automation, its source (by its last pass),
80
+ every stage (a PROCESS stage's agent runs and reaches what it calls; an ASK
81
+ stage has someone to ask and is not close to expiring questions), the
82
+ outcome note's contact, the weekly budget and the dead items. Same
83
+ `{…, overall, checks[]}` contract and exit codes as `agents check`.
84
+ The audit is `fruxon.checks.ledger.check_ledger`.
85
+ - `fruxon dash`: an automation's `2` Configuration tab shows those rows with
86
+ the selected one explained in place, `t` tests the source (admits nothing),
87
+ `p` previews what a stage sends its agent (saves nothing), and lists the
88
+ shape.
89
+
90
+ ## Unreleased — `fruxon dash` opens on the Fruxon wordmark
91
+
92
+ While Home makes its first reads, `fruxon dash` shows "FRUXON" spelled out
93
+ one letter at a time in outlined letters (the F in the brand's red) with a
94
+ spinner and what is being read. It leaves once Home has its numbers, after
95
+ at least 1.6 s; a failed read ends it at once, and enter, space or esc skip
96
+ it.
97
+
98
+ ## Unreleased — `fruxon dash`, a read-only console for an Application
99
+
100
+ Everything else in the CLI prints and exits. `fruxon dash` stays open and
101
+ shows one Application the way the web app's overview does, in the terminal.
102
+
103
+ - **`fruxon dash [--app A] [--demo] [--ground black|ink]`**. Home: runs,
104
+ spend, items finished and who is waiting in big digits; every agent and
105
+ automation; what needs a person. An agent: overview, the `agents check`
106
+ rows plus credential, model-config and budget health, runs and traces. An
107
+ automation: its stages and counts, items per day, what failed, its items.
108
+ Live parts every 5 s, totals every minute. Read-only.
109
+ - Needs the new `tui` extra: `pip install "fruxon[tui]"` (Textual). Nothing
110
+ imports it until the command runs, so the CLI starts as fast as before.
111
+ - `--demo` is a made-up workspace with a pinned clock and seeded live runs,
112
+ for recordings; `docs/tui/demo.tape` records it with VHS.
113
+ - The audit behind `agents check` moved to `fruxon.checks.agent` and returns
114
+ `CheckResult` rows; the command's output and exit codes are unchanged.
115
+ - New client reads: `get_application_spend`, `get_application_activity_counts`,
116
+ `get_application_support_kpis`, `get_agent_conversation_timeseries`,
117
+ `get_agent_judge_activity`, `get_agent_cost_forecast`,
118
+ `get_agent_llm_config_health`, `get_agent_credential_health`.
119
+
3
120
  ## Unreleased — edit and publish an integration config from the CLI
4
121
 
5
122
  `integrations configs` could only `list` and `get`, so changing a config
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: fruxon
3
- Version: 1.0.0
3
+ Version: 2.0.1
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
@@ -29,6 +29,8 @@ Requires-Dist: pre-commit; extra == 'test'
29
29
  Requires-Dist: pytest; extra == 'test'
30
30
  Requires-Dist: ruff; extra == 'test'
31
31
  Requires-Dist: ty; extra == 'test'
32
+ Provides-Extra: tui
33
+ Requires-Dist: textual>=8.2; extra == 'tui'
32
34
  Description-Content-Type: text/markdown
33
35
 
34
36
  # fruxon
@@ -80,7 +82,7 @@ Auth:
80
82
 
81
83
  | Command | What it does |
82
84
  | --- | --- |
83
- | `fruxon login` | Browser-based sign-in. token goes to your OS keychain (Keychain on macOS, Secret Service on Linux, Credential Manager on Windows). `--token $KEY` for headless. `--preset observer\|operator\|developer\|maintainer\|admin` declares the access level the key is minted with (default: developer); the approval page shows it for the user to approve or deny. |
85
+ | `fruxon login` | Browser-based sign-in. token goes to your OS keychain (Keychain on macOS, Secret Service on Linux, Credential Manager on Windows). `--token $KEY` for headless. `--preset observer\|operator\|developer\|maintainer\|admin` declares the access level the key is minted with (default: developer); the approval page shows it for the user to approve or deny. Adds a workspace without moving the default (the first sign-in sets it); `--set-default` moves it. |
84
86
  | `fruxon whoami` | Show the active key/workspace and where each value came from (flag, env, keychain, file). |
85
87
  | `fruxon logout` | Forget stored credentials. `--workspace <id>` signs out of one and leaves the rest; `--all` clears every one. |
86
88
  | `fruxon workspaces list` | Which workspaces this machine is signed in to, each with its host and cell, and which one is the default. Offline, no auth. |
@@ -96,7 +98,7 @@ Execution:
96
98
  | `fruxon agents executions get <agent> <record-id>` | Record summary for one execution — status, duration, cost, tokens. |
97
99
  | `fruxon agents executions trace <agent> <record-id>` | Step-by-step trace of one execution — LLM/tool calls, durations, and for a tool that records it, where the call's time went (service stages vs. unaccounted). Flags a result the model got offloaded or projected. Prints a quality check's verdict (score, criteria, rationale) under its `JUDGE` step, and marks a reply the check refused. `--show-io` also prints each tool step's parameters and result (the payload lives at `steps[].result`, a sibling of `toolTrace` — whose `resultDelivery` carries sizes only). `--show-system` labels each LLM step's system prompt block by block — your prompt vs. the platform's `<tag>` blocks, sizes, and the cache breakpoint. |
98
100
  | `fruxon agents executions result <agent> <record-id>` | The final output one execution produced. |
99
- | `fruxon agents approvals list/get/respond/cancel <agent>` | Operate a step's human-in-the-loop gate — see what a run is blocked on and answer it. `respond`/`cancel` confirm first and refuse without `--yes` in agent mode (an LLM must not silently auto-approve). |
101
+ | `fruxon agents approvals list/get/respond/cancel <agent>` | Operate human-in-the-loop approval gates — see what a run is blocked on and answer it. `respond`/`cancel` confirm first and refuse without `--yes` in agent mode (an LLM must not silently auto-approve). |
100
102
  | `fruxon agents memory list/subjects/get/forget-subject <agent>` | Inspect and prune what an agent remembers. `list` filters server-side by `--subject`/`--query`/`--scope`; `forget-subject` is a GDPR-style delete (confirms; refuses without `--yes` in agent mode). |
101
103
  | `fruxon agents topics list/search/get/messages/sessions <agent>` + `fruxon agents inbox <agent>` | Read the conversation spine — the agent's topics (threads), one topic's transcript, its episodes, and its inbox (what it's focused on). `list`/`search` filter server-side by `--state`/`--participant`/`--query`. Read-only. Each message carries `author` — a human colleague's turn is stored as `ASSISTANT` on purpose, so `role` alone cannot tell their reply from the agent's; `HUMAN_OPERATOR` can. A message the runtime *withheld* is in no transcript at all — see `fruxon messages list`. `sessions` explains a fresh, blind session: a topic outlives its sessions, and `endCause` on the previous one (`CONSOLE_ALREADY_RESOLVED` — a human closed it in the provider's console) is why the next started with no history. |
102
104
  | `fruxon escalations list/history/unhandled/policy/ack` | What happened when an agent handed a conversation to a person. `list` is the queue (live takeovers + degraded escalations nobody was reachable for); `unhandled` is the append-only ledger under it. **`history` is the record of closed console-routed takeovers** — those leave the queue, so `list --status ALL` answers `[]` about a conversation a human worked for half an hour. Read `humanObservedAt` before believing a `RESOLVED`: null means nobody was ever seen picking it up. `policy <agent>` resolves who the *next* escalation would reach — an empty ladder under `NATIVE_HANDOFF` is correct, the console supplies the human. |
@@ -146,8 +148,8 @@ Integrations + tools:
146
148
  | `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). |
147
149
  | `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. |
148
150
  | `fruxon llm-providers list/get/models` | Browse LLM providers and models supported at the tenant level. |
149
- | `fruxon llm-providers configs list/get/test/publish` | The workspace's saved LLM credentials — the `provider.tenantConfigId` a step 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`). |
150
- | `fruxon assets create/list/get/wait/operations/delete` | Manage knowledge-base (RAG) assets a step 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. |
151
+ | `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
+ | `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. |
151
153
  | `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. |
152
154
  | `fruxon knowledge-bases usage <kb>` | Whether agents actually use the base: searches per hour, day or week (`--interval`), the passage tokens each search handed the agent, how many found nothing (split from searches that **could not fully search** it — index missing or still building, a phrasing failed), and which agents searched (up to 20 you can open, the rest counted together). `--origin test` counts test runs instead of production. Retrieved tokens are counted by the search index, so what the model read can be less when a large result is trimmed or offloaded. Searches are recorded from 15 October 2026 and kept 90 days at most, so a window reaching further back says where counting began rather than showing empty days. A 404 is a wrong id or a backend that predates the read. |
153
155
  | `fruxon knowledge-bases notices/notice/post-notice/edit-notice/extend-notice/withdraw-notice` | Time-boxed notices on a base — an outage being fixed, a known bug, a closure. **Not documents**: never indexed or published, so every agent attaching the base is given each active notice whole from its next turn. `post-notice --valid-for 4h` (or `--until`) — every notice ends, at most 30 days after it was posted, and a base holds ten active ones (409 `notice_limit` / `notice_too_long`, each with its remedy). The body is Markdown, at most 1,200 characters, best as four headings: *What is happening*, *Applies when*, *Tell the customer*, *Do not*. A time without an offset is read in the notice's zone (`--time-zone`, else this machine's), which is also the zone agents tell customers the end in. `withdraw-notice` tells each conversation that was given it, on its next turn, that it ended; it confirms, and needs `--yes` in agent mode. An ended notice is not extended: `post-notice --from <id>` posts it again as a new one. |
@@ -156,13 +158,14 @@ Integrations + tools:
156
158
  | `fruxon knowledge-bases consolidate` → `apply` / `calibrate` | Two steps on purpose. `consolidate` queues a **report** of near-duplicate clusters and writes nothing — a standalone pass can no longer merge as it goes, and the old `--apply` is refused. `apply <kb> <run> --cluster <id>` (or `--all`) merges the clusters you accepted: the report's version rides along as a compare-and-set read off the run itself, and each member is revalidated against the content hash the report recorded, so a cluster whose documents moved comes back STALE and is left **entirely** alone rather than partly merged. It re-runs the base's consolidator agent per cluster, so a base without a deployed one is refused. `calibrate` records what a person reviewed a report under — it applies and publishes nothing, and is what reopens automatic source approval. |
157
159
  | `fruxon knowledge-bases source list/get/add/update/plan-update/preview/activity/passes/sync/pause/resume/archive/unarchive/fork/delete` | The pipelines that read a customer's history and write articles. `managementMode` decides **who owns the implementation**, which is what the verbs key on: a LINKED source's resources are yours and survive a `delete`, a MANAGED one's agents, automation and ledger go with it — and `fork` flips managed to linked, keeping the history, one-way. **Stopped is the default**: a built source lands paused and `unarchive` returns one paused, so `resume` and `sync` confirm (they start reading someone's history, and dispatched items cost a run each) while `pause` and `archive` do not. `preview` admits nothing and is deliberately not narrowed by the watermark — a caught-up source would otherwise look broken — and reports **floors, not totals**, once a ceiling stopped the scan. `activity` speaks the ledger's vocabulary, so `ready` is backlog rather than motion, and a missing work ledger is reduced health rather than an error. |
158
160
  | `fruxon assets search/documents/chunks` | Reproduce and inspect a retrieval. `search` runs the same query an agent's `search_assets` does (`--mode HYBRID/VECTOR/FULL_TEXT`, `--top-k`, fusion weights) — note `score` is on a different scale per mode, and in hybrid the top hit is always 1.0. `documents` lists what the index contains (a `chunkCount` of 0 means that file yielded no indexable text); `chunks` reads one document's chunks in order. The `documentId` these print is the **index's** id, which is what `chunks` takes — on a knowledge base's backing asset it is not the editable document's id, and it changes whenever a document's body is re-indexed. The `fileName` (`<knowledge document id>.md`) is the document a `knowledge_base` tool would edit. |
159
- | `fruxon metrics list` | Browse the evaluation-metric catalog — the ids a step's LLM-judge config (`judge.metrics[]`) binds, with a default weight each. |
161
+ | `fruxon metrics list` | Browse the evaluation-metric catalog — the ids an agent's quality check (`judge.metrics[]`) binds, with a default weight each. |
160
162
  | `fruxon triggers list/get/create/update/delete/fire/bind/unbind` | Manage triggers — the schedule/event sources that fire agents. `create`/`update` take `--file` (+`--schema`); `create` also takes `--application`, the Application that will own the trigger and must own everything it fires. `bind`/`unbind` wire which agents fire; `fire` runs it now (`--close-gathering` also runs every group a batch stage is gathering). `fire`/`delete`/`unbind` confirm + refuse without `--yes` in agent mode. The control plane for autonomy. `get` returns the full stored `workShape` and every `scheduleTimes` slot, so `get -o json | jq .workShape` produces a body `update --file` can post straight back — a PATCH replaces the whole shape, so edit what you read. Each binding carries its `outputDestination` (where a fired run's reply is delivered, e.g. a Telegram group) verbatim, and text output names it as `delivers to telegram -5129894975 (Daria Group)`; a `bindings` list posted back without it unpins the delivery. |
161
163
  | `fruxon triggers doors` | The pre-configured source doors a work shape can be built on — the **first** question of authoring an automation, and the one `--schema` cannot answer: it tells you a `workShape.door` carries a `doorId`, not which ids exist. Each row is the integration a chosen `integrationConfigId` must belong to, plus the one query field that is genuinely yours (label + example), and `supportsStartFrom` — whether that door's source accepts a lower bound, and so whether a `door.startFrom` horizon does anything there. `door.skipWhen` is the other field that is yours rather than the integration's: a `{match, conditions[]}` filter, same shape as a trigger's event filter, whose matching records are **never admitted** — no item, no stage, no model call. It has to live on the `door` because saving re-expands `source` from it, so the same filter written onto `source` is discarded by the next save. `door.projection` is the third: a list of dotted record paths naming the fields an admitted item **keeps**, with the door's own `defaultProjection` as the suggestion to seed it from — without one an item carries the whole record, into the Work grid and into every stage's prompt on every pass. Read that against the door's `labelPaths` (or, for the sheet and table doors, `labelFieldName` naming the field whose answer picks the label column): those are where an item's title comes from, and a projection that drops one is refused at save time rather than quietly repaired. Read-only and static: a door is a claim that one combination of list tool, identity path and watermark actually works, made in platform code by someone who verified it. An integration with no door is still fully usable — configure `workShape.source` by hand. |
162
- | `fruxon triggers preview-shape/test-source` | Dry-run a work shape before saving or activating it; both write nothing. `preview-shape` renders, per stage, the resolved instruction, the whole `user_query` the agent is handed (item payload included), and the JSON schema the stage's declared `produces` keys become — against a real backlog item when you name a trigger. `--file` takes a draft shape, or just the stages — the same file `set-stages` takes — which it merges into the named trigger's saved shape the way `set-stages` would, so the preview is of what that save would write. `test-source` runs the door's list tool once and says, per record, whether admission would take it, plus the exact arguments the tool was called with (for Gmail, your query with the day-granular `after:` folded in). Records a `door.skipWhen` filter would drop read `FILTERED`, with a footer count — which is where to see what a rule costs before it goes onto a live automation. `--count` pages the whole door instead and answers how much is waiting: the total, how many the filter drops, how many the ledger already holds, how many clear the watermark, and which server ceiling stopped the scan — so `total − filtered − already held` is the work that would actually arrive. Each record prints **as the item will carry it** — narrowed to the shape's `door.projection`, with an `item keeps` line naming the paths that survived, so a field your projection drops and a field the source never sent are not the same silence. `--file` tests a shape being edited — and a draft whose `door.startFrom` (Unix ms) sets a first-run horizon gets a `first run sends` line with the `after:` term that horizon produces, which is the only place to see it: `startFrom` seeds the ledger cursor once, when the ledger is created. |
164
+ | `fruxon triggers preview-shape/test-source` | Dry-run a work shape before saving or activating it; both write nothing. `preview-shape` renders, per stage, the resolved instruction, the whole `user_query` the agent is handed (item payload included), and the JSON schema the stage's declared `produces` keys become — against a real backlog item when you name a trigger. A FORM stage shows its form message and, under it, the `join notice` sent when more items join the open form, both in the stage's own `formClosing` / `formJoinNotice` wording. `--file` takes a draft shape, or just the stages — the same file `set-stages` takes — which it merges into the named trigger's saved shape the way `set-stages` would, so the preview is of what that save would write. `test-source` runs the door's list tool once and says, per record, whether admission would take it, plus the exact arguments the tool was called with (for Gmail, your query with the day-granular `after:` folded in). Records a `door.skipWhen` filter would drop read `FILTERED`, with a footer count — which is where to see what a rule costs before it goes onto a live automation. `--count` pages the whole door instead and answers how much is waiting: the total, how many the filter drops, how many the ledger already holds, how many clear the watermark, and which server ceiling stopped the scan — so `total − filtered − already held` is the work that would actually arrive. Each record prints **as the item will carry it** — narrowed to the shape's `door.projection`, with an `item keeps` line naming the paths that survived, so a field your projection drops and a field the source never sent are not the same silence. `--file` tests a shape being edited — and a draft whose `door.startFrom` (Unix ms) sets a first-run horizon gets a `first run sends` line with the `after:` term that horizon produces, which is the only place to see it: `startFrom` seeds the ledger cursor once, when the ledger is created. |
163
165
  | `fruxon triggers ledger funnel/items/cursor/transitions/effects/batches` | The work items a schedule trigger's work shape admitted. `funnel` counts per stage and status (every item is in exactly one cell). `items` is the grid — `--stage`, `--status`, `--ignore-reason`, `--item-key` (client-side; the endpoint has no such filter), each item's `label` (what the Work grid calls the row, as against the provider's opaque `itemKey`), and its accumulated `data`, which is where a stage's declared `produces` keys land. A listing is **one page of 20** that reports how many matched in total — a ledger's healthy state is thousands of rows served ten per request, so an unbounded walk is rate-limited long before the end; `-n` sizes the page and `--all` walks the lot. `--ignore-reason` narrows to the items one exclusion reason set aside — the same filter `redrive-many` selects by, matched whole and case-sensitively, so it is how you read a batch before putting it back. `--where field=value` (or `!=`, repeatable, dotted for a nested field) selects by what a stage wrote into `data` — `--where destination=SKIP` — matched whole and case-sensitively, as text and as the number, boolean or null it spells; the server filters and the CLI re-applies it, so an older server is slower, never wrong. `cursor` is the watermark and its freshness. `transitions` is one item's attempt history; `effects` is what it did to the outside world — an `UNKNOWN` row parks the item until someone settles it. `batches` is what an ASK stage is waiting on a human for — read its `delivery` first, because `UNDELIVERABLE` / `FAILED` mean nobody was asked and nobody will answer while the batch, the count and the stage all look healthy (`QUEUE_ONLY` is not a fault); a WAITING item's `batchId` is the join to it, and the pairing is total, so one `--open-only` listing resolves every waiting row. `batches --answers <batch>` is one question's answers item by item — what the person gave (`given`), what was handed back (`answer`), what the item holds now (`current`), with `mismatch` where the first two differ (a prose reply is every item's `given` at once, `givenIsReply`, and is never compared); a `given` the server did not record is read from the resolving run's sheet read (`givenFrom: "trace"`, `--no-trace` to skip). `batches --transcript <batch>` prints the conversation it was asked in, read under the agent that owns it — the stage's asking agent, not the desk that talks to the person. `funnel` and `items` also carry `cost` (all-in USD) and `runs` — so the funnel says *which stage* the money goes to, not merely how much. An item's `runs` spans every stage it passed through and so is not `stageAttempts`, which resets per stage; a `Cost` of `—` means nothing there has ever been claimed, as against `$0.0000`, which means it ran and was free. Cost is not sortable: the endpoint's `--order-by` allow-list does not carry it. |
164
166
  | `fruxon triggers ledger passes` | Every time the automation fired, newest first — `admitted` (what the door took), `dispatched` (what the pass claimed and sent to work, questions parked on a human included), `completed` (how many of *this pass's* dispatches reached the terminal stage), `runs`, and the pass's all-in USD. `runs` sits **below** `dispatched` while runs are in flight or when a dispatch went to a person rather than a model — that gap is normal, not a lost item. And a pass's `cost` **rises after** it fired, as those runs finish, so a pass read seconds later reports a floor rather than its bill. An `outcome` other than `FIRED` means no sweep happened: `FILTERED` / `COALESCED` are healthy (nothing to do, or folded into a pass already running), `FAILED` / `FILTER_ERROR` are not. `-n/--limit` bounds the walk. |
165
167
  | `fruxon triggers ledger spend/spend-series/item-series` | What the automation spent and what happened to its items, over time. `spend` is the **current UTC calendar** day, week (from Monday) or month (`--period`), so a weekly read on a Monday is hours of spend, not seven days — the figure the automation's spend chip shows: item runs, the conversations its questions are held in, and its report, one row per agent and part (`ITEM_RUNS` / `CONVERSATIONS` / `REPORTS` / `OTHER`). It is **not** the funnel's `cost`, which files item runs under the cell each item sits in *now*; the two overlap, so never add them. `spend-series` is the same figure per `day` or `week` over `--since`/`--until` (default the last 30 days), `--by-stage` adding a column per agent and part; it reads live run records, so a window older than the workspace's run history is counted from `countedFromMs` and the CLI says so. `--interval hour` is refused (tokens are priced per day), and a `week` bucket starts on a Thursday, so it does not line up with `spend --period weekly`. `item-series` counts admitted, finished, set aside and dead per `hour`, `day` or `week` — **events, not items**: a re-driven item that finishes again counts twice and an operator's ignore counts as set aside. Both series omit empty buckets; the footer says how many had anything. A server that predates the series reads 404s (exit 11). |
168
+ | `fruxon triggers check <trigger>` | Audit whether a work-shape automation can move its items, part by part in the order an item meets them: on and taking items in, the last pass read its source, each PROCESS stage's agent exists with a deployed revision and working credentials and models (a rejected credential's row says how many items failed there since), each ASK stage's contact is declared and bound on the asking agent and its oldest question has not waited past a fifth of its answer window, the outcome note's contact reaches a person, this week's spend against the weekly limit, and how many items died in the last 7 days. `{trigger, overall, checks[]}` like `agents check`; exits 12 only on a hard fail, warnings stay 0. Read-only. |
166
169
  | `fruxon triggers budget list/get/set/delete` | Spend limits on an automation **as a whole** (`--period DAILY/WEEKLY/MONTHLY`, UTC calendar windows), counting every production run it caused whichever agent made it. Once one is enforced they govern its runs, which then meet only these limits and not the agents' own; with none enforced, each stage stops at its agent's own limit. None by default and every one removable. An enforced limit refuses new work at the limit (stage runs, passes, a question's opening, the report) and holds the stages until the window resets, while turns of conversations it already opened still run (up to 110%); `--no-enforce` only alerts the Application's admins. Needs Editor. |
167
170
  | `fruxon triggers ledger redrive/ignore/settle/cancel-batch` | The operator verbs; all confirm and refuse without `--yes` in agent mode. `redrive` returns a DEAD or FAILED item to the queue with its attempts reset — settle any `UNKNOWN` effect first or the next attempt hits the same refusal. `ignore` excludes an item for a **required** reason, which is the only thing separating IGNORED from DEAD later. `settle` answers an effect the platform could not confirm (`--landed` / `--did-not-land` + `--reason`); wrong in the second direction duplicates the write. `cancel-batch` calls off a question — the items go back to READY and a later pass re-batches them. |
168
171
  | `fruxon triggers ledger set-data` | Correct a parked (DEAD / FAILED / IGNORED) item's `data` before re-driving it — the value a stage recorded wrong, such as an ASK answer written as `SKIP`, which a plain re-drive would route on again. `--set path=value` (or `path:=<json>`, repeatable) on a top-level key the item carries or a path the shape declares (`produces`, `answeredWhenPath`); a misspelled key is refused with the settable list rather than added. `--reason` is required and lands on the item's history beside every old → new value. `--redrive` re-drives in the same transaction; `redrive` and `redrive-many` take `--set` too. |
@@ -174,9 +177,9 @@ Integrations + tools:
174
177
  | `fruxon triggers ledger walk/walk-steps` | `walk --item <id> --yes` chains stage tests: the item's stage (or `--from`), then each stage it advances into, on the data it advanced with, until a real pass would stop too. Each ASK stage it reaches takes `--reply <stage>=<text>` or `--answer <stage>=<value>`; `--agent-revision <agent>=<N>` runs every stage of that agent on an undeployed revision. `stopped.code` is `COMPLETED`, `REACHED_UNTIL` (`--until`), `NOT_ADVANCED` (a `WAIT`, too, and an ADVANCE a pass would undo: a duplicate, or fields the next stage requires), `REPLY_NEEDED` (an ASK stage nobody answered), `STAGE_NOT_TESTABLE`, `MAX_STEPS` (default 10), `TIMEOUT` or `CANCELLED`. Every step is a paid run, so it confirms first and needs `--yes` in agent mode; it exits 0 whenever it ran. `walk-steps <trigger> <walk>` reads a walk back in step order. |
175
178
  | `fruxon triggers ledger reset-cursor/purge` | `reset-cursor` moves where the next scan starts, as a compare-and-swap on the version it just read (`--value` omitted = the door's own horizon); it never re-runs admitted work. It is the *after* to `workShape.door.startFrom`'s *before* — a horizon set at creation seeds the cursor before anything sweeps, and is ignored once a pass has read the source. `purge` starts the ledger over — cancels open questions, deletes items and effects, bumps the cursor version — and confirms the exact item count first. The items' transitions stay, as the ledger's history. Destructive; the effects' external targets are not touched. |
176
179
  | `fruxon triggers questions list/answer` | The human half of an ASK stage, tenant-wide. `ledger batches <trigger>` answers *what is this automation waiting on*; this answers *what is waiting on anyone* — soonest deadline first, which is how an undelivered question gets found at all. Read **`delivery`** first: `UNDELIVERABLE` (nobody reachable in the slot) and `FAILED` (a target resolved, the send did not land) both mean nobody was asked and nobody will answer unless you do — `QUEUE_ONLY` is healthy. `--undelivered-only` narrows to that pair; `--trigger` filters client-side (the endpoint has no such filter). `answer` settles one on the assignee's behalf and is `--yes`-gated: the reply interpreter **fails closed**, so an answer it cannot place advances nothing and the question stays open, and each item advances only once the stage's `answeredWhenPath` is filled — answering two of three advances two. |
177
- | `fruxon triggers set-stages <id> --file` | Replace an automation's stages without rewriting the rest of its work shape (a PATCH replaces the whole shape, so this reads it, splices, and posts). Takes a bare stages array or any document carrying one. Pre-flights the two refusals the server only reports after the write: live items at a stage your list drops, and a `door` that will re-expand `source` on save. `--dry-run` prints the merged shape and the warnings; `preview-shape <id> --file` takes the same file and renders what each stage would hand its agent. |
180
+ | `fruxon triggers set-stages <id> --file` | Replace an automation's stages without rewriting the rest of its work shape (a PATCH replaces the whole shape, so this reads it, splices, and posts). Takes a bare stages array or any document carrying one. Pre-flights the two refusals the server only reports after the write: live items at a stage your list drops, and a `door` that will re-expand `source` on save. `--dry-run` prints the merged shape and the warnings; `preview-shape <id> --file` takes the same file and renders what each stage would hand its agent. `--help` documents every stage field the server reads but the CLI passes through untouched — `routes`, `ignoreReasons`, a FORM stage's `answerField`, `formClosing` and `formJoinNotice`. |
178
181
  | `fruxon triggers revisions list/get/restore` | A trigger's audit history — every mutation writes a snapshot, and `changeReason` separates a config edit from a binding reconcile. `get -o json | jq .workShape` is the shape as it stood, which is how you undo an edit by hand. `restore` replays one and is **not** a universal undo: it runs the same validation a save does, so a snapshot that drops a stage still holding live items is refused. |
179
- | `fruxon secrets list/get/grants` | Discover tenant secrets a step can reference — the ids for `allowedSecretIds` and the `{{secret.KEY}}` names, with publish-state and per-agent grants. Metadata only; values are never returned. |
182
+ | `fruxon secrets list/get/grants` | Discover tenant secrets an agent can reference — the ids for `allowedSecretIds` and the `{{secret.KEY}}` names, with publish-state and per-agent grants. Metadata only; values are never returned. |
180
183
  | `fruxon participants list/get/create/update/delete/enable/disable` | Manage agent-network participants (people / groups / agents the network routes to). `list` takes `--query` (matches names **and channel addresses and account ids**, so a phone number or handle resolves to an id, and searches both directory tiers by default), `--kind`, `--tier`, `--agent`, `--environment-id`, and pages up to `--limit` / `--all`. `create`/`update` take `--file` (+`--schema`); `enable`/`disable` toggle routing; `delete` confirms + refuses without `--yes` in agent mode. |
181
184
  | `fruxon applications roles <id>` | The Application's roster roles — who holds each, who defers to it, and how many holders are `consultable`. The answer to what a `--role` contact binding leaves open: the server takes any role string, so a role with references and no holders refuses at delivery time. `--unheld-only` lists just those. Trust the consultable count — every role path, delivery and consult alike, resolves through the consult-allowed member edges, so a holder without one is tagged but unreachable. |
182
185
  | `fruxon applications list/get` | Inspect Applications — the container that owns agents, workflows and people. Every agent belongs to exactly one, and `agents create` needs its id, so this is where you find it. Creating and re-homing an Application itself stays in the dashboard. |
@@ -190,6 +193,30 @@ Integrations + tools:
190
193
  | `fruxon agents channels list` / `fruxon agents endpoints` | Inspect a networked agent's channel bindings and resolved provider endpoints (with bot identity). Superseded by `applications entry-points list`, which names the Application answering on each address; these read endpoints outside the published API. |
191
194
  | `fruxon skills list/show/create/update/delete` | Browse the tenant's product-skill catalog and author its own skills (resources that attach to agents at runtime). |
192
195
 
196
+ ### `fruxon dash` — the console
197
+
198
+ ![fruxon dash --demo](https://raw.githubusercontent.com/fruxon-ai/fruxon-sdk/develop/docs/tui/demo.gif)
199
+
200
+ A read-only, full-screen console for one Application. Home has the week's runs,
201
+ spend and finished items, every agent with its share of the spend, every
202
+ automation with what it holds, and what needs a person; Enter opens an agent
203
+ (overview, the `agents check` rows plus credential, model and budget health,
204
+ runs and their traces) or an automation (its stages, items per day, what
205
+ failed, the `triggers check` rows, a source test and a stage preview). Live
206
+ parts refresh every 5 seconds, totals every minute. It changes nothing.
207
+
208
+ ```bash
209
+ pip install "fruxon[tui]" # the console runs on Textual, an optional extra
210
+ fruxon dash # the Application with the most agents
211
+ fruxon dash --app "Support & Ops"
212
+ fruxon dash --demo # a made-up workspace, for a first look or a recording
213
+ ```
214
+
215
+ Keys: `↑↓` move, `enter` open, `tab` next panel, `esc` back, `1 2 3` tabs,
216
+ `7 3 1` (or `w` on a tabbed screen) the time window, `a` another Application,
217
+ `r` refresh, `?` every key, `q` quit. It needs a terminal, so agent mode and
218
+ piped output are refused (exit 16).
219
+
193
220
  ### `fruxon agents draft run` — examples
194
221
 
195
222
  ```bash
@@ -272,9 +299,11 @@ schema = client.get_agent_parameter_metadata("support-agent", revision=1)
272
299
  for p in schema["metadata"]:
273
300
  print(p["name"], p["type"], "required" if p.get("required") else "optional")
274
301
 
275
- # Test a draft flow without publishing it (same result shape as execute)
276
- result = client.test("support-agent", {"flow": {...}, "baseRevision": 3, "parameters": {...}})
277
- for event in client.stream_test("support-agent", {"flow": {...}}):
302
+ # Test a draft without publishing it (same result shape as execute):
303
+ # save the draft on top of revision 3, then run it
304
+ client.put_draft("support-agent", 3, {"definition": {...}, "parametersMetadata": [...]})
305
+ result = client.test("support-agent", 3, {"parameters": {"question": "Hi"}})
306
+ for event in client.stream_test("support-agent", 3, {"parameters": {"question": "Hi"}}):
278
307
  ...
279
308
 
280
309
  # Integrations & tools — the connections + capabilities agents are built from
@@ -47,7 +47,7 @@ Auth:
47
47
 
48
48
  | Command | What it does |
49
49
  | --- | --- |
50
- | `fruxon login` | Browser-based sign-in. token goes to your OS keychain (Keychain on macOS, Secret Service on Linux, Credential Manager on Windows). `--token $KEY` for headless. `--preset observer\|operator\|developer\|maintainer\|admin` declares the access level the key is minted with (default: developer); the approval page shows it for the user to approve or deny. |
50
+ | `fruxon login` | Browser-based sign-in. token goes to your OS keychain (Keychain on macOS, Secret Service on Linux, Credential Manager on Windows). `--token $KEY` for headless. `--preset observer\|operator\|developer\|maintainer\|admin` declares the access level the key is minted with (default: developer); the approval page shows it for the user to approve or deny. Adds a workspace without moving the default (the first sign-in sets it); `--set-default` moves it. |
51
51
  | `fruxon whoami` | Show the active key/workspace and where each value came from (flag, env, keychain, file). |
52
52
  | `fruxon logout` | Forget stored credentials. `--workspace <id>` signs out of one and leaves the rest; `--all` clears every one. |
53
53
  | `fruxon workspaces list` | Which workspaces this machine is signed in to, each with its host and cell, and which one is the default. Offline, no auth. |
@@ -63,7 +63,7 @@ Execution:
63
63
  | `fruxon agents executions get <agent> <record-id>` | Record summary for one execution — status, duration, cost, tokens. |
64
64
  | `fruxon agents executions trace <agent> <record-id>` | Step-by-step trace of one execution — LLM/tool calls, durations, and for a tool that records it, where the call's time went (service stages vs. unaccounted). Flags a result the model got offloaded or projected. Prints a quality check's verdict (score, criteria, rationale) under its `JUDGE` step, and marks a reply the check refused. `--show-io` also prints each tool step's parameters and result (the payload lives at `steps[].result`, a sibling of `toolTrace` — whose `resultDelivery` carries sizes only). `--show-system` labels each LLM step's system prompt block by block — your prompt vs. the platform's `<tag>` blocks, sizes, and the cache breakpoint. |
65
65
  | `fruxon agents executions result <agent> <record-id>` | The final output one execution produced. |
66
- | `fruxon agents approvals list/get/respond/cancel <agent>` | Operate a step's human-in-the-loop gate — see what a run is blocked on and answer it. `respond`/`cancel` confirm first and refuse without `--yes` in agent mode (an LLM must not silently auto-approve). |
66
+ | `fruxon agents approvals list/get/respond/cancel <agent>` | Operate human-in-the-loop approval gates — see what a run is blocked on and answer it. `respond`/`cancel` confirm first and refuse without `--yes` in agent mode (an LLM must not silently auto-approve). |
67
67
  | `fruxon agents memory list/subjects/get/forget-subject <agent>` | Inspect and prune what an agent remembers. `list` filters server-side by `--subject`/`--query`/`--scope`; `forget-subject` is a GDPR-style delete (confirms; refuses without `--yes` in agent mode). |
68
68
  | `fruxon agents topics list/search/get/messages/sessions <agent>` + `fruxon agents inbox <agent>` | Read the conversation spine — the agent's topics (threads), one topic's transcript, its episodes, and its inbox (what it's focused on). `list`/`search` filter server-side by `--state`/`--participant`/`--query`. Read-only. Each message carries `author` — a human colleague's turn is stored as `ASSISTANT` on purpose, so `role` alone cannot tell their reply from the agent's; `HUMAN_OPERATOR` can. A message the runtime *withheld* is in no transcript at all — see `fruxon messages list`. `sessions` explains a fresh, blind session: a topic outlives its sessions, and `endCause` on the previous one (`CONSOLE_ALREADY_RESOLVED` — a human closed it in the provider's console) is why the next started with no history. |
69
69
  | `fruxon escalations list/history/unhandled/policy/ack` | What happened when an agent handed a conversation to a person. `list` is the queue (live takeovers + degraded escalations nobody was reachable for); `unhandled` is the append-only ledger under it. **`history` is the record of closed console-routed takeovers** — those leave the queue, so `list --status ALL` answers `[]` about a conversation a human worked for half an hour. Read `humanObservedAt` before believing a `RESOLVED`: null means nobody was ever seen picking it up. `policy <agent>` resolves who the *next* escalation would reach — an empty ladder under `NATIVE_HANDOFF` is correct, the console supplies the human. |
@@ -113,8 +113,8 @@ Integrations + tools:
113
113
  | `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
114
  | `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
115
  | `fruxon llm-providers list/get/models` | Browse LLM providers and models supported at the tenant level. |
116
- | `fruxon llm-providers configs list/get/test/publish` | The workspace's saved LLM credentials — the `provider.tenantConfigId` a step 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
- | `fruxon assets create/list/get/wait/operations/delete` | Manage knowledge-base (RAG) assets a step 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. |
116
+ | `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
+ | `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
118
  | `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. |
119
119
  | `fruxon knowledge-bases usage <kb>` | Whether agents actually use the base: searches per hour, day or week (`--interval`), the passage tokens each search handed the agent, how many found nothing (split from searches that **could not fully search** it — index missing or still building, a phrasing failed), and which agents searched (up to 20 you can open, the rest counted together). `--origin test` counts test runs instead of production. Retrieved tokens are counted by the search index, so what the model read can be less when a large result is trimmed or offloaded. Searches are recorded from 15 October 2026 and kept 90 days at most, so a window reaching further back says where counting began rather than showing empty days. A 404 is a wrong id or a backend that predates the read. |
120
120
  | `fruxon knowledge-bases notices/notice/post-notice/edit-notice/extend-notice/withdraw-notice` | Time-boxed notices on a base — an outage being fixed, a known bug, a closure. **Not documents**: never indexed or published, so every agent attaching the base is given each active notice whole from its next turn. `post-notice --valid-for 4h` (or `--until`) — every notice ends, at most 30 days after it was posted, and a base holds ten active ones (409 `notice_limit` / `notice_too_long`, each with its remedy). The body is Markdown, at most 1,200 characters, best as four headings: *What is happening*, *Applies when*, *Tell the customer*, *Do not*. A time without an offset is read in the notice's zone (`--time-zone`, else this machine's), which is also the zone agents tell customers the end in. `withdraw-notice` tells each conversation that was given it, on its next turn, that it ended; it confirms, and needs `--yes` in agent mode. An ended notice is not extended: `post-notice --from <id>` posts it again as a new one. |
@@ -123,13 +123,14 @@ Integrations + tools:
123
123
  | `fruxon knowledge-bases consolidate` → `apply` / `calibrate` | Two steps on purpose. `consolidate` queues a **report** of near-duplicate clusters and writes nothing — a standalone pass can no longer merge as it goes, and the old `--apply` is refused. `apply <kb> <run> --cluster <id>` (or `--all`) merges the clusters you accepted: the report's version rides along as a compare-and-set read off the run itself, and each member is revalidated against the content hash the report recorded, so a cluster whose documents moved comes back STALE and is left **entirely** alone rather than partly merged. It re-runs the base's consolidator agent per cluster, so a base without a deployed one is refused. `calibrate` records what a person reviewed a report under — it applies and publishes nothing, and is what reopens automatic source approval. |
124
124
  | `fruxon knowledge-bases source list/get/add/update/plan-update/preview/activity/passes/sync/pause/resume/archive/unarchive/fork/delete` | The pipelines that read a customer's history and write articles. `managementMode` decides **who owns the implementation**, which is what the verbs key on: a LINKED source's resources are yours and survive a `delete`, a MANAGED one's agents, automation and ledger go with it — and `fork` flips managed to linked, keeping the history, one-way. **Stopped is the default**: a built source lands paused and `unarchive` returns one paused, so `resume` and `sync` confirm (they start reading someone's history, and dispatched items cost a run each) while `pause` and `archive` do not. `preview` admits nothing and is deliberately not narrowed by the watermark — a caught-up source would otherwise look broken — and reports **floors, not totals**, once a ceiling stopped the scan. `activity` speaks the ledger's vocabulary, so `ready` is backlog rather than motion, and a missing work ledger is reduced health rather than an error. |
125
125
  | `fruxon assets search/documents/chunks` | Reproduce and inspect a retrieval. `search` runs the same query an agent's `search_assets` does (`--mode HYBRID/VECTOR/FULL_TEXT`, `--top-k`, fusion weights) — note `score` is on a different scale per mode, and in hybrid the top hit is always 1.0. `documents` lists what the index contains (a `chunkCount` of 0 means that file yielded no indexable text); `chunks` reads one document's chunks in order. The `documentId` these print is the **index's** id, which is what `chunks` takes — on a knowledge base's backing asset it is not the editable document's id, and it changes whenever a document's body is re-indexed. The `fileName` (`<knowledge document id>.md`) is the document a `knowledge_base` tool would edit. |
126
- | `fruxon metrics list` | Browse the evaluation-metric catalog — the ids a step's LLM-judge config (`judge.metrics[]`) binds, with a default weight each. |
126
+ | `fruxon metrics list` | Browse the evaluation-metric catalog — the ids an agent's quality check (`judge.metrics[]`) binds, with a default weight each. |
127
127
  | `fruxon triggers list/get/create/update/delete/fire/bind/unbind` | Manage triggers — the schedule/event sources that fire agents. `create`/`update` take `--file` (+`--schema`); `create` also takes `--application`, the Application that will own the trigger and must own everything it fires. `bind`/`unbind` wire which agents fire; `fire` runs it now (`--close-gathering` also runs every group a batch stage is gathering). `fire`/`delete`/`unbind` confirm + refuse without `--yes` in agent mode. The control plane for autonomy. `get` returns the full stored `workShape` and every `scheduleTimes` slot, so `get -o json | jq .workShape` produces a body `update --file` can post straight back — a PATCH replaces the whole shape, so edit what you read. Each binding carries its `outputDestination` (where a fired run's reply is delivered, e.g. a Telegram group) verbatim, and text output names it as `delivers to telegram -5129894975 (Daria Group)`; a `bindings` list posted back without it unpins the delivery. |
128
128
  | `fruxon triggers doors` | The pre-configured source doors a work shape can be built on — the **first** question of authoring an automation, and the one `--schema` cannot answer: it tells you a `workShape.door` carries a `doorId`, not which ids exist. Each row is the integration a chosen `integrationConfigId` must belong to, plus the one query field that is genuinely yours (label + example), and `supportsStartFrom` — whether that door's source accepts a lower bound, and so whether a `door.startFrom` horizon does anything there. `door.skipWhen` is the other field that is yours rather than the integration's: a `{match, conditions[]}` filter, same shape as a trigger's event filter, whose matching records are **never admitted** — no item, no stage, no model call. It has to live on the `door` because saving re-expands `source` from it, so the same filter written onto `source` is discarded by the next save. `door.projection` is the third: a list of dotted record paths naming the fields an admitted item **keeps**, with the door's own `defaultProjection` as the suggestion to seed it from — without one an item carries the whole record, into the Work grid and into every stage's prompt on every pass. Read that against the door's `labelPaths` (or, for the sheet and table doors, `labelFieldName` naming the field whose answer picks the label column): those are where an item's title comes from, and a projection that drops one is refused at save time rather than quietly repaired. Read-only and static: a door is a claim that one combination of list tool, identity path and watermark actually works, made in platform code by someone who verified it. An integration with no door is still fully usable — configure `workShape.source` by hand. |
129
- | `fruxon triggers preview-shape/test-source` | Dry-run a work shape before saving or activating it; both write nothing. `preview-shape` renders, per stage, the resolved instruction, the whole `user_query` the agent is handed (item payload included), and the JSON schema the stage's declared `produces` keys become — against a real backlog item when you name a trigger. `--file` takes a draft shape, or just the stages — the same file `set-stages` takes — which it merges into the named trigger's saved shape the way `set-stages` would, so the preview is of what that save would write. `test-source` runs the door's list tool once and says, per record, whether admission would take it, plus the exact arguments the tool was called with (for Gmail, your query with the day-granular `after:` folded in). Records a `door.skipWhen` filter would drop read `FILTERED`, with a footer count — which is where to see what a rule costs before it goes onto a live automation. `--count` pages the whole door instead and answers how much is waiting: the total, how many the filter drops, how many the ledger already holds, how many clear the watermark, and which server ceiling stopped the scan — so `total − filtered − already held` is the work that would actually arrive. Each record prints **as the item will carry it** — narrowed to the shape's `door.projection`, with an `item keeps` line naming the paths that survived, so a field your projection drops and a field the source never sent are not the same silence. `--file` tests a shape being edited — and a draft whose `door.startFrom` (Unix ms) sets a first-run horizon gets a `first run sends` line with the `after:` term that horizon produces, which is the only place to see it: `startFrom` seeds the ledger cursor once, when the ledger is created. |
129
+ | `fruxon triggers preview-shape/test-source` | Dry-run a work shape before saving or activating it; both write nothing. `preview-shape` renders, per stage, the resolved instruction, the whole `user_query` the agent is handed (item payload included), and the JSON schema the stage's declared `produces` keys become — against a real backlog item when you name a trigger. A FORM stage shows its form message and, under it, the `join notice` sent when more items join the open form, both in the stage's own `formClosing` / `formJoinNotice` wording. `--file` takes a draft shape, or just the stages — the same file `set-stages` takes — which it merges into the named trigger's saved shape the way `set-stages` would, so the preview is of what that save would write. `test-source` runs the door's list tool once and says, per record, whether admission would take it, plus the exact arguments the tool was called with (for Gmail, your query with the day-granular `after:` folded in). Records a `door.skipWhen` filter would drop read `FILTERED`, with a footer count — which is where to see what a rule costs before it goes onto a live automation. `--count` pages the whole door instead and answers how much is waiting: the total, how many the filter drops, how many the ledger already holds, how many clear the watermark, and which server ceiling stopped the scan — so `total − filtered − already held` is the work that would actually arrive. Each record prints **as the item will carry it** — narrowed to the shape's `door.projection`, with an `item keeps` line naming the paths that survived, so a field your projection drops and a field the source never sent are not the same silence. `--file` tests a shape being edited — and a draft whose `door.startFrom` (Unix ms) sets a first-run horizon gets a `first run sends` line with the `after:` term that horizon produces, which is the only place to see it: `startFrom` seeds the ledger cursor once, when the ledger is created. |
130
130
  | `fruxon triggers ledger funnel/items/cursor/transitions/effects/batches` | The work items a schedule trigger's work shape admitted. `funnel` counts per stage and status (every item is in exactly one cell). `items` is the grid — `--stage`, `--status`, `--ignore-reason`, `--item-key` (client-side; the endpoint has no such filter), each item's `label` (what the Work grid calls the row, as against the provider's opaque `itemKey`), and its accumulated `data`, which is where a stage's declared `produces` keys land. A listing is **one page of 20** that reports how many matched in total — a ledger's healthy state is thousands of rows served ten per request, so an unbounded walk is rate-limited long before the end; `-n` sizes the page and `--all` walks the lot. `--ignore-reason` narrows to the items one exclusion reason set aside — the same filter `redrive-many` selects by, matched whole and case-sensitively, so it is how you read a batch before putting it back. `--where field=value` (or `!=`, repeatable, dotted for a nested field) selects by what a stage wrote into `data` — `--where destination=SKIP` — matched whole and case-sensitively, as text and as the number, boolean or null it spells; the server filters and the CLI re-applies it, so an older server is slower, never wrong. `cursor` is the watermark and its freshness. `transitions` is one item's attempt history; `effects` is what it did to the outside world — an `UNKNOWN` row parks the item until someone settles it. `batches` is what an ASK stage is waiting on a human for — read its `delivery` first, because `UNDELIVERABLE` / `FAILED` mean nobody was asked and nobody will answer while the batch, the count and the stage all look healthy (`QUEUE_ONLY` is not a fault); a WAITING item's `batchId` is the join to it, and the pairing is total, so one `--open-only` listing resolves every waiting row. `batches --answers <batch>` is one question's answers item by item — what the person gave (`given`), what was handed back (`answer`), what the item holds now (`current`), with `mismatch` where the first two differ (a prose reply is every item's `given` at once, `givenIsReply`, and is never compared); a `given` the server did not record is read from the resolving run's sheet read (`givenFrom: "trace"`, `--no-trace` to skip). `batches --transcript <batch>` prints the conversation it was asked in, read under the agent that owns it — the stage's asking agent, not the desk that talks to the person. `funnel` and `items` also carry `cost` (all-in USD) and `runs` — so the funnel says *which stage* the money goes to, not merely how much. An item's `runs` spans every stage it passed through and so is not `stageAttempts`, which resets per stage; a `Cost` of `—` means nothing there has ever been claimed, as against `$0.0000`, which means it ran and was free. Cost is not sortable: the endpoint's `--order-by` allow-list does not carry it. |
131
131
  | `fruxon triggers ledger passes` | Every time the automation fired, newest first — `admitted` (what the door took), `dispatched` (what the pass claimed and sent to work, questions parked on a human included), `completed` (how many of *this pass's* dispatches reached the terminal stage), `runs`, and the pass's all-in USD. `runs` sits **below** `dispatched` while runs are in flight or when a dispatch went to a person rather than a model — that gap is normal, not a lost item. And a pass's `cost` **rises after** it fired, as those runs finish, so a pass read seconds later reports a floor rather than its bill. An `outcome` other than `FIRED` means no sweep happened: `FILTERED` / `COALESCED` are healthy (nothing to do, or folded into a pass already running), `FAILED` / `FILTER_ERROR` are not. `-n/--limit` bounds the walk. |
132
132
  | `fruxon triggers ledger spend/spend-series/item-series` | What the automation spent and what happened to its items, over time. `spend` is the **current UTC calendar** day, week (from Monday) or month (`--period`), so a weekly read on a Monday is hours of spend, not seven days — the figure the automation's spend chip shows: item runs, the conversations its questions are held in, and its report, one row per agent and part (`ITEM_RUNS` / `CONVERSATIONS` / `REPORTS` / `OTHER`). It is **not** the funnel's `cost`, which files item runs under the cell each item sits in *now*; the two overlap, so never add them. `spend-series` is the same figure per `day` or `week` over `--since`/`--until` (default the last 30 days), `--by-stage` adding a column per agent and part; it reads live run records, so a window older than the workspace's run history is counted from `countedFromMs` and the CLI says so. `--interval hour` is refused (tokens are priced per day), and a `week` bucket starts on a Thursday, so it does not line up with `spend --period weekly`. `item-series` counts admitted, finished, set aside and dead per `hour`, `day` or `week` — **events, not items**: a re-driven item that finishes again counts twice and an operator's ignore counts as set aside. Both series omit empty buckets; the footer says how many had anything. A server that predates the series reads 404s (exit 11). |
133
+ | `fruxon triggers check <trigger>` | Audit whether a work-shape automation can move its items, part by part in the order an item meets them: on and taking items in, the last pass read its source, each PROCESS stage's agent exists with a deployed revision and working credentials and models (a rejected credential's row says how many items failed there since), each ASK stage's contact is declared and bound on the asking agent and its oldest question has not waited past a fifth of its answer window, the outcome note's contact reaches a person, this week's spend against the weekly limit, and how many items died in the last 7 days. `{trigger, overall, checks[]}` like `agents check`; exits 12 only on a hard fail, warnings stay 0. Read-only. |
133
134
  | `fruxon triggers budget list/get/set/delete` | Spend limits on an automation **as a whole** (`--period DAILY/WEEKLY/MONTHLY`, UTC calendar windows), counting every production run it caused whichever agent made it. Once one is enforced they govern its runs, which then meet only these limits and not the agents' own; with none enforced, each stage stops at its agent's own limit. None by default and every one removable. An enforced limit refuses new work at the limit (stage runs, passes, a question's opening, the report) and holds the stages until the window resets, while turns of conversations it already opened still run (up to 110%); `--no-enforce` only alerts the Application's admins. Needs Editor. |
134
135
  | `fruxon triggers ledger redrive/ignore/settle/cancel-batch` | The operator verbs; all confirm and refuse without `--yes` in agent mode. `redrive` returns a DEAD or FAILED item to the queue with its attempts reset — settle any `UNKNOWN` effect first or the next attempt hits the same refusal. `ignore` excludes an item for a **required** reason, which is the only thing separating IGNORED from DEAD later. `settle` answers an effect the platform could not confirm (`--landed` / `--did-not-land` + `--reason`); wrong in the second direction duplicates the write. `cancel-batch` calls off a question — the items go back to READY and a later pass re-batches them. |
135
136
  | `fruxon triggers ledger set-data` | Correct a parked (DEAD / FAILED / IGNORED) item's `data` before re-driving it — the value a stage recorded wrong, such as an ASK answer written as `SKIP`, which a plain re-drive would route on again. `--set path=value` (or `path:=<json>`, repeatable) on a top-level key the item carries or a path the shape declares (`produces`, `answeredWhenPath`); a misspelled key is refused with the settable list rather than added. `--reason` is required and lands on the item's history beside every old → new value. `--redrive` re-drives in the same transaction; `redrive` and `redrive-many` take `--set` too. |
@@ -141,9 +142,9 @@ Integrations + tools:
141
142
  | `fruxon triggers ledger walk/walk-steps` | `walk --item <id> --yes` chains stage tests: the item's stage (or `--from`), then each stage it advances into, on the data it advanced with, until a real pass would stop too. Each ASK stage it reaches takes `--reply <stage>=<text>` or `--answer <stage>=<value>`; `--agent-revision <agent>=<N>` runs every stage of that agent on an undeployed revision. `stopped.code` is `COMPLETED`, `REACHED_UNTIL` (`--until`), `NOT_ADVANCED` (a `WAIT`, too, and an ADVANCE a pass would undo: a duplicate, or fields the next stage requires), `REPLY_NEEDED` (an ASK stage nobody answered), `STAGE_NOT_TESTABLE`, `MAX_STEPS` (default 10), `TIMEOUT` or `CANCELLED`. Every step is a paid run, so it confirms first and needs `--yes` in agent mode; it exits 0 whenever it ran. `walk-steps <trigger> <walk>` reads a walk back in step order. |
142
143
  | `fruxon triggers ledger reset-cursor/purge` | `reset-cursor` moves where the next scan starts, as a compare-and-swap on the version it just read (`--value` omitted = the door's own horizon); it never re-runs admitted work. It is the *after* to `workShape.door.startFrom`'s *before* — a horizon set at creation seeds the cursor before anything sweeps, and is ignored once a pass has read the source. `purge` starts the ledger over — cancels open questions, deletes items and effects, bumps the cursor version — and confirms the exact item count first. The items' transitions stay, as the ledger's history. Destructive; the effects' external targets are not touched. |
143
144
  | `fruxon triggers questions list/answer` | The human half of an ASK stage, tenant-wide. `ledger batches <trigger>` answers *what is this automation waiting on*; this answers *what is waiting on anyone* — soonest deadline first, which is how an undelivered question gets found at all. Read **`delivery`** first: `UNDELIVERABLE` (nobody reachable in the slot) and `FAILED` (a target resolved, the send did not land) both mean nobody was asked and nobody will answer unless you do — `QUEUE_ONLY` is healthy. `--undelivered-only` narrows to that pair; `--trigger` filters client-side (the endpoint has no such filter). `answer` settles one on the assignee's behalf and is `--yes`-gated: the reply interpreter **fails closed**, so an answer it cannot place advances nothing and the question stays open, and each item advances only once the stage's `answeredWhenPath` is filled — answering two of three advances two. |
144
- | `fruxon triggers set-stages <id> --file` | Replace an automation's stages without rewriting the rest of its work shape (a PATCH replaces the whole shape, so this reads it, splices, and posts). Takes a bare stages array or any document carrying one. Pre-flights the two refusals the server only reports after the write: live items at a stage your list drops, and a `door` that will re-expand `source` on save. `--dry-run` prints the merged shape and the warnings; `preview-shape <id> --file` takes the same file and renders what each stage would hand its agent. |
145
+ | `fruxon triggers set-stages <id> --file` | Replace an automation's stages without rewriting the rest of its work shape (a PATCH replaces the whole shape, so this reads it, splices, and posts). Takes a bare stages array or any document carrying one. Pre-flights the two refusals the server only reports after the write: live items at a stage your list drops, and a `door` that will re-expand `source` on save. `--dry-run` prints the merged shape and the warnings; `preview-shape <id> --file` takes the same file and renders what each stage would hand its agent. `--help` documents every stage field the server reads but the CLI passes through untouched — `routes`, `ignoreReasons`, a FORM stage's `answerField`, `formClosing` and `formJoinNotice`. |
145
146
  | `fruxon triggers revisions list/get/restore` | A trigger's audit history — every mutation writes a snapshot, and `changeReason` separates a config edit from a binding reconcile. `get -o json | jq .workShape` is the shape as it stood, which is how you undo an edit by hand. `restore` replays one and is **not** a universal undo: it runs the same validation a save does, so a snapshot that drops a stage still holding live items is refused. |
146
- | `fruxon secrets list/get/grants` | Discover tenant secrets a step can reference — the ids for `allowedSecretIds` and the `{{secret.KEY}}` names, with publish-state and per-agent grants. Metadata only; values are never returned. |
147
+ | `fruxon secrets list/get/grants` | Discover tenant secrets an agent can reference — the ids for `allowedSecretIds` and the `{{secret.KEY}}` names, with publish-state and per-agent grants. Metadata only; values are never returned. |
147
148
  | `fruxon participants list/get/create/update/delete/enable/disable` | Manage agent-network participants (people / groups / agents the network routes to). `list` takes `--query` (matches names **and channel addresses and account ids**, so a phone number or handle resolves to an id, and searches both directory tiers by default), `--kind`, `--tier`, `--agent`, `--environment-id`, and pages up to `--limit` / `--all`. `create`/`update` take `--file` (+`--schema`); `enable`/`disable` toggle routing; `delete` confirms + refuses without `--yes` in agent mode. |
148
149
  | `fruxon applications roles <id>` | The Application's roster roles — who holds each, who defers to it, and how many holders are `consultable`. The answer to what a `--role` contact binding leaves open: the server takes any role string, so a role with references and no holders refuses at delivery time. `--unheld-only` lists just those. Trust the consultable count — every role path, delivery and consult alike, resolves through the consult-allowed member edges, so a holder without one is tagged but unreachable. |
149
150
  | `fruxon applications list/get` | Inspect Applications — the container that owns agents, workflows and people. Every agent belongs to exactly one, and `agents create` needs its id, so this is where you find it. Creating and re-homing an Application itself stays in the dashboard. |
@@ -157,6 +158,30 @@ Integrations + tools:
157
158
  | `fruxon agents channels list` / `fruxon agents endpoints` | Inspect a networked agent's channel bindings and resolved provider endpoints (with bot identity). Superseded by `applications entry-points list`, which names the Application answering on each address; these read endpoints outside the published API. |
158
159
  | `fruxon skills list/show/create/update/delete` | Browse the tenant's product-skill catalog and author its own skills (resources that attach to agents at runtime). |
159
160
 
161
+ ### `fruxon dash` — the console
162
+
163
+ ![fruxon dash --demo](https://raw.githubusercontent.com/fruxon-ai/fruxon-sdk/develop/docs/tui/demo.gif)
164
+
165
+ A read-only, full-screen console for one Application. Home has the week's runs,
166
+ spend and finished items, every agent with its share of the spend, every
167
+ automation with what it holds, and what needs a person; Enter opens an agent
168
+ (overview, the `agents check` rows plus credential, model and budget health,
169
+ runs and their traces) or an automation (its stages, items per day, what
170
+ failed, the `triggers check` rows, a source test and a stage preview). Live
171
+ parts refresh every 5 seconds, totals every minute. It changes nothing.
172
+
173
+ ```bash
174
+ pip install "fruxon[tui]" # the console runs on Textual, an optional extra
175
+ fruxon dash # the Application with the most agents
176
+ fruxon dash --app "Support & Ops"
177
+ fruxon dash --demo # a made-up workspace, for a first look or a recording
178
+ ```
179
+
180
+ Keys: `↑↓` move, `enter` open, `tab` next panel, `esc` back, `1 2 3` tabs,
181
+ `7 3 1` (or `w` on a tabbed screen) the time window, `a` another Application,
182
+ `r` refresh, `?` every key, `q` quit. It needs a terminal, so agent mode and
183
+ piped output are refused (exit 16).
184
+
160
185
  ### `fruxon agents draft run` — examples
161
186
 
162
187
  ```bash
@@ -239,9 +264,11 @@ schema = client.get_agent_parameter_metadata("support-agent", revision=1)
239
264
  for p in schema["metadata"]:
240
265
  print(p["name"], p["type"], "required" if p.get("required") else "optional")
241
266
 
242
- # Test a draft flow without publishing it (same result shape as execute)
243
- result = client.test("support-agent", {"flow": {...}, "baseRevision": 3, "parameters": {...}})
244
- for event in client.stream_test("support-agent", {"flow": {...}}):
267
+ # Test a draft without publishing it (same result shape as execute):
268
+ # save the draft on top of revision 3, then run it
269
+ client.put_draft("support-agent", 3, {"definition": {...}, "parametersMetadata": [...]})
270
+ result = client.test("support-agent", 3, {"parameters": {"question": "Hi"}})
271
+ for event in client.stream_test("support-agent", 3, {"parameters": {"question": "Hi"}}):
245
272
  ...
246
273
 
247
274
  # Integrations & tools — the connections + capabilities agents are built from
@@ -92,6 +92,12 @@ dependencies = [
92
92
  requires-python = ">= 3.10"
93
93
 
94
94
  [project.optional-dependencies]
95
+ # ``fruxon dash`` — the read-only, full-screen console. Kept out of the base
96
+ # install so ``pip install fruxon`` stays as small as it was and the CLI's
97
+ # start-up never pays for it: the console imports Textual only when it runs.
98
+ tui = [
99
+ "textual>=8.2",
100
+ ]
95
101
  test = [
96
102
  "coverage", # testing
97
103
  "pytest", # testing
@@ -52,6 +52,7 @@ def context_for(base_url: str) -> ssl.SSLContext | None:
52
52
  """
53
53
  host = urllib.parse.urlparse(base_url).hostname or ""
54
54
  if host in _LOOPBACK_HOSTS:
55
+ # nosemgrep: unverified-ssl-context — loopback hosts only; the request never leaves this machine
55
56
  return ssl._create_unverified_context() # noqa: S323 — loopback only
56
57
 
57
58
  bundle = os.environ.get("FRUXON_CA_BUNDLE", "").strip()
@@ -62,6 +63,7 @@ def context_for(base_url: str) -> ssl.SSLContext | None:
62
63
 
63
64
  if _insecure_enabled():
64
65
  _warn_insecure_once(host)
66
+ # nosemgrep: unverified-ssl-context — explicit FRUXON_INSECURE=1 opt-in, warned about on stderr
65
67
  return ssl._create_unverified_context() # noqa: S323 — opt-in
66
68
 
67
69
  return _default_or_certifi_context()
@@ -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 = '1.0.0'
22
- __version_tuple__ = version_tuple = (1, 0, 0)
21
+ __version__ = version = '2.0.1'
22
+ __version_tuple__ = version_tuple = (2, 0, 1)
23
23
 
24
24
  __commit_id__ = commit_id = None
@@ -0,0 +1,101 @@
1
+ """Read-only audits shared by the CLI and the ``fruxon dash`` console.
2
+
3
+ A check reads a resource and the surfaces around it and says, row by row,
4
+ whether it is wired to run. The rows are plain data: :class:`CheckResult`
5
+ carries no markup and decides nothing about how it is shown, so
6
+ ``fruxon agents check`` prints them as text or JSON and the console lays
7
+ them out in a panel, and both say the same thing because they ran the same
8
+ code.
9
+
10
+ Nothing here writes. Nothing here imports :mod:`fruxon.cli`, either — the
11
+ console is built without the CLI, and a check that reached into it would
12
+ drag the whole command tree in with it.
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ from dataclasses import dataclass, field
18
+
19
+ # The four outcomes a row can have. ``skip`` is "nothing to check here" (no
20
+ # consult step, so no roster to audit), which is a pass, not a warning.
21
+ STATUSES = ("ok", "warn", "fail", "skip")
22
+
23
+
24
+ @dataclass(frozen=True)
25
+ class CheckResult:
26
+ """One audited surface: what was checked, how it came out, and what to do.
27
+
28
+ ``detail`` is what was found, in a sentence. ``hint`` is the next step —
29
+ usually a CLI command — or ``None`` when there is nothing to do.
30
+ ``explain`` says what the row checks and why it matters, for a reader who
31
+ has never seen it before; the CLI does not print it, the console shows it
32
+ beside the selected row.
33
+
34
+ ``fix`` says where the finding is put right, as a short reference rather
35
+ than a URL — ``editor`` (the agent's definition), ``channels``,
36
+ ``integration:<id>``, ``llm-provider:<id>``, ``work:<status>:<stage>``
37
+ and so on — so the console can link to the page without the checks
38
+ knowing the web app's routes.
39
+ """
40
+
41
+ title: str
42
+ status: str
43
+ detail: str
44
+ hint: str | None = None
45
+ explain: str = ""
46
+ fix: str = ""
47
+
48
+ def to_dict(self) -> dict:
49
+ """The row as ``agents check -o json`` has always printed it."""
50
+ return {"title": self.title, "status": self.status, "detail": self.detail, "hint": self.hint}
51
+
52
+
53
+ @dataclass(frozen=True)
54
+ class CheckReport:
55
+ """Every row of one audit, plus what the console needs to draw around it.
56
+
57
+ ``revision`` is the revision the audit read (``None`` when there is
58
+ none). ``revision_body`` is that revision as fetched, kept so a caller
59
+ that wants to draw the agent does not have to fetch it again.
60
+ """
61
+
62
+ subject: str
63
+ checks: tuple[CheckResult, ...] = ()
64
+ revision: int | None = None
65
+ revision_body: dict | None = field(default=None, compare=False)
66
+
67
+ @property
68
+ def overall(self) -> str:
69
+ """``fail`` if any row failed, else ``warn`` if any warned, else ``ok``."""
70
+ return overall_status(self.checks)
71
+
72
+ def counts(self) -> dict[str, int]:
73
+ """How many rows passed (``ok`` and ``skip``), warned and failed."""
74
+ return {
75
+ "passed": sum(1 for c in self.checks if c.status in ("ok", "skip")),
76
+ "warnings": sum(1 for c in self.checks if c.status == "warn"),
77
+ "failing": sum(1 for c in self.checks if c.status == "fail"),
78
+ }
79
+
80
+
81
+ def overall_status(checks) -> str:
82
+ """The worst status among ``checks``: ``fail`` › ``warn`` › ``ok``."""
83
+ statuses = {c.status for c in checks}
84
+ if "fail" in statuses:
85
+ return "fail"
86
+ if "warn" in statuses:
87
+ return "warn"
88
+ return "ok"
89
+
90
+
91
+ def shape_get(node: dict, name: str) -> object:
92
+ """Read a work-shape key by camelCase name, tolerating PascalCase.
93
+
94
+ The API speaks camelCase, but the stored JSONB is PascalCase — so a
95
+ shape pasted out of the database (or a stage file hand-written against
96
+ it) reads the other way. Only the display path is tolerant; what gets
97
+ posted back is whatever the caller supplied, untouched.
98
+ """
99
+ if name in node:
100
+ return node[name]
101
+ return node.get(name[:1].upper() + name[1:])