gravity-cli 0.9.0__tar.gz → 0.11.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (285) hide show
  1. {gravity_cli-0.9.0 → gravity_cli-0.11.0}/AGENT_STANDARD.md +1 -1
  2. {gravity_cli-0.9.0 → gravity_cli-0.11.0}/AGENT_STANDARD_V2.md +1 -1
  3. {gravity_cli-0.9.0 → gravity_cli-0.11.0}/PKG-INFO +4 -4
  4. {gravity_cli-0.9.0 → gravity_cli-0.11.0}/README.md +2 -2
  5. {gravity_cli-0.9.0 → gravity_cli-0.11.0}/packages/gravity-schema/gravity_schema/catalog.yaml +45 -0
  6. {gravity_cli-0.9.0 → gravity_cli-0.11.0}/packages/gravity-schema/gravity_schema/contract.py +2 -0
  7. {gravity_cli-0.9.0 → gravity_cli-0.11.0}/packages/gravity-schema/gravity_schema/manifest.py +15 -3
  8. {gravity_cli-0.9.0 → gravity_cli-0.11.0}/packages/gravity-schema/gravity_schema/manifest_v2.py +2 -0
  9. {gravity_cli-0.9.0 → gravity_cli-0.11.0}/packages/gravity-schema/gravity_schema/rules.py +50 -0
  10. {gravity_cli-0.9.0 → gravity_cli-0.11.0}/packages/gravity-schema/gravity_schema/rules_cases.json +36 -2
  11. {gravity_cli-0.9.0 → gravity_cli-0.11.0}/packages/gravity-schema/gravity_schema/specs.py +67 -9
  12. {gravity_cli-0.9.0 → gravity_cli-0.11.0}/packages/gravity-schema/pyproject.toml +1 -1
  13. {gravity_cli-0.9.0 → gravity_cli-0.11.0}/packages/gravity-schema/schema/manifest.schema.json +76 -3
  14. {gravity_cli-0.9.0 → gravity_cli-0.11.0}/packages/gravity-schema/tests/test_manifest.py +8 -0
  15. {gravity_cli-0.9.0 → gravity_cli-0.11.0}/packages/gravity-schema/tests/test_rules.py +50 -0
  16. {gravity_cli-0.9.0 → gravity_cli-0.11.0}/pyproject.toml +2 -2
  17. {gravity_cli-0.9.0 → gravity_cli-0.11.0}/src/gravity_cli/BUILDER_GUIDE.md +4 -0
  18. {gravity_cli-0.9.0 → gravity_cli-0.11.0}/src/gravity_cli/BUILDER_GUIDE_V2.md +1 -1
  19. {gravity_cli-0.9.0 → gravity_cli-0.11.0}/src/gravity_cli/CHANGELOG.md +17 -0
  20. {gravity_cli-0.9.0 → gravity_cli-0.11.0}/src/gravity_cli/_runner_script.py +26 -1
  21. {gravity_cli-0.9.0 → gravity_cli-0.11.0}/src/gravity_cli/commands/run.py +2 -1
  22. {gravity_cli-0.9.0 → gravity_cli-0.11.0}/src/gravity_cli/dev_page/index.html +9 -3
  23. {gravity_cli-0.9.0 → gravity_cli-0.11.0}/src/gravity_cli/docs/Gravity.agent.md +49 -9
  24. {gravity_cli-0.9.0 → gravity_cli-0.11.0}/src/gravity_cli/inputs.py +15 -4
  25. {gravity_cli-0.9.0 → gravity_cli-0.11.0}/src/gravity_cli/timeline.py +17 -1
  26. {gravity_cli-0.9.0 → gravity_cli-0.11.0}/tests/test_inputs_memory.py +28 -0
  27. {gravity_cli-0.9.0 → gravity_cli-0.11.0}/tests/test_templates.py +27 -0
  28. {gravity_cli-0.9.0 → gravity_cli-0.11.0}/tests/test_timeline.py +17 -0
  29. {gravity_cli-0.9.0 → gravity_cli-0.11.0}/uv.lock +2 -2
  30. {gravity_cli-0.9.0 → gravity_cli-0.11.0}/.env.example +0 -0
  31. {gravity_cli-0.9.0 → gravity_cli-0.11.0}/.github/workflows/ci.yml +0 -0
  32. {gravity_cli-0.9.0 → gravity_cli-0.11.0}/.github/workflows/composio-watch.yml +0 -0
  33. {gravity_cli-0.9.0 → gravity_cli-0.11.0}/.github/workflows/release.yml +0 -0
  34. {gravity_cli-0.9.0 → gravity_cli-0.11.0}/.gitignore +0 -0
  35. {gravity_cli-0.9.0 → gravity_cli-0.11.0}/LICENSE +0 -0
  36. {gravity_cli-0.9.0 → gravity_cli-0.11.0}/NOTICE +0 -0
  37. {gravity_cli-0.9.0 → gravity_cli-0.11.0}/RELEASING.md +0 -0
  38. {gravity_cli-0.9.0 → gravity_cli-0.11.0}/TEMPLATES.md +0 -0
  39. {gravity_cli-0.9.0 → gravity_cli-0.11.0}/packages/gravity-schema/LICENSE +0 -0
  40. {gravity_cli-0.9.0 → gravity_cli-0.11.0}/packages/gravity-schema/NOTICE +0 -0
  41. {gravity_cli-0.9.0 → gravity_cli-0.11.0}/packages/gravity-schema/README.md +0 -0
  42. {gravity_cli-0.9.0 → gravity_cli-0.11.0}/packages/gravity-schema/gravity_schema/__init__.py +0 -0
  43. {gravity_cli-0.9.0 → gravity_cli-0.11.0}/packages/gravity-schema/gravity_schema/__main__.py +0 -0
  44. {gravity_cli-0.9.0 → gravity_cli-0.11.0}/packages/gravity-schema/gravity_schema/built_ins.py +0 -0
  45. {gravity_cli-0.9.0 → gravity_cli-0.11.0}/packages/gravity-schema/gravity_schema/catalog.py +0 -0
  46. {gravity_cli-0.9.0 → gravity_cli-0.11.0}/packages/gravity-schema/gravity_schema/catalog_imported.yaml +0 -0
  47. {gravity_cli-0.9.0 → gravity_cli-0.11.0}/packages/gravity-schema/gravity_schema/cron.py +0 -0
  48. {gravity_cli-0.9.0 → gravity_cli-0.11.0}/packages/gravity-schema/gravity_schema/describe.py +0 -0
  49. {gravity_cli-0.9.0 → gravity_cli-0.11.0}/packages/gravity-schema/gravity_schema/errors.py +0 -0
  50. {gravity_cli-0.9.0 → gravity_cli-0.11.0}/packages/gravity-schema/gravity_schema/export.py +0 -0
  51. {gravity_cli-0.9.0 → gravity_cli-0.11.0}/packages/gravity-schema/gravity_schema/gen_catalog.py +0 -0
  52. {gravity_cli-0.9.0 → gravity_cli-0.11.0}/packages/gravity-schema/gravity_schema/gen_vocabulary.py +0 -0
  53. {gravity_cli-0.9.0 → gravity_cli-0.11.0}/packages/gravity-schema/gravity_schema/instance.py +0 -0
  54. {gravity_cli-0.9.0 → gravity_cli-0.11.0}/packages/gravity-schema/gravity_schema/source_scan.py +0 -0
  55. {gravity_cli-0.9.0 → gravity_cli-0.11.0}/packages/gravity-schema/gravity_schema/validation.py +0 -0
  56. {gravity_cli-0.9.0 → gravity_cli-0.11.0}/packages/gravity-schema/gravity_schema/vocabulary/__init__.py +0 -0
  57. {gravity_cli-0.9.0 → gravity_cli-0.11.0}/packages/gravity-schema/gravity_schema/vocabulary/__main__.py +0 -0
  58. {gravity_cli-0.9.0 → gravity_cli-0.11.0}/packages/gravity-schema/gravity_schema/vocabulary/capabilities.yaml +0 -0
  59. {gravity_cli-0.9.0 → gravity_cli-0.11.0}/packages/gravity-schema/gravity_schema/vocabulary/connectors/airtable.yaml +0 -0
  60. {gravity_cli-0.9.0 → gravity_cli-0.11.0}/packages/gravity-schema/gravity_schema/vocabulary/connectors/asana.yaml +0 -0
  61. {gravity_cli-0.9.0 → gravity_cli-0.11.0}/packages/gravity-schema/gravity_schema/vocabulary/connectors/browser_tool.yaml +0 -0
  62. {gravity_cli-0.9.0 → gravity_cli-0.11.0}/packages/gravity-schema/gravity_schema/vocabulary/connectors/composio_search.yaml +0 -0
  63. {gravity_cli-0.9.0 → gravity_cli-0.11.0}/packages/gravity-schema/gravity_schema/vocabulary/connectors/dropbox.yaml +0 -0
  64. {gravity_cli-0.9.0 → gravity_cli-0.11.0}/packages/gravity-schema/gravity_schema/vocabulary/connectors/exa.yaml +0 -0
  65. {gravity_cli-0.9.0 → gravity_cli-0.11.0}/packages/gravity-schema/gravity_schema/vocabulary/connectors/excel.yaml +0 -0
  66. {gravity_cli-0.9.0 → gravity_cli-0.11.0}/packages/gravity-schema/gravity_schema/vocabulary/connectors/firecrawl.yaml +0 -0
  67. {gravity_cli-0.9.0 → gravity_cli-0.11.0}/packages/gravity-schema/gravity_schema/vocabulary/connectors/github.yaml +0 -0
  68. {gravity_cli-0.9.0 → gravity_cli-0.11.0}/packages/gravity-schema/gravity_schema/vocabulary/connectors/gmail.yaml +0 -0
  69. {gravity_cli-0.9.0 → gravity_cli-0.11.0}/packages/gravity-schema/gravity_schema/vocabulary/connectors/googlecalendar.yaml +0 -0
  70. {gravity_cli-0.9.0 → gravity_cli-0.11.0}/packages/gravity-schema/gravity_schema/vocabulary/connectors/googledocs.yaml +0 -0
  71. {gravity_cli-0.9.0 → gravity_cli-0.11.0}/packages/gravity-schema/gravity_schema/vocabulary/connectors/googledrive.yaml +0 -0
  72. {gravity_cli-0.9.0 → gravity_cli-0.11.0}/packages/gravity-schema/gravity_schema/vocabulary/connectors/googlesheets.yaml +0 -0
  73. {gravity_cli-0.9.0 → gravity_cli-0.11.0}/packages/gravity-schema/gravity_schema/vocabulary/connectors/googleslides.yaml +0 -0
  74. {gravity_cli-0.9.0 → gravity_cli-0.11.0}/packages/gravity-schema/gravity_schema/vocabulary/connectors/googletasks.yaml +0 -0
  75. {gravity_cli-0.9.0 → gravity_cli-0.11.0}/packages/gravity-schema/gravity_schema/vocabulary/connectors/hackernews.yaml +0 -0
  76. {gravity_cli-0.9.0 → gravity_cli-0.11.0}/packages/gravity-schema/gravity_schema/vocabulary/connectors/hubspot.yaml +0 -0
  77. {gravity_cli-0.9.0 → gravity_cli-0.11.0}/packages/gravity-schema/gravity_schema/vocabulary/connectors/instagram.yaml +0 -0
  78. {gravity_cli-0.9.0 → gravity_cli-0.11.0}/packages/gravity-schema/gravity_schema/vocabulary/connectors/jira.yaml +0 -0
  79. {gravity_cli-0.9.0 → gravity_cli-0.11.0}/packages/gravity-schema/gravity_schema/vocabulary/connectors/linear.yaml +0 -0
  80. {gravity_cli-0.9.0 → gravity_cli-0.11.0}/packages/gravity-schema/gravity_schema/vocabulary/connectors/linkedin.yaml +0 -0
  81. {gravity_cli-0.9.0 → gravity_cli-0.11.0}/packages/gravity-schema/gravity_schema/vocabulary/connectors/microsoft_teams.yaml +0 -0
  82. {gravity_cli-0.9.0 → gravity_cli-0.11.0}/packages/gravity-schema/gravity_schema/vocabulary/connectors/notion.yaml +0 -0
  83. {gravity_cli-0.9.0 → gravity_cli-0.11.0}/packages/gravity-schema/gravity_schema/vocabulary/connectors/outlook.yaml +0 -0
  84. {gravity_cli-0.9.0 → gravity_cli-0.11.0}/packages/gravity-schema/gravity_schema/vocabulary/connectors/perplexityai.yaml +0 -0
  85. {gravity_cli-0.9.0 → gravity_cli-0.11.0}/packages/gravity-schema/gravity_schema/vocabulary/connectors/pinterest.yaml +0 -0
  86. {gravity_cli-0.9.0 → gravity_cli-0.11.0}/packages/gravity-schema/gravity_schema/vocabulary/connectors/reddit.yaml +0 -0
  87. {gravity_cli-0.9.0 → gravity_cli-0.11.0}/packages/gravity-schema/gravity_schema/vocabulary/connectors/slack.yaml +0 -0
  88. {gravity_cli-0.9.0 → gravity_cli-0.11.0}/packages/gravity-schema/gravity_schema/vocabulary/connectors/tavily.yaml +0 -0
  89. {gravity_cli-0.9.0 → gravity_cli-0.11.0}/packages/gravity-schema/gravity_schema/vocabulary/connectors/youtube.yaml +0 -0
  90. {gravity_cli-0.9.0 → gravity_cli-0.11.0}/packages/gravity-schema/gravity_schema/vocabulary/core_shapes.yaml +0 -0
  91. {gravity_cli-0.9.0 → gravity_cli-0.11.0}/packages/gravity-schema/gravity_schema/vocabulary/generated_shapes.yaml +0 -0
  92. {gravity_cli-0.9.0 → gravity_cli-0.11.0}/packages/gravity-schema/gravity_schema/vocabulary/policy.yaml +0 -0
  93. {gravity_cli-0.9.0 → gravity_cli-0.11.0}/packages/gravity-schema/gravity_schema/vocabulary/schemas/airtable.json +0 -0
  94. {gravity_cli-0.9.0 → gravity_cli-0.11.0}/packages/gravity-schema/gravity_schema/vocabulary/schemas/asana.json +0 -0
  95. {gravity_cli-0.9.0 → gravity_cli-0.11.0}/packages/gravity-schema/gravity_schema/vocabulary/schemas/browser_tool.json +0 -0
  96. {gravity_cli-0.9.0 → gravity_cli-0.11.0}/packages/gravity-schema/gravity_schema/vocabulary/schemas/composio_search.json +0 -0
  97. {gravity_cli-0.9.0 → gravity_cli-0.11.0}/packages/gravity-schema/gravity_schema/vocabulary/schemas/dropbox.json +0 -0
  98. {gravity_cli-0.9.0 → gravity_cli-0.11.0}/packages/gravity-schema/gravity_schema/vocabulary/schemas/exa.json +0 -0
  99. {gravity_cli-0.9.0 → gravity_cli-0.11.0}/packages/gravity-schema/gravity_schema/vocabulary/schemas/excel.json +0 -0
  100. {gravity_cli-0.9.0 → gravity_cli-0.11.0}/packages/gravity-schema/gravity_schema/vocabulary/schemas/firecrawl.json +0 -0
  101. {gravity_cli-0.9.0 → gravity_cli-0.11.0}/packages/gravity-schema/gravity_schema/vocabulary/schemas/github.json +0 -0
  102. {gravity_cli-0.9.0 → gravity_cli-0.11.0}/packages/gravity-schema/gravity_schema/vocabulary/schemas/gmail.json +0 -0
  103. {gravity_cli-0.9.0 → gravity_cli-0.11.0}/packages/gravity-schema/gravity_schema/vocabulary/schemas/googlecalendar.json +0 -0
  104. {gravity_cli-0.9.0 → gravity_cli-0.11.0}/packages/gravity-schema/gravity_schema/vocabulary/schemas/googledocs.json +0 -0
  105. {gravity_cli-0.9.0 → gravity_cli-0.11.0}/packages/gravity-schema/gravity_schema/vocabulary/schemas/googledrive.json +0 -0
  106. {gravity_cli-0.9.0 → gravity_cli-0.11.0}/packages/gravity-schema/gravity_schema/vocabulary/schemas/googlesheets.json +0 -0
  107. {gravity_cli-0.9.0 → gravity_cli-0.11.0}/packages/gravity-schema/gravity_schema/vocabulary/schemas/googleslides.json +0 -0
  108. {gravity_cli-0.9.0 → gravity_cli-0.11.0}/packages/gravity-schema/gravity_schema/vocabulary/schemas/googletasks.json +0 -0
  109. {gravity_cli-0.9.0 → gravity_cli-0.11.0}/packages/gravity-schema/gravity_schema/vocabulary/schemas/hackernews.json +0 -0
  110. {gravity_cli-0.9.0 → gravity_cli-0.11.0}/packages/gravity-schema/gravity_schema/vocabulary/schemas/hubspot.json +0 -0
  111. {gravity_cli-0.9.0 → gravity_cli-0.11.0}/packages/gravity-schema/gravity_schema/vocabulary/schemas/instagram.json +0 -0
  112. {gravity_cli-0.9.0 → gravity_cli-0.11.0}/packages/gravity-schema/gravity_schema/vocabulary/schemas/jira.json +0 -0
  113. {gravity_cli-0.9.0 → gravity_cli-0.11.0}/packages/gravity-schema/gravity_schema/vocabulary/schemas/linear.json +0 -0
  114. {gravity_cli-0.9.0 → gravity_cli-0.11.0}/packages/gravity-schema/gravity_schema/vocabulary/schemas/linkedin.json +0 -0
  115. {gravity_cli-0.9.0 → gravity_cli-0.11.0}/packages/gravity-schema/gravity_schema/vocabulary/schemas/microsoft_teams.json +0 -0
  116. {gravity_cli-0.9.0 → gravity_cli-0.11.0}/packages/gravity-schema/gravity_schema/vocabulary/schemas/notion.json +0 -0
  117. {gravity_cli-0.9.0 → gravity_cli-0.11.0}/packages/gravity-schema/gravity_schema/vocabulary/schemas/outlook.json +0 -0
  118. {gravity_cli-0.9.0 → gravity_cli-0.11.0}/packages/gravity-schema/gravity_schema/vocabulary/schemas/perplexityai.json +0 -0
  119. {gravity_cli-0.9.0 → gravity_cli-0.11.0}/packages/gravity-schema/gravity_schema/vocabulary/schemas/pinterest.json +0 -0
  120. {gravity_cli-0.9.0 → gravity_cli-0.11.0}/packages/gravity-schema/gravity_schema/vocabulary/schemas/reddit.json +0 -0
  121. {gravity_cli-0.9.0 → gravity_cli-0.11.0}/packages/gravity-schema/gravity_schema/vocabulary/schemas/slack.json +0 -0
  122. {gravity_cli-0.9.0 → gravity_cli-0.11.0}/packages/gravity-schema/gravity_schema/vocabulary/schemas/tavily.json +0 -0
  123. {gravity_cli-0.9.0 → gravity_cli-0.11.0}/packages/gravity-schema/gravity_schema/vocabulary/schemas/youtube.json +0 -0
  124. {gravity_cli-0.9.0 → gravity_cli-0.11.0}/packages/gravity-schema/gravity_schema/vocabulary/schemas.py +0 -0
  125. {gravity_cli-0.9.0 → gravity_cli-0.11.0}/packages/gravity-schema/gravity_schema/vocabulary/shapes.d/ochat.yaml +0 -0
  126. {gravity_cli-0.9.0 → gravity_cli-0.11.0}/packages/gravity-schema/gravity_schema/vocabulary/shapes.d/ocrm.yaml +0 -0
  127. {gravity_cli-0.9.0 → gravity_cli-0.11.0}/packages/gravity-schema/gravity_schema/vocabulary/shapes.d/ogh.yaml +0 -0
  128. {gravity_cli-0.9.0 → gravity_cli-0.11.0}/packages/gravity-schema/gravity_schema/vocabulary/shapes.d/oms.yaml +0 -0
  129. {gravity_cli-0.9.0 → gravity_cli-0.11.0}/packages/gravity-schema/gravity_schema/vocabulary/shapes.d/oweb.yaml +0 -0
  130. {gravity_cli-0.9.0 → gravity_cli-0.11.0}/packages/gravity-schema/gravity_schema/vocabulary/shapes.d/owork.yaml +0 -0
  131. {gravity_cli-0.9.0 → gravity_cli-0.11.0}/packages/gravity-schema/gravity_schema/vocabulary/shapes.d/shared.yaml +0 -0
  132. {gravity_cli-0.9.0 → gravity_cli-0.11.0}/packages/gravity-schema/gravity_schema/vocabulary/sync.py +0 -0
  133. {gravity_cli-0.9.0 → gravity_cli-0.11.0}/packages/gravity-schema/gravity_schema/vocabulary/verbs.d/ochat.yaml +0 -0
  134. {gravity_cli-0.9.0 → gravity_cli-0.11.0}/packages/gravity-schema/gravity_schema/vocabulary/verbs.d/ocrm.yaml +0 -0
  135. {gravity_cli-0.9.0 → gravity_cli-0.11.0}/packages/gravity-schema/gravity_schema/vocabulary/verbs.d/ogh.yaml +0 -0
  136. {gravity_cli-0.9.0 → gravity_cli-0.11.0}/packages/gravity-schema/gravity_schema/vocabulary/verbs.d/oms.yaml +0 -0
  137. {gravity_cli-0.9.0 → gravity_cli-0.11.0}/packages/gravity-schema/gravity_schema/vocabulary/verbs.d/oweb.yaml +0 -0
  138. {gravity_cli-0.9.0 → gravity_cli-0.11.0}/packages/gravity-schema/gravity_schema/vocabulary/verbs.d/owork.yaml +0 -0
  139. {gravity_cli-0.9.0 → gravity_cli-0.11.0}/packages/gravity-schema/gravity_schema/vocabulary/verbs.d/rchat.yaml +0 -0
  140. {gravity_cli-0.9.0 → gravity_cli-0.11.0}/packages/gravity-schema/gravity_schema/vocabulary/verbs.d/rfiles.yaml +0 -0
  141. {gravity_cli-0.9.0 → gravity_cli-0.11.0}/packages/gravity-schema/gravity_schema/vocabulary/verbs.d/rgh.yaml +0 -0
  142. {gravity_cli-0.9.0 → gravity_cli-0.11.0}/packages/gravity-schema/gravity_schema/vocabulary/verbs.d/rtrack.yaml +0 -0
  143. {gravity_cli-0.9.0 → gravity_cli-0.11.0}/packages/gravity-schema/gravity_schema/vocabulary/verbs.d/rwork.yaml +0 -0
  144. {gravity_cli-0.9.0 → gravity_cli-0.11.0}/packages/gravity-schema/gravity_schema/vocabulary/verbs.yaml +0 -0
  145. {gravity_cli-0.9.0 → gravity_cli-0.11.0}/packages/gravity-schema/tests/conftest.py +0 -0
  146. {gravity_cli-0.9.0 → gravity_cli-0.11.0}/packages/gravity-schema/tests/fixtures/demo_manifests.yaml +0 -0
  147. {gravity_cli-0.9.0 → gravity_cli-0.11.0}/packages/gravity-schema/tests/test_accounts.py +0 -0
  148. {gravity_cli-0.9.0 → gravity_cli-0.11.0}/packages/gravity-schema/tests/test_actions_memory.py +0 -0
  149. {gravity_cli-0.9.0 → gravity_cli-0.11.0}/packages/gravity-schema/tests/test_catalog_parity.py +0 -0
  150. {gravity_cli-0.9.0 → gravity_cli-0.11.0}/packages/gravity-schema/tests/test_cron.py +0 -0
  151. {gravity_cli-0.9.0 → gravity_cli-0.11.0}/packages/gravity-schema/tests/test_describe.py +0 -0
  152. {gravity_cli-0.9.0 → gravity_cli-0.11.0}/packages/gravity-schema/tests/test_errors.py +0 -0
  153. {gravity_cli-0.9.0 → gravity_cli-0.11.0}/packages/gravity-schema/tests/test_generated_tables.py +0 -0
  154. {gravity_cli-0.9.0 → gravity_cli-0.11.0}/packages/gravity-schema/tests/test_instance.py +0 -0
  155. {gravity_cli-0.9.0 → gravity_cli-0.11.0}/packages/gravity-schema/tests/test_json_schema.py +0 -0
  156. {gravity_cli-0.9.0 → gravity_cli-0.11.0}/packages/gravity-schema/tests/test_kind_outputs.py +0 -0
  157. {gravity_cli-0.9.0 → gravity_cli-0.11.0}/packages/gravity-schema/tests/test_manifest_v2.py +0 -0
  158. {gravity_cli-0.9.0 → gravity_cli-0.11.0}/packages/gravity-schema/tests/test_place_maker.py +0 -0
  159. {gravity_cli-0.9.0 → gravity_cli-0.11.0}/packages/gravity-schema/tests/test_slots.py +0 -0
  160. {gravity_cli-0.9.0 → gravity_cli-0.11.0}/packages/gravity-schema/tests/test_source_scan.py +0 -0
  161. {gravity_cli-0.9.0 → gravity_cli-0.11.0}/packages/gravity-schema/tests/test_sync.py +0 -0
  162. {gravity_cli-0.9.0 → gravity_cli-0.11.0}/packages/gravity-schema/tests/test_validation.py +0 -0
  163. {gravity_cli-0.9.0 → gravity_cli-0.11.0}/packages/gravity-schema/tests/test_vocabulary.py +0 -0
  164. {gravity_cli-0.9.0 → gravity_cli-0.11.0}/scripts/composio-watch.sh +0 -0
  165. {gravity_cli-0.9.0 → gravity_cli-0.11.0}/src/gravity_cli/__init__.py +0 -0
  166. {gravity_cli-0.9.0 → gravity_cli-0.11.0}/src/gravity_cli/__main__.py +0 -0
  167. {gravity_cli-0.9.0 → gravity_cli-0.11.0}/src/gravity_cli/api.py +0 -0
  168. {gravity_cli-0.9.0 → gravity_cli-0.11.0}/src/gravity_cli/changelog.py +0 -0
  169. {gravity_cli-0.9.0 → gravity_cli-0.11.0}/src/gravity_cli/cli.py +0 -0
  170. {gravity_cli-0.9.0 → gravity_cli-0.11.0}/src/gravity_cli/commands/__init__.py +0 -0
  171. {gravity_cli-0.9.0 → gravity_cli-0.11.0}/src/gravity_cli/commands/adapt.py +0 -0
  172. {gravity_cli-0.9.0 → gravity_cli-0.11.0}/src/gravity_cli/commands/build.py +0 -0
  173. {gravity_cli-0.9.0 → gravity_cli-0.11.0}/src/gravity_cli/commands/capabilities.py +0 -0
  174. {gravity_cli-0.9.0 → gravity_cli-0.11.0}/src/gravity_cli/commands/changelog.py +0 -0
  175. {gravity_cli-0.9.0 → gravity_cli-0.11.0}/src/gravity_cli/commands/check.py +0 -0
  176. {gravity_cli-0.9.0 → gravity_cli-0.11.0}/src/gravity_cli/commands/connect.py +0 -0
  177. {gravity_cli-0.9.0 → gravity_cli-0.11.0}/src/gravity_cli/commands/dev.py +0 -0
  178. {gravity_cli-0.9.0 → gravity_cli-0.11.0}/src/gravity_cli/commands/docs.py +0 -0
  179. {gravity_cli-0.9.0 → gravity_cli-0.11.0}/src/gravity_cli/commands/guide.py +0 -0
  180. {gravity_cli-0.9.0 → gravity_cli-0.11.0}/src/gravity_cli/commands/help.py +0 -0
  181. {gravity_cli-0.9.0 → gravity_cli-0.11.0}/src/gravity_cli/commands/init.py +0 -0
  182. {gravity_cli-0.9.0 → gravity_cli-0.11.0}/src/gravity_cli/commands/login.py +0 -0
  183. {gravity_cli-0.9.0 → gravity_cli-0.11.0}/src/gravity_cli/commands/memory.py +0 -0
  184. {gravity_cli-0.9.0 → gravity_cli-0.11.0}/src/gravity_cli/commands/push.py +0 -0
  185. {gravity_cli-0.9.0 → gravity_cli-0.11.0}/src/gravity_cli/commands/runs.py +0 -0
  186. {gravity_cli-0.9.0 → gravity_cli-0.11.0}/src/gravity_cli/commands/schedule.py +0 -0
  187. {gravity_cli-0.9.0 → gravity_cli-0.11.0}/src/gravity_cli/commands/tools.py +0 -0
  188. {gravity_cli-0.9.0 → gravity_cli-0.11.0}/src/gravity_cli/commands/update.py +0 -0
  189. {gravity_cli-0.9.0 → gravity_cli-0.11.0}/src/gravity_cli/commands/validate.py +0 -0
  190. {gravity_cli-0.9.0 → gravity_cli-0.11.0}/src/gravity_cli/config.py +0 -0
  191. {gravity_cli-0.9.0 → gravity_cli-0.11.0}/src/gravity_cli/dev.py +0 -0
  192. {gravity_cli-0.9.0 → gravity_cli-0.11.0}/src/gravity_cli/docs/Gravity.getting-started.md +0 -0
  193. {gravity_cli-0.9.0 → gravity_cli-0.11.0}/src/gravity_cli/docs/problem-to-agent.md +0 -0
  194. {gravity_cli-0.9.0 → gravity_cli-0.11.0}/src/gravity_cli/localrun.py +0 -0
  195. {gravity_cli-0.9.0 → gravity_cli-0.11.0}/src/gravity_cli/manifest.py +0 -0
  196. {gravity_cli-0.9.0 → gravity_cli-0.11.0}/src/gravity_cli/project.py +0 -0
  197. {gravity_cli-0.9.0 → gravity_cli-0.11.0}/src/gravity_cli/templates/chat/.gitignore +0 -0
  198. {gravity_cli-0.9.0 → gravity_cli-0.11.0}/src/gravity_cli/templates/chat/manifest.yaml +0 -0
  199. {gravity_cli-0.9.0 → gravity_cli-0.11.0}/src/gravity_cli/templates/chat/requirements.txt +0 -0
  200. {gravity_cli-0.9.0 → gravity_cli-0.11.0}/src/gravity_cli/templates/chat/src/agent.py +0 -0
  201. {gravity_cli-0.9.0 → gravity_cli-0.11.0}/src/gravity_cli/templates/deepagent/.gitignore +0 -0
  202. {gravity_cli-0.9.0 → gravity_cli-0.11.0}/src/gravity_cli/templates/deepagent/manifest.yaml +0 -0
  203. {gravity_cli-0.9.0 → gravity_cli-0.11.0}/src/gravity_cli/templates/deepagent/requirements.txt +0 -0
  204. {gravity_cli-0.9.0 → gravity_cli-0.11.0}/src/gravity_cli/templates/deepagent/src/agent.py +0 -0
  205. {gravity_cli-0.9.0 → gravity_cli-0.11.0}/src/gravity_cli/templates/minimal/.gitignore +0 -0
  206. {gravity_cli-0.9.0 → gravity_cli-0.11.0}/src/gravity_cli/templates/minimal/manifest.yaml +0 -0
  207. {gravity_cli-0.9.0 → gravity_cli-0.11.0}/src/gravity_cli/templates/minimal/requirements.txt +0 -0
  208. {gravity_cli-0.9.0 → gravity_cli-0.11.0}/src/gravity_cli/templates/minimal/src/agent.py +0 -0
  209. {gravity_cli-0.9.0 → gravity_cli-0.11.0}/src/gravity_cli/templates/structured/.gitignore +0 -0
  210. {gravity_cli-0.9.0 → gravity_cli-0.11.0}/src/gravity_cli/templates/structured/manifest.yaml +0 -0
  211. {gravity_cli-0.9.0 → gravity_cli-0.11.0}/src/gravity_cli/templates/structured/requirements.txt +0 -0
  212. {gravity_cli-0.9.0 → gravity_cli-0.11.0}/src/gravity_cli/templates/structured/src/agent.py +0 -0
  213. {gravity_cli-0.9.0 → gravity_cli-0.11.0}/src/gravity_cli/templates/structured/src/gateway.py +0 -0
  214. {gravity_cli-0.9.0 → gravity_cli-0.11.0}/src/gravity_cli/templates/structured/src/nodes/agent.py +0 -0
  215. {gravity_cli-0.9.0 → gravity_cli-0.11.0}/src/gravity_cli/templates/structured/src/prompts.py +0 -0
  216. {gravity_cli-0.9.0 → gravity_cli-0.11.0}/src/gravity_cli/templates/structured/src/state.py +0 -0
  217. {gravity_cli-0.9.0 → gravity_cli-0.11.0}/src/gravity_cli/templates/v2/chat/.gitignore +0 -0
  218. {gravity_cli-0.9.0 → gravity_cli-0.11.0}/src/gravity_cli/templates/v2/chat/manifest.yaml +0 -0
  219. {gravity_cli-0.9.0 → gravity_cli-0.11.0}/src/gravity_cli/templates/v2/chat/requirements.txt +0 -0
  220. {gravity_cli-0.9.0 → gravity_cli-0.11.0}/src/gravity_cli/templates/v2/chat/src/agent.py +0 -0
  221. {gravity_cli-0.9.0 → gravity_cli-0.11.0}/src/gravity_cli/templates/v2/chat/tests/test_agent.py +0 -0
  222. {gravity_cli-0.9.0 → gravity_cli-0.11.0}/src/gravity_cli/templates/v2/deepagent/.gitignore +0 -0
  223. {gravity_cli-0.9.0 → gravity_cli-0.11.0}/src/gravity_cli/templates/v2/deepagent/manifest.yaml +0 -0
  224. {gravity_cli-0.9.0 → gravity_cli-0.11.0}/src/gravity_cli/templates/v2/deepagent/requirements.txt +0 -0
  225. {gravity_cli-0.9.0 → gravity_cli-0.11.0}/src/gravity_cli/templates/v2/deepagent/src/agent.py +0 -0
  226. {gravity_cli-0.9.0 → gravity_cli-0.11.0}/src/gravity_cli/templates/v2/deepagent/tests/test_agent.py +0 -0
  227. {gravity_cli-0.9.0 → gravity_cli-0.11.0}/src/gravity_cli/templates/v2/minimal/.gitignore +0 -0
  228. {gravity_cli-0.9.0 → gravity_cli-0.11.0}/src/gravity_cli/templates/v2/minimal/manifest.yaml +0 -0
  229. {gravity_cli-0.9.0 → gravity_cli-0.11.0}/src/gravity_cli/templates/v2/minimal/requirements.txt +0 -0
  230. {gravity_cli-0.9.0 → gravity_cli-0.11.0}/src/gravity_cli/templates/v2/minimal/src/agent.py +0 -0
  231. {gravity_cli-0.9.0 → gravity_cli-0.11.0}/src/gravity_cli/templates/v2/minimal/tests/test_agent.py +0 -0
  232. {gravity_cli-0.9.0 → gravity_cli-0.11.0}/src/gravity_cli/templates/v2/structured/.gitignore +0 -0
  233. {gravity_cli-0.9.0 → gravity_cli-0.11.0}/src/gravity_cli/templates/v2/structured/manifest.yaml +0 -0
  234. {gravity_cli-0.9.0 → gravity_cli-0.11.0}/src/gravity_cli/templates/v2/structured/requirements.txt +0 -0
  235. {gravity_cli-0.9.0 → gravity_cli-0.11.0}/src/gravity_cli/templates/v2/structured/src/__init__.py +0 -0
  236. {gravity_cli-0.9.0 → gravity_cli-0.11.0}/src/gravity_cli/templates/v2/structured/src/agent.py +0 -0
  237. {gravity_cli-0.9.0 → gravity_cli-0.11.0}/src/gravity_cli/templates/v2/structured/src/gateway.py +0 -0
  238. {gravity_cli-0.9.0 → gravity_cli-0.11.0}/src/gravity_cli/templates/v2/structured/src/nodes/__init__.py +0 -0
  239. {gravity_cli-0.9.0 → gravity_cli-0.11.0}/src/gravity_cli/templates/v2/structured/src/nodes/agent.py +0 -0
  240. {gravity_cli-0.9.0 → gravity_cli-0.11.0}/src/gravity_cli/templates/v2/structured/src/prompts.py +0 -0
  241. {gravity_cli-0.9.0 → gravity_cli-0.11.0}/src/gravity_cli/templates/v2/structured/src/state.py +0 -0
  242. {gravity_cli-0.9.0 → gravity_cli-0.11.0}/src/gravity_cli/templates/v2/structured/tests/test_agent.py +0 -0
  243. {gravity_cli-0.9.0 → gravity_cli-0.11.0}/src/gravity_cli/ui.py +0 -0
  244. {gravity_cli-0.9.0 → gravity_cli-0.11.0}/src/gravity_cli/updater.py +0 -0
  245. {gravity_cli-0.9.0 → gravity_cli-0.11.0}/src/gravity_cli/workspace.py +0 -0
  246. {gravity_cli-0.9.0 → gravity_cli-0.11.0}/starter/manifest.yaml +0 -0
  247. {gravity_cli-0.9.0 → gravity_cli-0.11.0}/starter/requirements.txt +0 -0
  248. {gravity_cli-0.9.0 → gravity_cli-0.11.0}/starter/src/agent.py +0 -0
  249. {gravity_cli-0.9.0 → gravity_cli-0.11.0}/starter/src/summary.py +0 -0
  250. {gravity_cli-0.9.0 → gravity_cli-0.11.0}/starter/tests/test_summary.py +0 -0
  251. {gravity_cli-0.9.0 → gravity_cli-0.11.0}/starter-v2/manifest.yaml +0 -0
  252. {gravity_cli-0.9.0 → gravity_cli-0.11.0}/starter-v2/requirements.txt +0 -0
  253. {gravity_cli-0.9.0 → gravity_cli-0.11.0}/starter-v2/src/agent.py +0 -0
  254. {gravity_cli-0.9.0 → gravity_cli-0.11.0}/starter-v2/src/summary.py +0 -0
  255. {gravity_cli-0.9.0 → gravity_cli-0.11.0}/starter-v2/tests/test_summary.py +0 -0
  256. {gravity_cli-0.9.0 → gravity_cli-0.11.0}/tests/conftest.py +0 -0
  257. {gravity_cli-0.9.0 → gravity_cli-0.11.0}/tests/test_adapt.py +0 -0
  258. {gravity_cli-0.9.0 → gravity_cli-0.11.0}/tests/test_agent_guide.py +0 -0
  259. {gravity_cli-0.9.0 → gravity_cli-0.11.0}/tests/test_agents.py +0 -0
  260. {gravity_cli-0.9.0 → gravity_cli-0.11.0}/tests/test_api.py +0 -0
  261. {gravity_cli-0.9.0 → gravity_cli-0.11.0}/tests/test_build_push.py +0 -0
  262. {gravity_cli-0.9.0 → gravity_cli-0.11.0}/tests/test_changelog.py +0 -0
  263. {gravity_cli-0.9.0 → gravity_cli-0.11.0}/tests/test_check.py +0 -0
  264. {gravity_cli-0.9.0 → gravity_cli-0.11.0}/tests/test_connect.py +0 -0
  265. {gravity_cli-0.9.0 → gravity_cli-0.11.0}/tests/test_connect_v2.py +0 -0
  266. {gravity_cli-0.9.0 → gravity_cli-0.11.0}/tests/test_dev.py +0 -0
  267. {gravity_cli-0.9.0 → gravity_cli-0.11.0}/tests/test_dev_v2.py +0 -0
  268. {gravity_cli-0.9.0 → gravity_cli-0.11.0}/tests/test_gateway_token.py +0 -0
  269. {gravity_cli-0.9.0 → gravity_cli-0.11.0}/tests/test_localrun.py +0 -0
  270. {gravity_cli-0.9.0 → gravity_cli-0.11.0}/tests/test_manifest_integration.py +0 -0
  271. {gravity_cli-0.9.0 → gravity_cli-0.11.0}/tests/test_run.py +0 -0
  272. {gravity_cli-0.9.0 → gravity_cli-0.11.0}/tests/test_run_v2.py +0 -0
  273. {gravity_cli-0.9.0 → gravity_cli-0.11.0}/tests/test_runs.py +0 -0
  274. {gravity_cli-0.9.0 → gravity_cli-0.11.0}/tests/test_schedule.py +0 -0
  275. {gravity_cli-0.9.0 → gravity_cli-0.11.0}/tests/test_schedule_v2.py +0 -0
  276. {gravity_cli-0.9.0 → gravity_cli-0.11.0}/tests/test_session.py +0 -0
  277. {gravity_cli-0.9.0 → gravity_cli-0.11.0}/tests/test_templates_v2.py +0 -0
  278. {gravity_cli-0.9.0 → gravity_cli-0.11.0}/tests/test_tools.py +0 -0
  279. {gravity_cli-0.9.0 → gravity_cli-0.11.0}/tests/test_ui.py +0 -0
  280. {gravity_cli-0.9.0 → gravity_cli-0.11.0}/tests/test_updater.py +0 -0
  281. {gravity_cli-0.9.0 → gravity_cli-0.11.0}/tests/test_v2_cli.py +0 -0
  282. {gravity_cli-0.9.0 → gravity_cli-0.11.0}/tests/test_validate.py +0 -0
  283. {gravity_cli-0.9.0 → gravity_cli-0.11.0}/tests/test_validate_v2.py +0 -0
  284. {gravity_cli-0.9.0 → gravity_cli-0.11.0}/tests/test_welcome.py +0 -0
  285. {gravity_cli-0.9.0 → gravity_cli-0.11.0}/tests/test_workspace.py +0 -0
