graph-agents-cli 0.3.1__py3-none-any.whl

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 (291) hide show
  1. graph_agents_cli/__init__.py +26 -0
  2. graph_agents_cli/_api_policy.py +2145 -0
  3. graph_agents_cli/_approvals.py +400 -0
  4. graph_agents_cli/_build.py +186 -0
  5. graph_agents_cli/_build_info.json +7 -0
  6. graph_agents_cli/_chat_client.py +462 -0
  7. graph_agents_cli/_click.py +157 -0
  8. graph_agents_cli/_defaults.py +139 -0
  9. graph_agents_cli/_experiments.py +64 -0
  10. graph_agents_cli/_http.py +192 -0
  11. graph_agents_cli/_output.py +83 -0
  12. graph_agents_cli/_project.py +462 -0
  13. graph_agents_cli/_remote.py +220 -0
  14. graph_agents_cli/_response_schema.py +264 -0
  15. graph_agents_cli/_runner.py +319 -0
  16. graph_agents_cli/_skills_check.py +274 -0
  17. graph_agents_cli/_tools.py +189 -0
  18. graph_agents_cli/_trust.py +66 -0
  19. graph_agents_cli/api/__init__.py +15 -0
  20. graph_agents_cli/api/_changes.py +506 -0
  21. graph_agents_cli/api/_files.py +658 -0
  22. graph_agents_cli/api/cmd_api.py +2480 -0
  23. graph_agents_cli/deploy/__init__.py +15 -0
  24. graph_agents_cli/deploy/_config.py +171 -0
  25. graph_agents_cli/deploy/_image.py +128 -0
  26. graph_agents_cli/deploy/_kube.py +286 -0
  27. graph_agents_cli/deploy/_modes.py +234 -0
  28. graph_agents_cli/deploy/_preflight.py +370 -0
  29. graph_agents_cli/deploy/_values.py +168 -0
  30. graph_agents_cli/deploy/cmd_deploy.py +1866 -0
  31. graph_agents_cli/deploy/gitops.py +562 -0
  32. graph_agents_cli/deploy/local_load.py +273 -0
  33. graph_agents_cli/dev/__init__.py +13 -0
  34. graph_agents_cli/dev/cmd_build.py +131 -0
  35. graph_agents_cli/dev/cmd_install.py +78 -0
  36. graph_agents_cli/dev/cmd_lint.py +119 -0
  37. graph_agents_cli/dev/cmd_playground.py +297 -0
  38. graph_agents_cli/dev/policy_check.py +1287 -0
  39. graph_agents_cli/eval/__init__.py +22 -0
  40. graph_agents_cli/eval/_client.py +670 -0
  41. graph_agents_cli/eval/_common.py +177 -0
  42. graph_agents_cli/eval/_judge.py +168 -0
  43. graph_agents_cli/eval/_judge_runner.py +238 -0
  44. graph_agents_cli/eval/_paths.py +212 -0
  45. graph_agents_cli/eval/checks.py +581 -0
  46. graph_agents_cli/eval/cmd_analyze.py +278 -0
  47. graph_agents_cli/eval/cmd_compare.py +284 -0
  48. graph_agents_cli/eval/cmd_eval_group.py +80 -0
  49. graph_agents_cli/eval/cmd_generate.py +558 -0
  50. graph_agents_cli/eval/cmd_grade.py +466 -0
  51. graph_agents_cli/eval/cmd_metric.py +156 -0
  52. graph_agents_cli/eval/cmd_run.py +370 -0
  53. graph_agents_cli/eval/cmd_submit.py +400 -0
  54. graph_agents_cli/eval/config.py +435 -0
  55. graph_agents_cli/eval/dataset.py +350 -0
  56. graph_agents_cli/eval/gate.py +420 -0
  57. graph_agents_cli/eval/transcript.py +192 -0
  58. graph_agents_cli/extension/__init__.py +13 -0
  59. graph_agents_cli/extension/_compat.py +86 -0
  60. graph_agents_cli/extension/_loader.py +293 -0
  61. graph_agents_cli/extension/_manifest.py +135 -0
  62. graph_agents_cli/extension/_overrides.py +195 -0
  63. graph_agents_cli/extension/_paths.py +91 -0
  64. graph_agents_cli/extension/_refs.py +193 -0
  65. graph_agents_cli/extension/_resolver.py +453 -0
  66. graph_agents_cli/extension/_schema.py +106 -0
  67. graph_agents_cli/extension/_spec.py +253 -0
  68. graph_agents_cli/extension/_sync.py +102 -0
  69. graph_agents_cli/extension/_trust.py +58 -0
  70. graph_agents_cli/extension/cmd_extension_add.py +259 -0
  71. graph_agents_cli/extension/cmd_extension_group.py +57 -0
  72. graph_agents_cli/extension/cmd_extension_list.py +56 -0
  73. graph_agents_cli/extension/cmd_extension_remove.py +61 -0
  74. graph_agents_cli/extension/cmd_extension_update.py +195 -0
  75. graph_agents_cli/info/__init__.py +13 -0
  76. graph_agents_cli/info/cmd_info.py +222 -0
  77. graph_agents_cli/infra/__init__.py +15 -0
  78. graph_agents_cli/infra/checks.py +1169 -0
  79. graph_agents_cli/infra/cmd_infra.py +103 -0
  80. graph_agents_cli/main.py +591 -0
  81. graph_agents_cli/peer/__init__.py +15 -0
  82. graph_agents_cli/peer/_generate.py +254 -0
  83. graph_agents_cli/peer/cmd_peer.py +1151 -0
  84. graph_agents_cli/run/__init__.py +13 -0
  85. graph_agents_cli/run/_local_server.py +1157 -0
  86. graph_agents_cli/run/_signals.py +141 -0
  87. graph_agents_cli/run/cmd_approvals.py +530 -0
  88. graph_agents_cli/run/cmd_run.py +1421 -0
  89. graph_agents_cli/scaffold/__init__.py +19 -0
  90. graph_agents_cli/scaffold/agents/README.md +24 -0
  91. graph_agents_cli/scaffold/agents/empty_py/.template/templateconfig.yaml +22 -0
  92. graph_agents_cli/scaffold/agents/langgraph/.env.example +292 -0
  93. graph_agents_cli/scaffold/agents/langgraph/.template/templateconfig.yaml +28 -0
  94. graph_agents_cli/scaffold/agents/langgraph/Dockerfile +59 -0
  95. graph_agents_cli/scaffold/agents/langgraph/Dockerfile.langgraph-server +59 -0
  96. graph_agents_cli/scaffold/agents/langgraph/README.md +571 -0
  97. graph_agents_cli/scaffold/agents/langgraph/api-policy.yaml +60 -0
  98. graph_agents_cli/scaffold/agents/langgraph/app/__init__.py +20 -0
  99. graph_agents_cli/scaffold/agents/langgraph/app/agent.py +174 -0
  100. graph_agents_cli/scaffold/agents/langgraph/app/app_utils/__init__.py +15 -0
  101. graph_agents_cli/scaffold/agents/langgraph/app/app_utils/a2a.py +2162 -0
  102. graph_agents_cli/scaffold/agents/langgraph/app/app_utils/a2a_client.py +1167 -0
  103. graph_agents_cli/scaffold/agents/langgraph/app/app_utils/api_client.py +4220 -0
  104. graph_agents_cli/scaffold/agents/langgraph/app/app_utils/approvals.py +1349 -0
  105. graph_agents_cli/scaffold/agents/langgraph/app/app_utils/auth.py +1986 -0
  106. graph_agents_cli/scaffold/agents/langgraph/app/app_utils/chat.py +2962 -0
  107. graph_agents_cli/scaffold/agents/langgraph/app/app_utils/checkpointer.py +432 -0
  108. graph_agents_cli/scaffold/agents/langgraph/app/app_utils/content.py +569 -0
  109. graph_agents_cli/scaffold/agents/langgraph/app/app_utils/db.py +580 -0
  110. graph_agents_cli/scaffold/agents/langgraph/app/app_utils/limits.py +203 -0
  111. graph_agents_cli/scaffold/agents/langgraph/app/app_utils/metrics.py +231 -0
  112. graph_agents_cli/scaffold/agents/langgraph/app/app_utils/middleware.py +361 -0
  113. graph_agents_cli/scaffold/agents/langgraph/app/app_utils/model.py +611 -0
  114. graph_agents_cli/scaffold/agents/langgraph/app/app_utils/playground.py +230 -0
  115. graph_agents_cli/scaffold/agents/langgraph/app/app_utils/run_locks.py +459 -0
  116. graph_agents_cli/scaffold/agents/langgraph/app/app_utils/structured.py +755 -0
  117. graph_agents_cli/scaffold/agents/langgraph/app/app_utils/telemetry.py +681 -0
  118. graph_agents_cli/scaffold/agents/langgraph/app/app_utils/threads.py +493 -0
  119. graph_agents_cli/scaffold/agents/langgraph/app/app_utils/token_exchange.py +959 -0
  120. graph_agents_cli/scaffold/agents/langgraph/app/fast_api_app.py +770 -0
  121. graph_agents_cli/scaffold/agents/langgraph/app/policies/__init__.py +55 -0
  122. graph_agents_cli/scaffold/agents/langgraph/app/policies/custom.py +97 -0
  123. graph_agents_cli/scaffold/agents/langgraph/app/tools/__init__.py +46 -0
  124. graph_agents_cli/scaffold/agents/langgraph/app/tools/example_api.py +92 -0
  125. graph_agents_cli/scaffold/agents/langgraph/app/tools/weather.py +33 -0
  126. graph_agents_cli/scaffold/agents/langgraph/langgraph.json +14 -0
  127. graph_agents_cli/scaffold/agents/langgraph/pyproject.toml +78 -0
  128. graph_agents_cli/scaffold/agents/langgraph/tests/conftest.py +376 -0
  129. graph_agents_cli/scaffold/agents/langgraph/tests/eval/datasets/basic-dataset.json +53 -0
  130. graph_agents_cli/scaffold/agents/langgraph/tests/eval/eval_config.yaml +32 -0
  131. graph_agents_cli/scaffold/agents/langgraph/tests/integration/approval_graph.py +137 -0
  132. graph_agents_cli/scaffold/agents/langgraph/tests/integration/fake_issuer.py +216 -0
  133. graph_agents_cli/scaffold/agents/langgraph/tests/integration/fake_openai.py +357 -0
  134. graph_agents_cli/scaffold/agents/langgraph/tests/integration/test_a2a_outcomes.py +569 -0
  135. graph_agents_cli/scaffold/agents/langgraph/tests/integration/test_a2a_relay.py +479 -0
  136. graph_agents_cli/scaffold/agents/langgraph/tests/integration/test_api_surface.py +812 -0
  137. graph_agents_cli/scaffold/agents/langgraph/tests/integration/test_approvals.py +1367 -0
  138. graph_agents_cli/scaffold/agents/langgraph/tests/integration/test_approvals_server.py +794 -0
  139. graph_agents_cli/scaffold/agents/langgraph/tests/integration/test_cross_actor.py +497 -0
  140. graph_agents_cli/scaffold/agents/langgraph/tests/integration/test_cross_actor_server.py +247 -0
  141. graph_agents_cli/scaffold/agents/langgraph/tests/integration/test_history_repair.py +278 -0
  142. graph_agents_cli/scaffold/agents/langgraph/tests/integration/test_model_apis.py +242 -0
  143. graph_agents_cli/scaffold/agents/langgraph/tests/integration/test_postgres.py +637 -0
  144. graph_agents_cli/scaffold/agents/langgraph/tests/integration/test_resilience_postgres.py +770 -0
  145. graph_agents_cli/scaffold/agents/langgraph/tests/integration/test_runtime_guardrails.py +854 -0
  146. graph_agents_cli/scaffold/agents/langgraph/tests/integration/test_server_e2e.py +340 -0
  147. graph_agents_cli/scaffold/agents/langgraph/tests/integration/test_server_runtime.py +989 -0
  148. graph_agents_cli/scaffold/agents/langgraph/tests/integration/test_structured_answers.py +584 -0
  149. graph_agents_cli/scaffold/agents/langgraph/tests/integration/test_structured_server.py +222 -0
  150. graph_agents_cli/scaffold/agents/langgraph/tests/integration/test_token_exchange_issuer.py +650 -0
  151. graph_agents_cli/scaffold/agents/langgraph/tests/load_test/.results/.placeholder +0 -0
  152. graph_agents_cli/scaffold/agents/langgraph/tests/load_test/README.md +22 -0
  153. graph_agents_cli/scaffold/agents/langgraph/tests/load_test/conftest.py +21 -0
  154. graph_agents_cli/scaffold/agents/langgraph/tests/load_test/load_test.py +81 -0
  155. graph_agents_cli/scaffold/agents/langgraph/tests/unit/test_a2a_client.py +824 -0
  156. graph_agents_cli/scaffold/agents/langgraph/tests/unit/test_a2a_scoping.py +724 -0
  157. graph_agents_cli/scaffold/agents/langgraph/tests/unit/test_api_client.py +1214 -0
  158. graph_agents_cli/scaffold/agents/langgraph/tests/unit/test_api_client_hardening.py +716 -0
  159. graph_agents_cli/scaffold/agents/langgraph/tests/unit/test_api_policy_rpc.py +767 -0
  160. graph_agents_cli/scaffold/agents/langgraph/tests/unit/test_approval_ledger.py +1536 -0
  161. graph_agents_cli/scaffold/agents/langgraph/tests/unit/test_fake_model.py +115 -0
  162. graph_agents_cli/scaffold/agents/langgraph/tests/unit/test_jwt_policy.py +991 -0
  163. graph_agents_cli/scaffold/agents/langgraph/tests/unit/test_limits.py +310 -0
  164. graph_agents_cli/scaffold/agents/langgraph/tests/unit/test_logging.py +148 -0
  165. graph_agents_cli/scaffold/agents/langgraph/tests/unit/test_logging_hardening.py +271 -0
  166. graph_agents_cli/scaffold/agents/langgraph/tests/unit/test_policy.py +378 -0
  167. graph_agents_cli/scaffold/agents/langgraph/tests/unit/test_resilience.py +610 -0
  168. graph_agents_cli/scaffold/agents/langgraph/tests/unit/test_server_auth.py +702 -0
  169. graph_agents_cli/scaffold/agents/langgraph/tests/unit/test_structured.py +673 -0
  170. graph_agents_cli/scaffold/agents/langgraph/tests/unit/test_telemetry.py +404 -0
  171. graph_agents_cli/scaffold/agents/langgraph/tests/unit/test_thread_listing.py +255 -0
  172. graph_agents_cli/scaffold/agents/langgraph/tests/unit/test_threads.py +268 -0
  173. graph_agents_cli/scaffold/agents/langgraph/tests/unit/test_token_exchange.py +1320 -0
  174. graph_agents_cli/scaffold/agents/langgraph/tests/unit/test_untrusted_content.py +393 -0
  175. graph_agents_cli/scaffold/agents/langgraph/uv-fastapi.lock +2084 -0
  176. graph_agents_cli/scaffold/agents/langgraph/uv-langgraph-server.lock +2106 -0
  177. graph_agents_cli/scaffold/agents/langgraph/{{cookiecutter.agent_guidance_filename}} +129 -0
  178. graph_agents_cli/scaffold/base_templates/_shared/graph-agents-cli-manifest.yaml +36 -0
  179. graph_agents_cli/scaffold/base_templates/python/.dockerignore +32 -0
  180. graph_agents_cli/scaffold/base_templates/python/.github/CODEOWNERS +30 -0
  181. graph_agents_cli/scaffold/base_templates/python/.github/agent.env +7 -0
  182. graph_agents_cli/scaffold/base_templates/python/.github/workflows/pr_checks.yaml +214 -0
  183. graph_agents_cli/scaffold/base_templates/python/.gitignore +209 -0
  184. graph_agents_cli/scaffold/base_templates/python/tests/unit/test_dummy.py +23 -0
  185. graph_agents_cli/scaffold/base_templates/python/{{cookiecutter.agent_guidance_filename}} +35 -0
  186. graph_agents_cli/scaffold/cmd_scaffold_group.py +49 -0
  187. graph_agents_cli/scaffold/commands/__init__.py +13 -0
  188. graph_agents_cli/scaffold/commands/create.py +1424 -0
  189. graph_agents_cli/scaffold/commands/enhance.py +1652 -0
  190. graph_agents_cli/scaffold/commands/upgrade.py +570 -0
  191. graph_agents_cli/scaffold/deployment_targets/kubernetes/python/.github/agent.env +12 -0
  192. graph_agents_cli/scaffold/deployment_targets/kubernetes/python/.github/workflows/promote-to-prod.yaml +371 -0
  193. graph_agents_cli/scaffold/deployment_targets/kubernetes/python/.github/workflows/staging.yaml +450 -0
  194. graph_agents_cli/scaffold/deployment_targets/kubernetes/python/deployment/argocd/application-dev.yaml +43 -0
  195. graph_agents_cli/scaffold/deployment_targets/kubernetes/python/deployment/argocd/application-prod.yaml +41 -0
  196. graph_agents_cli/scaffold/deployment_targets/kubernetes/python/deployment/argocd/application-staging.yaml +43 -0
  197. graph_agents_cli/scaffold/deployment_targets/kubernetes/python/deployment/helm/{{cookiecutter.project_name}}/.helmignore +14 -0
  198. graph_agents_cli/scaffold/deployment_targets/kubernetes/python/deployment/helm/{{cookiecutter.project_name}}/Chart.yaml +21 -0
  199. graph_agents_cli/scaffold/deployment_targets/kubernetes/python/deployment/helm/{{cookiecutter.project_name}}/examples/networkpolicy.yaml +103 -0
  200. graph_agents_cli/scaffold/deployment_targets/kubernetes/python/deployment/helm/{{cookiecutter.project_name}}/templates/NOTES.txt +48 -0
  201. graph_agents_cli/scaffold/deployment_targets/kubernetes/python/deployment/helm/{{cookiecutter.project_name}}/templates/_helpers.tpl +189 -0
  202. graph_agents_cli/scaffold/deployment_targets/kubernetes/python/deployment/helm/{{cookiecutter.project_name}}/templates/certificate.yaml +15 -0
  203. graph_agents_cli/scaffold/deployment_targets/kubernetes/python/deployment/helm/{{cookiecutter.project_name}}/templates/configmap.yaml +10 -0
  204. graph_agents_cli/scaffold/deployment_targets/kubernetes/python/deployment/helm/{{cookiecutter.project_name}}/templates/deployment.yaml +199 -0
  205. graph_agents_cli/scaffold/deployment_targets/kubernetes/python/deployment/helm/{{cookiecutter.project_name}}/templates/hpa.yaml +22 -0
  206. graph_agents_cli/scaffold/deployment_targets/kubernetes/python/deployment/helm/{{cookiecutter.project_name}}/templates/httproute.yaml +30 -0
  207. graph_agents_cli/scaffold/deployment_targets/kubernetes/python/deployment/helm/{{cookiecutter.project_name}}/templates/ingress.yaml +39 -0
  208. graph_agents_cli/scaffold/deployment_targets/kubernetes/python/deployment/helm/{{cookiecutter.project_name}}/templates/networkpolicy.yaml +48 -0
  209. graph_agents_cli/scaffold/deployment_targets/kubernetes/python/deployment/helm/{{cookiecutter.project_name}}/templates/pdb.yaml +13 -0
  210. graph_agents_cli/scaffold/deployment_targets/kubernetes/python/deployment/helm/{{cookiecutter.project_name}}/templates/postgresql-secret.yaml +37 -0
  211. graph_agents_cli/scaffold/deployment_targets/kubernetes/python/deployment/helm/{{cookiecutter.project_name}}/templates/service.yaml +15 -0
  212. graph_agents_cli/scaffold/deployment_targets/kubernetes/python/deployment/helm/{{cookiecutter.project_name}}/templates/serviceaccount.yaml +13 -0
  213. graph_agents_cli/scaffold/deployment_targets/kubernetes/python/deployment/helm/{{cookiecutter.project_name}}/templates/servicemonitor.yaml +42 -0
  214. graph_agents_cli/scaffold/deployment_targets/kubernetes/python/deployment/helm/{{cookiecutter.project_name}}/values-dev.yaml +22 -0
  215. graph_agents_cli/scaffold/deployment_targets/kubernetes/python/deployment/helm/{{cookiecutter.project_name}}/values-prod.yaml +45 -0
  216. graph_agents_cli/scaffold/deployment_targets/kubernetes/python/deployment/helm/{{cookiecutter.project_name}}/values-staging.yaml +29 -0
  217. graph_agents_cli/scaffold/deployment_targets/kubernetes/python/deployment/helm/{{cookiecutter.project_name}}/values.yaml +396 -0
  218. graph_agents_cli/scaffold/deployment_targets/kubernetes/python/tests/integration/test_chart.py +269 -0
  219. graph_agents_cli/scaffold/deployment_targets/none/README.md +5 -0
  220. graph_agents_cli/scaffold/deployment_targets/none/python/README.md +6 -0
  221. graph_agents_cli/scaffold/utils/__init__.py +13 -0
  222. graph_agents_cli/scaffold/utils/backup.py +212 -0
  223. graph_agents_cli/scaffold/utils/build_record.py +257 -0
  224. graph_agents_cli/scaffold/utils/cli_options.py +184 -0
  225. graph_agents_cli/scaffold/utils/fs.py +83 -0
  226. graph_agents_cli/scaffold/utils/generate_locks.py +214 -0
  227. graph_agents_cli/scaffold/utils/generation_metadata.py +88 -0
  228. graph_agents_cli/scaffold/utils/keyedit.py +768 -0
  229. graph_agents_cli/scaffold/utils/keymerge.py +537 -0
  230. graph_agents_cli/scaffold/utils/language.py +138 -0
  231. graph_agents_cli/scaffold/utils/lock_utils.py +94 -0
  232. graph_agents_cli/scaffold/utils/logging.py +77 -0
  233. graph_agents_cli/scaffold/utils/manifest.py +292 -0
  234. graph_agents_cli/scaffold/utils/merge.py +970 -0
  235. graph_agents_cli/scaffold/utils/merge3.py +216 -0
  236. graph_agents_cli/scaffold/utils/openapi_seed.py +199 -0
  237. graph_agents_cli/scaffold/utils/remote_template.py +376 -0
  238. graph_agents_cli/scaffold/utils/template.py +1352 -0
  239. graph_agents_cli/scaffold/utils/upgrade.py +894 -0
  240. graph_agents_cli/scaffold/utils/version.py +438 -0
  241. graph_agents_cli/secrets/__init__.py +15 -0
  242. graph_agents_cli/secrets/_apply.py +954 -0
  243. graph_agents_cli/secrets/_required.py +188 -0
  244. graph_agents_cli/secrets/cmd_secrets.py +211 -0
  245. graph_agents_cli/setup/__init__.py +13 -0
  246. graph_agents_cli/setup/_antigravity.py +221 -0
  247. graph_agents_cli/setup/cmd_auth.py +1030 -0
  248. graph_agents_cli/setup/cmd_dev_token.py +513 -0
  249. graph_agents_cli/setup/cmd_setup.py +428 -0
  250. graph_agents_cli/setup/cmd_update.py +140 -0
  251. graph_agents_cli/skills/__init__.py +13 -0
  252. graph_agents_cli/skills/_bundle.py +65 -0
  253. graph_agents_cli/skills/data/README.md +19 -0
  254. graph_agents_cli/skills/data/graph-agents-cli-deploy/SKILL.md +357 -0
  255. graph_agents_cli/skills/data/graph-agents-cli-deploy/references/github-settings.md +113 -0
  256. graph_agents_cli/skills/data/graph-agents-cli-deploy/references/gitops.md +137 -0
  257. graph_agents_cli/skills/data/graph-agents-cli-deploy/references/kubernetes.md +315 -0
  258. graph_agents_cli/skills/data/graph-agents-cli-deploy/references/secrets.md +160 -0
  259. graph_agents_cli/skills/data/graph-agents-cli-eval/SKILL.md +303 -0
  260. graph_agents_cli/skills/data/graph-agents-cli-eval/references/dataset_schema.md +282 -0
  261. graph_agents_cli/skills/data/graph-agents-cli-eval/references/metrics-guide.md +143 -0
  262. graph_agents_cli/skills/data/graph-agents-cli-langgraph-code/SKILL.md +659 -0
  263. graph_agents_cli/skills/data/graph-agents-cli-langgraph-code/references/langchain-models.md +124 -0
  264. graph_agents_cli/skills/data/graph-agents-cli-langgraph-code/references/langgraph.md +235 -0
  265. graph_agents_cli/skills/data/graph-agents-cli-langgraph-code/references/template-contract.md +477 -0
  266. graph_agents_cli/skills/data/graph-agents-cli-observability/SKILL.md +231 -0
  267. graph_agents_cli/skills/data/graph-agents-cli-observability/references/langsmith.md +46 -0
  268. graph_agents_cli/skills/data/graph-agents-cli-observability/references/otel.md +59 -0
  269. graph_agents_cli/skills/data/graph-agents-cli-scaffold/SKILL.md +414 -0
  270. graph_agents_cli/skills/data/graph-agents-cli-scaffold/references/flags.md +134 -0
  271. graph_agents_cli/skills/data/graph-agents-cli-workflow/SKILL.md +478 -0
  272. graph_agents_cli/skills/data/graph-agents-cli-workflow/references/brainstorming.md +118 -0
  273. graph_agents_cli/skills/data/graph-agents-cli-workflow/references/commands.md +419 -0
  274. graph_agents_cli/skills/data/graph-agents-cli-workflow/references/extension.md +156 -0
  275. graph_agents_cli/skills/data/graph-agents-cli-workflow/references/internals.md +67 -0
  276. graph_agents_cli/skills/data/graph-agents-cli-workflow/references/spec-template.md +56 -0
  277. graph_agents_cli/skills/data/graph-agents-cli-workflow/references/terminology.md +119 -0
  278. graph_agents_cli/system/__init__.py +15 -0
  279. graph_agents_cli/system/_apply.py +519 -0
  280. graph_agents_cli/system/_checks.py +1023 -0
  281. graph_agents_cli/system/_deploy.py +215 -0
  282. graph_agents_cli/system/_model.py +363 -0
  283. graph_agents_cli/system/_system.py +664 -0
  284. graph_agents_cli/system/_views.py +208 -0
  285. graph_agents_cli/system/cmd_system.py +423 -0
  286. graph_agents_cli-0.3.1.dist-info/METADATA +162 -0
  287. graph_agents_cli-0.3.1.dist-info/RECORD +291 -0
  288. graph_agents_cli-0.3.1.dist-info/WHEEL +4 -0
  289. graph_agents_cli-0.3.1.dist-info/entry_points.txt +2 -0
  290. graph_agents_cli-0.3.1.dist-info/licenses/LICENSE +201 -0
  291. graph_agents_cli-0.3.1.dist-info/licenses/NOTICE +19 -0
