ellf-cli 6.0.4__tar.gz → 6.0.5__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 (185) hide show
  1. {ellf_cli-6.0.4/ellf_cli.egg-info → ellf_cli-6.0.5}/PKG-INFO +1 -1
  2. {ellf_cli-6.0.4 → ellf_cli-6.0.5}/README.md +105 -0
  3. {ellf_cli-6.0.4 → ellf_cli-6.0.5}/ellf_cli/about.json +1 -1
  4. {ellf_cli-6.0.4 → ellf_cli-6.0.5}/ellf_cli/commands/__init__.py +2 -0
  5. {ellf_cli-6.0.4 → ellf_cli-6.0.5}/ellf_cli/commands/_recipe_subcommand.py +31 -1
  6. ellf_cli-6.0.5/ellf_cli/commands/services.py +390 -0
  7. {ellf_cli-6.0.4 → ellf_cli-6.0.5}/ellf_cli/config.py +8 -2
  8. {ellf_cli-6.0.4 → ellf_cli-6.0.5}/ellf_cli/ellf.json +604 -1
  9. {ellf_cli-6.0.4 → ellf_cli-6.0.5}/ellf_cli/query.py +63 -6
  10. {ellf_cli-6.0.4 → ellf_cli-6.0.5/ellf_cli.egg-info}/PKG-INFO +1 -1
  11. {ellf_cli-6.0.4 → ellf_cli-6.0.5}/ellf_cli.egg-info/SOURCES.txt +1 -0
  12. {ellf_cli-6.0.4 → ellf_cli-6.0.5}/LICENSE +0 -0
  13. {ellf_cli-6.0.4 → ellf_cli-6.0.5}/MANIFEST.in +0 -0
  14. {ellf_cli-6.0.4 → ellf_cli-6.0.5}/ellf_cli/__init__.py +0 -0
  15. {ellf_cli-6.0.4 → ellf_cli-6.0.5}/ellf_cli/__main__.py +0 -0
  16. {ellf_cli-6.0.4 → ellf_cli-6.0.5}/ellf_cli/about.py +0 -0
  17. {ellf_cli-6.0.4 → ellf_cli-6.0.5}/ellf_cli/appdirs.py +0 -0
  18. {ellf_cli-6.0.4 → ellf_cli-6.0.5}/ellf_cli/auth.py +0 -0
  19. {ellf_cli-6.0.4 → ellf_cli-6.0.5}/ellf_cli/cli.py +0 -0
  20. {ellf_cli-6.0.4 → ellf_cli-6.0.5}/ellf_cli/cloud/__init__.py +0 -0
  21. {ellf_cli-6.0.4 → ellf_cli-6.0.5}/ellf_cli/cloud/gcp.py +0 -0
  22. {ellf_cli-6.0.4 → ellf_cli-6.0.5}/ellf_cli/cluster_config.py +0 -0
  23. {ellf_cli-6.0.4 → ellf_cli-6.0.5}/ellf_cli/commands/_cluster_select.py +0 -0
  24. {ellf_cli-6.0.4 → ellf_cli-6.0.5}/ellf_cli/commands/_org_select.py +0 -0
  25. {ellf_cli-6.0.4 → ellf_cli-6.0.5}/ellf_cli/commands/_recipe_file.py +0 -0
  26. {ellf_cli-6.0.4 → ellf_cli-6.0.5}/ellf_cli/commands/_state.py +0 -0
  27. {ellf_cli-6.0.4 → ellf_cli-6.0.5}/ellf_cli/commands/actions.py +0 -0
  28. {ellf_cli-6.0.4 → ellf_cli-6.0.5}/ellf_cli/commands/agents.py +0 -0
  29. {ellf_cli-6.0.4 → ellf_cli-6.0.5}/ellf_cli/commands/assets.py +0 -0
  30. {ellf_cli-6.0.4 → ellf_cli-6.0.5}/ellf_cli/commands/auth.py +0 -0
  31. {ellf_cli-6.0.4 → ellf_cli-6.0.5}/ellf_cli/commands/clusters.py +0 -0
  32. {ellf_cli-6.0.4 → ellf_cli-6.0.5}/ellf_cli/commands/config.py +0 -0
  33. {ellf_cli-6.0.4 → ellf_cli-6.0.5}/ellf_cli/commands/datasets.py +0 -0
  34. {ellf_cli-6.0.4 → ellf_cli-6.0.5}/ellf_cli/commands/files/__init__.py +0 -0
  35. {ellf_cli-6.0.4 → ellf_cli-6.0.5}/ellf_cli/commands/files/cp.py +0 -0
  36. {ellf_cli-6.0.4 → ellf_cli-6.0.5}/ellf_cli/commands/files/ls.py +0 -0
  37. {ellf_cli-6.0.4 → ellf_cli-6.0.5}/ellf_cli/commands/files/rm.py +0 -0
  38. {ellf_cli-6.0.4 → ellf_cli-6.0.5}/ellf_cli/commands/files/rsync.py +0 -0
  39. {ellf_cli-6.0.4 → ellf_cli-6.0.5}/ellf_cli/commands/files/stats.py +0 -0
  40. {ellf_cli-6.0.4 → ellf_cli-6.0.5}/ellf_cli/commands/general.py +0 -0
  41. {ellf_cli-6.0.4 → ellf_cli-6.0.5}/ellf_cli/commands/import_export.py +0 -0
  42. {ellf_cli-6.0.4 → ellf_cli-6.0.5}/ellf_cli/commands/infra/__init__.py +0 -0
  43. {ellf_cli-6.0.4 → ellf_cli-6.0.5}/ellf_cli/commands/infra/_helpers.py +0 -0
  44. {ellf_cli-6.0.4 → ellf_cli-6.0.5}/ellf_cli/commands/infra/deploy.py +0 -0
  45. {ellf_cli-6.0.4 → ellf_cli-6.0.5}/ellf_cli/commands/infra/init_values.py +0 -0
  46. {ellf_cli-6.0.4 → ellf_cli-6.0.5}/ellf_cli/commands/infra/provision.py +0 -0
  47. {ellf_cli-6.0.4 → ellf_cli-6.0.5}/ellf_cli/commands/infra/register.py +0 -0
  48. {ellf_cli-6.0.4 → ellf_cli-6.0.5}/ellf_cli/commands/infra/setup.py +0 -0
  49. {ellf_cli-6.0.4 → ellf_cli-6.0.5}/ellf_cli/commands/infra/start.py +0 -0
  50. {ellf_cli-6.0.4 → ellf_cli-6.0.5}/ellf_cli/commands/infra/terraform.py +0 -0
  51. {ellf_cli-6.0.4 → ellf_cli-6.0.5}/ellf_cli/commands/infra/tls.py +0 -0
  52. {ellf_cli-6.0.4 → ellf_cli-6.0.5}/ellf_cli/commands/jobs.py +0 -0
  53. {ellf_cli-6.0.4 → ellf_cli-6.0.5}/ellf_cli/commands/packages.py +0 -0
  54. {ellf_cli-6.0.4 → ellf_cli-6.0.5}/ellf_cli/commands/paths.py +0 -0
  55. {ellf_cli-6.0.4 → ellf_cli-6.0.5}/ellf_cli/commands/plans.py +0 -0
  56. {ellf_cli-6.0.4 → ellf_cli-6.0.5}/ellf_cli/commands/projects.py +0 -0
  57. {ellf_cli-6.0.4 → ellf_cli-6.0.5}/ellf_cli/commands/publish_code.py +0 -0
  58. {ellf_cli-6.0.4 → ellf_cli-6.0.5}/ellf_cli/commands/publish_data.py +0 -0
  59. {ellf_cli-6.0.4 → ellf_cli-6.0.5}/ellf_cli/commands/recipes.py +0 -0
  60. {ellf_cli-6.0.4 → ellf_cli-6.0.5}/ellf_cli/commands/secrets.py +0 -0
  61. {ellf_cli-6.0.4 → ellf_cli-6.0.5}/ellf_cli/commands/support.py +0 -0
  62. {ellf_cli-6.0.4 → ellf_cli-6.0.5}/ellf_cli/commands/tasks.py +0 -0
  63. {ellf_cli-6.0.4 → ellf_cli-6.0.5}/ellf_cli/commands/todos.py +0 -0
  64. {ellf_cli-6.0.4 → ellf_cli-6.0.5}/ellf_cli/ellf_skills/.claude-plugin/plugin.json +0 -0
  65. {ellf_cli-6.0.4 → ellf_cli-6.0.5}/ellf_cli/ellf_skills/.gitignore +0 -0
  66. {ellf_cli-6.0.4 → ellf_cli-6.0.5}/ellf_cli/ellf_skills/skill_variants.json +0 -0
  67. {ellf_cli-6.0.4 → ellf_cli-6.0.5}/ellf_cli/ellf_skills/skills/ellf-annotate.assistant/SKILL.md +0 -0
  68. {ellf_cli-6.0.4 → ellf_cli-6.0.5}/ellf_cli/ellf_skills/skills/ellf-annotate.assistant/references/annotation_audit.md +0 -0
  69. {ellf_cli-6.0.4 → ellf_cli-6.0.5}/ellf_cli/ellf_skills/skills/ellf-annotate.assistant/references/builtin_ellf_annotation_recipes.md +0 -0
  70. {ellf_cli-6.0.4 → ellf_cli-6.0.5}/ellf_cli/ellf_skills/skills/ellf-annotate.coding/SKILL.md +0 -0
  71. {ellf_cli-6.0.4 → ellf_cli-6.0.5}/ellf_cli/ellf_skills/skills/ellf-annotate.coding/references/annotation_audit.md +0 -0
  72. {ellf_cli-6.0.4 → ellf_cli-6.0.5}/ellf_cli/ellf_skills/skills/ellf-annotate.coding/references/builtin_ellf_annotation_recipes.md +0 -0
  73. {ellf_cli-6.0.4 → ellf_cli-6.0.5}/ellf_cli/ellf_skills/skills/ellf-annotate.coding/references/builtin_prodigy_recipes.md +0 -0
  74. {ellf_cli-6.0.4 → ellf_cli-6.0.5}/ellf_cli/ellf_skills/skills/ellf-ask/SKILL.md +0 -0
  75. {ellf_cli-6.0.4 → ellf_cli-6.0.5}/ellf_cli/ellf_skills/skills/ellf-handoff/SKILL.md +0 -0
  76. {ellf_cli-6.0.4 → ellf_cli-6.0.5}/ellf_cli/ellf_skills/skills/ellf-monitor.assistant/SKILL.md +0 -0
  77. {ellf_cli-6.0.4 → ellf_cli-6.0.5}/ellf_cli/ellf_skills/skills/ellf-monitor.assistant/references/annotation_metrics.md +0 -0
  78. {ellf_cli-6.0.4 → ellf_cli-6.0.5}/ellf_cli/ellf_skills/skills/ellf-monitor.assistant/references/training_monitoring.md +0 -0
  79. {ellf_cli-6.0.4 → ellf_cli-6.0.5}/ellf_cli/ellf_skills/skills/ellf-monitor.coding/SKILL.md +0 -0
  80. {ellf_cli-6.0.4 → ellf_cli-6.0.5}/ellf_cli/ellf_skills/skills/ellf-monitor.coding/references/annotation_metrics.md +0 -0
  81. {ellf_cli-6.0.4 → ellf_cli-6.0.5}/ellf_cli/ellf_skills/skills/ellf-monitor.coding/references/training_monitoring.md +0 -0
  82. {ellf_cli-6.0.4 → ellf_cli-6.0.5}/ellf_cli/ellf_skills/skills/ellf-monitor.coding/scripts/check_training.py +0 -0
  83. {ellf_cli-6.0.4 → ellf_cli-6.0.5}/ellf_cli/ellf_skills/skills/ellf-ops.assistant/SKILL.md +0 -0
  84. {ellf_cli-6.0.4 → ellf_cli-6.0.5}/ellf_cli/ellf_skills/skills/ellf-ops.coding/SKILL.md +0 -0
  85. {ellf_cli-6.0.4 → ellf_cli-6.0.5}/ellf_cli/ellf_skills/skills/ellf-ops.coding/references/data_infra_cli.md +0 -0
  86. {ellf_cli-6.0.4 → ellf_cli-6.0.5}/ellf_cli/ellf_skills/skills/ellf-ops.coding/scripts/run_job.py +0 -0
  87. {ellf_cli-6.0.4 → ellf_cli-6.0.5}/ellf_cli/ellf_skills/skills/ellf-patterns/SKILL.md +0 -0
  88. {ellf_cli-6.0.4 → ellf_cli-6.0.5}/ellf_cli/ellf_skills/skills/ellf-patterns/references/pattern_strategies.md +0 -0
  89. {ellf_cli-6.0.4 → ellf_cli-6.0.5}/ellf_cli/ellf_skills/skills/ellf-prodigy/SKILL.md +0 -0
  90. {ellf_cli-6.0.4 → ellf_cli-6.0.5}/ellf_cli/ellf_skills/skills/ellf-prodigy/assets/templates/template_action_recipe.py +0 -0
  91. {ellf_cli-6.0.4 → ellf_cli-6.0.5}/ellf_cli/ellf_skills/skills/ellf-prodigy/assets/templates/template_agent_recipe.py +0 -0
  92. {ellf_cli-6.0.4 → ellf_cli-6.0.5}/ellf_cli/ellf_skills/skills/ellf-prodigy/assets/templates/template_blocks_ui.py +0 -0
  93. {ellf_cli-6.0.4 → ellf_cli-6.0.5}/ellf_cli/ellf_skills/skills/ellf-prodigy/assets/templates/template_correct.py +0 -0
  94. {ellf_cli-6.0.4 → ellf_cli-6.0.5}/ellf_cli/ellf_skills/skills/ellf-prodigy/assets/templates/template_custom_ui.py +0 -0
  95. {ellf_cli-6.0.4 → ellf_cli-6.0.5}/ellf_cli/ellf_skills/skills/ellf-prodigy/assets/templates/template_manual.py +0 -0
  96. {ellf_cli-6.0.4 → ellf_cli-6.0.5}/ellf_cli/ellf_skills/skills/ellf-prodigy/assets/templates/template_pages_ui.py +0 -0
  97. {ellf_cli-6.0.4 → ellf_cli-6.0.5}/ellf_cli/ellf_skills/skills/ellf-prodigy/assets/templates/template_routing.py +0 -0
  98. {ellf_cli-6.0.4 → ellf_cli-6.0.5}/ellf_cli/ellf_skills/skills/ellf-prodigy/assets/templates/template_task_recipe.py +0 -0
  99. {ellf_cli-6.0.4 → ellf_cli-6.0.5}/ellf_cli/ellf_skills/skills/ellf-prodigy/assets/templates/template_teach.py +0 -0
  100. {ellf_cli-6.0.4 → ellf_cli-6.0.5}/ellf_cli/ellf_skills/skills/ellf-prodigy/references/builtin_recipes.md +0 -0
  101. {ellf_cli-6.0.4 → ellf_cli-6.0.5}/ellf_cli/ellf_skills/skills/ellf-prodigy/references/ellf_recipe_sdk.md +0 -0
  102. {ellf_cli-6.0.4 → ellf_cli-6.0.5}/ellf_cli/ellf_skills/skills/ellf-prodigy/references/lint_recipe.py +0 -0
  103. {ellf_cli-6.0.4 → ellf_cli-6.0.5}/ellf_cli/ellf_skills/skills/ellf-prodigy/references/prodigy_recipe_api.md +0 -0
  104. {ellf_cli-6.0.4 → ellf_cli-6.0.5}/ellf_cli/ellf_skills/skills/ellf-prodigy/references/template_index.md +0 -0
  105. {ellf_cli-6.0.4 → ellf_cli-6.0.5}/ellf_cli/ellf_skills/skills/ellf-project.assistant/SKILL.md +0 -0
  106. {ellf_cli-6.0.4 → ellf_cli-6.0.5}/ellf_cli/ellf_skills/skills/ellf-project.assistant/references/consulting_patterns.md +0 -0
  107. {ellf_cli-6.0.4 → ellf_cli-6.0.5}/ellf_cli/ellf_skills/skills/ellf-project.assistant/references/explosion_strategy.md +0 -0
  108. {ellf_cli-6.0.4 → ellf_cli-6.0.5}/ellf_cli/ellf_skills/skills/ellf-project.assistant/references/prodigy_llm_bot.md +0 -0
  109. {ellf_cli-6.0.4 → ellf_cli-6.0.5}/ellf_cli/ellf_skills/skills/ellf-project.coding/SKILL.md +0 -0
  110. {ellf_cli-6.0.4 → ellf_cli-6.0.5}/ellf_cli/ellf_skills/skills/ellf-project.coding/references/consulting_patterns.md +0 -0
  111. {ellf_cli-6.0.4 → ellf_cli-6.0.5}/ellf_cli/ellf_skills/skills/ellf-project.coding/references/explosion_strategy.md +0 -0
  112. {ellf_cli-6.0.4 → ellf_cli-6.0.5}/ellf_cli/ellf_skills/skills/ellf-project.coding/references/prodigy_llm_bot.md +0 -0
  113. {ellf_cli-6.0.4 → ellf_cli-6.0.5}/ellf_cli/ellf_skills/skills/ellf-support.assistant/SKILL.md +0 -0
  114. {ellf_cli-6.0.4 → ellf_cli-6.0.5}/ellf_cli/ellf_skills/skills/ellf-support.coding/SKILL.md +0 -0
  115. {ellf_cli-6.0.4 → ellf_cli-6.0.5}/ellf_cli/ellf_skills/skills/ellf-todo/SKILL.md +0 -0
  116. {ellf_cli-6.0.4 → ellf_cli-6.0.5}/ellf_cli/ellf_skills/skills/ellf-train.assistant/SKILL.md +0 -0
  117. {ellf_cli-6.0.4 → ellf_cli-6.0.5}/ellf_cli/ellf_skills/skills/ellf-train.assistant/references/diagnostics.md +0 -0
  118. {ellf_cli-6.0.4 → ellf_cli-6.0.5}/ellf_cli/ellf_skills/skills/ellf-train.assistant/references/evaluation_guide.md +0 -0
  119. {ellf_cli-6.0.4 → ellf_cli-6.0.5}/ellf_cli/ellf_skills/skills/ellf-train.assistant/references/model_selection.md +0 -0
  120. {ellf_cli-6.0.4 → ellf_cli-6.0.5}/ellf_cli/ellf_skills/skills/ellf-train.assistant/references/training_paradigms.md +0 -0
  121. {ellf_cli-6.0.4 → ellf_cli-6.0.5}/ellf_cli/ellf_skills/skills/ellf-train.assistant/references/workflow.md +0 -0
  122. {ellf_cli-6.0.4 → ellf_cli-6.0.5}/ellf_cli/ellf_skills/skills/ellf-train.coding/SKILL.md +0 -0
  123. {ellf_cli-6.0.4 → ellf_cli-6.0.5}/ellf_cli/ellf_skills/skills/ellf-train.coding/references/config_advanced.md +0 -0
  124. {ellf_cli-6.0.4 → ellf_cli-6.0.5}/ellf_cli/ellf_skills/skills/ellf-train.coding/references/config_architectures.md +0 -0
  125. {ellf_cli-6.0.4 → ellf_cli-6.0.5}/ellf_cli/ellf_skills/skills/ellf-train.coding/references/config_training.md +0 -0
  126. {ellf_cli-6.0.4 → ellf_cli-6.0.5}/ellf_cli/ellf_skills/skills/ellf-train.coding/references/diagnostics.md +0 -0
  127. {ellf_cli-6.0.4 → ellf_cli-6.0.5}/ellf_cli/ellf_skills/skills/ellf-train.coding/references/evaluation_guide.md +0 -0
  128. {ellf_cli-6.0.4 → ellf_cli-6.0.5}/ellf_cli/ellf_skills/skills/ellf-train.coding/references/experiment_patterns.md +0 -0
  129. {ellf_cli-6.0.4 → ellf_cli-6.0.5}/ellf_cli/ellf_skills/skills/ellf-train.coding/references/model_selection.md +0 -0
  130. {ellf_cli-6.0.4 → ellf_cli-6.0.5}/ellf_cli/ellf_skills/skills/ellf-train.coding/references/training_paradigms.md +0 -0
  131. {ellf_cli-6.0.4 → ellf_cli-6.0.5}/ellf_cli/ellf_skills/skills/ellf-train.coding/references/training_troubleshooting.md +0 -0
  132. {ellf_cli-6.0.4 → ellf_cli-6.0.5}/ellf_cli/ellf_skills/skills/ellf-train.coding/references/workflow.md +0 -0
  133. {ellf_cli-6.0.4 → ellf_cli-6.0.5}/ellf_cli/ellf_skills/skills/ellf-train.coding/scripts/ellf_logger.py +0 -0
  134. {ellf_cli-6.0.4 → ellf_cli-6.0.5}/ellf_cli/errors.py +0 -0
  135. {ellf_cli-6.0.4 → ellf_cli-6.0.5}/ellf_cli/helm.py +0 -0
  136. {ellf_cli-6.0.4 → ellf_cli-6.0.5}/ellf_cli/key_pair.py +0 -0
  137. {ellf_cli-6.0.4 → ellf_cli-6.0.5}/ellf_cli/main.py +0 -0
  138. {ellf_cli-6.0.4 → ellf_cli-6.0.5}/ellf_cli/messages.py +0 -0
  139. {ellf_cli-6.0.4 → ellf_cli-6.0.5}/ellf_cli/recipes_cookiecutter/cookiecutter.json +0 -0
  140. {ellf_cli-6.0.4 → ellf_cli-6.0.5}/ellf_cli/recipes_cookiecutter/{{cookiecutter.package_dir}}/.gitignore +0 -0
  141. {ellf_cli-6.0.4 → ellf_cli-6.0.5}/ellf_cli/recipes_cookiecutter/{{cookiecutter.package_dir}}/README.md.tmpl +0 -0
  142. {ellf_cli-6.0.4 → ellf_cli-6.0.5}/ellf_cli/recipes_cookiecutter/{{cookiecutter.package_dir}}/requirements-dev.in +0 -0
  143. {ellf_cli-6.0.4 → ellf_cli-6.0.5}/ellf_cli/recipes_cookiecutter/{{cookiecutter.package_dir}}/requirements.in +0 -0
  144. {ellf_cli-6.0.4 → ellf_cli-6.0.5}/ellf_cli/recipes_cookiecutter/{{cookiecutter.package_dir}}/setup.py.tmpl +0 -0
  145. {ellf_cli-6.0.4 → ellf_cli-6.0.5}/ellf_cli/recipes_cookiecutter/{{cookiecutter.package_dir}}/{{cookiecutter.package_name}}/__init__.py +0 -0
  146. {ellf_cli-6.0.4 → ellf_cli-6.0.5}/ellf_cli/recipes_cookiecutter/{{cookiecutter.package_dir}}/{{cookiecutter.package_name}}/about.py +0 -0
  147. {ellf_cli-6.0.4 → ellf_cli-6.0.5}/ellf_cli/recipes_cookiecutter/{{cookiecutter.package_dir}}/{{cookiecutter.package_name}}/recipes/__init__.py +0 -0
  148. {ellf_cli-6.0.4 → ellf_cli-6.0.5}/ellf_cli/recipes_cookiecutter/{{cookiecutter.package_dir}}/{{cookiecutter.package_name}}/recipes/example_task.py +0 -0
  149. {ellf_cli-6.0.4 → ellf_cli-6.0.5}/ellf_cli/testing/__init__.py +0 -0
  150. {ellf_cli-6.0.4 → ellf_cli-6.0.5}/ellf_cli/ty.py +0 -0
  151. {ellf_cli-6.0.4 → ellf_cli-6.0.5}/ellf_cli/ui.py +0 -0
  152. {ellf_cli-6.0.4 → ellf_cli-6.0.5}/ellf_cli/url.py +0 -0
  153. {ellf_cli-6.0.4 → ellf_cli-6.0.5}/ellf_cli/util.py +0 -0
  154. {ellf_cli-6.0.4 → ellf_cli-6.0.5}/ellf_cli.egg-info/dependency_links.txt +0 -0
  155. {ellf_cli-6.0.4 → ellf_cli-6.0.5}/ellf_cli.egg-info/entry_points.txt +0 -0
  156. {ellf_cli-6.0.4 → ellf_cli-6.0.5}/ellf_cli.egg-info/not-zip-safe +0 -0
  157. {ellf_cli-6.0.4 → ellf_cli-6.0.5}/ellf_cli.egg-info/requires.txt +0 -0
  158. {ellf_cli-6.0.4 → ellf_cli-6.0.5}/ellf_cli.egg-info/top_level.txt +0 -0
  159. {ellf_cli-6.0.4 → ellf_cli-6.0.5}/pyproject.toml +0 -0
  160. {ellf_cli-6.0.4 → ellf_cli-6.0.5}/setup.cfg +0 -0
  161. {ellf_cli-6.0.4 → ellf_cli-6.0.5}/setup.py +0 -0
  162. {ellf_cli-6.0.4 → ellf_cli-6.0.5}/tests/test_appdirs.py +0 -0
  163. {ellf_cli-6.0.4 → ellf_cli-6.0.5}/tests/test_auth.py +0 -0
  164. {ellf_cli-6.0.4 → ellf_cli-6.0.5}/tests/test_config.py +0 -0
  165. {ellf_cli-6.0.4 → ellf_cli-6.0.5}/tests/test_errors.py +0 -0
  166. {ellf_cli-6.0.4 → ellf_cli-6.0.5}/tests/test_files_cp.py +0 -0
  167. {ellf_cli-6.0.4 → ellf_cli-6.0.5}/tests/test_files_cp_helpers.py +0 -0
  168. {ellf_cli-6.0.4 → ellf_cli-6.0.5}/tests/test_info.py +0 -0
  169. {ellf_cli-6.0.4 → ellf_cli-6.0.5}/tests/test_invalid_secrets.py +0 -0
  170. {ellf_cli-6.0.4 → ellf_cli-6.0.5}/tests/test_key_pair.py +0 -0
  171. {ellf_cli-6.0.4 → ellf_cli-6.0.5}/tests/test_login.py +0 -0
  172. {ellf_cli-6.0.4 → ellf_cli-6.0.5}/tests/test_logout.py +0 -0
  173. {ellf_cli-6.0.4 → ellf_cli-6.0.5}/tests/test_main.py +0 -0
  174. {ellf_cli-6.0.4 → ellf_cli-6.0.5}/tests/test_org_select.py +0 -0
  175. {ellf_cli-6.0.4 → ellf_cli-6.0.5}/tests/test_plans.py +0 -0
  176. {ellf_cli-6.0.4 → ellf_cli-6.0.5}/tests/test_projects.py +0 -0
  177. {ellf_cli-6.0.4 → ellf_cli-6.0.5}/tests/test_query.py +0 -0
  178. {ellf_cli-6.0.4 → ellf_cli-6.0.5}/tests/test_recipe_file.py +0 -0
  179. {ellf_cli-6.0.4 → ellf_cli-6.0.5}/tests/test_recipes.py +0 -0
  180. {ellf_cli-6.0.4 → ellf_cli-6.0.5}/tests/test_state.py +0 -0
  181. {ellf_cli-6.0.4 → ellf_cli-6.0.5}/tests/test_support.py +0 -0
  182. {ellf_cli-6.0.4 → ellf_cli-6.0.5}/tests/test_ty.py +0 -0
  183. {ellf_cli-6.0.4 → ellf_cli-6.0.5}/tests/test_ui.py +0 -0
  184. {ellf_cli-6.0.4 → ellf_cli-6.0.5}/tests/test_ui_extras.py +0 -0
  185. {ellf_cli-6.0.4 → ellf_cli-6.0.5}/tests/test_util.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ellf-cli
