comfy-cli 1.22.0__tar.gz → 1.23.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 (308) hide show
  1. {comfy_cli-1.22.0 → comfy_cli-1.23.0}/PKG-INFO +4 -4
  2. {comfy_cli-1.22.0 → comfy_cli-1.23.0}/README.md +3 -3
  3. {comfy_cli-1.22.0 → comfy_cli-1.23.0}/comfy_cli/_lazy.py +4 -1
  4. {comfy_cli-1.22.0 → comfy_cli-1.23.0}/comfy_cli/agent/__init__.py +11 -6
  5. comfy_cli-1.23.0/comfy_cli/agent/command.py +569 -0
  6. {comfy_cli-1.22.0 → comfy_cli-1.23.0}/comfy_cli/builder_api.py +4 -1
  7. {comfy_cli-1.22.0 → comfy_cli-1.23.0}/comfy_cli/cloud/oauth.py +115 -17
  8. {comfy_cli-1.22.0 → comfy_cli-1.23.0}/comfy_cli/cmdline.py +920 -8
  9. {comfy_cli-1.22.0 → comfy_cli-1.23.0}/comfy_cli/command/_cloud_errors.py +79 -3
  10. {comfy_cli-1.22.0 → comfy_cli-1.23.0}/comfy_cli/command/assets_library.py +114 -21
  11. {comfy_cli-1.22.0 → comfy_cli-1.23.0}/comfy_cli/command/build.py +136 -8
  12. {comfy_cli-1.22.0 → comfy_cli-1.23.0}/comfy_cli/command/cloud_http.py +42 -4
  13. comfy_cli-1.23.0/comfy_cli/command/code_search.py +770 -0
  14. {comfy_cli-1.22.0 → comfy_cli-1.23.0}/comfy_cli/command/deploy.py +293 -26
  15. comfy_cli-1.23.0/comfy_cli/command/deploy_cancel.py +195 -0
  16. {comfy_cli-1.22.0 → comfy_cli-1.23.0}/comfy_cli/command/deploy_ls.py +31 -7
  17. {comfy_cli-1.22.0 → comfy_cli-1.23.0}/comfy_cli/command/deploy_progress.py +25 -11
  18. comfy_cli-1.23.0/comfy_cli/command/deploy_promote.py +55 -0
  19. {comfy_cli-1.22.0 → comfy_cli-1.23.0}/comfy_cli/command/deploy_read.py +127 -13
  20. comfy_cli-1.23.0/comfy_cli/command/deploy_rename.py +110 -0
  21. comfy_cli-1.23.0/comfy_cli/command/deploy_resolve.py +545 -0
  22. comfy_cli-1.23.0/comfy_cli/command/deploy_rollback.py +291 -0
  23. {comfy_cli-1.22.0 → comfy_cli-1.23.0}/comfy_cli/command/deploy_run.py +53 -8
  24. {comfy_cli-1.22.0 → comfy_cli-1.23.0}/comfy_cli/command/deploy_runtime.py +29 -6
  25. {comfy_cli-1.22.0 → comfy_cli-1.23.0}/comfy_cli/command/deploy_status.py +94 -9
  26. comfy_cli-1.23.0/comfy_cli/command/deploy_types.py +381 -0
  27. comfy_cli-1.23.0/comfy_cli/command/deploy_up.py +780 -0
  28. {comfy_cli-1.22.0 → comfy_cli-1.23.0}/comfy_cli/command/generate/app.py +1 -1
  29. {comfy_cli-1.22.0 → comfy_cli-1.23.0}/comfy_cli/command/generate/client.py +65 -11
  30. {comfy_cli-1.22.0 → comfy_cli-1.23.0}/comfy_cli/command/generate/emit.py +6 -0
  31. {comfy_cli-1.22.0 → comfy_cli-1.23.0}/comfy_cli/command/generate/output.py +1 -1
  32. comfy_cli-1.23.0/comfy_cli/command/generate/spec/openapi.yml +1 -0
  33. comfy_cli-1.23.0/comfy_cli/command/generate/spec.py +1285 -0
  34. {comfy_cli-1.22.0 → comfy_cli-1.23.0}/comfy_cli/command/knowledge.py +111 -0
  35. {comfy_cli-1.22.0 → comfy_cli-1.23.0}/comfy_cli/command/models/models.py +456 -182
  36. {comfy_cli-1.22.0 → comfy_cli-1.23.0}/comfy_cli/command/nodes.py +88 -4
  37. {comfy_cli-1.22.0 → comfy_cli-1.23.0}/comfy_cli/command/run/__init__.py +199 -106
  38. {comfy_cli-1.22.0 → comfy_cli-1.23.0}/comfy_cli/command/run/execution.py +47 -3
  39. {comfy_cli-1.22.0 → comfy_cli-1.23.0}/comfy_cli/command/run/watcher.py +40 -5
  40. {comfy_cli-1.22.0 → comfy_cli-1.23.0}/comfy_cli/command/templates.py +60 -4
  41. {comfy_cli-1.22.0 → comfy_cli-1.23.0}/comfy_cli/command/transfer.py +14 -0
  42. {comfy_cli-1.22.0 → comfy_cli-1.23.0}/comfy_cli/command/workflow.py +159 -6
  43. {comfy_cli-1.22.0 → comfy_cli-1.23.0}/comfy_cli/command/workflow_edit.py +104 -12
  44. {comfy_cli-1.22.0 → comfy_cli-1.23.0}/comfy_cli/cql/engine.py +1306 -134
  45. {comfy_cli-1.22.0 → comfy_cli-1.23.0}/comfy_cli/cql/loader.py +8 -157
  46. {comfy_cli-1.22.0 → comfy_cli-1.23.0}/comfy_cli/cql/promoted.py +31 -7
  47. {comfy_cli-1.22.0 → comfy_cli-1.23.0}/comfy_cli/cql/widget_catalog.py +42 -4
  48. {comfy_cli-1.22.0 → comfy_cli-1.23.0}/comfy_cli/credentials.py +79 -2
  49. {comfy_cli-1.22.0 → comfy_cli-1.23.0}/comfy_cli/deploy_api.py +62 -1
  50. {comfy_cli-1.22.0 → comfy_cli-1.23.0}/comfy_cli/deploy_api_errors.py +32 -0
  51. {comfy_cli-1.22.0 → comfy_cli-1.23.0}/comfy_cli/deploy_assets.py +12 -3
  52. {comfy_cli-1.22.0 → comfy_cli-1.23.0}/comfy_cli/deploy_download.py +9 -2
  53. {comfy_cli-1.22.0 → comfy_cli-1.23.0}/comfy_cli/deploy_events.py +24 -7
  54. {comfy_cli-1.22.0 → comfy_cli-1.23.0}/comfy_cli/discovery.py +12 -0
  55. {comfy_cli-1.22.0 → comfy_cli-1.23.0}/comfy_cli/download_state.py +406 -127
  56. {comfy_cli-1.22.0 → comfy_cli-1.23.0}/comfy_cli/error_codes.py +215 -35
  57. {comfy_cli-1.22.0 → comfy_cli-1.23.0}/comfy_cli/knowledge.py +191 -17
  58. {comfy_cli-1.22.0 → comfy_cli-1.23.0}/comfy_cli/layout.py +28 -2
  59. comfy_cli-1.23.0/comfy_cli/link_integrity.py +204 -0
  60. comfy_cli-1.23.0/comfy_cli/model_variants.py +256 -0
  61. {comfy_cli-1.22.0 → comfy_cli-1.23.0}/comfy_cli/registry/config_parser.py +259 -25
  62. {comfy_cli-1.22.0 → comfy_cli-1.23.0}/comfy_cli/schemas/agent.json +6 -8
  63. {comfy_cli-1.22.0 → comfy_cli-1.23.0}/comfy_cli/schemas/assets_library.json +2 -0
  64. comfy_cli-1.23.0/comfy_cli/schemas/deploy_cancel.json +116 -0
  65. {comfy_cli-1.22.0 → comfy_cli-1.23.0}/comfy_cli/schemas/deploy_events.json +16 -1
  66. comfy_cli-1.23.0/comfy_cli/schemas/deploy_history.json +68 -0
  67. {comfy_cli-1.22.0 → comfy_cli-1.23.0}/comfy_cli/schemas/deploy_ls.json +11 -0
  68. {comfy_cli-1.22.0 → comfy_cli-1.23.0}/comfy_cli/schemas/deploy_progress_event.json +5 -1
  69. comfy_cli-1.23.0/comfy_cli/schemas/deploy_promote.json +101 -0
  70. comfy_cli-1.23.0/comfy_cli/schemas/deploy_rename.json +39 -0
  71. comfy_cli-1.23.0/comfy_cli/schemas/deploy_rollback.json +86 -0
  72. {comfy_cli-1.22.0 → comfy_cli-1.23.0}/comfy_cli/schemas/deploy_run.json +6 -1
  73. {comfy_cli-1.22.0 → comfy_cli-1.23.0}/comfy_cli/schemas/deploy_status.json +28 -2
  74. {comfy_cli-1.22.0 → comfy_cli-1.23.0}/comfy_cli/schemas/deploy_up.json +24 -3
  75. {comfy_cli-1.22.0 → comfy_cli-1.23.0}/comfy_cli/schemas/generate_list.json +1 -1
  76. {comfy_cli-1.22.0 → comfy_cli-1.23.0}/comfy_cli/schemas/knowledge.json +8 -2
  77. {comfy_cli-1.22.0 → comfy_cli-1.23.0}/comfy_cli/schemas/widget_catalog.json +16 -2
  78. {comfy_cli-1.22.0 → comfy_cli-1.23.0}/comfy_cli/selector.py +195 -8
  79. {comfy_cli-1.22.0 → comfy_cli-1.23.0}/comfy_cli/skills/__init__.py +45 -4
  80. {comfy_cli-1.22.0 → comfy_cli-1.23.0}/comfy_cli/skills/comfy/SKILL.md +8 -4
  81. comfy_cli-1.23.0/comfy_cli/skills/comfy-agent-permissions/SKILL.md +159 -0
  82. {comfy_cli-1.22.0 → comfy_cli-1.23.0}/comfy_cli/skills/comfy-build/SKILL.md +9 -2
  83. {comfy_cli-1.22.0 → comfy_cli-1.23.0}/comfy_cli/skills/comfy-build-failures/SKILL.md +9 -4
  84. comfy_cli-1.23.0/comfy_cli/skills/comfy-custom-nodes/SKILL.md +59 -0
  85. comfy_cli-1.23.0/comfy_cli/skills/comfy-custom-nodes-api-schema/SKILL.md +14 -0
  86. comfy_cli-1.23.0/comfy_cli/skills/comfy-custom-nodes-caching/SKILL.md +14 -0
  87. comfy_cli-1.23.0/comfy_cli/skills/comfy-custom-nodes-dependencies/SKILL.md +11 -0
  88. comfy_cli-1.23.0/comfy_cli/skills/comfy-custom-nodes-frontend/SKILL.md +14 -0
  89. comfy_cli-1.23.0/comfy_cli/skills/comfy-custom-nodes-invocation/SKILL.md +16 -0
  90. comfy_cli-1.23.0/comfy_cli/skills/comfy-custom-nodes-migration/SKILL.md +14 -0
  91. comfy_cli-1.23.0/comfy_cli/skills/comfy-custom-nodes-packaging/SKILL.md +14 -0
  92. comfy_cli-1.23.0/comfy_cli/skills/comfy-custom-nodes-search/SKILL.md +12 -0
  93. comfy_cli-1.23.0/comfy_cli/skills/comfy-custom-nodes-troubleshooting/SKILL.md +14 -0
  94. comfy_cli-1.23.0/comfy_cli/skills/comfy-custom-nodes-validation/SKILL.md +15 -0
  95. {comfy_cli-1.22.0 → comfy_cli-1.23.0}/comfy_cli/skills/comfy-debug/SKILL.md +12 -0
  96. {comfy_cli-1.22.0 → comfy_cli-1.23.0}/comfy_cli/skills/comfy-deploy/SKILL.md +140 -19
  97. {comfy_cli-1.22.0 → comfy_cli-1.23.0}/comfy_cli/skills/comfy-deploy-failures/SKILL.md +11 -2
  98. {comfy_cli-1.22.0 → comfy_cli-1.23.0}/comfy_cli/skills/command.py +21 -8
  99. {comfy_cli-1.22.0 → comfy_cli-1.23.0}/comfy_cli/tracking.py +8 -0
  100. {comfy_cli-1.22.0 → comfy_cli-1.23.0}/comfy_cli/workflow_ops.py +1298 -80
  101. {comfy_cli-1.22.0 → comfy_cli-1.23.0}/comfy_cli/workflow_print.py +191 -20
  102. {comfy_cli-1.22.0 → comfy_cli-1.23.0}/comfy_cli/workflow_to_api.py +85 -11
  103. {comfy_cli-1.22.0 → comfy_cli-1.23.0}/comfy_cli.egg-info/PKG-INFO +4 -4
  104. {comfy_cli-1.22.0 → comfy_cli-1.23.0}/comfy_cli.egg-info/SOURCES.txt +21 -0
  105. {comfy_cli-1.22.0 → comfy_cli-1.23.0}/pyproject.toml +1 -1
  106. comfy_cli-1.22.0/comfy_cli/agent/command.py +0 -330
  107. comfy_cli-1.22.0/comfy_cli/command/code_search.py +0 -194
  108. comfy_cli-1.22.0/comfy_cli/command/deploy_resolve.py +0 -236
  109. comfy_cli-1.22.0/comfy_cli/command/deploy_types.py +0 -133
  110. comfy_cli-1.22.0/comfy_cli/command/deploy_up.py +0 -309
  111. comfy_cli-1.22.0/comfy_cli/command/generate/spec/openapi.yml +0 -31636
  112. comfy_cli-1.22.0/comfy_cli/command/generate/spec.py +0 -547
  113. comfy_cli-1.22.0/comfy_cli/skills/comfy-agent-permissions/SKILL.md +0 -92
  114. comfy_cli-1.22.0/comfy_cli/skills/comfy-custom-nodes/SKILL.md +0 -190
  115. {comfy_cli-1.22.0 → comfy_cli-1.23.0}/LICENSE +0 -0
  116. {comfy_cli-1.22.0 → comfy_cli-1.23.0}/comfy_cli/__init__.py +0 -0
  117. {comfy_cli-1.22.0 → comfy_cli-1.23.0}/comfy_cli/__main__.py +0 -0
  118. {comfy_cli-1.22.0 → comfy_cli-1.23.0}/comfy_cli/_safe_exec.py +0 -0
  119. {comfy_cli-1.22.0 → comfy_cli-1.23.0}/comfy_cli/auth/__init__.py +0 -0
  120. {comfy_cli-1.22.0 → comfy_cli-1.23.0}/comfy_cli/auth/command.py +0 -0
  121. {comfy_cli-1.22.0 → comfy_cli-1.23.0}/comfy_cli/auth/store.py +0 -0
  122. {comfy_cli-1.22.0 → comfy_cli-1.23.0}/comfy_cli/builder_pagination.py +0 -0
  123. {comfy_cli-1.22.0 → comfy_cli-1.23.0}/comfy_cli/caller.py +0 -0
  124. {comfy_cli-1.22.0 → comfy_cli-1.23.0}/comfy_cli/cancellation.py +0 -0
  125. {comfy_cli-1.22.0 → comfy_cli-1.23.0}/comfy_cli/cloud/__init__.py +0 -0
  126. {comfy_cli-1.22.0 → comfy_cli-1.23.0}/comfy_cli/cloud/billing.py +0 -0
  127. {comfy_cli-1.22.0 → comfy_cli-1.23.0}/comfy_cli/cloud/command.py +0 -0
  128. {comfy_cli-1.22.0 → comfy_cli-1.23.0}/comfy_cli/comfy_client.py +0 -0
  129. {comfy_cli-1.22.0 → comfy_cli-1.23.0}/comfy_cli/command/__init__.py +0 -0
  130. {comfy_cli-1.22.0 → comfy_cli-1.23.0}/comfy_cli/command/build_diff.py +0 -0
  131. {comfy_cli-1.22.0 → comfy_cli-1.23.0}/comfy_cli/command/build_digest_cache.py +0 -0
  132. {comfy_cli-1.22.0 → comfy_cli-1.23.0}/comfy_cli/command/build_package.py +0 -0
  133. {comfy_cli-1.22.0 → comfy_cli-1.23.0}/comfy_cli/command/build_paths.py +0 -0
  134. {comfy_cli-1.22.0 → comfy_cli-1.23.0}/comfy_cli/command/build_pull.py +0 -0
  135. {comfy_cli-1.22.0 → comfy_cli-1.23.0}/comfy_cli/command/build_push.py +0 -0
  136. {comfy_cli-1.22.0 → comfy_cli-1.23.0}/comfy_cli/command/build_spec.py +0 -0
  137. {comfy_cli-1.22.0 → comfy_cli-1.23.0}/comfy_cli/command/build_targets.py +0 -0
  138. {comfy_cli-1.22.0 → comfy_cli-1.23.0}/comfy_cli/command/build_upload_progress.py +0 -0
  139. {comfy_cli-1.22.0 → comfy_cli-1.23.0}/comfy_cli/command/build_validation.py +0 -0
  140. {comfy_cli-1.22.0 → comfy_cli-1.23.0}/comfy_cli/command/custom_nodes/__init__.py +0 -0
  141. {comfy_cli-1.22.0 → comfy_cli-1.23.0}/comfy_cli/command/custom_nodes/bisect_custom_nodes.py +0 -0
  142. {comfy_cli-1.22.0 → comfy_cli-1.23.0}/comfy_cli/command/custom_nodes/cm_cli_util.py +0 -0
  143. {comfy_cli-1.22.0 → comfy_cli-1.23.0}/comfy_cli/command/custom_nodes/command.py +0 -0
  144. {comfy_cli-1.22.0 → comfy_cli-1.23.0}/comfy_cli/command/deploy_compute.py +0 -0
  145. {comfy_cli-1.22.0 → comfy_cli-1.23.0}/comfy_cli/command/deploy_lifecycle.py +0 -0
  146. {comfy_cli-1.22.0 → comfy_cli-1.23.0}/comfy_cli/command/deploy_refs.py +0 -0
  147. {comfy_cli-1.22.0 → comfy_cli-1.23.0}/comfy_cli/command/deploy_run_assets.py +0 -0
  148. {comfy_cli-1.22.0 → comfy_cli-1.23.0}/comfy_cli/command/deploy_run_watch.py +0 -0
  149. {comfy_cli-1.22.0 → comfy_cli-1.23.0}/comfy_cli/command/deploy_workflow.py +0 -0
  150. {comfy_cli-1.22.0 → comfy_cli-1.23.0}/comfy_cli/command/generate/__init__.py +0 -0
  151. {comfy_cli-1.22.0 → comfy_cli-1.23.0}/comfy_cli/command/generate/adapters.py +0 -0
  152. {comfy_cli-1.22.0 → comfy_cli-1.23.0}/comfy_cli/command/generate/poll.py +0 -0
  153. {comfy_cli-1.22.0 → comfy_cli-1.23.0}/comfy_cli/command/generate/schema.py +0 -0
  154. {comfy_cli-1.22.0 → comfy_cli-1.23.0}/comfy_cli/command/generate/upload.py +0 -0
  155. {comfy_cli-1.22.0 → comfy_cli-1.23.0}/comfy_cli/command/github/pr_info.py +0 -0
  156. {comfy_cli-1.22.0 → comfy_cli-1.23.0}/comfy_cli/command/install.py +0 -0
  157. {comfy_cli-1.22.0 → comfy_cli-1.23.0}/comfy_cli/command/job_watcher.py +0 -0
  158. {comfy_cli-1.22.0 → comfy_cli-1.23.0}/comfy_cli/command/jobs.py +0 -0
  159. {comfy_cli-1.22.0 → comfy_cli-1.23.0}/comfy_cli/command/launch.py +0 -0
  160. {comfy_cli-1.22.0 → comfy_cli-1.23.0}/comfy_cli/command/models/search.py +0 -0
  161. {comfy_cli-1.22.0 → comfy_cli-1.23.0}/comfy_cli/command/node_deps.py +0 -0
  162. {comfy_cli-1.22.0 → comfy_cli-1.23.0}/comfy_cli/command/outdated.py +0 -0
  163. {comfy_cli-1.22.0 → comfy_cli-1.23.0}/comfy_cli/command/pack_scan.py +0 -0
  164. {comfy_cli-1.22.0 → comfy_cli-1.23.0}/comfy_cli/command/pr_command.py +0 -0
  165. {comfy_cli-1.22.0 → comfy_cli-1.23.0}/comfy_cli/command/preview.py +0 -0
  166. {comfy_cli-1.22.0 → comfy_cli-1.23.0}/comfy_cli/command/project.py +0 -0
  167. {comfy_cli-1.22.0 → comfy_cli-1.23.0}/comfy_cli/command/run/loader.py +0 -0
  168. {comfy_cli-1.22.0 → comfy_cli-1.23.0}/comfy_cli/command/run/preflight.py +0 -0
  169. {comfy_cli-1.22.0 → comfy_cli-1.23.0}/comfy_cli/command/run_cli.py +0 -0
  170. {comfy_cli-1.22.0 → comfy_cli-1.23.0}/comfy_cli/command/setup.py +0 -0
  171. {comfy_cli-1.22.0 → comfy_cli-1.23.0}/comfy_cli/command/stop_port.py +0 -0
  172. {comfy_cli-1.22.0 → comfy_cli-1.23.0}/comfy_cli/command/system.py +0 -0
  173. {comfy_cli-1.22.0 → comfy_cli-1.23.0}/comfy_cli/command/version_validators.py +0 -0
  174. {comfy_cli-1.22.0 → comfy_cli-1.23.0}/comfy_cli/command/workflow_fragments.py +0 -0
  175. {comfy_cli-1.22.0 → comfy_cli-1.23.0}/comfy_cli/command_mentions.py +0 -0
  176. {comfy_cli-1.22.0 → comfy_cli-1.23.0}/comfy_cli/config_manager.py +0 -0
  177. {comfy_cli-1.22.0 → comfy_cli-1.23.0}/comfy_cli/constants.py +0 -0
  178. {comfy_cli-1.22.0 → comfy_cli-1.23.0}/comfy_cli/cql/__init__.py +0 -0
  179. {comfy_cli-1.22.0 → comfy_cli-1.23.0}/comfy_cli/cql/_net.py +0 -0
  180. {comfy_cli-1.22.0 → comfy_cli-1.23.0}/comfy_cli/cql/annotations_source.py +0 -0
  181. {comfy_cli-1.22.0 → comfy_cli-1.23.0}/comfy_cli/cql/data/__init__.py +0 -0
  182. {comfy_cli-1.22.0 → comfy_cli-1.23.0}/comfy_cli/cql/data/cloud_disable_config.yaml +0 -0
  183. {comfy_cli-1.22.0 → comfy_cli-1.23.0}/comfy_cli/cql/data/default_text2img.json +0 -0
  184. {comfy_cli-1.22.0 → comfy_cli-1.23.0}/comfy_cli/cql/data/supported_nodes.yaml +0 -0
  185. {comfy_cli-1.22.0 → comfy_cli-1.23.0}/comfy_cli/cql/default_workflow.py +0 -0
  186. {comfy_cli-1.22.0 → comfy_cli-1.23.0}/comfy_cli/cql/errors.py +0 -0
  187. {comfy_cli-1.22.0 → comfy_cli-1.23.0}/comfy_cli/cuda_detect.py +0 -0
  188. {comfy_cli-1.22.0 → comfy_cli-1.23.0}/comfy_cli/deploy_jobs.py +0 -0
  189. {comfy_cli-1.22.0 → comfy_cli-1.23.0}/comfy_cli/deprecation.py +0 -0
  190. {comfy_cli-1.22.0 → comfy_cli-1.23.0}/comfy_cli/env_checker.py +0 -0
  191. {comfy_cli-1.22.0 → comfy_cli-1.23.0}/comfy_cli/execution_errors.py +0 -0
  192. {comfy_cli-1.22.0 → comfy_cli-1.23.0}/comfy_cli/file_utils.py +0 -0
  193. {comfy_cli-1.22.0 → comfy_cli-1.23.0}/comfy_cli/fragments.py +0 -0
  194. {comfy_cli-1.22.0 → comfy_cli-1.23.0}/comfy_cli/git_utils.py +0 -0
  195. {comfy_cli-1.22.0 → comfy_cli-1.23.0}/comfy_cli/hardware.py +0 -0
  196. {comfy_cli-1.22.0 → comfy_cli-1.23.0}/comfy_cli/hashing.py +0 -0
  197. {comfy_cli-1.22.0 → comfy_cli-1.23.0}/comfy_cli/help_json.py +0 -0
  198. {comfy_cli-1.22.0 → comfy_cli-1.23.0}/comfy_cli/host_port.py +0 -0
  199. {comfy_cli-1.22.0 → comfy_cli-1.23.0}/comfy_cli/http.py +0 -0
  200. {comfy_cli-1.22.0 → comfy_cli-1.23.0}/comfy_cli/interaction.py +0 -0
  201. {comfy_cli-1.22.0 → comfy_cli-1.23.0}/comfy_cli/jobs_state.py +0 -0
  202. {comfy_cli-1.22.0 → comfy_cli-1.23.0}/comfy_cli/layout_quality.py +0 -0
  203. {comfy_cli-1.22.0 → comfy_cli-1.23.0}/comfy_cli/local_address.py +0 -0
  204. {comfy_cli-1.22.0 → comfy_cli-1.23.0}/comfy_cli/locking.py +0 -0
  205. {comfy_cli-1.22.0 → comfy_cli-1.23.0}/comfy_cli/logging.py +0 -0
  206. {comfy_cli-1.22.0 → comfy_cli-1.23.0}/comfy_cli/onboarding.py +0 -0
  207. {comfy_cli-1.22.0 → comfy_cli-1.23.0}/comfy_cli/output/__init__.py +0 -0
  208. {comfy_cli-1.22.0 → comfy_cli-1.23.0}/comfy_cli/output/branding.py +0 -0
  209. {comfy_cli-1.22.0 → comfy_cli-1.23.0}/comfy_cli/output/glyphs.py +0 -0
  210. {comfy_cli-1.22.0 → comfy_cli-1.23.0}/comfy_cli/output/panels.py +0 -0
  211. {comfy_cli-1.22.0 → comfy_cli-1.23.0}/comfy_cli/output/preview.py +0 -0
  212. {comfy_cli-1.22.0 → comfy_cli-1.23.0}/comfy_cli/output/progress.py +0 -0
  213. {comfy_cli-1.22.0 → comfy_cli-1.23.0}/comfy_cli/output/renderer.py +0 -0
  214. {comfy_cli-1.22.0 → comfy_cli-1.23.0}/comfy_cli/output/rich_compat.py +0 -0
  215. {comfy_cli-1.22.0 → comfy_cli-1.23.0}/comfy_cli/output/sanitize.py +0 -0
  216. {comfy_cli-1.22.0 → comfy_cli-1.23.0}/comfy_cli/pr_cache.py +0 -0
  217. {comfy_cli-1.22.0 → comfy_cli-1.23.0}/comfy_cli/project.py +0 -0
  218. {comfy_cli-1.22.0 → comfy_cli-1.23.0}/comfy_cli/registry/__init__.py +0 -0
  219. {comfy_cli-1.22.0 → comfy_cli-1.23.0}/comfy_cli/registry/api.py +0 -0
  220. {comfy_cli-1.22.0 → comfy_cli-1.23.0}/comfy_cli/registry/types.py +0 -0
  221. {comfy_cli-1.22.0 → comfy_cli-1.23.0}/comfy_cli/resolve_python.py +0 -0
  222. {comfy_cli-1.22.0 → comfy_cli-1.23.0}/comfy_cli/schemas/__init__.py +0 -0
  223. {comfy_cli-1.22.0 → comfy_cli-1.23.0}/comfy_cli/schemas/assets.json +0 -0
  224. {comfy_cli-1.22.0 → comfy_cli-1.23.0}/comfy_cli/schemas/auth.json +0 -0
  225. {comfy_cli-1.22.0 → comfy_cli-1.23.0}/comfy_cli/schemas/build_blob_ls.json +0 -0
  226. {comfy_cli-1.22.0 → comfy_cli-1.23.0}/comfy_cli/schemas/build_delete.json +0 -0
  227. {comfy_cli-1.22.0 → comfy_cli-1.23.0}/comfy_cli/schemas/build_init.json +0 -0
  228. {comfy_cli-1.22.0 → comfy_cli-1.23.0}/comfy_cli/schemas/build_ls.json +0 -0
  229. {comfy_cli-1.22.0 → comfy_cli-1.23.0}/comfy_cli/schemas/build_pull.json +0 -0
  230. {comfy_cli-1.22.0 → comfy_cli-1.23.0}/comfy_cli/schemas/build_push.json +0 -0
  231. {comfy_cli-1.22.0 → comfy_cli-1.23.0}/comfy_cli/schemas/build_push_event.json +0 -0
  232. {comfy_cli-1.22.0 → comfy_cli-1.23.0}/comfy_cli/schemas/build_refs_base_images.json +0 -0
  233. {comfy_cli-1.22.0 → comfy_cli-1.23.0}/comfy_cli/schemas/build_refs_build_targets.json +0 -0
  234. {comfy_cli-1.22.0 → comfy_cli-1.23.0}/comfy_cli/schemas/build_refs_model_dirs.json +0 -0
  235. {comfy_cli-1.22.0 → comfy_cli-1.23.0}/comfy_cli/schemas/build_refs_resolve.json +0 -0
  236. {comfy_cli-1.22.0 → comfy_cli-1.23.0}/comfy_cli/schemas/build_release_create.json +0 -0
  237. {comfy_cli-1.22.0 → comfy_cli-1.23.0}/comfy_cli/schemas/build_release_delete.json +0 -0
  238. {comfy_cli-1.22.0 → comfy_cli-1.23.0}/comfy_cli/schemas/build_release_logs.json +0 -0
  239. {comfy_cli-1.22.0 → comfy_cli-1.23.0}/comfy_cli/schemas/build_release_ls.json +0 -0
  240. {comfy_cli-1.22.0 → comfy_cli-1.23.0}/comfy_cli/schemas/build_release_manifest.json +0 -0
  241. {comfy_cli-1.22.0 → comfy_cli-1.23.0}/comfy_cli/schemas/build_release_show.json +0 -0
  242. {comfy_cli-1.22.0 → comfy_cli-1.23.0}/comfy_cli/schemas/build_show.json +0 -0
  243. {comfy_cli-1.22.0 → comfy_cli-1.23.0}/comfy_cli/schemas/build_status.json +0 -0
  244. {comfy_cli-1.22.0 → comfy_cli-1.23.0}/comfy_cli/schemas/build_update.json +0 -0
  245. {comfy_cli-1.22.0 → comfy_cli-1.23.0}/comfy_cli/schemas/build_validate.json +0 -0
  246. {comfy_cli-1.22.0 → comfy_cli-1.23.0}/comfy_cli/schemas/cloud_status.json +0 -0
  247. {comfy_cli-1.22.0 → comfy_cli-1.23.0}/comfy_cli/schemas/deploy_delete.json +0 -0
  248. {comfy_cli-1.22.0 → comfy_cli-1.23.0}/comfy_cli/schemas/deploy_logs.json +0 -0
  249. {comfy_cli-1.22.0 → comfy_cli-1.23.0}/comfy_cli/schemas/deploy_refs_compute.json +0 -0
  250. {comfy_cli-1.22.0 → comfy_cli-1.23.0}/comfy_cli/schemas/deploy_scale.json +0 -0
  251. {comfy_cli-1.22.0 → comfy_cli-1.23.0}/comfy_cli/schemas/deploy_show.json +0 -0
  252. {comfy_cli-1.22.0 → comfy_cli-1.23.0}/comfy_cli/schemas/deploy_start.json +0 -0
  253. {comfy_cli-1.22.0 → comfy_cli-1.23.0}/comfy_cli/schemas/deploy_stop.json +0 -0
  254. {comfy_cli-1.22.0 → comfy_cli-1.23.0}/comfy_cli/schemas/discover.json +0 -0
  255. {comfy_cli-1.22.0 → comfy_cli-1.23.0}/comfy_cli/schemas/download.json +0 -0
  256. {comfy_cli-1.22.0 → comfy_cli-1.23.0}/comfy_cli/schemas/download_status.json +0 -0
  257. {comfy_cli-1.22.0 → comfy_cli-1.23.0}/comfy_cli/schemas/downloads.json +0 -0
  258. {comfy_cli-1.22.0 → comfy_cli-1.23.0}/comfy_cli/schemas/env.json +0 -0
  259. {comfy_cli-1.22.0 → comfy_cli-1.23.0}/comfy_cli/schemas/envelope.json +0 -0
  260. {comfy_cli-1.22.0 → comfy_cli-1.23.0}/comfy_cli/schemas/error.json +0 -0
  261. {comfy_cli-1.22.0 → comfy_cli-1.23.0}/comfy_cli/schemas/error_codes.md +0 -0
  262. {comfy_cli-1.22.0 → comfy_cli-1.23.0}/comfy_cli/schemas/free.json +0 -0
  263. {comfy_cli-1.22.0 → comfy_cli-1.23.0}/comfy_cli/schemas/generate_result.json +0 -0
  264. {comfy_cli-1.22.0 → comfy_cli-1.23.0}/comfy_cli/schemas/generate_schema.json +0 -0
  265. {comfy_cli-1.22.0 → comfy_cli-1.23.0}/comfy_cli/schemas/help.json +0 -0
  266. {comfy_cli-1.22.0 → comfy_cli-1.23.0}/comfy_cli/schemas/jobs.json +0 -0
  267. {comfy_cli-1.22.0 → comfy_cli-1.23.0}/comfy_cli/schemas/jobs_wait.json +0 -0
  268. {comfy_cli-1.22.0 → comfy_cli-1.23.0}/comfy_cli/schemas/knowledge_block.json +0 -0
  269. {comfy_cli-1.22.0 → comfy_cli-1.23.0}/comfy_cli/schemas/launch.json +0 -0
  270. {comfy_cli-1.22.0 → comfy_cli-1.23.0}/comfy_cli/schemas/logs.json +0 -0
  271. {comfy_cli-1.22.0 → comfy_cli-1.23.0}/comfy_cli/schemas/models.json +0 -0
  272. {comfy_cli-1.22.0 → comfy_cli-1.23.0}/comfy_cli/schemas/node_deps.json +0 -0
  273. {comfy_cli-1.22.0 → comfy_cli-1.23.0}/comfy_cli/schemas/nodes.json +0 -0
  274. {comfy_cli-1.22.0 → comfy_cli-1.23.0}/comfy_cli/schemas/outdated.json +0 -0
  275. {comfy_cli-1.22.0 → comfy_cli-1.23.0}/comfy_cli/schemas/preview.json +0 -0
  276. {comfy_cli-1.22.0 → comfy_cli-1.23.0}/comfy_cli/schemas/project.json +0 -0
  277. {comfy_cli-1.22.0 → comfy_cli-1.23.0}/comfy_cli/schemas/run.json +0 -0
  278. {comfy_cli-1.22.0 → comfy_cli-1.23.0}/comfy_cli/schemas/run_event.json +0 -0
  279. {comfy_cli-1.22.0 → comfy_cli-1.23.0}/comfy_cli/schemas/set_default.json +0 -0
  280. {comfy_cli-1.22.0 → comfy_cli-1.23.0}/comfy_cli/schemas/skill.json +0 -0
  281. {comfy_cli-1.22.0 → comfy_cli-1.23.0}/comfy_cli/schemas/stop.json +0 -0
  282. {comfy_cli-1.22.0 → comfy_cli-1.23.0}/comfy_cli/schemas/system_stats.json +0 -0
  283. {comfy_cli-1.22.0 → comfy_cli-1.23.0}/comfy_cli/schemas/templates.json +0 -0
  284. {comfy_cli-1.22.0 → comfy_cli-1.23.0}/comfy_cli/schemas/transfer.json +0 -0
  285. {comfy_cli-1.22.0 → comfy_cli-1.23.0}/comfy_cli/schemas/update.json +0 -0
  286. {comfy_cli-1.22.0 → comfy_cli-1.23.0}/comfy_cli/schemas/version.json +0 -0
  287. {comfy_cli-1.22.0 → comfy_cli-1.23.0}/comfy_cli/schemas/which.json +0 -0
  288. {comfy_cli-1.22.0 → comfy_cli-1.23.0}/comfy_cli/schemas/workflow.json +0 -0
  289. {comfy_cli-1.22.0 → comfy_cli-1.23.0}/comfy_cli/skills/comfy/__init__.py +0 -0
  290. {comfy_cli-1.22.0 → comfy_cli-1.23.0}/comfy_cli/skills/comfy-build-authoring/SKILL.md +0 -0
  291. {comfy_cli-1.22.0 → comfy_cli-1.23.0}/comfy_cli/skills/comfy-build-pins/SKILL.md +0 -0
  292. {comfy_cli-1.22.0 → comfy_cli-1.23.0}/comfy_cli/skills/comfy-director/SKILL.md +0 -0
  293. {comfy_cli-1.22.0 → comfy_cli-1.23.0}/comfy_cli/skills/comfy-relay/SKILL.md +0 -0
  294. {comfy_cli-1.22.0 → comfy_cli-1.23.0}/comfy_cli/standalone.py +0 -0
  295. {comfy_cli-1.22.0 → comfy_cli-1.23.0}/comfy_cli/target.py +0 -0
  296. {comfy_cli-1.22.0 → comfy_cli-1.23.0}/comfy_cli/typing.py +0 -0
  297. {comfy_cli-1.22.0 → comfy_cli-1.23.0}/comfy_cli/ui.py +0 -0
  298. {comfy_cli-1.22.0 → comfy_cli-1.23.0}/comfy_cli/update.py +0 -0
  299. {comfy_cli-1.22.0 → comfy_cli-1.23.0}/comfy_cli/utils.py +0 -0
  300. {comfy_cli-1.22.0 → comfy_cli-1.23.0}/comfy_cli/uv.py +0 -0
  301. {comfy_cli-1.22.0 → comfy_cli-1.23.0}/comfy_cli/where.py +0 -0
  302. {comfy_cli-1.22.0 → comfy_cli-1.23.0}/comfy_cli/workspace_manager.py +0 -0
  303. {comfy_cli-1.22.0 → comfy_cli-1.23.0}/comfy_cli.egg-info/dependency_links.txt +0 -0
  304. {comfy_cli-1.22.0 → comfy_cli-1.23.0}/comfy_cli.egg-info/entry_points.txt +0 -0
  305. {comfy_cli-1.22.0 → comfy_cli-1.23.0}/comfy_cli.egg-info/requires.txt +0 -0
  306. {comfy_cli-1.22.0 → comfy_cli-1.23.0}/comfy_cli.egg-info/top_level.txt +0 -0
  307. {comfy_cli-1.22.0 → comfy_cli-1.23.0}/setup.cfg +0 -0
  308. {comfy_cli-1.22.0 → comfy_cli-1.23.0}/tests/test_file_utils_network.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: comfy-cli