@@ -0,0 +1,253 @@
1
+ # Copyright 2026 Google LLC
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # https://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ """Parse and validate graph-agents-cli-extension.yaml (schema: graph-agents-cli-extension/v1alpha1)."""
16
+
17
+ from __future__ import annotations
18
+
19
+ import difflib
20
+ import logging
21
+ import re
22
+ from collections.abc import Mapping
23
+ from dataclasses import dataclass
24
+ from pathlib import Path, PurePosixPath
25
+ from types import UnionType
26
+ from typing import Annotated, Any, Literal, Union, get_args, get_origin
27
+
28
+ from pydantic import (
29
+ BaseModel,
30
+ BeforeValidator,
31
+ ConfigDict,
32
+ Field,
33
+ ValidationError,
34
+ model_validator,
35
+ )
36
+
37
+ from graph_agents_cli.extension._compat import validate_range
38
+
39
+ # The manifest format version, tracked separately from the graph-agents-cli version.
40
+ # A second format would widen `ExtensionManifest.schema_version` to a Union.
41
+ # Alpha while the extension system is experimental: the format may still change
42
+ # in a breaking way, and `v1` is reserved for the point it stops doing so.
43
+ V1ALPHA1 = "graph-agents-cli-extension/v1alpha1"
44
+ EXTENSION_FILE = "graph-agents-cli-extension.yaml"
45
+
46
+
47
+ class ExtensionSpecError(Exception):
48
+ """Raised on structural errors in an extension spec."""
49
+
50
+
51
+ # --- Wire models -------------------------------------------------------------
52
+ # These mirror graph-agents-cli-extension.yaml and generate the published JSON Schema.
53
+ # `extra="forbid"` so a typo like `reqires:` is an error rather than a section
54
+ # that silently does nothing.
55
+
56
+
57
+ def _reject_bool(value: Any) -> Any:
58
+ """YAML 1.1 reads `yes`/`on` as True, which is not a number in JSON Schema."""
59
+ if isinstance(value, bool):
60
+ raise ValueError('expected a version range, e.g. ">=1.3,<2"')
61
+ return value
62
+
63
+
64
+ class _Strict(BaseModel):
65
+ model_config = ConfigDict(extra="forbid")
66
+
67
+ @model_validator(mode="before")
68
+ @classmethod
69
+ def _empty_key_means_absent(cls, data: Any) -> Any:
70
+ """`commands:` with nothing under it is how authors comment a section out.
71
+
72
+ Only for keys this model knows: dropping unknown ones would hide the
73
+ typo that `extra="forbid"` exists to catch.
74
+ """
75
+ if not isinstance(data, dict):
76
+ return data
77
+ known = {f.alias or n for n, f in cls.model_fields.items()}
78
+ return {k: v for k, v in data.items() if v is not None or k not in known}
79
+
80
+
81
+ class CommandEntry(_Strict):
82
+ run: list[str] = Field(min_length=1)
83
+ description: str = ""
84
+
85
+
86
+ class Commands(_Strict):
87
+ override: dict[str, CommandEntry] = Field(default_factory=dict)
88
+ add: dict[str, CommandEntry] = Field(default_factory=dict)
89
+
90
+
91
+ class Requires(_Strict):
92
+ # `agents_cli: 1.2` is natural YAML and arrives as a number; the range is
93
+ # validated semantically below, and a bad one is dropped rather than fatal.
94
+ agents_cli: Annotated[str | float | None, BeforeValidator(_reject_bool)] = None
95
+ on_incompatible: Literal["warn", "error"] = "warn"
96
+
97
+
98
+ class ExtensionManifest(_Strict):
99
+ """`graph-agents-cli-extension.yaml` as written on disk."""
100
+
101
+ # This is the v1alpha1 document, so it accepts only v1alpha1: an editor
102
+ # validating against it has to agree with the loader, which rejects
103
+ # anything else.
104
+ schema_version: Literal["graph-agents-cli-extension/v1alpha1"] = Field(
105
+ default=V1ALPHA1, alias="schema"
106
+ )
107
+ name: str | None = Field(default=None, min_length=1)
108
+ description: str = ""
109
+ requires: Requires | None = None
110
+ commands: Commands = Field(default_factory=Commands)
111
+
112
+
113
+ def _describe(error: Mapping[str, Any]) -> str:
114
+ """One validation error as a line an extension author can act on."""
115
+ loc = ".".join(str(part) for part in error["loc"] if part != "[key]")
116
+ if error["type"] == "extra_forbidden":
117
+ key = str(error["loc"][-1])
118
+ near = difflib.get_close_matches(key, _known_keys(error["loc"][:-1]), n=1)
119
+ hint = f" (did you mean {near[0]!r}?)" if near else ""
120
+ return f"unknown key {loc!r}{hint}"
121
+ # Model class names mean nothing to someone editing YAML.
122
+ msg = re.sub(r" or instance of \w+", "", error["msg"])
123
+ return f"{loc or '(root)'}: {msg}"
124
+
125
+
126
+ def _known_keys(loc: tuple[Any, ...]) -> list[str]:
127
+ """Field names of the manifest model at `loc`, for did-you-mean hints.
128
+
129
+ A container's key or index consumes a `loc` part without changing the model,
130
+ so `commands.add.<name>.<typo>` still resolves to CommandEntry's fields.
131
+ """
132
+ current: Any = ExtensionManifest
133
+ for part in loc:
134
+ fields = getattr(current, "model_fields", {})
135
+ field = fields.get(str(part)) or next(
136
+ (f for f in fields.values() if f.alias == str(part)), None
137
+ )
138
+ if field is None:
139
+ # This part is a dict key or a list index, not a field name.
140
+ nested = _model_in(current)
141
+ if nested is None:
142
+ return []
143
+ current = nested
144
+ continue
145
+ current = _unwrap_optional(field.annotation)
146
+ return [f.alias or n for n, f in getattr(current, "model_fields", {}).items()]
147
+
148
+
149
+ def _unwrap_optional(annotation: Any) -> Any:
150
+ """`X | None` -> X, leaving `dict[str, X]` and `list[X]` for the key/index step."""
151
+ if get_origin(annotation) not in (Union, UnionType):
152
+ return annotation
153
+ args = [a for a in get_args(annotation) if a is not type(None)]
154
+ return args[0] if len(args) == 1 else annotation
155
+
156
+
157
+ def _model_in(annotation: Any) -> Any:
158
+ """The model a container holds, e.g. dict[str, CommandEntry] -> CommandEntry."""
159
+ for arg in getattr(annotation, "__args__", ()):
160
+ if isinstance(arg, type) and issubclass(arg, BaseModel):
161
+ return arg
162
+ return None
163
+
164
+
165
+ @dataclass(frozen=True)
166
+ class ExtensionCommand:
167
+ name: str # dotted command path, e.g. "deploy" or "eval.generate"
168
+ run: tuple[str, ...]
169
+ description: str
170
+ kind: str # "override" | "add"
171
+
172
+
173
+ @dataclass(frozen=True)
174
+ class ExtensionSpec:
175
+ name: str
176
+ description: str
177
+ root: Path
178
+ commands: tuple[ExtensionCommand, ...]
179
+ # Install scope ("user" | "project"), stamped by the loader at discovery —
180
+ # it comes from where the extension was found, not from the manifest.
181
+ scope: str = ""
182
+ # Optional author-declared graph-agents-cli compatibility range (e.g. ">=1.1,<2").
183
+ requires_agents_cli: str | None = None
184
+ # The manifest's "warn" | "error", resolved at parse time. Only two states
185
+ # exist and the manifest model already validates them, so the rest of the
186
+ # CLI reads a bool rather than re-comparing strings.
187
+ error_on_incompatible: bool = False
188
+
189
+
190
+ def _to_command(name: str, entry: CommandEntry, kind: str) -> ExtensionCommand:
191
+ _warn_if_script_entrypoint(name, kind, entry.run[0])
192
+ return ExtensionCommand(
193
+ name=name, run=tuple(entry.run), description=entry.description, kind=kind
194
+ )
195
+
196
+
197
+ # Suffixes that are run by an interpreter, not executed directly on Windows.
198
+ _SCRIPT_SUFFIXES = frozenset({".py", ".sh", ".js", ".ts", ".rb", ".pl"})
199
+
200
+
201
+ def _warn_if_script_entrypoint(name: str, kind: str, first_token: str) -> None:
202
+ """Executed without a shell: a bare script needs +x and a shebang on POSIX, and never runs on Windows."""
203
+ if PurePosixPath(first_token).suffix in _SCRIPT_SUFFIXES:
204
+ logging.warning(
205
+ "commands.%s.%s: `run` starts with the script %r; prefix an "
206
+ 'interpreter (e.g. ["uv", "run", "python", %r]) so it also '
207
+ "runs on Windows.",
208
+ kind,
209
+ name,
210
+ first_token,
211
+ first_token,
212
+ )
213
+
214
+
215
+ def parse_extension_spec(data: dict[str, Any], *, root: Path, default_name: str) -> ExtensionSpec:
216
+ """Raises ExtensionSpecError on any structural problem; the caller decides whether that is fatal."""
217
+ try:
218
+ manifest = ExtensionManifest.model_validate(data)
219
+ except ValidationError as e:
220
+ problems = "\n - ".join(_describe(err) for err in e.errors())
221
+ if len(e.errors()) > 1:
222
+ problems = f"\n - {problems}"
223
+ raise ExtensionSpecError(problems) from e
224
+
225
+ commands = [
226
+ _to_command(name, entry, kind)
227
+ for kind in ("override", "add")
228
+ for name, entry in getattr(manifest.commands, kind).items()
229
+ ]
230
+ requires_agents_cli, error_on_incompatible = _parse_requires(manifest.requires)
231
+ return ExtensionSpec(
232
+ name=manifest.name or default_name,
233
+ description=manifest.description,
234
+ root=root,
235
+ commands=tuple(commands),
236
+ requires_agents_cli=requires_agents_cli,
237
+ error_on_incompatible=error_on_incompatible,
238
+ )
239
+
240
+
241
+ def _parse_requires(requires: Requires | None) -> tuple[str | None, bool]:
242
+ """An unparseable range is warned about and dropped: a bad compat hint must not make an otherwise-valid extension unloadable."""
243
+ if requires is None:
244
+ return None, False
245
+ raw_range = None if requires.agents_cli is None else str(requires.agents_cli)
246
+ if raw_range is not None and not validate_range(raw_range):
247
+ logging.warning(
248
+ "requires.agents_cli %r is not a valid version range; ignoring the "
249
+ "range (the extension still loads).",
250
+ raw_range,
251
+ )
252
+ raw_range = None
253
+ return raw_range, requires.on_incompatible == "error"
@@ -0,0 +1,102 @@
1
+ # Copyright 2026 Google LLC
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # https://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ """Re-materialize vendored extension working copies from pinned SHAs."""
16
+
17
+ from __future__ import annotations
18
+
19
+ import logging
20
+ from pathlib import Path
21
+
22
+ import click
23
+
24
+ from graph_agents_cli.__init__ import __version__
25
+ from graph_agents_cli.extension._loader import incompatible_spec
26
+ from graph_agents_cli.extension._manifest import (
27
+ EXTENSIONS_FILE,
28
+ read_extension_entries,
29
+ )
30
+ from graph_agents_cli.extension._paths import user_config_root, vendored_extensions_dir
31
+ from graph_agents_cli.extension._refs import RefParseError, anchor_local, parse_ref
32
+ from graph_agents_cli.extension._resolver import (
33
+ ResolverError,
34
+ expected_stamp,
35
+ materialize,
36
+ read_stamp,
37
+ )
38
+ from graph_agents_cli.extension._spec import ExtensionSpecError
39
+
40
+
41
+ def _scope_roots(project_root: Path | None) -> list[Path]:
42
+ """Roots to sync, project first. The scope label is not needed: both scopes
43
+ store the list under the same filename."""
44
+ roots = [] if project_root is None else [project_root]
45
+ roots.append(user_config_root())
46
+ return roots
47
+
48
+
49
+ def sync_extensions(project_root: Path | None) -> list[str]:
50
+ """Ensure every recorded extension is vendored on disk.
51
+
52
+ Reports what it restored and returns those names.
53
+ """
54
+ restored: list[str] = []
55
+ for scope_root in _scope_roots(project_root):
56
+ manifest = scope_root / EXTENSIONS_FILE
57
+ for entry in read_extension_entries(manifest):
58
+ extension_dir = vendored_extensions_dir(scope_root) / entry.name
59
+ try:
60
+ ref = anchor_local(parse_ref(entry.source, ref_override=entry.sha), scope_root)
61
+ # Re-materialize when the copy is missing OR no longer matches
62
+ # its source: a bumped pin, a partial copy, or — for a local
63
+ # path, whose source is a working tree rather than a commit —
64
+ # any edit made to it since the copy was taken.
65
+ if extension_dir.exists() and read_stamp(extension_dir) == expected_stamp(
66
+ ref, entry.sha
67
+ ):
68
+ continue
69
+ # name=entry.name pins the vendored dir to the stable id (see
70
+ # materialize) so a restore survives an upstream rename.
71
+ materialize(
72
+ ref,
73
+ entry.sha,
74
+ vendored_extensions_dir(scope_root),
75
+ name=entry.name,
76
+ )
77
+ restored.append(entry.name)
78
+ # Never block a restore (that would brick `install`); just warn
79
+ # if the restored pin is out of range for the running CLI.
80
+ try:
81
+ stale = incompatible_spec(extension_dir)
82
+ except ExtensionSpecError as e:
83
+ # Restore must never fail: it is how a bad pin gets fixed.
84
+ logging.warning("%s: %s.", extension_dir, e)
85
+ stale = None
86
+ if stale is not None:
87
+ logging.warning(
88
+ "Restored extension %r requires graph-agents-cli %s but running %s.",
89
+ entry.name,
90
+ stale.requires_agents_cli,
91
+ __version__,
92
+ )
93
+ except (ResolverError, RefParseError) as e:
94
+ logging.warning(
95
+ "Could not restore extension %r from %s: %s",
96
+ entry.name,
97
+ entry.source,
98
+ e,
99
+ )
100
+ if restored:
101
+ click.echo(f"Restored extensions: {', '.join(restored)}")
102
+ return restored
@@ -0,0 +1,58 @@
1
+ # Copyright 2026 Google LLC
2
+ # Modifications Copyright 2026 graph-agents-cli contributors
3
+ #
4
+ # Licensed under the Apache License, Version 2.0 (the "License");
5
+ # you may not use this file except in compliance with the License.
6
+ # You may obtain a copy of the License at
7
+ #
8
+ # https://www.apache.org/licenses/LICENSE-2.0
9
+ #
10
+ # Unless required by applicable law or agreed to in writing, software
11
+ # distributed under the License is distributed on an "AS IS" BASIS,
12
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ # See the License for the specific language governing permissions and
14
+ # limitations under the License.
15
+
16
+ """Consent gate for running third-party extension code."""
17
+
18
+ from __future__ import annotations
19
+
20
+ import sys
21
+
22
+ import click
23
+
24
+ from graph_agents_cli.extension._refs import ExtensionRef
25
+
26
+ NO_TERMINAL_HINT = (
27
+ " Not asking: stdin is not a terminal. Pass -y to trust this extension non-interactively."
28
+ )
29
+
30
+
31
+ def stdin_is_interactive() -> bool:
32
+ """True when a person can answer a prompt (stdin is a terminal)."""
33
+ try:
34
+ return sys.stdin is not None and sys.stdin.isatty()
35
+ except (AttributeError, ValueError): # replaced or closed stdin
36
+ return False
37
+
38
+
39
+ def confirm_trust(ref: ExtensionRef, *, auto_approve: bool) -> bool:
40
+ """Return True if it's OK to install/run this extension's code.
41
+
42
+ First-party extensions are trusted. Everything else prompts unless
43
+ `auto_approve`. Without a terminal to answer on (CI, a pipe, `< /dev/null`)
44
+ it does not prompt, which would abort or block: it says to pass -y and
45
+ returns False.
46
+ """
47
+ if ref.kind == "first_party" or auto_approve:
48
+ return True
49
+ click.echo()
50
+ click.secho(
51
+ f" Extension source {ref.raw!r} is third-party. It can run arbitrary code "
52
+ "on your machine when its commands are invoked.",
53
+ fg="yellow",
54
+ )
55
+ if not stdin_is_interactive():
56
+ click.secho(NO_TERMINAL_HINT, fg="yellow")
57
+ return False
58
+ return click.confirm(" Install and trust this extension?", default=False)
@@ -0,0 +1,259 @@
1
+ # Copyright 2026 Google LLC
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # https://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ """graph-agents-cli extension add command."""
16
+
17
+ from __future__ import annotations
18
+
19
+ import logging
20
+ import os
21
+ import shutil
22
+ from pathlib import Path
23
+
24
+ import click
25
+
26
+ from graph_agents_cli.__init__ import __version__
27
+ from graph_agents_cli._project import NotInProjectError, find_project_root
28
+ from graph_agents_cli._trust import require_confirmation
29
+ from graph_agents_cli.extension._compat import is_compatible
30
+ from graph_agents_cli.extension._loader import (
31
+ RECOVERY_COMMANDS,
32
+ load_extension_set,
33
+ load_extension_spec,
34
+ )
35
+ from graph_agents_cli.extension._manifest import (
36
+ EXTENSIONS_FILE,
37
+ ExtensionEntry,
38
+ read_extension_entries,
39
+ upsert_extension_entry,
40
+ )
41
+ from graph_agents_cli.extension._paths import (
42
+ scope_root,
43
+ user_config_root,
44
+ vendored_extensions_dir,
45
+ )
46
+ from graph_agents_cli.extension._refs import (
47
+ ExtensionRef,
48
+ RefParseError,
49
+ anchor_local,
50
+ parse_ref,
51
+ )
52
+ from graph_agents_cli.extension._resolver import ResolverError, materialize, resolve_sha
53
+ from graph_agents_cli.extension._spec import (
54
+ EXTENSION_FILE,
55
+ ExtensionSpec,
56
+ ExtensionSpecError,
57
+ )
58
+ from graph_agents_cli.extension._trust import confirm_trust
59
+
60
+
61
+ def _recorded_local_source(ref: ExtensionRef, root: Path, scope: str) -> str:
62
+ """The ``local@`` source to record, resolvable from the scope root later.
63
+
64
+ ``install`` and ``update`` resolve a relative local source against the scope
65
+ root, so the path typed here (relative to wherever the command ran) is
66
+ rewritten: relative to the project root for project scope, which keeps the
67
+ committed entry portable, absolute for user scope.
68
+ """
69
+ absolute = (ref.local_path or Path.cwd()).resolve()
70
+ path = absolute.as_posix()
71
+ if scope == "project":
72
+ try:
73
+ path = Path(os.path.relpath(absolute, root.resolve())).as_posix()
74
+ except ValueError: # another drive on Windows: only an absolute path works
75
+ pass
76
+ selector = f"#{ref.selector}" if ref.selector else ""
77
+ return f"local@{path}{selector}"
78
+
79
+
80
+ def _resolve_hint(reference: str, ref: ExtensionRef) -> str:
81
+ """A pointer to ``local@`` when a repo shorthand also names a local directory."""
82
+ if ref.kind in ("github", "first_party") and Path(reference.split("#", 1)[0]).is_dir():
83
+ return (
84
+ f"\n {reference!r} is also a local directory: to install it from disk, use "
85
+ f"local@{reference} (or ./{reference})."
86
+ )
87
+ return ""
88
+
89
+
90
+ def _parse_or_fail(extension_dir: Path) -> ExtensionSpec | None:
91
+ """Parse the vendored manifest, turning a bad one into a failed install.
92
+
93
+ Parsed once here and handed to both gates, so a manifest is not read twice
94
+ and the two cannot disagree about what it says.
95
+ """
96
+ try:
97
+ return load_extension_spec(extension_dir)
98
+ except ExtensionSpecError as e:
99
+ raise click.ClickException(
100
+ f"{EXTENSION_FILE} is not valid: {e}\nNothing was installed."
101
+ ) from e
102
+
103
+
104
+ def _check_compat(spec: ExtensionSpec | None) -> None:
105
+ """Gate a freshly vendored extension against the running CLI version.
106
+
107
+ `error` mode raises so the install fails; `warn` mode logs and proceeds.
108
+ This is the deliberate install gate, the only place an incompatibility
109
+ hard-fails; at runtime it only ever warns.
110
+ """
111
+ if (
112
+ spec is None
113
+ or not spec.requires_agents_cli
114
+ or is_compatible(__version__, spec.requires_agents_cli)
115
+ ):
116
+ return
117
+ msg = (
118
+ f"Extension requires graph-agents-cli {spec.requires_agents_cli} but you're "
119
+ f"running {__version__}."
120
+ )
121
+ if spec.error_on_incompatible:
122
+ raise click.ClickException(
123
+ f"{msg} Not installing (on_incompatible: error). "
124
+ "Pin a compatible ref with --ref, or upgrade the CLI."
125
+ )
126
+ logging.warning("%s Installing anyway (on_incompatible: warn).", msg)
127
+
128
+
129
+ def _check_reserved_commands(spec: ExtensionSpec | None) -> None:
130
+ """Refuse an extension that claims a command used to repair extensions.
131
+
132
+ Caught at install because that is where a human can read why. The loader
133
+ ignores such a claim too, for manifests edited by hand.
134
+ """
135
+ if spec is None:
136
+ return
137
+ reserved = sorted({c.name for c in spec.commands} & RECOVERY_COMMANDS)
138
+ if reserved:
139
+ raise click.ClickException(
140
+ f"Extension claims {', '.join(repr(r) for r in reserved)}, which "
141
+ "cannot be overridden: these are how you repair or remove an "
142
+ "extension, so an extension owning them could own its own uninstall."
143
+ )
144
+
145
+
146
+ def _check_command_conflicts(
147
+ project_root: Path | None, spec: ExtensionSpec | None, scope: str
148
+ ) -> None:
149
+ """Raise if the candidate claims a command *another* extension already owns.
150
+
151
+ Re-adding the same name at the same scope is a replace, which is how a pin
152
+ moves, so the candidate must not be read as conflicting with the copy it is
153
+ about to overwrite.
154
+ """
155
+ if spec is None:
156
+ return
157
+ installed = load_extension_set(project_root, user_config_root())
158
+ clashes = sorted(
159
+ (contribution.name, owner.extension_name)
160
+ for contribution in spec.commands
161
+ if (owner := installed.commands.get(contribution.name)) is not None
162
+ and owner.scope == scope
163
+ and owner.extension_name != spec.name
164
+ )
165
+ if clashes:
166
+ raise click.ClickException(
167
+ "Command conflict(s) with an already-installed extension: "
168
+ + ", ".join(f"{cmd} (owned by {owner!r})" for cmd, owner in clashes)
169
+ + ". Remove or rename the conflicting extension."
170
+ )
171
+
172
+
173
+ @click.command("add")
174
+ @click.argument("reference")
175
+ @click.option(
176
+ "--global",
177
+ "global_",
178
+ is_flag=True,
179
+ default=False,
180
+ help=(
181
+ "Install for all projects (user scope, global/org-wide). Default is "
182
+ "project scope (committed to this repo only), so the extension cannot "
183
+ "affect your other projects."
184
+ ),
185
+ )
186
+ @click.option("--ref", "ref_override", default=None, help="Pin a branch, tag, or SHA.")
187
+ @require_confirmation("Install this extension?")
188
+ def cmd_add(
189
+ reference: str,
190
+ global_: bool,
191
+ ref_override: str | None,
192
+ auto_approve: bool,
193
+ ) -> None:
194
+ """Add an extension from a git reference or local path."""
195
+ scope = "user" if global_ else "project"
196
+ try:
197
+ ref = parse_ref(reference, ref_override=ref_override)
198
+ except RefParseError as e:
199
+ raise click.ClickException(str(e)) from e
200
+
201
+ root = scope_root(scope)
202
+ if root is None:
203
+ # Checked before the trust prompt: nothing to confirm when it cannot install.
204
+ raise NotInProjectError(
205
+ "No graph-agents-cli-manifest.yaml found. Run inside a project (extensions "
206
+ "install at project scope by default), or pass --global to install "
207
+ "for all projects."
208
+ )
209
+
210
+ if not confirm_trust(ref, auto_approve=auto_approve):
211
+ raise click.ClickException("Aborted: extension not trusted.")
212
+
213
+ project_root = root if scope == "project" else find_project_root(Path.cwd())
214
+
215
+ source = ref.raw
216
+ if ref.kind == "local":
217
+ # Resolved against the current directory now; recorded against the root.
218
+ ref = anchor_local(ref, Path.cwd())
219
+ source = _recorded_local_source(ref, root, scope)
220
+
221
+ try:
222
+ sha = resolve_sha(ref)
223
+ name, extension_dir = materialize(ref, sha, vendored_extensions_dir(root))
224
+ except ResolverError as e:
225
+ error = click.ClickException(f"{e}{_resolve_hint(reference, ref)}")
226
+ # A local path that is not an extension is a configuration error (3);
227
+ # git or the network failing to resolve a repo is a tool failure (2).
228
+ error.exit_code = 3 if ref.kind == "local" else 2
229
+ raise error from e
230
+
231
+ # Every gate runs before anything is recorded, so a refused install leaves
232
+ # no entry to roll back. The vendored copy has to exist first (the gates read
233
+ # its manifest), so that is the only thing to clean up.
234
+ try:
235
+ spec = _parse_or_fail(extension_dir)
236
+ _check_compat(spec)
237
+ _check_reserved_commands(spec)
238
+ _check_command_conflicts(project_root, spec, scope)
239
+ except click.ClickException as e:
240
+ if extension_dir.exists():
241
+ shutil.rmtree(extension_dir)
242
+ # Removing the copy of an extension that is still recorded leaves the
243
+ # entry pointing at nothing, and its commands silently fall back to the
244
+ # built-ins until the copy is back.
245
+ if any(entry.name == name for entry in read_extension_entries(root / EXTENSIONS_FILE)):
246
+ raise click.ClickException(
247
+ f"{e.format_message()}\n"
248
+ f" {name!r} is still recorded; restore its copy with: graph-agents-cli install"
249
+ ) from e
250
+ raise
251
+
252
+ entry = ExtensionEntry(name=name, source=source, ref=ref.ref or "HEAD", sha=sha, scope=scope)
253
+ upsert_extension_entry(root / EXTENSIONS_FILE, entry)
254
+
255
+ click.secho(f"Added extension {name!r} ({scope} scope) from {source}.", fg="green")
256
+ click.secho(
257
+ "Extensions are experimental; the manifest format may still change.",
258
+ dim=True,
259
+ )