3
- Version: 6.0.4
3
+ Version: 6.0.5
4
4
  Summary: Ellf Command Line Interface
5
5
  Home-page: https://prodi.gy
6
6
  Author: ExplosionAI GmbH
@@ -1248,6 +1248,111 @@ Manage TLS certificates for local cluster access. Use --self-signed when you hav
1248
1248
  | `--namespace` | `str` | Kubernetes namespace | `'ellf'` |
1249
1249
  | `--output` | `str` | Output format for trust commands: human, json, shell | `'human'` |
1250
1250
 
1251
+ ### `ellf services`
1252
+
1253
+ #### `ellf services create`
1254
+
1255
+ Create a new service. The available service recipes are fetched from your cluster and are added as dynamic subcommands. You can see more details and available arguments by calling the subcommand with --help, e.g. create [name] --help
1256
+
1257
+ | Argument | Type | Description | Default |
1258
+ | --- | --- | --- | --- |
1259
+ | `--exists-ok` | `bool` | Don't raise an error if it exists | `False` |
1260
+ | `--no-start` | `bool` | Don't start {noun} after creation | `False` |
1261
+ | `--no-wait` | `bool` | Don't wait for the service to be ready after starting | `False` |
1262
+ | `--help`, `-h` | `bool` | Show help message | `False` |
1263
+ | `_extra` | `str` | | `[]` |
1264
+
1265
+ #### `ellf services list`
1266
+
1267
+ List the services on the cluster.
1268
+
1269
+ | Argument | Type | Description | Default |
1270
+ | --- | --- | --- | --- |
1271
+ | `--select` | `list[str]` | Comma-separated fields to select and show in output. Available: ['id', 'created', 'updated', 'cluster_id', 'name', 'project_id', 'job_type', 'plan', 'cli_command', 'state'] | `['id', 'name', 'state', 'project_name']` |
1272
+ | `--json` | `bool` | Output the result as JSON | `False` |
1273
+
1274
+ #### `ellf services info`
1275
+
1276
+ Print information about a service on the cluster.
1277
+
1278
+ | Argument | Type | Description | Default |
1279
+ | --- | --- | --- | --- |
1280
+ | `name_or_id` | `Union[str, UUID]` | Name or ID of the service | |
1281
+ | `project_id` | `str` | Name, slug, or ID of service's project (or the last project if not set) | `None` |
1282
+ | `cluster_id` | `str` | Name, slug, or ID of the cluster to search for service name (or the last cluster if not set) | `None` |
1283
+ | `--select` | `list[str]` | Comma-separated fields to select and show in output. Available: ['id', 'created', 'updated', 'cluster_id', 'name', 'project_id', 'job_type', 'plan', 'cli_command', 'recipe_name', 'recipe_title', 'project_name', 'error', 'last_execution_id', 'url', 'url_ui', 'url_logs', 'created_by_user'] | `None` |
1284
+ | `--json` | `bool` | Output the result as JSON | `False` |
1285
+
1286
+ #### `ellf services logs`
1287
+
1288
+ Get logs for a service on the cluster.
1289
+
1290
+ | Argument | Type | Description | Default |
1291
+ | --- | --- | --- | --- |
1292
+ | `name_or_id` | `str` | Name or ID of the service (or the last service if not set) | `None` |
1293
+ | `project_id` | `str` | Name, slug, or ID of service's project (or the last project if not set) | `None` |
1294
+ | `cluster_id` | `str` | Name, slug, or ID of the cluster to search for service name (or the last cluster if not set) | `None` |
1295
+ | `--json` | `bool` | Output the result as JSON | `False` |
1296
+ | `--errors` | `bool` | Show structured error information instead of full logs | `False` |
1297
+ | `--query` | `str` | Filter log lines matching this text (requires Loki) | `None` |
1298
+
1299
+ #### `ellf services start`
1300
+
1301
+ Start a service on the cluster.
1302
+
1303
+ | Argument | Type | Description | Default |
1304
+ | --- | --- | --- | --- |
1305
+ | `name_or_id` | `str` | Name or ID of the service (or the last service if not set) | `None` |
1306
+ | `project_id` | `str` | Name, slug, or ID of service's project (or the last project if not set) | `None` |
1307
+ | `cluster_id` | `str` | Name, slug, or ID of the cluster to search for service name (or the last cluster if not set) | `None` |
1308
+ | `--worker-class` | `str` | Worker class to launch the service on. Generally one of: ['medium', 'large', 'gpu'] | `None` |
1309
+ | `--no-wait` | `bool` | Don't wait for the service to be ready after starting | `False` |
1310
+ | `--json` | `bool` | Output the result as JSON | `False` |
1311
+
1312
+ #### `ellf services stop`
1313
+
1314
+ Stop a service on the cluster.
1315
+
1316
+ | Argument | Type | Description | Default |
1317
+ | --- | --- | --- | --- |
1318
+ | `name_or_id` | `str` | Name or ID of the service (or the last service if not set) | `None` |
1319
+ | `project_id` | `str` | Name, slug, or ID of service's project (or the last project if not set) | `None` |
1320
+ | `cluster_id` | `str` | Name, slug, or ID of the cluster to search for service name (or the last cluster if not set) | `None` |
1321
+ | `--json` | `bool` | Output the result as JSON | `False` |
1322
+
1323
+ #### `ellf services delete`
1324
+
1325
+ Delete a service by name or ID.
1326
+
1327
+ | Argument | Type | Description | Default |
1328
+ | --- | --- | --- | --- |
1329
+ | `name_or_id` | `Union[str, UUID]` | Name or ID of the service | |
1330
+ | `project_id` | `str` | Name, slug, or ID of service's project (or the last project if not set) | `None` |
1331
+ | `cluster_id` | `str` | Name, slug, or ID of the cluster to search for service name (or the last cluster if not set) | `None` |
1332
+ | `--json` | `bool` | Output the result as JSON | `False` |
1333
+
1334
+ #### `ellf services url`
1335
+
1336
+ Print the bearer-token-authenticated URL for a service. Mints a fresh 90-day token via ``POST /v1/service/issue-token`` and formats it together with the service's public URL so the result can be dropped into an MCP client config or curl invocation: curl -H "Authorization: Bearer <token>" <url> Use ``services rotate-token`` to mint a new token without printing the URL again.
1337
+
1338
+ | Argument | Type | Description | Default |
1339
+ | --- | --- | --- | --- |
1340
+ | `name_or_id` | `Union[str, UUID]` | Name or ID of the service | |
1341
+ | `project_id` | `str` | Name, slug, or ID of service's project (or the last project if not set) | `None` |
1342
+ | `cluster_id` | `str` | Name, slug, or ID of the cluster to search for service name (or the last cluster if not set) | `None` |
1343
+ | `--json` | `bool` | Output the result as JSON | `False` |
1344
+
1345
+ #### `ellf services rotate-token`
1346
+
1347
+ Mint a fresh 90-day token for an existing service. Issuing a new token does not invalidate previous ones -- they remain valid until their own expiration. Use this when the previous token is leaked, or when you want a longer remaining window for an MCP client config.
1348
+
1349
+ | Argument | Type | Description | Default |
1350
+ | --- | --- | --- | --- |
1351
+ | `name_or_id` | `Union[str, UUID]` | Name or ID of the service | |
1352
+ | `project_id` | `str` | Name, slug, or ID of service's project (or the last project if not set) | `None` |
1353
+ | `cluster_id` | `str` | Name, slug, or ID of the cluster to search for service name (or the last cluster if not set) | `None` |
1354
+ | `--json` | `bool` | Output the result as JSON | `False` |
1355
+
1251
1356
  ### `ellf support`