3
- Version: 1.22.0
3
+ Version: 1.23.0
4
4
  Summary: A CLI tool for installing and using ComfyUI.
5
5
  Maintainer-email: Yoland Yan <yoland@drip.art>, James Kwon <hongilkwon316@gmail.com>, Robin Huang <robin@drip.art>, "Dr.Lt.Data" <dr.lt.data@gmail.com>
6
6
  License: GPL-3.0-only
@@ -72,7 +72,7 @@ workflows, and call hosted partner image models, all from your terminal.
72
72
  ## Features
73
73
 
74
74
  - 🚀 One-command ComfyUI install and launch
75
- - 🎨 Direct calls to partner image and video nodes (Flux, Ideogram, DALL·E, Recraft, Stability, Gemini/nano-banana, Kling, Luma, Runway, Pika, Vidu, Hailuo, Seedance, …) via `comfy generate`, no workflow JSON required
75
+ - 🎨 Direct calls to partner image and video nodes (Flux, Ideogram, DALL·E, Recraft, Reve, Gemini/nano-banana, Kling, Luma, Runway, Pika, Vidu, Hailuo, Seedance, …) via `comfy generate`, no workflow JSON required
76
76
  - 🔧 Custom node management — install, update, snapshot, bisect
77
77
  - 📦 Fast dependency resolution with `uv` (`--fast-deps`, `--uv-compile`)