@@ -348,7 +348,7 @@ to one address, with the output's content in the message body and no
348
348
  attachment; a thread sends at most 200 a day. The platform sends it, not the
349
349
  agent, so the agent needs no Gmail tool and no approval for it.
350
350
 
351
- Renderers: `markdown`, `docx`, `pptx`, `rows` (the first table, as CSV). Each
351
+ Renderers: `markdown`, `docx`, `pptx`, `pdf`, `rows` (the first table, as CSV), `xlsx` (every table, a sheet each). Each
352
352
  consumer picks outputs and destinations per run. The agent writes one
353
353
  **corpus** into its final state, and every output is rendered from it:
354
354
 
@@ -384,7 +384,7 @@ record tagged with its app in `extra.app`. The platform delivers it after the
384
384
  run, not the agent, so the agent declares no write for it; an email goes at
385
385
  most once per run and a thread sends at most 200 a day, extra places included.
386
386
 
387
- Renderers: `markdown`, `docx`, `pptx`, `rows` (the first table, as CSV). Each
387
+ Renderers: `markdown`, `docx`, `pptx`, `pdf`, `rows` (the first table, as CSV), `xlsx` (every table, a sheet each). Each
388
388
  consumer picks outputs and destinations per run. The agent writes one
389
389
  **corpus** into its final state, and every output is rendered from it:
390
390
 
@@ -1,12 +1,12 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: gravity-cli
3
- Version: 0.9.0
3
+ Version: 0.11.0
4
4
  Summary: gravity cli — init, run, and push python agents to the Gravity marketplace
5
5
  License-Expression: Apache-2.0
6
6
  License-File: LICENSE
7
7
  License-File: NOTICE
8
8
  Requires-Python: >=3.12
9
- Requires-Dist: gravity-schema==0.9.0
9
+ Requires-Dist: gravity-schema==0.11.0
10
10
  Requires-Dist: httpx>=0.27.0
11
11
  Requires-Dist: keyring>=25.0.0
12
12
  Requires-Dist: python-dotenv>=1.0.0
@@ -720,7 +720,7 @@ slots: # a part the consumer chooses; each adapter ge
720
720
 
721
721
  outputs: # extra formats the platform renders and delivers itself
722
722
  report:
723
- renderer: markdown # markdown | docx | pptx | rows
723
+ renderer: markdown # markdown | docx | pptx | pdf | rows | xlsx
724
724
  label: "Digest as a document"
725
725
  destinations: [download]
726
726
  ```
@@ -752,7 +752,7 @@ outputs: # extra formats the platform renders and deliv
752
752
  | `triggers.webhook` | no | none | A list of events; `github.push` is the only one today. |
753
753
  | `state.max_kb` | no | no memory | Include `state:` to keep memory between runs, in `state["memory"]`. 1 to 256 KB. |
754
754
  | `slots.<name>` | no | none | A swappable part: `label`, `default` adapter, and `adapters` mapping each adapter name to the tools it may call. Code lives in `src/adapters/<slot>/<adapter>.py`. Slot names cannot repeat an input's name. |
755
- | `outputs.<name>` | no | none | Up to 8 extra formats rendered from the result: `renderer` (`markdown`, `docx`, `pptx` or `rows`), `label` and `destinations` (`download`, or a catalog tool such as `gmail.send`). |
755
+ | `outputs.<name>` | no | none | Up to 8 extra formats rendered from the result: `renderer` (`markdown`, `docx`, `pptx`, `pdf`, `rows` or `xlsx`), `label` and `destinations` (`download`, or a catalog tool such as `gmail.send`). |
756
756
 
757
757
  Unknown fields are rejected rather than ignored, so a typo is reported instead of silently dropped.
758
758
 
@@ -703,7 +703,7 @@ slots: # a part the consumer chooses; each adapter ge
703
703
 
704
704
  outputs: # extra formats the platform renders and delivers itself
705
705
  report:
706
- renderer: markdown # markdown | docx | pptx | rows
706
+ renderer: markdown # markdown | docx | pptx | pdf | rows | xlsx
707
707
  label: "Digest as a document"
708
708
  destinations: [download]
709
709
  ```
