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,537 @@
1
+ # Copyright 2026 graph-agents-cli contributors
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
+ """Key-level three-way merge for YAML and env config files.
16
+
17
+ The line merge in :mod:`merge3` gives up as soon as the developer edited a line
18
+ next to one the template changes (``replicaCount: 3`` two lines above
19
+ ``runtime: fastapi`` is enough). A runtime or model-provider change must still
20
+ reach the keys the chart and the app read, so this module applies the
21
+ template's change key by key instead:
22
+
23
+ * the template's change is the list of keys whose value differs between the
24
+ old render and the new one (set, added or removed);
25
+ * each such key the developer left as the old render had it is edited in place
26
+ in the developer's text: a scalar value is replaced where it stands, a new
27
+ entry is inserted next to its neighbour in the template (with the comment
28
+ lines directly above it), a removed entry is deleted;
29
+ * a key the developer changed too is left alone and returned as a
30
+ :class:`KeyConflict`, so the caller can say exactly what is left to do.
31
+
32
+ Comments, formatting and every other edit of the developer's file are kept.
33
+ The edited text is parsed again (a repeated key is refused) and must equal the
34
+ developer's document with exactly the applied changes; anything else returns
35
+ ``None`` and the caller treats the whole file as not mergeable.
36
+ """
37
+
38
+ from __future__ import annotations
39
+
40
+ import copy
41
+ import re
42
+ from collections.abc import Callable
43
+ from dataclasses import dataclass, field
44
+ from typing import Any
45
+
46
+ import yaml
47
+
48
+ from .merge3 import _parse_env, _StrictLoader
49
+
50
+ MISSING: Any = type("Missing", (), {"__repr__": lambda self: "<absent>"})()
51
+
52
+ _ENV_LINE = re.compile(r"^\s*(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*=")
53
+ _COMMENT_LINE = re.compile(r"^\s*#")
54
+ # Line breaks PyYAML counts that a split on "\n" would not: refused, so that
55
+ # the line numbers of the parsed nodes and of the text always agree.
56
+ _OTHER_BREAKS = re.compile("\r(?!\n)|[\x85\u2028\u2029]")
57
+
58
+
59
+ @dataclass(frozen=True)
60
+ class KeyConflict:
61
+ """A key the template changed that the developer changed differently."""
62
+
63
+ path: tuple[Any, ...]
64
+ yours: Any # MISSING: the developer removed it
65
+ template: Any # MISSING: the new settings no longer set it
66
+
67
+ @property
68
+ def dotted(self) -> str:
69
+ return ".".join(str(part) for part in self.path)
70
+
71
+ def describe(self) -> str:
72
+ if self.yours is MISSING:
73
+ return f"{self.dotted}: you removed it; the new settings set {_show(self.template)}"
74
+ if self.template is MISSING:
75
+ return (
76
+ f"{self.dotted}: you set {_show(self.yours)}; the new settings no longer "
77
+ "set it (remove it if nothing needs it)"
78
+ )
79
+ return (
80
+ f"{self.dotted}: you set {_show(self.yours)}; the new settings set "
81
+ f"{_show(self.template)}"
82
+ )
83
+
84
+
85
+ @dataclass
86
+ class KeyMergeResult:
87
+ """The developer's text with every non-conflicting template change applied."""
88
+
89
+ text: str
90
+ applied: list[tuple[Any, ...]] = field(default_factory=list)
91
+ conflicts: list[KeyConflict] = field(default_factory=list)
92
+
93
+
94
+ def _show(value: Any) -> str:
95
+ if isinstance(value, dict | list):
96
+ text = yaml.safe_dump(value, default_flow_style=True, width=10_000).strip()
97
+ return text if len(text) <= 80 else text[:77] + "..."
98
+ return repr(value)
99
+
100
+
101
+ @dataclass(frozen=True)
102
+ class _Change:
103
+ path: tuple[Any, ...]
104
+ old: Any
105
+ new: Any
106
+
107
+
108
+ def _changes(base: dict, theirs: dict, prefix: tuple[Any, ...] = ()) -> list[_Change]:
109
+ """Keys whose value differs between the two renders (mappings compared key by key)."""
110
+ changes: list[_Change] = []
111
+ for key in [*theirs, *(k for k in base if k not in theirs)]:
112
+ old, new = base.get(key, MISSING), theirs.get(key, MISSING)
113
+ if old is not MISSING and new is not MISSING and old == new:
114
+ continue
115
+ if isinstance(old, dict) and isinstance(new, dict):
116
+ changes.extend(_changes(old, new, (*prefix, key)))
117
+ else:
118
+ changes.append(_Change((*prefix, key), old, new))
119
+ return changes
120
+
121
+
122
+ def _get(data: Any, path: tuple[Any, ...]) -> Any:
123
+ for key in path:
124
+ if not isinstance(data, dict) or key not in data:
125
+ return MISSING
126
+ data = data[key]
127
+ return data
128
+
129
+
130
+ def _set(data: dict, path: tuple[Any, ...], value: Any) -> None:
131
+ for key in path[:-1]:
132
+ data = data[key]
133
+ if value is MISSING:
134
+ data.pop(path[-1], None)
135
+ else:
136
+ data[path[-1]] = copy.deepcopy(value)
137
+
138
+
139
+ def _lines(text: str) -> list[str]:
140
+ """Lines with their ``\\n`` kept (a missing final newline stays missing)."""
141
+ parts = text.split("\n")
142
+ lines = [part + "\n" for part in parts[:-1]]
143
+ if parts[-1]:
144
+ lines.append(parts[-1])
145
+ return lines
146
+
147
+
148
+ def _newline(text: str) -> str:
149
+ return "\r\n" if "\r\n" in text else "\n"
150
+
151
+
152
+ def _with_newline(line: str, newline: str) -> str:
153
+ return line.rstrip("\r\n") + newline
154
+
155
+
156
+ def _reindent(lines: list[str], delta: int) -> list[str]:
157
+ if delta == 0:
158
+ return list(lines)
159
+ out: list[str] = []
160
+ for line in lines:
161
+ if not line.strip():
162
+ out.append(line)
163
+ elif delta > 0:
164
+ out.append(" " * delta + line)
165
+ else:
166
+ strip = min(-delta, len(line) - len(line.lstrip(" ")))
167
+ out.append(line[strip:])
168
+ return out
169
+
170
+
171
+ def _comment_block_above(lines: list[str], first: int, column: int) -> int:
172
+ """First line of the comment lines directly above ``first`` at ``column`` or deeper."""
173
+ start = first
174
+ while start > 0:
175
+ above = lines[start - 1]
176
+ if not _COMMENT_LINE.match(above):
177
+ break
178
+ if len(above) - len(above.lstrip(" ")) < column:
179
+ break
180
+ start -= 1
181
+ return start
182
+
183
+
184
+ def _stripped(lines: list[str]) -> set[str]:
185
+ return {line.strip() for line in lines if line.strip()}
186
+
187
+
188
+ def _template_comments(
189
+ lines: list[str],
190
+ first: int,
191
+ column: int,
192
+ base_lines: list[str],
193
+ base_first: int,
194
+ base_column: int,
195
+ theirs_lines: list[str],
196
+ ) -> list[int]:
197
+ """Indexes of the comment lines above an entry being removed that go with it.
198
+
199
+ Only the template's own comment block qualifies: the lines directly above
200
+ the entry must read exactly as they do above the same entry in the old
201
+ render, and a line the new render still has (a section heading) stays.
202
+ """
203
+ attached = _comment_block_above(lines, first, column)
204
+ base_start = _comment_block_above(base_lines, base_first, base_column)
205
+ ours_block = [line.strip() for line in lines[attached:first]]
206
+ if not ours_block or ours_block != [line.strip() for line in base_lines[base_start:base_first]]:
207
+ return []
208
+ keep = _stripped(theirs_lines)
209
+ return [i for i in range(attached, first) if lines[i].strip() not in keep]
210
+
211
+
212
+ def _new_entry_block(
213
+ theirs_lines: list[str], first: int, last: int, column: int, ours_lines: list[str]
214
+ ) -> list[str]:
215
+ """The template's lines for an entry being added, with the comments directly above it.
216
+
217
+ A comment line the developer's file already has (a section heading the
218
+ entry sits under in both) is not repeated.
219
+ """
220
+ start = _comment_block_above(theirs_lines, first, column)
221
+ present = _stripped(ours_lines)
222
+ comments = [line for line in theirs_lines[start:first] if line.strip() not in present]
223
+ return [*comments, *theirs_lines[first : last + 1]]
224
+
225
+
226
+ def _insert(lines: list[str], at: int, block: list[str], newline: str) -> str:
227
+ block = [_with_newline(line, newline) for line in block]
228
+ if at > 0 and not lines[at - 1].endswith("\n"):
229
+ lines = [*lines[: at - 1], lines[at - 1] + newline, *lines[at:]]
230
+ return "".join([*lines[:at], *block, *lines[at:]])
231
+
232
+
233
+ def _remove(lines: list[str], first: int, last: int, comments: list[int]) -> str:
234
+ drop = set(comments) | set(range(first, last + 1))
235
+ return "".join(line for number, line in enumerate(lines) if number not in drop)
236
+
237
+
238
+ # ---------------------------------------------------------------------------
239
+ # YAML
240
+ # ---------------------------------------------------------------------------
241
+
242
+
243
+ _MERGE_TAG = "tag:yaml.org,2002:merge"
244
+
245
+
246
+ class _Unsupported(Exception):
247
+ """The edit cannot be made safely in this text (flow style, missing parent...)."""
248
+
249
+
250
+ class _YamlDoc:
251
+ """A composed YAML document: node positions for editing its text."""
252
+
253
+ def __init__(self, text: str) -> None:
254
+ self.text = text
255
+ self.lines = _lines(text)
256
+ root = yaml.compose(text, Loader=yaml.SafeLoader)
257
+ if not isinstance(root, yaml.MappingNode) or root.flow_style:
258
+ raise _Unsupported("not a block mapping")
259
+ self.root = root
260
+ self._loader = yaml.SafeLoader("")
261
+
262
+ def _key(self, node: yaml.Node) -> Any:
263
+ return self._loader.construct_object(node, deep=True)
264
+
265
+ def mapping(self, path: tuple[Any, ...]) -> yaml.MappingNode:
266
+ node: yaml.Node = self.root
267
+ for key in path:
268
+ pair = self._pair(node, key)
269
+ if pair is None:
270
+ raise _Unsupported(f"no {key!r}")
271
+ node = pair[1]
272
+ if not isinstance(node, yaml.MappingNode) or node.flow_style or not node.value:
273
+ raise _Unsupported("not a non-empty block mapping")
274
+ return node
275
+
276
+ @staticmethod
277
+ def _own_pairs(node: yaml.MappingNode) -> list[tuple[yaml.Node, yaml.Node]]:
278
+ """The entries written in this mapping (a ``<<`` merge key brings others in)."""
279
+ return [
280
+ (key_node, value_node)
281
+ for key_node, value_node in node.value
282
+ if isinstance(key_node, yaml.ScalarNode) and key_node.tag != _MERGE_TAG
283
+ ]
284
+
285
+ def _pair(self, node: yaml.Node, key: Any) -> tuple[yaml.Node, yaml.Node] | None:
286
+ if not isinstance(node, yaml.MappingNode):
287
+ return None
288
+ for key_node, value_node in self._own_pairs(node):
289
+ if self._key(key_node) == key:
290
+ return key_node, value_node
291
+ return None
292
+
293
+ def keys(self, mapping: yaml.MappingNode) -> list[Any]:
294
+ return [self._key(k) for k, _v in self._own_pairs(mapping)]
295
+
296
+ def entry(self, path: tuple[Any, ...]) -> tuple[yaml.Node, yaml.Node, int, int]:
297
+ """``(key node, value node, first line, last line)`` of the entry at ``path``."""
298
+ parent = self.mapping(path[:-1])
299
+ pair = self._pair(parent, path[-1])
300
+ if pair is None:
301
+ raise _Unsupported(f"no {path[-1]!r}")
302
+ key_node, value_node = pair
303
+ first = key_node.start_mark.line
304
+ last = max(_last_line(value_node), key_node.end_mark.line)
305
+ return key_node, value_node, first, last
306
+
307
+ def column(self, path: tuple[Any, ...]) -> int:
308
+ """The key column of the mapping at ``path``."""
309
+ return self.mapping(path).value[0][0].start_mark.column
310
+
311
+
312
+ def _is_flow(node: yaml.Node) -> bool:
313
+ return isinstance(node, yaml.CollectionNode) and bool(node.flow_style)
314
+
315
+
316
+ def _last_line(node: yaml.Node) -> int:
317
+ """The last line holding content of ``node`` (not the blank or comment lines after it)."""
318
+ if isinstance(node, yaml.ScalarNode):
319
+ end = node.end_mark
320
+ if node.style in ("|", ">") and end.column == 0:
321
+ return max(end.line - 1, node.start_mark.line)
322
+ return end.line
323
+ if isinstance(node, yaml.CollectionNode) and (node.flow_style or not node.value):
324
+ return node.end_mark.line
325
+ last = node.value[-1]
326
+ if isinstance(node, yaml.MappingNode):
327
+ return max(_last_line(last[1]), last[0].end_mark.line)
328
+ return _last_line(last)
329
+
330
+
331
+ def _yaml_apply(ours: str, theirs: _YamlDoc, base: _YamlDoc, change: _Change) -> str:
332
+ """``ours`` with one template change applied (``_Unsupported`` when it cannot be)."""
333
+ doc = _YamlDoc(ours)
334
+ lines = doc.lines
335
+ newline = _newline(ours)
336
+ path = change.path
337
+ if change.old is not MISSING and change.new is not MISSING:
338
+ key_node, value_node, first, last = doc.entry(path)
339
+ t_key, t_value, t_first, t_last = theirs.entry(path)
340
+ if (
341
+ isinstance(value_node, yaml.ScalarNode)
342
+ and isinstance(t_value, yaml.ScalarNode)
343
+ and value_node.style not in ("|", ">")
344
+ and t_value.style not in ("|", ">")
345
+ and value_node.start_mark.line == value_node.end_mark.line
346
+ ):
347
+ new_value = theirs.text[t_value.start_mark.index : t_value.end_mark.index]
348
+ if value_node.start_mark.index == value_node.end_mark.index:
349
+ # `key:` with nothing after it (null): keep one space before the value.
350
+ new_value = " " + new_value
351
+ return (
352
+ ours[: value_node.start_mark.index] + new_value + ours[value_node.end_mark.index :]
353
+ )
354
+ if _is_flow(value_node) and value_node.start_mark.line != first:
355
+ raise _Unsupported("flow collection on its own line")
356
+ block = _reindent(
357
+ theirs.lines[t_first : t_last + 1], key_node.start_mark.column - t_key.start_mark.column
358
+ )
359
+ block = [_with_newline(line, newline) for line in block]
360
+ if last + 1 >= len(lines) and not lines[last].endswith("\n"):
361
+ block[-1] = block[-1].rstrip("\r\n")
362
+ return "".join([*lines[:first], *block, *lines[last + 1 :]])
363
+
364
+ if change.new is MISSING: # the template removed the entry
365
+ key_node, _value, first, last = doc.entry(path)
366
+ if len(doc.mapping(path[:-1]).value) == 1:
367
+ raise _Unsupported("the last key of its mapping")
368
+ b_key, _bv, b_first, _bl = base.entry(path)
369
+ comments = _template_comments(
370
+ lines,
371
+ first,
372
+ key_node.start_mark.column,
373
+ base.lines,
374
+ b_first,
375
+ b_key.start_mark.column,
376
+ theirs.lines,
377
+ )
378
+ return _remove(lines, first, last, comments)
379
+
380
+ # The template added the entry.
381
+ parent = doc.mapping(path[:-1])
382
+ t_keys = theirs.keys(theirs.mapping(path[:-1]))
383
+ ours_keys = doc.keys(parent)
384
+ index = t_keys.index(path[-1])
385
+ t_key, _tv, t_first, t_last = theirs.entry(path)
386
+ column = parent.value[0][0].start_mark.column
387
+ block = _reindent(
388
+ _new_entry_block(theirs.lines, t_first, t_last, t_key.start_mark.column, lines),
389
+ column - t_key.start_mark.column,
390
+ )
391
+ before = next((k for k in reversed(t_keys[:index]) if k in ours_keys), MISSING)
392
+ after = next((k for k in t_keys[index + 1 :] if k in ours_keys), MISSING)
393
+ if before is not MISSING:
394
+ at = doc.entry((*path[:-1], before))[3] + 1
395
+ elif after is not MISSING:
396
+ k_node, _v, first, _l = doc.entry((*path[:-1], after))
397
+ at = _comment_block_above(lines, first, k_node.start_mark.column)
398
+ else:
399
+ at = doc.entry((*path[:-1], ours_keys[-1]))[3] + 1
400
+ return _insert(lines, at, block, newline)
401
+
402
+
403
+ # ---------------------------------------------------------------------------
404
+ # env files (NAME=value lines)
405
+ # ---------------------------------------------------------------------------
406
+
407
+
408
+ def _env_index(lines: list[str]) -> dict[str, int]:
409
+ found: dict[str, int] = {}
410
+ for number, line in enumerate(lines):
411
+ match = _ENV_LINE.match(line)
412
+ if match:
413
+ found[match.group(1)] = number
414
+ return found
415
+
416
+
417
+ def _env_apply(ours: str, theirs: str, base: str, change: _Change) -> str:
418
+ lines = _lines(ours)
419
+ newline = _newline(ours)
420
+ index = _env_index(lines)
421
+ t_lines = _lines(theirs)
422
+ t_index = _env_index(t_lines)
423
+ name = change.path[0]
424
+ if change.old is not MISSING and change.new is not MISSING:
425
+ at = index[name]
426
+ new_line = _with_newline(t_lines[t_index[name]], newline)
427
+ if not lines[at].endswith("\n"):
428
+ new_line = new_line.rstrip("\r\n")
429
+ return "".join([*lines[:at], new_line, *lines[at + 1 :]])
430
+ if change.new is MISSING:
431
+ at = index[name]
432
+ b_lines = _lines(base)
433
+ comments = _template_comments(lines, at, 0, b_lines, _env_index(b_lines)[name], 0, t_lines)
434
+ return _remove(lines, at, at, comments)
435
+ t_at = t_index[name]
436
+ block = _new_entry_block(t_lines, t_at, t_at, 0, lines)
437
+ order = list(t_index)
438
+ position = order.index(name)
439
+ before = next((k for k in reversed(order[:position]) if k in index), None)
440
+ after = next((k for k in order[position + 1 :] if k in index), None)
441
+ if before is not None:
442
+ at = index[before] + 1
443
+ elif after is not None:
444
+ at = _comment_block_above(lines, index[after], 0)
445
+ else:
446
+ at = len(lines)
447
+ return _insert(lines, at, block, newline)
448
+
449
+
450
+ # ---------------------------------------------------------------------------
451
+ # entry point
452
+ # ---------------------------------------------------------------------------
453
+
454
+
455
+ def _kind(path: str) -> str | None:
456
+ name = path.replace("\\", "/").rsplit("/", 1)[-1]
457
+ if name.endswith((".yaml", ".yml")):
458
+ return "yaml"
459
+ if name.startswith(".env") or name.endswith(".env"):
460
+ return "env"
461
+ return None
462
+
463
+
464
+ def supports_key_merge(path: str) -> bool:
465
+ """True for the file types :func:`merge_keys` understands (YAML, env files)."""
466
+ return _kind(path) is not None
467
+
468
+
469
+ def merge_keys(base: str, ours: str, theirs: str, path: str) -> KeyMergeResult | None:
470
+ """Apply the template's change (``base`` -> ``theirs``) to ``ours`` key by key.
471
+
472
+ ``None`` when the file type is not supported, a text does not parse as a
473
+ single mapping, or the edited text would not mean exactly the developer's
474
+ document plus the applied changes.
475
+ """
476
+ kind = _kind(path)
477
+ if kind is None or any(_OTHER_BREAKS.search(text) for text in (base, ours, theirs)):
478
+ return None
479
+ parse: Callable[[str], Any]
480
+ if kind == "yaml":
481
+
482
+ def parse(text: str) -> Any:
483
+ return yaml.load(text, Loader=_StrictLoader)
484
+
485
+ else:
486
+ parse = _parse_env
487
+ try:
488
+ base_data, ours_data, theirs_data = parse(base), parse(ours), parse(theirs)
489
+ except (ValueError, yaml.YAMLError):
490
+ return None
491
+ if not all(isinstance(d, dict) for d in (base_data, ours_data, theirs_data)):
492
+ return None
493
+ try:
494
+ base_doc = _YamlDoc(base) if kind == "yaml" else None
495
+ theirs_doc = _YamlDoc(theirs) if kind == "yaml" else None
496
+ except (_Unsupported, yaml.YAMLError):
497
+ return None
498
+
499
+ text = ours
500
+ expected = copy.deepcopy(ours_data)
501
+ result = KeyMergeResult(text=ours)
502
+ for change in _changes(base_data, theirs_data):
503
+ yours = _get(ours_data, change.path)
504
+ if yours == change.new:
505
+ continue # already what the new settings render (or absent from both)
506
+ if yours is MISSING and change.old is not MISSING:
507
+ # The developer removed a key the template still changes.
508
+ result.conflicts.append(KeyConflict(change.path, MISSING, change.new))
509
+ continue
510
+ if yours is not MISSING and (change.old is MISSING or yours != change.old):
511
+ result.conflicts.append(KeyConflict(change.path, yours, change.new))
512
+ continue
513
+ wanted = copy.deepcopy(expected)
514
+ try:
515
+ _set(wanted, change.path, change.new)
516
+ if kind == "yaml":
517
+ assert theirs_doc is not None and base_doc is not None
518
+ edited = _yaml_apply(text, theirs_doc, base_doc, change)
519
+ else:
520
+ edited = _env_apply(text, theirs, base, change)
521
+ applied = parse(edited) == wanted
522
+ except (_Unsupported, KeyError, ValueError, IndexError, TypeError, yaml.YAMLError):
523
+ applied = False
524
+ if not applied:
525
+ # No safe edit in this text (a flow mapping, a missing parent, ...):
526
+ # the key is left for the developer like one they changed.
527
+ result.conflicts.append(KeyConflict(change.path, yours, change.new))
528
+ continue
529
+ text, expected = edited, wanted
530
+ result.applied.append(change.path)
531
+ try:
532
+ if parse(text) != expected:
533
+ return None
534
+ except (ValueError, yaml.YAMLError):
535
+ return None
536
+ result.text = text
537
+ return result
@@ -0,0 +1,138 @@
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
+ """Language configuration for CLI commands: Python only.
17
+
18
+ The single-entry table keeps the ``dispatch_language`` shape the command
19
+ modules were written against, so adding a language later is additive.
20
+ """
21
+
22
+ import logging
23
+ import pathlib
24
+ import tomllib
25
+ from collections.abc import Callable, Mapping
26
+ from typing import Any
27
+
28
+ import click
29
+
30
+ SUPPORTED_LANGUAGES: tuple[str, ...] = ("python",)
31
+
32
+
33
+ def _read_python_version(root: pathlib.Path) -> str:
34
+ """Return the ``[project].version`` from ``pyproject.toml`` or raise."""
35
+ pyproject_path = root / "pyproject.toml"
36
+ if not pyproject_path.exists():
37
+ raise FileNotFoundError(f"pyproject.toml not found in {root}")
38
+ with open(pyproject_path, "rb") as f:
39
+ data = tomllib.load(f) # PEP 621 [project] section
40
+ version = data.get("project", {}).get("version")
41
+ if version and isinstance(version, str):
42
+ return version
43
+ raise KeyError(f"no [project].version in {pyproject_path}")
44
+
45
+
46
+ LANGUAGE_CONFIGS: dict[str, dict[str, Any]] = {
47
+ "python": {
48
+ "lock_file": "uv.lock",
49
+ "lock_command": ["uv", "lock"],
50
+ "lock_command_name": "uv lock",
51
+ "display_name": "Python",
52
+ "agent_file": "agent.py",
53
+ # `app/agent.py` exports the compiled graph.
54
+ "agent_variable": "graph",
55
+ "agent_in_subdirectory": False,
56
+ "version_reader": _read_python_version,
57
+ "api_base_path": "",
58
+ "a2a_base_path_factory": lambda app_name: f"/a2a/{app_name}",
59
+ },
60
+ }
61
+
62
+
63
+ def get_language_config(language: str) -> dict[str, Any]:
64
+ """Get the configuration dict for a language (Python config as fallback)."""
65
+ return LANGUAGE_CONFIGS.get(language, LANGUAGE_CONFIGS["python"])
66
+
67
+
68
+ class UnsupportedLanguageError(click.ClickException):
69
+ """Raised when a command has no handler for the project's language."""
70
+
71
+
72
+ def dispatch_language(
73
+ command_name: str,
74
+ handlers: Mapping[str, Callable | None],
75
+ language: str,
76
+ ) -> Callable:
77
+ """Return the handler for ``language``, or raise a clear error."""
78
+ handler = handlers.get(language)
79
+ if handler is None:
80
+ supported = ", ".join(sorted(k for k, v in handlers.items() if v is not None))
81
+ raise UnsupportedLanguageError(
82
+ f"`graph-agents-cli {command_name}` isn't supported for '{language}' "
83
+ f"projects.\n Supported languages: {supported}."
84
+ )
85
+ return handler
86
+
87
+
88
+ def find_agent_file(
89
+ project_dir: pathlib.Path,
90
+ agent_directory: str,
91
+ language: str = "python",
92
+ ) -> pathlib.Path | None:
93
+ """Return ``{agent_directory}/agent.py`` when it exists, else None."""
94
+ agent_file = project_dir / agent_directory / get_language_config(language)["agent_file"]
95
+ return agent_file if agent_file.is_file() else None
96
+
97
+
98
+ def validate_agent_file(
99
+ agent_file: pathlib.Path,
100
+ language: str = "python",
101
+ ) -> tuple[bool, str | None]:
102
+ """Check that the agent file defines the exported variable (``graph``)."""
103
+ required_var = get_language_config(language)["agent_variable"]
104
+ try:
105
+ content = agent_file.read_text(encoding="utf-8")
106
+ except Exception as e:
107
+ return False, f"Could not read {agent_file.name}: {e}"
108
+ if required_var in content:
109
+ return True, None
110
+ return False, f"Missing '{required_var}' variable in {agent_file.name}"
111
+
112
+
113
+ def get_agent_file_hint(dir_path: pathlib.Path, language: str = "python") -> str:
114
+ """Hint string for directory selection, e.g. ``' (has agent.py)'``."""
115
+ if not dir_path.is_dir():
116
+ return ""
117
+ agent_file = get_language_config(language)["agent_file"]
118
+ if (dir_path / agent_file).exists():
119
+ return f" (has {agent_file})"
120
+ return ""
121
+
122
+
123
+ def get_project_version(
124
+ project_dir: str | pathlib.Path,
125
+ default_version: str = "0.0.0",
126
+ ) -> str:
127
+ """Extract the project version from ``pyproject.toml``, falling back to ``default_version``."""
128
+ root = pathlib.Path(project_dir)
129
+ try:
130
+ return _read_python_version(root)
131
+ except Exception as e:
132
+ logging.warning(
133
+ "Could not read the project version (%s). Falling back to %s; set "
134
+ "[project].version in pyproject.toml.",
135
+ e,
136
+ default_version,
137
+ )
138
+ return default_version