78
78
  - 🗄️ Model downloads from CivitAI, Hugging Face, and direct URLs
@@ -529,8 +529,8 @@ Notes:
529
529
  `comfy generate` calls Comfy's partner nodes directly from the terminal — no
530
530
  local ComfyUI or workflow JSON required. It hits the same hosted partner nodes
531
531
  you'd otherwise wire into a ComfyUI workflow, but as one-shot CLI calls. Image
532
- models (Flux, Ideogram, DALL·E, Recraft, Stability, Runway, Reve, xAI Grok,
533
- Google Gemini Flash Image aka **nano-banana**, …) and video models (Kling,
532
+ models (Flux, Ideogram, DALL·E, Recraft, Runway, Reve, xAI Grok, Google
533
+ Gemini Flash Image aka **nano-banana**, …) and video models (Kling,
534
534
  Luma, Runway Gen-3, Pika, Vidu, Moonvalley, Hailuo, Grok video, ByteDance
535
535
  **Seedance**) are all covered; video jobs run async and the CLI polls until
536
536
  the result is ready.
@@ -19,7 +19,7 @@ workflows, and call hosted partner image models, all from your terminal.
19
19
  ## Features
20
20
 
21
21
  - 🚀 One-command ComfyUI install and launch
22
- - 🎨 Direct calls to partner image and video nodes (Flux, Ideogram, DALL·E, Recraft, Stability, Gemini/nano-banana, Kling, Luma, Runway, Pika, Vidu, Hailuo, Seedance, …) via `comfy generate`, no workflow JSON required
22
+ - 🎨 Direct calls to partner image and video nodes (Flux, Ideogram, DALL·E, Recraft, Reve, Gemini/nano-banana, Kling, Luma, Runway, Pika, Vidu, Hailuo, Seedance, …) via `comfy generate`, no workflow JSON required
23
23
  - 🔧 Custom node management — install, update, snapshot, bisect
24
24
  - 📦 Fast dependency resolution with `uv` (`--fast-deps`, `--uv-compile`)
25
25
  - 🗄️ Model downloads from CivitAI, Hugging Face, and direct URLs
@@ -476,8 +476,8 @@ Notes:
476
476
  `comfy generate` calls Comfy's partner nodes directly from the terminal — no
477
477
  local ComfyUI or workflow JSON required. It hits the same hosted partner nodes
478
478
  you'd otherwise wire into a ComfyUI workflow, but as one-shot CLI calls. Image
479
- models (Flux, Ideogram, DALL·E, Recraft, Stability, Runway, Reve, xAI Grok,
480
- Google Gemini Flash Image aka **nano-banana**, …) and video models (Kling,
479
+ models (Flux, Ideogram, DALL·E, Recraft, Runway, Reve, xAI Grok, Google
480
+ Gemini Flash Image aka **nano-banana**, …) and video models (Kling,
481
481
  Luma, Runway Gen-3, Pika, Vidu, Moonvalley, Hailuo, Grok video, ByteDance
482
482
  **Seedance**) are all covered; video jobs run async and the CLI polls until
483
483
  the result is ready.
@@ -102,6 +102,7 @@ class LazyCommand:
102
102
  attr: str | None = None
103
103
  register: str | None = None
104
104
  help: str | None = None
105
+ hidden: bool = False
105
106
 
106
107
 
107
108
  class LazyTyperGroup(TyperGroup):
@@ -164,9 +165,11 @@ class LazyTyperGroup(TyperGroup):
164
165
  infos = [info for info in scratch.registered_commands if info.name == name]
165
166
  assert len(infos) == 1, f"{spec.module}.{spec.register} did not register exactly one {name!r} command"
166
167
  info = infos[0]
168
+ if spec.hidden:
169
+ info.hidden = True
167
170
  else:
168
171
  assert spec.attr, "LazyCommand needs attr or register"
169
- info = CommandInfo(name=name, callback=getattr(module, spec.attr), help=spec.help)
172
+ info = CommandInfo(name=name, callback=getattr(module, spec.attr), help=spec.help, hidden=spec.hidden)
170
173
  return typer.main.get_command_from_info(info, **self._factory_kwargs(typer.main.get_command_from_info))
171
174
 
172
175
  def _build_group(self, name: str, spec: LazySubcommand) -> click.Command:
@@ -12,12 +12,17 @@ lets it reach beyond the defaults lives in two files in its data dir:
12
12
  The agent's ``request_path`` / ``request_host`` tools only append to
13
13
  ``pending``: the model can ask, and injected content in a workflow can make it
14
14
  ask, but nothing it can call moves a target into an approved list. A human
15
- does that through a channel the model cannot reach — this module from a
16
- terminal (``comfy agent allow --approve <id>`` / ``comfy agent deny <id>``), or
17
- a panel button — after vetting the exact target the request names. The same
18
- module also grants a folder or host outright (``comfy agent allow --path`` /
19
- ``--host``). A running agent re-reads both files every ~20 s; otherwise they
20
- apply at its next start. The data dir is ``AGENT_DATA_DIR`` when set, else
15
+ does that through a channel the model cannot reach — the agent panel or the
16
+ in-chat card while an agent is running, or this module from a terminal
17
+ (``comfy agent allow --approve <id>`` / ``comfy agent deny <id>``) while none
18
+ is — after vetting the exact target the request names. The same module also
19
+ grants a folder or host outright (``comfy agent allow --path`` / ``--host``).
20
+
21
+ **A running agent is the only writer of these files.** It loads them at start
22
+ and saves its whole state on every change, so an edit written here while it
23
+ runs is never read and is overwritten by its next save; the commands in this
24
+ module refuse while an agent answers on its port, and otherwise apply at the
25
+ agent's next start. The data dir is ``AGENT_DATA_DIR`` when set, else
21
26
  ``~/.comfy-agent`` — the same rule the agent and its launcher use.
22
27
 
23
28
  The checks here are a courtesy, not the fence: the agent's sandbox deny list
@@ -0,0 +1,569 @@
1
+ """``comfy agent`` — the Typer surface over :mod:`comfy_cli.agent`.
2
+
3
+ ``permissions`` shows the agent's requests waiting for a human, what the local
4
+ agent may reach beyond its defaults, and whether one is running; ``allow
5
+ --approve <id>`` / ``deny <id>`` answer a request the agent's ``request_path``
6
+ / ``request_host`` tools recorded (the model cannot answer its own request);
7
+ ``allow --path`` / ``--host`` grants a folder or a host outright.
8
+
9
+ Every one of those writes refuses while an agent is answering on its port: a
10
+ running agent is the only writer of its permission files, so the write would
11
+ be ignored and then overwritten. While one runs, requests are answered in the
12
+ agent panel or on the in-chat card, which call the agent's own API.
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ import http.client
18
+ import json
19
+ import sys
20
+ import urllib.error
21
+ import urllib.request
22
+ from datetime import datetime, timezone
23
+ from pathlib import Path
24
+ from typing import Annotated
25
+
26
+ import typer
27
+
28
+ from comfy_cli import tracking
29
+ from comfy_cli.agent import (
30
+ DISCOVERY_FILE,
31
+ StateError,
32
+ UnknownRequestError,
33
+ allow_host,
34
+ allow_path,
35
+ approve,
36
+ data_dir,
37
+ deny,
38
+ read_state,
39
+ vet_host,
40
+ vet_path,
41
+ )
42
+ from comfy_cli.http import build_http_only_opener
43
+ from comfy_cli.output import get_renderer, rprint
44
+ from comfy_cli.output.sanitize import sanitize_markup as esc
45
+
46
+ app = typer.Typer(
47
+ no_args_is_help=True,
48
+ help="The local comfy agent: what it may reach, and how to let it reach more.",
49
+ )
50
+
51
+ _DATA_DIR_OPT = Annotated[
52
+ str | None,
53
+ typer.Option(
54
+ "--data-dir", show_default=False, help="The agent's data dir (default: AGENT_DATA_DIR, else ~/.comfy-agent)."
55
+ ),
56
+ ]
57
+
58
+ # A running agent is the only writer of its permission files: it loads them at
59
+ # start and saves its whole state on every change, so an edit made here while
60
+ # it runs is never read and is then overwritten by the agent's next save.
61
+ # Hence NEXT_START_NOTE, and the refusals below. The rule is the agent's, and
62
+ # is stated in its own `services/agent/local/README.md`.
63
+ NEXT_START_NOTE = "this applies at the agent's next start; a running agent does not read it"
64
+ AGENT_RUNNING_MESSAGE = (
65
+ "an agent is answering on port {port} from {root}: a running agent is the only writer of its "
66
+ "permission files, so this edit would be ignored and then overwritten"
67
+ )
68
+ ANSWER_IN_PANEL = (
69
+ "a running agent is the only writer of these files: answer a waiting request in the agent panel or "
70
+ "on the card in chat, or stop the agent to edit them from here"
71
+ )
72
+ AGENT_RUNNING_HINT = (
73
+ "answer a waiting request in the agent panel or on the card in chat, which call the running agent's "
74
+ "own API; to write these files from here instead, stop the agent first"
75
+ )
76
+ AGENT_UNCONFIRMED_MESSAGE = (
77
+ "something is answering on port {port}, which {root} names as its agent's, but it did not answer as that "
78
+ "agent: a write here is lost without a trace if an agent is in fact running, so none was made"
79
+ )
80
+ AGENT_UNCONFIRMED_HINT = (
81
+ "if an agent is running, answer the request in the agent panel or on the card in chat; if none is, delete "
82
+ "the stale agent.json in that data dir and run this again"
83
+ )
84
+ REFUSED_HINT = (
85
+ "a narrower folder that holds only what is needed may be allowed; credential stores, the home folder "
86
+ "and the whole disk never are. A host must be a full name or a routable address"
87
+ )
88
+ REFUSED_REQUEST_HINT = (
89
+ "the request stays pending and nothing was approved: `comfy agent deny <id>` clears it, or "
90
+ "`comfy agent allow --path <narrower folder>` grants what is actually needed. " + REFUSED_HINT
91
+ )
92
+ UNKNOWN_REQUEST_HINT = "run `comfy agent permissions` to see the requests waiting; each shows its id"
93
+ GRANT_HINTS = {
94
+ "approve": "{cli} agent allow{dir} --approve <id>",
95
+ "deny": "{cli} agent deny{dir} <id>",
96
+ "path": '{cli} agent allow{dir} --path "<folder>" --reason "<why>"',
97
+ "host": "{cli} agent allow{dir} --host <host> --reason <why>",
98
+ }
99
+
100
+ # The entry points pyproject installs. Anything else in argv[0] -- a test
101
+ # runner, an embedding host -- is not a way to call this CLI, so the hints name
102
+ # the installed command rather than whatever started the process.
103
+ _CLI_NAMES = frozenset({"comfy", "comfy-cli", "comfycli"})
104
+
105
+
106
+ def _cli_invocation() -> str:
107
+ """How to call this CLI, as this process was started.
108
+
109
+ ``comfy`` for an installed entry point (``comfy.exe`` folded to the same),
110
+ ``<python> -m comfy_cli`` when started as a module, and ``comfy`` for
111
+ anything else -- a hint is for the user's terminal, and a path that only
112
+ works inside the agent's sandbox is exactly what sent users to paste a venv
113
+ path that answered "Access is denied".
114
+ """
115
+ argv0 = Path(sys.argv[0] or "")
116
+ if argv0.name == "__main__.py":
117
+ return f"{Path(sys.executable).name} -m comfy_cli"
118
+ name = argv0.name[:-4] if argv0.name.lower().endswith(".exe") else argv0.name
119
+ return name if name in _CLI_NAMES else "comfy"
120
+
121
+
122
+ def grant_hints(root: Path) -> dict[str, str]:
123
+ """The exact commands that approve or grant against ``root``.
124
+
125
+ ``--data-dir`` rides along whenever the agent's dir is not the one a bare
126
+ terminal resolves (``AGENT_DATA_DIR``, else ``~/.comfy-agent``): the user's
127
+ shell is a different process, and without it ``--approve`` answers
128
+ ``agent_unknown_request`` while the request is still waiting. A relative
129
+ dir is emitted absolute: the user's shell need not share this process's cwd.
130
+ """
131
+ flag = "" if root.is_absolute() and root == data_dir(None) else f' --data-dir "{root.absolute()}"'
132
+ cli = _cli_invocation()
133
+ return {k: v.format(cli=cli, dir=flag) for k, v in GRANT_HINTS.items()}
134
+
135
+
136
+ def _age(requested_at: object, now: datetime | None = None) -> str:
137
+ """How long ago an RFC 3339 timestamp was, for a human; ``?`` when it does not parse."""
138
+ if not isinstance(requested_at, str):
139
+ return "?"
140
+ try:
141
+ then = datetime.fromisoformat(requested_at.replace("Z", "+00:00"))
142
+ except ValueError:
143
+ return "?"
144
+ if then.tzinfo is None:
145
+ then = then.replace(tzinfo=timezone.utc)
146
+ seconds = max(0, int(((now or datetime.now(timezone.utc)) - then).total_seconds()))
147
+ if seconds < 60:
148
+ return f"{seconds}s"
149
+ if seconds < 3600:
150
+ return f"{seconds // 60}m"
151
+ if seconds < 86400:
152
+ return f"{seconds // 3600}h"
153
+ return f"{seconds // 86400}d"
154
+
155
+
156
+ def _no_agent_warning(root) -> str:
157
+ # No agent has ever published itself here. A running agent started with
158
+ # another AGENT_DATA_DIR (the launcher's, or a typo in --data-dir) will
159
+ # never read this file; say so rather than promise a pick-up.
160
+ return (
161
+ f"no {DISCOVERY_FILE} in {root}: no agent has run from this data dir. A running agent that uses "
162
+ "another data dir will not see this approval; pass --data-dir or set AGENT_DATA_DIR to match it"
163
+ )
164
+
165
+
166
+ def _refuse_while_live(root, state) -> None:
167
+ """Refuse a write to the permission files unless the agent is known gone.
168
+
169
+ ``state.running`` only says a discovery file exists; a crashed agent
170
+ leaves one behind, so this probes ``/health`` rather than trusting the
171
+ file — otherwise a stale file would lock the user out of the only way to
172
+ edit these files.
173
+
174
+ It refuses on ``unconfirmed`` as well as ``live``, which is the whole
175
+ reason :func:`_probe_agent` has three outcomes rather than two. The two
176
+ mistakes are not symmetric: writing while an agent runs is overwritten by
177
+ its next save and the user never learns the approval was lost, while a
178
+ refusal names the data dir and what to do about it. Only what the probe
179
+ *establishes* is not this agent permits the write.
180
+ """
181
+ if not state.running:
182
+ return
183
+ verdict, _ = _probe_agent(state.port)
184
+ if verdict == _AGENT_GONE:
185
+ return
186
+ renderer = get_renderer()
187
+ if verdict == _AGENT_LIVE:
188
+ renderer.error(
189
+ code="agent_running",
190
+ message=AGENT_RUNNING_MESSAGE.format(port=state.port, root=root),
191
+ hint=AGENT_RUNNING_HINT,
192
+ )
193
+ else:
194
+ renderer.error(
195
+ code="agent_unconfirmed",
196
+ message=AGENT_UNCONFIRMED_MESSAGE.format(port=state.port, root=root),
197
+ hint=AGENT_UNCONFIRMED_HINT,
198
+ )
199
+ raise typer.Exit(code=1)
200
+
201
+
202
+ def _open_health(url: str):
203
+ """Open the agent's /health on loopback, ignoring any HTTP(S)_PROXY in the environment.
204
+
205
+ The global ``urlopen`` honours ``HTTP_PROXY``, which a shell behind the
206
+ agent's own egress proxy exports; routing a loopback probe through it
207
+ made a live agent look absent. An empty ``ProxyHandler`` skips the
208
+ environment.
209
+ """
210
+ return build_http_only_opener(urllib.request.ProxyHandler({})).open(url, timeout=2)
211
+
212
+
213
+ # The keys only the agent's own /health carries. The standalone binary serves
214
+ # the plain string "ok" until it has something to report and a JSON object
215
+ # once it has, and its startup reports sandbox and cli together, so a JSON
216
+ # object carrying none of these is some other service on a reused port (cloud
217
+ # services/agent/local/internal/server/server.go, healthPayload). `status` is
218
+ # deliberately not in the list: every other health endpoint has one too.
219
+ # Checked against that struct's JSON tags at cloud commit 6c54e206, and pinned
220
+ # by test_health_markers_are_fields_the_agent_actually_sends so a rename on the
221
+ # agent's side fails here instead of reading a live agent as gone.
222
+ _AGENT_HEALTH_KEYS = (
223
+ "sandbox",
224
+ "skills",
225
+ "crdt",
226
+ "doc_host",
227
+ "prompt",
228
+ "cli",
229
+ "assets",
230
+ "memory",
231
+ "node_trees",
232
+ )
233
+
234
+
235
+ # What a /health probe on the discovery port established. Three outcomes, not
236
+ # two, because the write guard needs the distinction: GONE is only for what the
237
+ # probe *proved* is not this agent, and everything it could not settle is
238
+ # UNCONFIRMED. `_sandbox_status` collapses both onto None, which is right for
239
+ # display — either way there is no sandbox to show — and wrong for a refusal.
240
+ _AGENT_LIVE = "live"
241
+ _AGENT_GONE = "gone"
242
+ _AGENT_UNCONFIRMED = "unconfirmed"
243
+
244
+ # The bare answer the agent's health handler falls through to while every
245
+ # reporter is still nil, in both forms it could arrive in: the server sends the
246
+ # two bytes as text/plain, a JSON encoder would quote them.
247
+ #
248
+ # No agent can serve it on the port this probe reaches. `app.go` calls
249
+ # ReportSandbox/ReportCLI/ReportNodeTrees/ReportAssets/ReportMemory/
250
+ # ReportPrompt/ReportSkills/ReportCRDT (lines 233-255 on cloud main) BEFORE
251
+ # net.Listen (257) and before WriteDiscoveryWith puts the port in agent.json
252
+ # (263), so by the time anything can connect, or even learn the port, eight
253
+ # reporters are non-nil and the handler serves the JSON payload. The
254
+ # fall-through is live code on a path nothing can observe from here.
255
+ #
256
+ # So this body means "something else holds the port", not "the agent, early".
257
+ _AGENT_EARLY_OK = ("ok", '"ok"')
258
+
259
+
260
+ def _probe_agent(port: int | None) -> tuple[str, dict | None]:
261
+ """Ask the discovery port whether this agent is answering on it.
262
+
263
+ Returns the verdict and, when the answer carried them, the sandbox and
264
+ workspace fields `permissions` displays.
265
+ """
266
+ if not port:
267
+ return _AGENT_GONE, None
268
+ try:
269
+ with _open_health(f"http://127.0.0.1:{port}/health") as resp:
270
+ raw = resp.read().decode("utf-8", "replace")
271
+ except urllib.error.HTTPError:
272
+ # Something serves HTTP here and refused the probe. Whether it is the
273
+ # agent is exactly what this failed to establish.
274
+ return _AGENT_UNCONFIRMED, None
275
+ except urllib.error.URLError as exc:
276
+ # urllib wraps the transport error; a refused connection is the one
277
+ # that proves nothing is listening, so agent.json is stale.
278
+ return (_AGENT_GONE if isinstance(exc.reason, ConnectionRefusedError) else _AGENT_UNCONFIRMED), None
279
+ except ConnectionRefusedError:
280
+ return _AGENT_GONE, None
281
+ except (http.client.RemoteDisconnected, http.client.IncompleteRead):
282
+ # Both reach the HTTPException clause below and must not: they come
283
+ # from a peer that accepted the connection and then failed mid-exchange
284
+ # -- a live agent shutting down, crashing or overloaded does exactly
285
+ # this. urllib wraps only h.request() in URLError, not h.getresponse(),
286
+ # so a RemoteDisconnected (a BadStatusLine as well as a
287
+ # ConnectionResetError) arrives here unwrapped; an IncompleteRead comes
288
+ # from resp.read() on a short body. Neither establishes that the
289
+ # listener is not this agent.
290
+ return _AGENT_UNCONFIRMED, None
291
+ except http.client.HTTPException:
292
+ # HTTPException is not an OSError: a stale port taken by something
293
+ # that is not HTTP (an SSH banner) raises BadStatusLine here. Proof
294
+ # enough that this is not the agent.
295
+ return _AGENT_GONE, None
296
+ except (OSError, ValueError):
297
+ # A timeout (the 2 s budget in _open_health) or a reset mid-read.
298
+ # Something is there; what it is is unsettled.
299
+ return _AGENT_UNCONFIRMED, None
300
+ if raw.strip() in _AGENT_EARLY_OK:
301
+ # Unsettled, and deliberately not either certainty. It cannot be this
302
+ # agent answering early (see `_AGENT_EARLY_OK`: the port does not exist
303
+ # until eight reporters do), so "live" asserts something false and
304
+ # sends a user with no agent to stop to the agent panel. But it is not
305
+ # proof of absence either — something is listening on the port
306
+ # agent.json names, and if an agent is in fact running from another
307
+ # data dir, a write made here is lost without a trace. Refusing while
308
+ # naming the stale-agent.json way out is the only honest answer.
309
+ #
310
+ # What must not come back is `_AGENT_GONE`: parsing this body as JSON
311
+ # before looking at it raised ValueError and fell through to a "gone"
312
+ # verdict, which let the write through. That is the silent-loss hole,
313
+ # and it stays closed. The JSON-quoted form is matched too, so a later
314
+ # switch to `c.JSON` does not reopen it.
315
+ return _AGENT_UNCONFIRMED, None
316
+ try:
317
+ body = json.loads(raw)
318
+ except ValueError:
319
+ # Not the early answer, and not JSON either. Something is there; what
320
+ # it is is unsettled.
321
+ return _AGENT_UNCONFIRMED, None
322
+ if not isinstance(body, dict):
323
+ # Neither the agent's own early answer nor anything judgeable.
324
+ return _AGENT_UNCONFIRMED, None
325
+ if not any(key in body for key in _AGENT_HEALTH_KEYS):
326
+ # A reused port held by an unrelated local service whose /health
327
+ # answers `{"status": "ok"}`. Established: not this agent, so the
328
+ # user is not locked out of editing the files at all.
329
+ return _AGENT_GONE, None
330
+ # The envelope schema says sandbox is an object or null and comfy_path a
331
+ # string or null; a stale or foreign server's shapes are not passed on.
332
+ sandbox = body.get("sandbox")
333
+ out: dict = {"sandbox": sandbox if isinstance(sandbox, dict) else None}
334
+ cli = body.get("cli")
335
+ if isinstance(cli, dict) and isinstance(cli.get("workspace"), str) and cli["workspace"]:
336
+ out["comfy_path"] = cli["workspace"]
337
+ return _AGENT_LIVE, out
338
+
339
+
340
+ def _sandbox_status(port: int | None) -> dict | None:
341
+ """The sandbox and workspace to display, or ``None`` when there are none.
342
+
343
+ Two-valued on purpose: a caller that only renders state has nothing to do
344
+ differently for a port it could not reach and one it reached and dismissed.
345
+ A caller that *writes* must use :func:`_probe_agent` directly.
346
+ """
347
+ verdict, detail = _probe_agent(port)
348
+ return detail if verdict == _AGENT_LIVE else None
349
+
350
+
351
+ @app.command(
352
+ "permissions",
353
+ help="Show the agent's requests waiting for approval, the approved folders and hosts, and a running agent's sandbox.",
354
+ )
355
+ @tracking.track_command("agent")
356
+ def permissions_cmd(data_dir_opt: _DATA_DIR_OPT = None):
357
+ renderer = get_renderer()
358
+ root = data_dir(data_dir_opt)
359
+ try:
360
+ state = read_state(root)
361
+ except StateError as exc:
362
+ renderer.error(code="agent_state_unreadable", message=str(exc), hint="fix or remove the file and try again")
363
+ raise typer.Exit(code=1) from exc
364
+ # One probe serves both readings: whether to show the agent as running,
365
+ # and whether a `grant` command offered here would be refused by
366
+ # `_refuse_while_live`. Offering one it would refuse was the contradiction.
367
+ verdict, health = _probe_agent(state.port) if state.running else (_AGENT_GONE, None)
368
+ live = verdict == _AGENT_LIVE
369
+ writable = verdict == _AGENT_GONE
370
+ payload = {
371
+ "data_dir": str(root),
372
+ "agent": {"running": live, "port": state.port},
373
+ "sandbox": (health or {}).get("sandbox"),
374
+ "comfy_path": (health or {}).get("comfy_path"),
375
+ "pending": state.pending,
376
+ "paths": state.paths,
377
+ "hosts": state.hosts,
378
+ # Offered only when a write from here would actually land. While an
379
+ # agent is answering it is the only writer of these files, and when
380
+ # the probe could not settle that, `allow`/`deny` refuse too — so an
381
+ # empty `grant` is the honest answer, not a missing one.
382
+ "grant": grant_hints(root) if writable else {},
383
+ }
384
+ if not writable:
385
+ payload["answer_in"] = ANSWER_IN_PANEL
386
+ if renderer.is_pretty():
387
+ # Everything interpolated below came from files on disk or a local
388
+ # server; it is escaped so a "[/]" in a reason cannot crash the print.
389
+ rprint(f"[bold]data dir[/bold] {esc(root)}")
390
+ status = f"running on port {state.port}" if payload["agent"]["running"] else "not running"
391
+ rprint(f"[bold]agent[/bold] {status}")
392
+ if payload["sandbox"]:
393
+ rprint(f"[bold]sandbox[/bold] {esc(json.dumps(payload['sandbox']))}")
394
+ if payload["comfy_path"]:
395
+ rprint(f"[bold]ComfyUI[/bold] {esc(payload['comfy_path'])}")
396
+ rprint("[bold]requests waiting for your approval[/bold]" + ("" if state.pending else " (none)"))
397
+ for e in state.pending:
398
+ rprint(
399
+ f" {esc(e['id'])} {e['kind']:<4} {esc(e['target'])} — {esc(e.get('reason', ''))}"
400
+ f" [dim]({_age(e.get('requested_at'))} ago)[/dim]"
401
+ )
402
+ if state.pending and writable:
403
+ first = state.pending[0]["id"]
404
+ approve_cmd, deny_cmd = (payload["grant"][k].replace("<id>", first) for k in ("approve", "deny"))
405
+ rprint(f" [dim]{esc(approve_cmd)} {esc(deny_cmd)}[/dim]")
406
+ elif state.pending:
407
+ rprint(f" [dim]{esc(ANSWER_IN_PANEL)}[/dim]")
408
+ rprint("[bold]folders the user approved[/bold]" + ("" if state.paths else " (none)"))
409
+ for e in state.paths:
410
+ rprint(f" {esc(e.get('path'))} — {esc(e.get('reason', ''))}")
411
+ rprint("[bold]hosts the user approved[/bold]" + ("" if state.hosts else " (none)"))
412
+ for e in state.hosts:
413
+ rprint(f" {esc(e.get('host'))} — {esc(e.get('reason', ''))}")
414
+ if writable:
415
+ rprint("\n[dim]" + "\n".join(esc(h) for h in payload["grant"].values()) + "[/dim]")
416
+ else:
417
+ rprint(f"\n[dim]{esc(ANSWER_IN_PANEL)}[/dim]")
418
+ renderer.emit(payload, command="agent permissions")
419
+
420
+
421
+ def _approve_request(root, request_id: str) -> None:
422
+ """``comfy agent allow --approve <id>``: answer one request the agent recorded."""
423
+ renderer = get_renderer()
424
+ try:
425
+ state = read_state(root)
426
+ _refuse_while_live(root, state)
427
+ record, kind = approve(root, request_id)
428
+ except StateError as exc:
429
+ renderer.error(code="agent_state_unreadable", message=str(exc), hint="fix or remove the file and try again")
430
+ raise typer.Exit(code=1) from exc
431
+ except UnknownRequestError as exc:
432
+ renderer.error(code="agent_unknown_request", message=str(exc), hint=UNKNOWN_REQUEST_HINT)
433
+ raise typer.Exit(code=1) from exc
434
+ except ValueError as exc:
435
+ renderer.error(code="agent_refused", message=str(exc), hint=REFUSED_REQUEST_HINT)
436
+ raise typer.Exit(code=1) from exc
437
+ except OSError as exc:
438
+ renderer.error(
439
+ code="agent_state_unwritable",
440
+ message=f"{root}: cannot write: {exc}",
441
+ hint="check the data dir's permissions, or pass --data-dir for the dir the agent uses",
442
+ )
443
+ raise typer.Exit(code=1) from exc
444
+ approved = {"id": request_id, "kind": kind, "target": record[kind]}
445
+ payload: dict = {"data_dir": str(root), "approved": approved, "note": NEXT_START_NOTE}
446
+ if not state.running:
447
+ payload["warning"] = _no_agent_warning(root)
448
+ if renderer.is_pretty():
449
+ rprint(f"[green]approved[/green] {kind}: {esc(approved['target'])} (request {esc(request_id)})")
450
+ rprint(f"[dim]{NEXT_START_NOTE}[/dim]")
451
+ if "warning" in payload:
452
+ renderer.warn(payload["warning"])
453
+ renderer.emit(payload, command="agent allow")
454
+
455
+
456
+ @app.command(
457
+ "allow",
458
+ help="Approve one of the agent's pending requests (--approve <id>), or let it reach a folder (--path) or host (--host).",
459
+ )
460
+ @tracking.track_command("agent")
461
+ def allow_cmd(
462
+ approve_id: Annotated[
463
+ str | None,
464
+ typer.Option(
465
+ "--approve", show_default=False, help="The id of a pending request (see `comfy agent permissions`)."
466
+ ),
467
+ ] = None,
468
+ path: Annotated[
469
+ str | None, typer.Option("--path", show_default=False, help="An absolute, existing folder.")
470
+ ] = None,
471
+ host: Annotated[str | None, typer.Option("--host", show_default=False, help="A host name, no scheme.")] = None,
472
+ reason: Annotated[
473
+ str, typer.Option("--reason", help="Why; recorded with the approval.")
474
+ ] = "allowed with comfy agent allow",
475
+ data_dir_opt: _DATA_DIR_OPT = None,
476
+ ):
477
+ renderer = get_renderer()
478
+ if approve_id and (path or host):
479
+ # A request names its own target and reason; --path/--host beside it
480
+ # would leave which target got approved to the reader's guess.
481
+ renderer.error(
482
+ code="agent_bad_args",
483
+ message="--approve answers one recorded request and cannot be combined with --path or --host",
484
+ hint="run `comfy agent allow --approve <id>` on its own, then `--path`/`--host` as a separate command",
485
+ )
486
+ raise typer.Exit(code=2)
487
+ if not path and not host and not approve_id:
488
+ renderer.error(
489
+ code="agent_bad_args",
490
+ message="nothing to allow",
491
+ hint="pass --approve <id> for a pending request, or --path <folder> and/or --host <host>",
492
+ )
493
+ raise typer.Exit(code=2)
494
+ root = data_dir(data_dir_opt)
495
+ if approve_id:
496
+ _approve_request(root, approve_id.strip())
497
+ return
498
+ added: dict = {}
499
+ try:
500
+ # Read every state file and vet every value before the first write, so
501
+ # a refused --host or a broken egress-allow.json does not leave a
502
+ # --path approved behind a failure exit.
503
+ state = read_state(root)
504
+ _refuse_while_live(root, state)
505
+ if path:
506
+ vet_path(path, root=root)
507
+ if host:
508
+ vet_host(host)
509
+ if path:
510
+ folder, is_new = allow_path(root, path, reason)
511
+ added["path"] = {"path": str(folder), "added": is_new}
512
+ if host:
513
+ h, is_new = allow_host(root, host, reason)
514
+ added["host"] = {"host": h, "added": is_new}
515
+ except StateError as exc:
516
+ renderer.error(code="agent_state_unreadable", message=str(exc), hint="fix or remove the file and try again")
517
+ raise typer.Exit(code=1) from exc
518
+ except ValueError as exc:
519
+ renderer.error(code="agent_refused", message=str(exc), hint=REFUSED_HINT)
520
+ raise typer.Exit(code=1) from exc
521
+ except OSError as exc:
522
+ renderer.error(
523
+ code="agent_state_unwritable",
524
+ message=f"{root}: cannot write: {exc}",
525
+ hint="check the data dir's permissions, or pass --data-dir for the dir the agent uses",
526
+ )
527
+ raise typer.Exit(code=1) from exc
528
+ payload: dict = {"data_dir": str(root), **added, "note": NEXT_START_NOTE}
529
+ if not state.running:
530
+ payload["warning"] = _no_agent_warning(root)
531
+ if renderer.is_pretty():
532
+ for kind, entry in added.items():
533
+ rprint(f"[green]{'allowed' if entry['added'] else 'already allowed'}[/green] {kind}: {esc(entry[kind])}")
534
+ rprint(f"[dim]{NEXT_START_NOTE}[/dim]")
535
+ if "warning" in payload:
536
+ renderer.warn(payload["warning"])
537
+ renderer.emit(payload, command="agent allow")
538
+
539
+
540
+ @app.command("deny", help="Refuse one of the agent's pending requests; nothing is approved and the request is dropped.")
541
+ @tracking.track_command("agent")
542
+ def deny_cmd(
543
+ request_id: Annotated[str, typer.Argument(help="The id of a pending request (see `comfy agent permissions`).")],
544
+ data_dir_opt: _DATA_DIR_OPT = None,
545
+ ):
546
+ renderer = get_renderer()
547
+ root = data_dir(data_dir_opt)
548
+ request_id = request_id.strip()
549
+ try:
550
+ _refuse_while_live(root, read_state(root))
551
+ req = deny(root, request_id)
552
+ except StateError as exc:
553
+ renderer.error(code="agent_state_unreadable", message=str(exc), hint="fix or remove the file and try again")
554
+ raise typer.Exit(code=1) from exc
555
+ except UnknownRequestError as exc:
556
+ renderer.error(code="agent_unknown_request", message=str(exc), hint=UNKNOWN_REQUEST_HINT)
557
+ raise typer.Exit(code=1) from exc
558
+ except OSError as exc:
559
+ renderer.error(
560
+ code="agent_state_unwritable",
561
+ message=f"{root}: cannot write: {exc}",
562
+ hint="check the data dir's permissions, or pass --data-dir for the dir the agent uses",
563
+ )
564
+ raise typer.Exit(code=1) from exc
565
+ denied = {"id": request_id, "kind": req["kind"], "target": req["target"]}
566
+ payload = {"data_dir": str(root), "denied": denied}
567
+ if renderer.is_pretty():
568
+ rprint(f"[yellow]denied[/yellow] {denied['kind']}: {esc(denied['target'])} (request {esc(request_id)})")
569
+ renderer.emit(payload, command="agent deny")