@@ -735,7 +735,7 @@ outputs: # extra formats the platform renders and deliv
735
735
  | `triggers.webhook` | no | none | A list of events; `github.push` is the only one today. |
736
736
  | `state.max_kb` | no | no memory | Include `state:` to keep memory between runs, in `state["memory"]`. 1 to 256 KB. |
737
737
  | `slots.<name>` | no | none | A swappable part: `label`, `default` adapter, and `adapters` mapping each adapter name to the tools it may call. Code lives in `src/adapters/<slot>/<adapter>.py`. Slot names cannot repeat an input's name. |
738
- | `outputs.<name>` | no | none | Up to 8 extra formats rendered from the result: `renderer` (`markdown`, `docx`, `pptx` or `rows`), `label` and `destinations` (`download`, or a catalog tool such as `gmail.send`). |
738
+ | `outputs.<name>` | no | none | Up to 8 extra formats rendered from the result: `renderer` (`markdown`, `docx`, `pptx`, `pdf`, `rows` or `xlsx`), `label` and `destinations` (`download`, or a catalog tool such as `gmail.send`). |
739
739
 
740
740
  Unknown fields are rejected rather than ignored, so a typo is reported instead of silently dropped.
741
741
 
@@ -74,6 +74,15 @@
74
74
  guarantee: certified