1252
1357
 
1253
1358
  #### `ellf support create`
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "title": "Ellf CLI",
3
3
  "name": "ellf-cli",
4
- "version": "6.0.4",
4
+ "version": "6.0.5",
5
5
  "summary": "Ellf Command Line Interface",
6
6
  "uri": "https://prodi.gy",
7
7
  "prog": "ellf",
@@ -19,6 +19,7 @@ from . import (
19
19
  publish_data,
20
20
  recipes,
21
21
  secrets,
22
+ services,
22
23
  support,
23
24
  tasks,
24
25
  todos,
@@ -50,6 +51,7 @@ __all__ = [
50
51
  "recipes",
51
52
  "actions",
52
53
  "tasks",
54
+ "services",
53
55
  "secrets",
54
56
  "support",
55
57
  "general",
@@ -25,6 +25,7 @@ from ellf_pam_sdk.models import (
25
25
  RecipeDetail,
26
26
  RecipeListingLatest,
27
27
  RecipePlanCreating,
28
+ ServiceCreating,
28
29
  TaskCreating,
29
30
  )
30
31
  from ellf_pam_sdk.recipe_utils import (
@@ -53,7 +54,7 @@ def get_converter(arg_type: type) -> ConverterType | None:
53
54
  def _get_noun(recipe: RecipeDetail) -> str:
54
55
  """Derive a human-readable noun from a recipe's job_type or is_action flag."""
55
56
  jt = getattr(recipe, "job_type", None)
56
- if jt in ("task", "action", "agent"):
57
+ if jt in ("task", "action", "agent", "service"):
57
58
  return jt
58
59
  return "action" if recipe.is_action else "task"
59
60
 
@@ -135,6 +136,10 @@ def create_from_recipe(
135
136
  job_id = _create_agent(
136
137
  auth.client, task_or_action_name, project_id, plan, exists_ok=exists_ok
137
138
  )
139
+ elif jt == "service":
140
+ job_id = _create_service(
141
+ auth.client, task_or_action_name, project_id, plan, exists_ok=exists_ok
142
+ )
138
143
  elif recipe.is_action:
139
144
  job_id = _create_action(
140
145
  auth.client, task_or_action_name, project_id, plan, exists_ok=exists_ok
@@ -381,6 +386,31 @@ def _create_agent(
381
386
  return JobID(id=agent.id, job_type=JobType.agent)
382
387
 
383
388
 
389
+ def _create_service(
390
+ client: Client,
391
+ name: str | None,
392
+ project_id: UUID,
393
+ plan: RecipePlanCreating,
394
+ exists_ok: bool = False,
395
+ ) -> JobID:
396
+ body = ServiceCreating(name=name, project_id=project_id, plan=plan)
397
+ try:
398
+ service = client.service.create(body)
399
+ except EllfErrors.ProjectNotFound:
400
+ err = Messages.E013.format(name=project_id)
401
+ raise CLIError(err)
402
+ except EllfErrors.ServiceExists:
403
+ if exists_ok:
404
+ msg.info(Messages.T001.format(noun="service", name=name))
405
+ service = client.service.read(name=name, project_id=project_id)
406
+ return JobID(id=service.id, job_type=JobType.service)
407
+ err = Messages.E014.format(noun="service", name=name, project=project_id)
408
+ raise CLIError(err)
409
+ except EllfErrors.ServiceInvalid:
410
+ raise CLIError(Messages.E004.format(noun="service", name=name))
411
+ return JobID(id=service.id, job_type=JobType.service)
412
+
413
+
384
414
  def _uuid7() -> UUID:
385
415
  """Generate a UUIDv7 (time-sortable) as a stdlib uuid.UUID."""
386
416
  return UUID(int=uuid_utils.uuid7().int)
@@ -0,0 +1,390 @@
1
+ import builtins
2
+ from collections.abc import Sequence
3
+ from typing import Optional, Union
4
+ from uuid import UUID
5
+
6
+ from radicli import Arg
7
+ from wasabi import msg
8
+
9
+ from ellf_pam_sdk.models import ServiceDetail, ServiceSummary
10
+
11
+ from ..cli import cli
12
+ from ..errors import BrokerError, CLIError, EllfError, HTTPXErrors
13
+ from ..messages import Messages
14
+ from ..query import (
15
+ delete_job,
16
+ resolve_recipe,
17
+ resolve_service,
18
+ start_job,
19
+ stop_job,
20
+ )
21
+ from ..ui import (
22
+ print_args_table,
23
+ print_info_table,
24
+ print_logs,
25
+ print_mutation_result,
26
+ print_recipes_help,
27
+ print_table_with_select,
28
+ )
29
+ from ._recipe_subcommand import (
30
+ create_from_recipe,
31
+ create_from_recipe_file,
32
+ request_recipes,
33
+ )
34
+ from ._state import get_auth_state, get_saved_settings
35
+
36
+
37
+ @cli.subcommand_with_extra(
38
+ "services",
39
+ "create",
40
+ exists_ok=Arg("--exists-ok", help=Messages.exists_ok),
41
+ no_start=Arg("--no-start", help=Messages.no_start),
42
+ no_wait=Arg("--no-wait", help=Messages.no_wait.format(noun="service")),
43
+ _show_help=Arg("--help", "-h", help=Messages.help),
44
+ )
45
+ def create(
46
+ exists_ok: bool = False,
47
+ no_start: bool = False,
48
+ no_wait: bool = False,
49
+ _show_help: bool = False,
50
+ _extra: builtins.list[str] = [],
51
+ ) -> UUID | None:
52
+ """
53
+ Create a new service. The available service recipes are fetched from your
54
+ cluster and are added as dynamic subcommands. You can see more details
55
+ and available arguments by calling the subcommand with --help, e.g. create
56
+ [name] --help
57
+ """
58
+ auth = get_auth_state()
59
+ settings = get_saved_settings()
60
+ is_file_mode = settings.recipes_file is not None
61
+ try:
62
+ schemas = request_recipes(auth=auth, job_type="service")
63
+ except EllfError as e:
64
+ raise CLIError(Messages.E009, e)
65
+ except HTTPXErrors as e:
66
+ raise CLIError(Messages.E009, e)
67
+ if not _extra:
68
+ print_recipes_help(schemas, "Create a new service", "services create")
69
+ return None
70
+ args = [*_extra]
71
+ name = args.pop(0)
72
+ if name not in schemas:
73
+ opts = f"Available: {', '.join(schemas.keys())}"
74
+ raise CLIError(Messages.E010.format(noun="service", name=name), opts)
75
+ if is_file_mode:
76
+ schema = schemas[name]
77
+ job_spec = create_from_recipe_file(
78
+ schema, args, command="services", show_help=_show_help, job_type="service"
79
+ )
80
+ msg.good(Messages.T002.format(noun="service", name=job_spec.name))
81
+ print_args_table(job_spec.plan.args or {}, job_spec.plan.cli_names or {})
82
+ if not no_start:
83
+ response = auth.broker_client.jobs.start_job(job_spec)
84
+ if response.cluster_change is not None:
85
+ msg.good(Messages.T005.format(noun="service", name=job_spec.job_id))
86
+ elif response.cluster_error is not None:
87
+ raise CLIError(
88
+ Messages.E025.format(noun="service", name=job_spec.job_id),
89
+ response.cluster_error,
90
+ )
91
+ return job_spec.job_id
92
+ schema = resolve_recipe(name, cluster_id=auth.cluster_id)
93
+ service_id, plan = create_from_recipe(
94
+ schema, args, command="services", show_help=_show_help, exists_ok=exists_ok
95
+ )
96
+ msg.good(Messages.T002.format(noun="service", name=service_id))
97
+ print_args_table(plan.args, schema.form_schema.cli_names)
98
+ if not no_start:
99
+ try:
100
+ start(service_id, no_wait=no_wait)
101
+ except Exception:
102
+ auth.client.service.delete(id=service_id)
103
+ raise
104
+ return service_id
105
+
106
+
107
+ @cli.subcommand(
108
+ "services",
109
+ "list",
110
+ select=Arg(
111
+ "--select",
112
+ help=Messages.select.format(
113
+ opts=[*builtins.list(ServiceSummary.model_fields), "state"]
114
+ ),
115
+ ),
116
+ as_json=Arg("--json", help=Messages.as_json),
117
+ )
118
+ def list(
119
+ select: builtins.list[str] = ["id", "name", "state", "project_name"],
120
+ as_json: bool = False,
121
+ ) -> Sequence[ServiceSummary]:
122
+ """List the services on the cluster."""
123
+ auth = get_auth_state()
124
+ items = builtins.list(auth.client.service.all(page_size=100))
125
+ if "state" in select and items:
126
+ job_ids = [item.id for item in items]
127
+ try:
128
+ statuses = auth.broker_client.jobs.get_batch_status(job_ids).statuses
129
+ except (BrokerError, *HTTPXErrors):
130
+ statuses = {}
131
+ enriched = []
132
+ for item in items:
133
+ d = item.model_dump(include=set(s for s in select if s != "state"))
134
+ status = statuses.get(str(item.id))
135
+ d["state"] = status.state.value if status else "unknown"
136
+ enriched.append(d)
137
+ print_table_with_select(enriched, select=select, as_json=as_json)
138
+ else:
139
+ print_table_with_select(items, select=select, as_json=as_json)
140
+ return items
141
+
142
+
143
+ @cli.subcommand(
144
+ "services",
145
+ "info",
146
+ name_or_id=Arg(help=Messages.name_or_id.format(noun="service")),
147
+ project_id=Arg(help=Messages.project_id.format(noun="service")),
148
+ cluster_id=Arg(help=Messages.cluster_id.format(noun="service")),
149
+ select=Arg(
150
+ "--select",
151
+ help=Messages.select.format(opts=builtins.list(ServiceDetail.model_fields)),
152
+ ),
153
+ as_json=Arg("--json", help=Messages.as_json),
154
+ )
155
+ def info(
156
+ name_or_id: Union[str, UUID],
157
+ cluster_id: Optional[Union[str, UUID]] = None,
158
+ project_id: Optional[Union[str, UUID]] = None,
159
+ select: Optional[builtins.list[str]] = None,
160
+ as_json: bool = False,
161
+ ) -> ServiceDetail:
162
+ """Print information about a service on the cluster."""
163
+ auth = get_auth_state()
164
+ res = resolve_service(name_or_id, cluster_id=cluster_id, project_id=project_id)
165
+ try:
166
+ status = auth.broker_client.jobs.get_status(res.id)
167
+ msg.text(f"State: {status.state.value}", icon="info")
168
+ except (BrokerError, *HTTPXErrors):
169
+ pass
170
+ print_info_table(res, exclude=["plan"], as_json=as_json, select=select)
171
+ return res
172
+
173
+
174
+ @cli.subcommand(
175
+ "services",
176
+ "logs",
177
+ name_or_id=Arg(help=Messages.name_or_id_optional.format(noun="service")),
178
+ project_id=Arg(help=Messages.project_id.format(noun="service")),
179
+ cluster_id=Arg(help=Messages.cluster_id.format(noun="service")),
180
+ as_json=Arg("--json", help=Messages.as_json),
181
+ show_errors=Arg(
182
+ "--errors", help="Show structured error information instead of full logs"
183
+ ),
184
+ query=Arg("--query", help="Filter log lines matching this text (requires Loki)"),
185
+ )
186
+ def logs(
187
+ name_or_id: Optional[Union[str, UUID]] = None,
188
+ project_id: Optional[Union[str, UUID]] = None,
189
+ cluster_id: Optional[Union[str, UUID]] = None,
190
+ as_json: bool = False,
191
+ show_errors: bool = False,
192
+ query: Optional[str] = None,
193
+ ) -> str | None:
194
+ """Get logs for a service on the cluster."""
195
+ job = resolve_service(name_or_id, cluster_id=cluster_id, project_id=project_id)
196
+ auth = get_auth_state()
197
+ if job.last_execution_id is None:
198
+ raise CLIError(Messages.E051.format(noun="service", name=job.name, id=job.id))
199
+ if show_errors:
200
+ try:
201
+ resp = auth.broker_client.jobs.errors(job_id=job.last_execution_id)
202
+ except BrokerError as e:
203
+ raise CLIError(Messages.E011.format(noun="service"), e)
204
+ if as_json:
205
+ print(resp.model_dump_json(indent=2))
206
+ else:
207
+ msg.text(f"State: {resp.state}", icon="info")
208
+ if resp.errors:
209
+ for err in resp.errors:
210
+ msg.fail(err.message)
211
+ if err.traceback:
212
+ print(err.traceback)
213
+ print()
214
+ elif resp.tail:
215
+ msg.warn("No structured errors found. Last 50 lines:")
216
+ print(resp.tail)
217
+ else:
218
+ msg.good("No errors found")
219
+ return None
220
+ try:
221
+ text = auth.broker_client.jobs.logs(job_id=job.last_execution_id, query=query)
222
+ except BrokerError as e:
223
+ raise CLIError(Messages.E011.format(noun="service"), e)
224
+ print_logs(text, as_json=as_json)
225
+ return text
226
+
227
+
228
+ @cli.subcommand(
229
+ "services",
230
+ "start",
231
+ name_or_id=Arg(help=Messages.name_or_id_optional.format(noun="service")),
232
+ project_id=Arg(help=Messages.project_id.format(noun="service")),
233
+ cluster_id=Arg(help=Messages.cluster_id.format(noun="service")),
234
+ worker_class=Arg(
235
+ "--worker-class",
236
+ help=Messages.recipe_worker_class.format(noun="service"),
237
+ ),
238
+ no_wait=Arg("--no-wait", help=Messages.no_wait.format(noun="service")),
239
+ as_json=Arg("--json", help=Messages.as_json),
240
+ )
241
+ def start(
242
+ name_or_id: Optional[Union[str, UUID]] = None,
243
+ project_id: Optional[Union[str, UUID]] = None,
244
+ cluster_id: Optional[Union[str, UUID]] = None,
245
+ worker_class: Optional[str] = None,
246
+ no_wait: bool = False,
247
+ as_json: bool = False,
248
+ ) -> UUID:
249
+ """Start a service on the cluster."""
250
+ job = resolve_service(name_or_id, cluster_id=cluster_id, project_id=project_id)
251
+ start_job(
252
+ job,
253
+ worker_class,
254
+ get_auth_state(),
255
+ wait=not no_wait,
256
+ quiet=as_json,
257
+ )
258
+ if as_json:
259
+ print_mutation_result(
260
+ {"id": str(job.id), "status": "started"}, "", as_json=True
261
+ )
262
+ return job.id
263
+
264
+
265
+ @cli.subcommand(
266
+ "services",
267
+ "stop",
268
+ name_or_id=Arg(help=Messages.name_or_id_optional.format(noun="service")),
269
+ project_id=Arg(help=Messages.project_id.format(noun="service")),
270
+ cluster_id=Arg(help=Messages.cluster_id.format(noun="service")),
271
+ as_json=Arg("--json", help=Messages.as_json),
272
+ )
273
+ def stop(
274
+ name_or_id: Optional[Union[str, UUID]] = None,
275
+ project_id: Optional[Union[str, UUID]] = None,
276
+ cluster_id: Optional[Union[str, UUID]] = None,
277
+ as_json: bool = False,
278
+ ) -> UUID:
279
+ """Stop a service on the cluster."""
280
+ job = resolve_service(name_or_id, cluster_id=cluster_id, project_id=project_id)
281
+ stop_job(job, get_auth_state(), quiet=as_json)
282
+ if as_json:
283
+ print_mutation_result(
284
+ {"id": str(job.id), "status": "stopped"}, "", as_json=True
285
+ )
286
+ return job.id
287
+
288
+
289
+ @cli.subcommand(
290
+ "services",
291
+ "delete",
292
+ name_or_id=Arg(help=Messages.name_or_id.format(noun="service")),
293
+ project_id=Arg(help=Messages.project_id.format(noun="service")),
294
+ cluster_id=Arg(help=Messages.cluster_id.format(noun="service")),
295
+ as_json=Arg("--json", help=Messages.as_json),
296
+ )
297
+ def delete(
298
+ name_or_id: Union[str, UUID],
299
+ project_id: Optional[Union[str, UUID]] = None,
300
+ cluster_id: Optional[Union[str, UUID]] = None,
301
+ as_json: bool = False,
302
+ ) -> UUID:
303
+ """Delete a service by name or ID."""
304
+ job = resolve_service(name_or_id, cluster_id=cluster_id, project_id=project_id)
305
+ delete_job(job, get_auth_state(), quiet=as_json)
306
+ if as_json:
307
+ print_mutation_result({"id": str(job.id), "deleted": True}, "", as_json=True)
308
+ return job.id
309
+
310
+
311
+ @cli.subcommand(
312
+ "services",
313
+ "url",
314
+ name_or_id=Arg(help=Messages.name_or_id.format(noun="service")),
315
+ project_id=Arg(help=Messages.project_id.format(noun="service")),
316
+ cluster_id=Arg(help=Messages.cluster_id.format(noun="service")),
317
+ as_json=Arg("--json", help=Messages.as_json),
318
+ )
319
+ def url(
320
+ name_or_id: Union[str, UUID],
321
+ project_id: Optional[Union[str, UUID]] = None,
322
+ cluster_id: Optional[Union[str, UUID]] = None,
323
+ as_json: bool = False,
324
+ ) -> dict[str, str]:
325
+ """Print the bearer-token-authenticated URL for a service.
326
+
327
+ Mints a fresh 90-day token via ``POST /v1/service/issue-token`` and
328
+ formats it together with the service's public URL so the result can
329
+ be dropped into an MCP client config or curl invocation:
330
+
331
+ curl -H "Authorization: Bearer <token>" <url>
332
+
333
+ Use ``services rotate-token`` to mint a new token without printing
334
+ the URL again.
335
+ """
336
+ auth = get_auth_state()
337
+ service = resolve_service(name_or_id, cluster_id=cluster_id, project_id=project_id)
338
+ token = auth.client.service.issue_token(id=service.id)
339
+ result = {
340
+ "url": service.url,
341
+ "access_token": token.access_token,
342
+ "expires_at": token.expires_at.isoformat(),
343
+ }
344
+ if as_json:
345
+ print_mutation_result(result, "", as_json=True)
346
+ else:
347
+ msg.good(f"URL: {service.url}")
348
+ msg.text(f"Token: {token.access_token}")
349
+ msg.text(f"Expires at: {token.expires_at.isoformat()}")
350
+ msg.text(
351
+ f"Try it: curl -H 'Authorization: Bearer {token.access_token}' "
352
+ f"{service.url}"
353
+ )
354
+ return result
355
+
356
+
357
+ @cli.subcommand(
358
+ "services",
359
+ "rotate-token",
360
+ name_or_id=Arg(help=Messages.name_or_id.format(noun="service")),
361
+ project_id=Arg(help=Messages.project_id.format(noun="service")),
362
+ cluster_id=Arg(help=Messages.cluster_id.format(noun="service")),
363
+ as_json=Arg("--json", help=Messages.as_json),
364
+ )
365
+ def rotate_token(
366
+ name_or_id: Union[str, UUID],
367
+ project_id: Optional[Union[str, UUID]] = None,
368
+ cluster_id: Optional[Union[str, UUID]] = None,
369
+ as_json: bool = False,
370
+ ) -> dict[str, str]:
371
+ """Mint a fresh 90-day token for an existing service.
372
+
373
+ Issuing a new token does not invalidate previous ones -- they remain
374
+ valid until their own expiration. Use this when the previous token
375
+ is leaked, or when you want a longer remaining window for an MCP
376
+ client config.
377
+ """
378
+ auth = get_auth_state()
379
+ service = resolve_service(name_or_id, cluster_id=cluster_id, project_id=project_id)
380
+ token = auth.client.service.issue_token(id=service.id)
381
+ result = {
382
+ "access_token": token.access_token,
383
+ "expires_at": token.expires_at.isoformat(),
384
+ }
385
+ if as_json:
386
+ print_mutation_result(result, "", as_json=True)
387
+ else:
388
+ msg.good(f"Token: {token.access_token}")
389
+ msg.text(f"Expires at: {token.expires_at.isoformat()}")
390
+ return result
@@ -38,6 +38,7 @@ class SavedSettings(BaseModel):
38
38
  task: UUID | None = None
39
39
  action: UUID | None = None
40
40
  agent: UUID | None = None
41
+ service: UUID | None = None
41
42
  pam_host: str | None = DEFAULT_PAM_HOST
42
43
  cluster_id: UUID | None = None
43
44
  org_id: UUID | None = None
@@ -51,6 +52,7 @@ class SavedSettings(BaseModel):
51
52
  task=None,
52
53
  action=None,
53
54
  agent=None,
55
+ service=None,
54
56
  pam_host=DEFAULT_PAM_HOST,
55
57
  cluster_id=None,
56
58
  org_id=None,
@@ -68,11 +70,12 @@ class SavedSettings(BaseModel):
68
70
  raise
69
71
 
70
72
  def reset_defaults(self) -> None:
71
- """Reset defaut project/task/action/agent, e.g. on host changes."""
73
+ """Reset defaut project/task/action/agent/service, e.g. on host changes."""
72
74
  self.project = None
73
75
  self.task = None
74
76
  self.action = None
75
77
  self.agent = None
78
+ self.service = None
76
79
 
77
80
  def to_json(self) -> JSONableDict:
78
81
  data = {}
@@ -98,7 +101,9 @@ class SavedSettings(BaseModel):
98
101
  @overload
99
102
  def update(
100
103
  self,
101
- field: Literal["project", "task", "action", "agent", "cluster_id", "org_id"],
104
+ field: Literal[
105
+ "project", "task", "action", "agent", "service", "cluster_id", "org_id"
106
+ ],
102
107
  value: UUID | None = None,
103
108
  ) -> UUID | None: ...
104
109
 
@@ -111,6 +116,7 @@ class SavedSettings(BaseModel):
111
116
  "task",
112
117
  "action",
113
118
  "agent",
119
+ "service",
114
120
  "cluster_id",
115
121
  "org_id",
116
122
  "recipes_file",