75
75
  side_effect: read
76
76
 
77
+ - name: treg.email_find
78
+ slug: null
79
+ toolkit: null
80
+ description: find and verify a person's work email from their LinkedIn URL, or a full name plus the company's email domain; served by the gateway through the team's lookup account (a fraction of a cent a call), capped per call and verified in the same call
81
+ scopes: []
82
+ breadth: generic
83
+ guarantee: certified
84
+ side_effect: read
85
+
77
86
  - name: browser.act
78
87
  slug: null
79
88
  toolkit: null
@@ -155,6 +164,42 @@
155
164
  guarantee: certified
156
165
  side_effect: write
157
166
 
167
+ - name: sheets.create
168
+ slug: GOOGLESHEETS_CREATE_GOOGLE_SHEET1
169
+ toolkit: googlesheets
170
+ description: create a new Google Sheet in the connected Google account
171
+ scopes: [sheets]
172
+ breadth: generic
173
+ guarantee: certified
174
+ side_effect: write
175
+
176
+ - name: sheets.add_tab
177
+ slug: GOOGLESHEETS_ADD_SHEET
178
+ toolkit: googlesheets
179
+ description: add a tab to a Google Sheet in the connected Google account
180
+ scopes: [sheets]
181
+ breadth: generic
182
+ guarantee: certified
183
+ side_effect: write
184
+
185
+ - name: docs.read
186
+ slug: GOOGLEDOCS_GET_DOCUMENT_PLAINTEXT
187
+ toolkit: googledocs
188
+ description: read a Google Doc in the connected Google account as plain text (document_id, from a google-doc input)
189
+ scopes: [docs]
190
+ breadth: generic
191
+ guarantee: certified
192
+ side_effect: read
193
+
194
+ - name: docs.create
195
+ slug: GOOGLEDOCS_CREATE_DOCUMENT_MARKDOWN
196
+ toolkit: googledocs
197
+ description: create a Google Doc from markdown in the connected Google account
198
+ scopes: [docs]
199
+ breadth: generic
200
+ guarantee: certified
201
+ side_effect: write
202
+
158
203
  - name: slack.send
159
204
  slug: SLACK_SEND_MESSAGE
160
205
  toolkit: slack
@@ -106,6 +106,8 @@ MAX_INPUTS: Final[int] = 50
106
106
  MAX_TOOLS: Final[int] = 50
107
107
  MAX_OPTIONS: Final[int] = 100
108
108
  MAX_FILE_EXTENSIONS: Final[int] = 20
109
+ # A file input with `max` (several files): the most it may ask for. Each upload is up to 25 MB.
110
+ MAX_FILES: Final[int] = 20
109
111
  MAX_NAME_LENGTH: Final[int] = 64
110
112
  MAX_ENTRYPOINT_LENGTH: Final[int] = 256
111
113
  MAX_LABEL_LENGTH: Final[int] = 200
@@ -319,11 +319,23 @@ class Manifest(BaseModel):
319
319
 
320
320
  @model_validator(mode="after")
321
321
  def _check_monitor_has_a_trigger(self) -> Manifest:
322
- if self.kind is AgentKind.MONITOR and self.triggers.schedule is None and not self.triggers.webhook:
322
+ if (self.kind is AgentKind.MONITOR and self.triggers.schedule is None and not self.triggers.webhook
323
+ and self.triggers.email is None):
323
324
  raise PydanticCustomError(
324
325
  ErrorCode.INVALID_KIND.value,
325
- "A monitor needs a schedule or a webhook trigger.",
326
- {"remediation": "Add triggers.schedule or triggers.webhook, or use kind: interactive."},
326
+ "A monitor needs a schedule, an email or a webhook trigger.",
327
+ {"remediation": "Add triggers.schedule, triggers.email or triggers.webhook, or use kind: interactive."},
328
+ )
329
+ return self
330
+
331
+ @model_validator(mode="after")
332
+ def _check_email_trigger_reads_gmail(self) -> Manifest:
333
+ # Gravity reads the new mail with the person's Gmail, under the agent's own permission.
334
+ if self.triggers.email is not None and "gmail.read" not in self.permissions.tools:
335
+ raise PydanticCustomError(
336
+ ErrorCode.INVALID_TRIGGER.value,
337
+ "triggers.email watches Gmail, but the agent does not declare gmail.read.",
338
+ {"remediation": "Add gmail.read to permissions.tools."},
327
339
  )
328
340
  return self
329
341
 
@@ -53,7 +53,9 @@ RENDERER_DESTINATIONS: dict[Renderer, tuple[str, ...]] = {
53
53
  "post.create", "file.create"),
54
54
  Renderer.DOCX: ("file.create", "document.create", "message.send"),
55
55
  Renderer.PPTX: ("file.create", "presentation.create", "message.send"),
56
+ Renderer.PDF: ("file.create", "message.send"),
56
57
  Renderer.ROWS: ("row.append", "file.create"),
58
+ Renderer.XLSX: ("file.create",),
57
59
  }
58
60
 
59
61
  # A filter on one of these fields takes one of the shape's listed values.
@@ -14,6 +14,8 @@ would get them: a condition on an input nobody answered and nothing defaulted do
14
14
 
15
15
  from __future__ import annotations
16
16
 
17
+ import datetime as dt
18
+ import re
17
19
  from typing import Any
18
20
 
19
21
  from .specs import ActionSpec, InputSpec, InputType
@@ -115,8 +117,52 @@ def apply_rules(manifest: Manifest, answers: dict[str, Any]) -> dict[str, Any]:
115
117
  Problem = tuple[str, str] # (the input or choose_one label it is about, what to tell the person)
116
118
 
117
119
 
120
+ DAY = re.compile(r"^\d{4}-\d{2}-\d{2}$")
121
+
122
+
123
+ def _is_day(value: Any) -> bool:
124
+ if not isinstance(value, str) or not DAY.match(value):
125
+ return False
126
+ try:
127
+ dt.date.fromisoformat(value)
128
+ except ValueError:
129
+ return False
130
+ return True
131
+
132
+
133
+ # The file id in a Google Doc or Sheet link, or a bare id (Google's are 25 or more of these characters).
134
+ _GOOGLE_PATH = {"google-doc": "document", "google-sheet": "spreadsheets"}
135
+ _GOOGLE_ID = re.compile(r"^[A-Za-z0-9_-]{25,}$")
136
+
137
+
138
+ def google_file_id(kind: str, value: Any) -> str | None:
139
+ """'https://docs.google.com/spreadsheets/d/<id>/edit#gid=0' -> '<id>' for a google-sheet input (document for a
140
+ google-doc one); a bare id as it is; None for anything else, a Sheet's link in a Doc input included."""
141
+ text = value.strip() if isinstance(value, str) else ""
142
+ if _GOOGLE_ID.match(text):
143
+ return text
144
+ m = re.search(rf"docs\.google\.com/{_GOOGLE_PATH.get(kind, '-')}/d/([A-Za-z0-9_-]{{20,}})", text)
145
+ return m.group(1) if m else None
146
+
147
+
118
148
  def _bounds(spec: InputSpec, value: Any) -> str | None:
119
149
  label = spec.label or spec.name
150
+ if spec.many_days and not _blank(value):
151
+ if not isinstance(value, list) or not all(_is_day(v) for v in value):
152
+ return f'"{label}": pick days on the calendar.'
153
+ if spec.min is not None and len(value) < spec.min:
154
+ return f'"{label}": pick at least {spec.min} days.'
155
+ if spec.max is not None and len(value) > spec.max:
156
+ return f'"{label}": pick at most {spec.max} days.'
157
+ return None
158
+ if spec.many_files and not _blank(value):
159
+ if not isinstance(value, list) or not all(isinstance(v, str) and v.strip() for v in value):
160
+ return f'"{label}": upload the files again.'
161
+ if spec.min is not None and len(value) < spec.min:
162
+ return f'"{label}": upload at least {spec.min} files.'
163
+ if spec.max is not None and len(value) > spec.max:
164
+ return f'"{label}": upload at most {spec.max} files.'
165
+ return None
120
166
  if spec.type is InputType.MULTI_SELECT and not _blank(value):
121
167
  if not isinstance(value, list) or any(v not in (spec.options or []) for v in value):
122
168
  return f'"{label}": pick from {", ".join(spec.options or [])}.'
@@ -141,6 +187,10 @@ def _check(inputs: list[InputSpec], shown: list[str], answers: dict[str, Any]) -
141
187
  out.append((spec.name, f'"{spec.label or spec.name}" is required.'))
142
188
  elif spec.type is InputType.MULTI_SELECT and spec.min and _blank(value):
143
189
  out.append((spec.name, f'"{spec.label or spec.name}": pick at least {spec.min}.'))
190
+ elif spec.many_days and spec.min and _blank(value):
191
+ out.append((spec.name, f'"{spec.label or spec.name}": pick at least {spec.min} days.'))
192
+ elif spec.many_files and spec.min and _blank(value):
193
+ out.append((spec.name, f'"{spec.label or spec.name}": upload at least {spec.min} files.'))
144
194
  elif (problem := _bounds(spec, value)) is not None:
145
195
  out.append((spec.name, problem))
146
196
  return out
@@ -11,7 +11,9 @@
11
11
  {"name": "brand_kit", "type": "text", "label": "Brand kit link", "show_if": {"platforms": ["instagram", "youtube"]}},
12
12
  {"name": "contact_source", "type": "static-dropdown", "label": "Contacts from", "options": ["gravity", "hunter", "both"], "default": "gravity"},
13
13
  {"name": "hunter_key", "type": "secret", "label": "Your Hunter API key", "required": true, "show_if": {"contact_source": ["hunter", "both"]}},
14
- {"name": "goal", "type": "action-choice", "label": "What do you want to do first?", "actions": ["find_companies", "write_emails"]}
14
+ {"name": "goal", "type": "action-choice", "label": "What do you want to do first?", "actions": ["find_companies", "write_emails"]},
15
+ {"name": "call_days", "type": "date", "label": "Days for a call", "min": 1, "max": 3, "show_if": {"goal": "write_emails"}},
16
+ {"name": "past_pitches", "type": "file", "label": "Past pitch decks", "accept": ["pdf"], "min": 1, "max": 2, "show_if": {"goal": "find_companies"}}
15
17
  ],
16
18
  "slots": {
17
19
  "deliver": {"label": "Deliver to", "default": "gravity", "adapters": {"gravity": [], "gmail": ["gmail.draft"]}},
@@ -62,6 +64,38 @@
62
64
  {"name": "action choice must be offered",
63
65
  "answers": {"resume_text": "cv", "platforms": ["x"], "goal": "delete_everything"},
64
66
  "visible": ["resume_text", "platforms", "contact_source", "goal"],
65
- "problems": ["goal"]}
67
+ "problems": ["goal"]},
68
+ {"name": "a date with max takes a list of days",
69
+ "answers": {"resume_text": "cv", "platforms": ["x"], "goal": "write_emails", "call_days": ["2026-11-03", "2026-11-05"]},
70
+ "visible": ["resume_text", "platforms", "contact_source", "goal", "call_days"],
71
+ "problems": []},
72
+ {"name": "days must be real calendar days",
73
+ "answers": {"resume_text": "cv", "platforms": ["x"], "goal": "write_emails", "call_days": ["next tuesday", "2026-02-30"]},
74
+ "visible": ["resume_text", "platforms", "contact_source", "goal", "call_days"],
75
+ "problems": ["call_days"]},
76
+ {"name": "no more days than max",
77
+ "answers": {"resume_text": "cv", "platforms": ["x"], "goal": "write_emails", "call_days": ["2026-11-03", "2026-11-04", "2026-11-05", "2026-11-06"]},
78
+ "visible": ["resume_text", "platforms", "contact_source", "goal", "call_days"],
79
+ "problems": ["call_days"]},
80
+ {"name": "min days asks for at least one",
81
+ "answers": {"resume_text": "cv", "platforms": ["x"], "goal": "write_emails"},
82
+ "visible": ["resume_text", "platforms", "contact_source", "goal", "call_days"],
83
+ "problems": ["call_days"]},
84
+ {"name": "a file with max takes a list of links",
85
+ "answers": {"resume_text": "cv", "platforms": ["x"], "goal": "find_companies", "past_pitches": ["https://x.test/uploads/a.pdf", "https://x.test/uploads/b.pdf"]},
86
+ "visible": ["resume_text", "platforms", "contact_source", "goal", "past_pitches"],
87
+ "problems": []},
88
+ {"name": "no more files than max",
89
+ "answers": {"resume_text": "cv", "platforms": ["x"], "goal": "find_companies", "past_pitches": ["https://x.test/a.pdf", "https://x.test/b.pdf", "https://x.test/c.pdf"]},
90
+ "visible": ["resume_text", "platforms", "contact_source", "goal", "past_pitches"],
91
+ "problems": ["past_pitches"]},
92
+ {"name": "several files are a list, not one link",
93
+ "answers": {"resume_text": "cv", "platforms": ["x"], "goal": "find_companies", "past_pitches": "https://x.test/a.pdf"},
94
+ "visible": ["resume_text", "platforms", "contact_source", "goal", "past_pitches"],
95
+ "problems": ["past_pitches"]},
96
+ {"name": "min files asks for at least one",
97
+ "answers": {"resume_text": "cv", "platforms": ["x"], "goal": "find_companies"},
98
+ "visible": ["resume_text", "platforms", "contact_source", "goal", "past_pitches"],
99
+ "problems": ["past_pitches"]}
66
100
  ]
67
101
  }
@@ -35,12 +35,16 @@ class InputType(str, Enum):
35
35
  # An API key or password. The platform seals it with KMS when it is submitted, keeps it
36
36
  # out of every API response and log, and opens it only to hand it to the running agent.
37
37
  SECRET = "secret"
38
- # The agent gets a short-lived URL it fetches through the gateway.
38
+ # The agent gets a short-lived URL it fetches through the gateway; with `max`, a list of them (several files).
39
39
  FILE = "file"
40
40
  # Several of `options`; the value is a list.
41
41
  MULTI_SELECT = "multi-select"
42
42
  # One of this agent's declared `actions`; the chosen action runs (rules.py).
43
43
  ACTION_CHOICE = "action-choice"
44
+ # A Google Doc or Sheet of the person's: they paste its link, the agent gets the file id (rules.google_file_id),
45
+ # which is what docs.read and sheets.read take.
46
+ GOOGLE_DOC = "google-doc"
47
+ GOOGLE_SHEET = "google-sheet"
44
48
 
45
49
 
46
50
  # Which Python types each input's `default` may be.
@@ -57,6 +61,8 @@ _PYTHON_TYPES: dict[InputType, tuple[type, ...]] = {
57
61
  InputType.FILE: (), # never has a default
58
62
  InputType.MULTI_SELECT: (list,),
59
63
  InputType.ACTION_CHOICE: (str,),
64
+ InputType.GOOGLE_DOC: (str,),
65
+ InputType.GOOGLE_SHEET: (str,),
60
66
  }
61
67
 
62
68
  # A `show_if` value: what the named input or slot must be for this question to be asked.
@@ -86,7 +92,9 @@ class Renderer(str, Enum):
86
92
  MARKDOWN = "markdown"
87
93
  DOCX = "docx"
88
94
  PPTX = "pptx"
89
- ROWS = "rows"
95
+ PDF = "pdf"
96
+ ROWS = "rows" # CSV of the first table
97
+ XLSX = "xlsx" # every table a sheet
90
98
 
91
99
 
92
100
  class WebhookSource(str, Enum):
@@ -204,7 +212,8 @@ class InputSpec(BaseModel):
204
212
  default: Any | None = None
205
213
  # Asked only when every named earlier input or slot has one of the given values (rules.py).
206
214
  show_if: Annotated[ShowIf | None, Field(max_length=C.MAX_INPUTS)] = None
207
- # multi-select only: how many options may be picked.
215
+ # multi-select: how many options may be picked. date: how many days; with `max` the answer is a list of
216
+ # "YYYY-MM-DD" days, picked on a calendar. file: how many files; with `max` the answer is a list of links.
208
217
  min: Annotated[int | None, Field(ge=0, le=C.MAX_OPTIONS)] = None
209
218
  max: Annotated[int | None, Field(ge=1, le=C.MAX_OPTIONS)] = None
210
219
  # action-choice only: the declared actions offered, in order.
@@ -253,14 +262,40 @@ class InputSpec(BaseModel):
253
262
  )
254
263
  return self
255
264
 
265
+ @property
266
+ def many_days(self) -> bool:
267
+ """A date input that takes several days: its answer is a list of "YYYY-MM-DD"."""
268
+ return self.type is InputType.DATE and self.max is not None
269
+
270
+ @property
271
+ def many_files(self) -> bool:
272
+ """A file input that takes several files: its answer is a list of links, one per file."""
273
+ return self.type is InputType.FILE and self.max is not None
274
+
256
275
  @model_validator(mode="after")
257
276
  def _check_choice_fields(self) -> InputSpec:
258
277
  multi = self.type is InputType.MULTI_SELECT
259
- if (self.min is not None or self.max is not None) and not multi:
278
+ if (self.min is not None or self.max is not None) and not (multi or self.type in (InputType.DATE, InputType.FILE)):
279
+ raise PydanticCustomError(
280
+ ErrorCode.INVALID_INPUT.value,
281
+ "Input '{value}' sets min or max but is not a multi-select, a date or a file.",
282
+ {"value": self.name, "remediation": "Set type to 'multi-select', 'date' or 'file', or remove min and max."},
283
+ )
284
+ file_bounds = self.type is InputType.FILE and (self.min is not None or self.max is not None)
285
+ if file_bounds and (self.max is None or (self.min or 0) > self.max or self.max > C.MAX_FILES):
260
286
  raise PydanticCustomError(
261
287
  ErrorCode.INVALID_INPUT.value,
262
- "Input '{value}' sets min or max but is not a multi-select.",
263
- {"value": self.name, "remediation": "Set type to 'multi-select', or remove min and max."},
288
+ "File input '{value}' asks for between {lo} and {hi} files.",
289
+ {"value": self.name, "lo": self.min or 0, "hi": self.max,
290
+ "remediation": f"Set `max` (how many files may be uploaded, at most {C.MAX_FILES}), and keep min <= max."},
291
+ )
292
+ days_bounds = self.type is InputType.DATE and (self.min is not None or self.max is not None)
293
+ if days_bounds and (self.max is None or (self.min or 0) > self.max):
294
+ raise PydanticCustomError(
295
+ ErrorCode.INVALID_INPUT.value,
296
+ "Date input '{value}' asks for between {lo} and {hi} days.",
297
+ {"value": self.name, "lo": self.min or 0, "hi": self.max,
298
+ "remediation": "Set `max` (how many days may be picked), and keep min <= max."},
264
299
  )
265
300
  if multi:
266
301
  count = len(self.options or [])
@@ -344,7 +379,7 @@ class InputSpec(BaseModel):
344
379
  },
345
380
  )
346
381
 
347
- expected = _PYTHON_TYPES[self.type]
382
+ expected = (list,) if self.many_days else _PYTHON_TYPES[self.type]
348
383
  # bool is an int subclass, so NUMBER needs this guard.
349
384
  is_bool = isinstance(self.default, bool)
350
385
  ok = (
@@ -451,6 +486,16 @@ class ScheduleSpec(BaseModel):
451
486
  return v
452
487
 
453
488
 
489
+ class EmailTriggerSpec(BaseModel):
490
+ """Run when new mail matching a Gmail search arrives in the person's inbox. Gravity checks every 5 minutes and
491
+ starts a run only when there is new mail, with the messages in inputs["event"]["emails"]."""
492
+
493
+ model_config = _STRICT
494
+
495
+ query: Annotated[str, Field(min_length=1, max_length=500)] # a Gmail search: "from:recruiting@acme.com is:unread"
496
+ consumer_can_change: bool = True
497
+
498
+
454
499
  class TriggersSpec(BaseModel):
455
500
  """What starts a run. Declared so the consumer sees it before installing."""
456
501
 
@@ -459,14 +504,15 @@ class TriggersSpec(BaseModel):
459
504
  manual: bool = True
460
505
  schedule: ScheduleSpec | None = None
461
506
  webhook: list[WebhookSource] = Field(default_factory=list)
507
+ email: EmailTriggerSpec | None = None
462
508
 
463
509
  @model_validator(mode="after")
464
510
  def _check_something_starts_it(self) -> TriggersSpec:
465
- if not self.manual and self.schedule is None and not self.webhook:
511
+ if not self.manual and self.schedule is None and not self.webhook and self.email is None:
466
512
  raise PydanticCustomError(
467
513
  ErrorCode.INVALID_TRIGGER.value,
468
514
  "Nothing can start this agent.",
469
- {"remediation": "Set manual: true, or add a schedule or a webhook."},
515
+ {"remediation": "Set manual: true, or add a schedule, an email trigger or a webhook."},
470
516
  )
471
517
  return self
472
518
 
@@ -536,6 +582,15 @@ class ChartHint(BaseModel):
536
582
  )
537
583
 
538
584
 
585
+ class TabHint(BaseModel):
586
+ """One tab of the result, as the agent's page in Explore lists it: its name and what is on it."""
587
+
588
+ model_config = _HINT
589
+
590
+ name: Annotated[str, Field(max_length=C.MAX_NAME_LENGTH)]
591
+ about: Annotated[str, Field(max_length=C.MAX_LABEL_LENGTH)] | None = None
592
+
593
+
539
594
  class DisplaySpec(BaseModel):
540
595
  """How the run's result shows on the page, when the builder wants to say so.
541
596
 
@@ -546,6 +601,8 @@ class DisplaySpec(BaseModel):
546
601
  view: document | report | table | chart | code | files
547
602
  field: the result key that is the main thing to show
548
603
  chart: {type: line | bar, x: date, y: [reach, likes]}
604
+ tabs: [{name: Overview, about: "Your best options and what they cost"}], the
605
+ result's tabs in order, listed on the agent's page before anyone runs it
549
606
  """
550
607
 
551
608
  model_config = _HINT
@@ -553,6 +610,7 @@ class DisplaySpec(BaseModel):
553
610
  view: Annotated[str, Field(max_length=C.MAX_NAME_LENGTH)] | None = None
554
611
  field: Annotated[str, Field(max_length=C.MAX_NAME_LENGTH)] | None = None
555
612
  chart: ChartHint | None = None
613
+ tabs: Annotated[list[TabHint], Field(max_length=12)] = Field(default_factory=list)
556
614
 
557
615
 
558
616
  class ChooseOneSpec(BaseModel):
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "gravity-schema"
3
- version = "0.9.0"
3
+ version = "0.11.0"
4
4
  description = "Gravity agent manifest contract — models, validation, and JSON Schema export"
5
5
  readme = "README.md"
6
6
  license = "Apache-2.0"
@@ -301,7 +301,7 @@
301
301
  "type": "object"
302
302
  },
303
303
  "DisplaySpec": {
304
- "description": "How the run's result shows on the page, when the builder wants to say so.\n\nA hint, not a rule: without it the page reads the result's own shape, and a\nvalue the page doesn't know (a typo, a newer view) is ignored the same way,\nso it never fails a push or a run.\n\n view: document | report | table | chart | code | files\n field: the result key that is the main thing to show\n chart: {type: line | bar, x: date, y: [reach, likes]}",
304
+ "description": "How the run's result shows on the page, when the builder wants to say so.\n\nA hint, not a rule: without it the page reads the result's own shape, and a\nvalue the page doesn't know (a typo, a newer view) is ignored the same way,\nso it never fails a push or a run.\n\n view: document | report | table | chart | code | files\n field: the result key that is the main thing to show\n chart: {type: line | bar, x: date, y: [reach, likes]}\n tabs: [{name: Overview, about: \"Your best options and what they cost\"}], the\n result's tabs in order, listed on the agent's page before anyone runs it",
305
305
  "properties": {
306
306
  "chart": {
307
307
  "anyOf": [
@@ -327,6 +327,14 @@
327
327
  "default": null,
328
328
  "title": "Field"
329
329
  },
330
+ "tabs": {
331
+ "items": {
332
+ "$ref": "#/$defs/TabHint"
333
+ },
334
+ "maxItems": 12,
335
+ "title": "Tabs",
336
+ "type": "array"
337
+ },
330
338
  "view": {
331
339
  "anyOf": [
332
340
  {
@@ -344,6 +352,28 @@
344
352
  "title": "DisplaySpec",
345
353
  "type": "object"
346
354
  },
355
+ "EmailTriggerSpec": {
356
+ "additionalProperties": false,
357
+ "description": "Run when new mail matching a Gmail search arrives in the person's inbox. Gravity checks every 5 minutes and\nstarts a run only when there is new mail, with the messages in inputs[\"event\"][\"emails\"].",
358
+ "properties": {
359
+ "consumer_can_change": {
360
+ "default": true,
361
+ "title": "Consumer Can Change",
362
+ "type": "boolean"
363
+ },
364
+ "query": {
365
+ "maxLength": 500,
366
+ "minLength": 1,
367
+ "title": "Query",
368
+ "type": "string"
369
+ }
370
+ },
371
+ "required": [
372
+ "query"
373
+ ],
374
+ "title": "EmailTriggerSpec",
375
+ "type": "object"
376
+ },
347
377
  "InputSpec": {
348
378
  "additionalProperties": false,
349
379
  "description": "One value the consumer supplies. Renders as one form field.",
@@ -523,7 +553,9 @@
523
553
  "secret",
524
554
  "file",
525
555
  "multi-select",
526
- "action-choice"
556
+ "action-choice",
557
+ "google-doc",
558
+ "google-sheet"
527
559
  ],
528
560
  "title": "InputType",
529
561
  "type": "string"
@@ -1034,7 +1066,9 @@
1034
1066
  "markdown",
1035
1067
  "docx",
1036
1068
  "pptx",
1037
- "rows"
1069
+ "pdf",
1070
+ "rows",
1071
+ "xlsx"
1038
1072
  ],
1039
1073
  "title": "Renderer",
1040
1074
  "type": "string"
@@ -1339,10 +1373,49 @@
1339
1373
  "title": "StateSpec",
1340
1374
  "type": "object"
1341
1375
  },
1376
+ "TabHint": {
1377
+ "description": "One tab of the result, as the agent's page in Explore lists it: its name and what is on it.",
1378
+ "properties": {
1379
+ "about": {
1380
+ "anyOf": [
1381
+ {
1382
+ "maxLength": 200,
1383
+ "type": "string"
1384
+ },
1385
+ {
1386
+ "type": "null"
1387
+ }
1388
+ ],
1389
+ "default": null,
1390
+ "title": "About"
1391
+ },
1392
+ "name": {
1393
+ "maxLength": 64,
1394
+ "title": "Name",
1395
+ "type": "string"
1396
+ }
1397
+ },
1398
+ "required": [
1399
+ "name"
1400
+ ],
1401
+ "title": "TabHint",
1402
+ "type": "object"
1403
+ },
1342
1404
  "TriggersSpec": {
1343
1405
  "additionalProperties": false,
1344
1406
  "description": "What starts a run. Declared so the consumer sees it before installing.",
1345
1407
  "properties": {
1408
+ "email": {
1409
+ "anyOf": [
1410
+ {
1411
+ "$ref": "#/$defs/EmailTriggerSpec"
1412
+ },
1413
+ {
1414
+ "type": "null"
1415
+ }
1416
+ ],
1417
+ "default": null
1418
+ },
1346
1419
  "manual": {
1347
1420
  "default": true,
1348
1421
  "title": "Manual",
@@ -765,6 +765,14 @@ def test_display_hint_parses(manifest_data):
765
765
  assert (d.view, d.field, d.chart.x, d.chart.y) == ("chart", "daily", "date", ["reach"])
766
766
 
767
767
 
768
+ def test_display_tabs_parse_and_survive_the_dump(manifest_data):
769
+ # The push sends model_dump, so the tabs must be in it for the catalog to list them.
770
+ manifest_data["display"] = {"tabs": [{"name": "Overview", "about": "Your best options"}, {"name": "Money"}]}
771
+ d = validate_manifest(manifest_data).display
772
+ assert [t.name for t in d.tabs] == ["Overview", "Money"]
773
+ assert validate_manifest(manifest_data).model_dump(mode="json", exclude_none=True)["display"]["tabs"][0] == {"name": "Overview", "about": "Your best options"}
774
+
775
+
768
776
  def test_display_never_fails_on_what_the_page_does_not_know(manifest_data):
769
777
  # An unknown view is kept for the page to ignore; an unknown key is dropped.
770
778
  manifest_data["display"] = {"view": "carousel", "colour": "blue"}