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,188 @@
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
+ """Which allow-listed Secret keys an environment cannot run without.
15
+
16
+ A missing key only shows up once the pods start: a crash loop (no provider key,
17
+ no database URI) or every request answered 503 (no ``API_KEY`` under
18
+ ``shared-bearer``). Outside dev the chart requires the Secret itself
19
+ (``secretOptional: false``), so a missing Secret keeps the pods from starting,
20
+ but it cannot tell which keys the Secret holds.
21
+ ``deploy`` checks the live Secret against this list before ``helm upgrade``,
22
+ and ``secrets status`` exits 1 only when one of these is missing, so both work
23
+ as gates while optional keys (judge key, LangSmith key, API tokens) stay
24
+ optional.
25
+
26
+ The list follows the environment's merged chart values, the same inputs the pod
27
+ gets: ``env.MODEL_PROVIDER`` (the manifest's provider when unset) needs its
28
+ key, except ``openai-compatible`` servers (often keyless on-network) and the
29
+ ``fake`` test model; ``env.AUTH_POLICY`` ``shared-bearer`` needs ``API_KEY``;
30
+ the database and Redis URIs are needed unless the bundled subchart provides
31
+ them (``postgresql.enabled`` / ``redis.enabled``) or ``env.CHECKPOINTER`` is
32
+ ``memory``. A key set as a plain chart ``env`` value does not need the Secret.
33
+ Only keys in the manifest's ``secrets.keys`` are ever required; removing one
34
+ from the allow-list is how a deployment opts out.
35
+
36
+ One key joins the allow-list without being listed there: ``AUTH_JWT_SECRET``,
37
+ the shared secret of the ``jwt`` policy's HS* algorithms. A project opts into
38
+ HS* with ``AUTH_JWT_ALLOW_HS=true`` / ``AUTH_JWT_ALGORITHMS=HS256`` in the
39
+ environment's chart values or its env file; the secret itself must then reach
40
+ the pods through the Secret (never the ConfigMap). It is required when the
41
+ chart values list an HS* algorithm: the pod refuses to start without it.
42
+ """
43
+
44
+ from __future__ import annotations
45
+
46
+ import dataclasses
47
+ from collections.abc import Mapping
48
+ from typing import Any
49
+
50
+ from graph_agents_cli._defaults import PROVIDER_KEY_VARS, normalize_auth_policy
51
+ from graph_agents_cli._output import Console
52
+ from graph_agents_cli.deploy._config import DeploySettings
53
+
54
+ # Providers whose key is optional: on-network OpenAI-compatible servers are often
55
+ # keyless, and the fake model needs none.
56
+ KEYLESS_PROVIDERS = frozenset({"openai-compatible", "fake"})
57
+
58
+ JWT_SECRET_KEY = "AUTH_JWT_SECRET"
59
+ # The HS* settings the app reads (see the template's app/app_utils/auth.py).
60
+ JWT_HS_SETTINGS = ("AUTH_JWT_ALLOW_HS", "AUTH_JWT_ALGORITHMS")
61
+ _HMAC_ALGORITHMS = frozenset({"HS256", "HS384", "HS512"})
62
+ _TRUE = ("1", "true", "yes", "on")
63
+
64
+
65
+ def _env_value(values: dict[str, Any], key: str) -> str:
66
+ env = values.get("env") if isinstance(values.get("env"), dict) else {}
67
+ value = env.get(key)
68
+ return "" if value is None else str(value).strip()
69
+
70
+
71
+ def _auth_policy(settings: DeploySettings, values: dict[str, Any]) -> str:
72
+ return normalize_auth_policy(
73
+ _env_value(values, "AUTH_POLICY") or settings.auth_policy, warn=False
74
+ )
75
+
76
+
77
+ def _lists_hmac(algorithms: str) -> bool:
78
+ return any(a.strip().upper() in _HMAC_ALGORITHMS for a in algorithms.split(","))
79
+
80
+
81
+ def _hs_settings(source: Mapping[str, Any]) -> tuple[bool, bool]:
82
+ """``(allow_hs, lists_hs)`` read from one source of settings."""
83
+ allow = str(source.get("AUTH_JWT_ALLOW_HS") or "").strip().lower() in _TRUE
84
+ return allow, _lists_hmac(str(source.get("AUTH_JWT_ALGORITHMS") or ""))
85
+
86
+
87
+ def jwt_hs_in_values(settings: DeploySettings, values: dict[str, Any]) -> bool:
88
+ """The chart values run the ``jwt`` policy with an HS* algorithm (the pod needs the secret)."""
89
+ env = values.get("env") if isinstance(values.get("env"), dict) else {}
90
+ return _auth_policy(settings, values) == "jwt" and _hs_settings(env)[1]
91
+
92
+
93
+ def jwt_hs_indicated(
94
+ settings: DeploySettings, values: dict[str, Any], file_values: Mapping[str, str] | None = None
95
+ ) -> bool:
96
+ """HS* JWTs are opted into by the chart values or the env file (``jwt`` policy only)."""
97
+ if _auth_policy(settings, values) != "jwt":
98
+ return False
99
+ env = values.get("env") if isinstance(values.get("env"), dict) else {}
100
+ return any(any(_hs_settings(source)) for source in (env, file_values or {}))
101
+
102
+
103
+ def for_environment(
104
+ settings: DeploySettings, values: dict[str, Any], file_values: Mapping[str, str] | None = None
105
+ ) -> DeploySettings:
106
+ """``settings`` whose allow-list also holds ``AUTH_JWT_SECRET`` when HS* JWTs are in use."""
107
+ if JWT_SECRET_KEY in settings.secret_keys or not jwt_hs_indicated(
108
+ settings, values, file_values
109
+ ):
110
+ return settings
111
+ return dataclasses.replace(settings, secret_keys=[*settings.secret_keys, JWT_SECRET_KEY])
112
+
113
+
114
+ def _enabled(values: dict[str, Any], section: str) -> bool:
115
+ block = values.get(section) if isinstance(values.get(section), dict) else {}
116
+ value = block.get("enabled")
117
+ return value is True or (
118
+ isinstance(value, str) and value.strip().lower() in ("true", "yes", "1")
119
+ )
120
+
121
+
122
+ def required_keys(settings: DeploySettings, values: dict[str, Any]) -> list[str]:
123
+ """Allow-listed keys the pods need from the Secret, in allow-list order."""
124
+ needed: set[str] = set()
125
+ provider = (_env_value(values, "MODEL_PROVIDER") or settings.model_provider).lower()
126
+ if provider not in KEYLESS_PROVIDERS:
127
+ needed.add(PROVIDER_KEY_VARS.get(provider, "MODEL_API_KEY"))
128
+ if _auth_policy(settings, values) == "shared-bearer":
129
+ needed.add("API_KEY")
130
+ if jwt_hs_in_values(settings, values):
131
+ needed.add(JWT_SECRET_KEY)
132
+ if settings.runtime == "langgraph-server":
133
+ if not _enabled(values, "postgresql"):
134
+ needed.add("DATABASE_URI")
135
+ if not _enabled(values, "redis"):
136
+ needed.add("REDIS_URI")
137
+ elif (
138
+ not _enabled(values, "postgresql")
139
+ and (_env_value(values, "CHECKPOINTER") or "postgres").lower() != "memory"
140
+ ):
141
+ needed.add("POSTGRES_DSN")
142
+ allowed = for_environment(settings, values).secret_keys
143
+ return [k for k in allowed if k in needed and not _env_value(values, k)]
144
+
145
+
146
+ def optional_keys(settings: DeploySettings, values: dict[str, Any]) -> list[str]:
147
+ """The allow-listed keys that are not required, in allow-list order."""
148
+ required = set(required_keys(settings, values))
149
+ return [k for k in for_environment(settings, values).secret_keys if k not in required]
150
+
151
+
152
+ def unreached_hs_settings(
153
+ settings: DeploySettings, values: dict[str, Any], file_values: Mapping[str, str]
154
+ ) -> list[str]:
155
+ """HS* settings the env file sets that never reach the pods.
156
+
157
+ Only allow-listed keys go from the env file into the Secret, and these are
158
+ plain settings that belong in the chart values' ``env`` (the ConfigMap).
159
+ """
160
+ if _auth_policy(settings, values) != "jwt":
161
+ return []
162
+ return [
163
+ key
164
+ for key in JWT_HS_SETTINGS
165
+ if str(file_values.get(key) or "").strip()
166
+ and key not in settings.secret_keys
167
+ and not _env_value(values, key)
168
+ ]
169
+
170
+
171
+ def print_unreached_hs_settings(
172
+ settings: DeploySettings,
173
+ env: str,
174
+ values: dict[str, Any],
175
+ file_values: Mapping[str, str],
176
+ *,
177
+ source: object,
178
+ console: Console,
179
+ ) -> None:
180
+ """Warn that HS* settings in the env file stay local (see :func:`unreached_hs_settings`)."""
181
+ keys = unreached_hs_settings(settings, values, file_values)
182
+ if keys:
183
+ console.print(
184
+ f" {', '.join(keys)} in {source} never reach the pods (only allow-listed keys go "
185
+ f"into the Secret); set them under env: in {settings.values_file(env)}.",
186
+ style="yellow",
187
+ markup=False,
188
+ )
@@ -0,0 +1,211 @@
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
+ """graph-agents-cli secrets commands — provision and inspect the app Secret."""
15
+
16
+ from __future__ import annotations
17
+
18
+ import click
19
+
20
+ from graph_agents_cli._click import LazyGroup
21
+ from graph_agents_cli._output import Console
22
+ from graph_agents_cli.deploy import _modes, _preflight
23
+ from graph_agents_cli.deploy._config import DeploySettings, load_settings
24
+ from graph_agents_cli.deploy._kube import Target
25
+ from graph_agents_cli.deploy._values import load_chart_values
26
+ from graph_agents_cli.secrets import _apply, _required
27
+
28
+
29
+ @click.group("secrets", cls=LazyGroup)
30
+ def secrets_group() -> None:
31
+ """Provision and inspect the application Secret per environment.
32
+
33
+ \b
34
+ Subcommands:
35
+ apply Create or update <name>-app from the allow-listed keys of an env file
36
+ status List which allow-listed keys are present (values are never printed)
37
+ """
38
+
39
+
40
+ def _resolve(
41
+ settings: DeploySettings, env: str, context: str | None
42
+ ) -> tuple[_modes.ResolvedContext, Target]:
43
+ namespace = settings.target(env).namespace
44
+ resolved = _modes.resolve(settings, env, context)
45
+ return resolved, Target(context=resolved.name, namespace=namespace)
46
+
47
+
48
+ _CONTEXT_HELP = "Kube context to use instead of environments.<env>.context."
49
+
50
+
51
+ def _default_env_file_values(env: str) -> dict[str, str]:
52
+ """The env file ``secrets apply --env <env>`` reads by default (empty when absent or unreadable)."""
53
+ path = _apply.resolve_env_file(env, None)
54
+ if path is None:
55
+ return {}
56
+ try:
57
+ return _apply.read_env_file(path)
58
+ except (OSError, UnicodeDecodeError):
59
+ return {}
60
+
61
+
62
+ @secrets_group.command("apply")
63
+ @click.option("--env", "env", required=True, help="Target environment (dev, staging, prod).")
64
+ @click.option(
65
+ "--env-file",
66
+ "env_file",
67
+ default=None,
68
+ help="Env file; defaults to .env.<env> (dev also falls back to .env).",
69
+ )
70
+ @click.option("--context", "context", default=None, help=_CONTEXT_HELP)
71
+ @click.option(
72
+ "--yes",
73
+ "-y",
74
+ "yes",
75
+ is_flag=True,
76
+ help="Accept the kubeconfig's current context outside dev without prompting.",
77
+ )
78
+ @click.option(
79
+ "--rotate-api-key",
80
+ "rotate_api_key",
81
+ is_flag=True,
82
+ help="Replace the live API_KEY with the one in the env file (otherwise the live key wins).",
83
+ )
84
+ @click.option(
85
+ "--dry-run", "dry_run", is_flag=True, help="Print the kubectl pipeline and a redacted manifest."
86
+ )
87
+ def cmd_secrets_apply(
88
+ env: str,
89
+ env_file: str | None,
90
+ context: str | None,
91
+ yes: bool,
92
+ rotate_api_key: bool,
93
+ dry_run: bool,
94
+ ) -> None:
95
+ """Create or update the app Secret from the allow-listed keys of an env file."""
96
+ console = Console()
97
+ settings = load_settings()
98
+ resolved, target = _resolve(settings, env, context)
99
+ path = _apply.resolve_env_file(env, env_file)
100
+ chart_values = load_chart_values(settings.chart_dir, env)
101
+ if path is None:
102
+ raise _apply.missing_env_file_error(
103
+ env, _required.for_environment(settings, chart_values).secret_keys
104
+ )
105
+ values = _apply.read_env_file(path)
106
+ # The allow-list for this environment (AUTH_JWT_SECRET joins it for HS* JWTs).
107
+ settings = _required.for_environment(settings, chart_values, values)
108
+ _apply.check_file_values(
109
+ values, settings.secret_keys, rotate_api_key=rotate_api_key, source=path
110
+ )
111
+ console.print(f"Secret {settings.secret_name} in {target.namespace} from {path}")
112
+ _modes.announce(env, resolved, console=console)
113
+ _modes.confirm(
114
+ env, resolved, yes=yes, dry_run=dry_run, console=console, action="apply the Secret for"
115
+ )
116
+ _required.print_unreached_hs_settings(
117
+ settings, env, chart_values, values, source=path, console=console
118
+ )
119
+ plan = _apply.provision(
120
+ name=settings.secret_name,
121
+ env=env,
122
+ target=target,
123
+ allowed=settings.secret_keys,
124
+ path=path,
125
+ values=values,
126
+ rotate_api_key=rotate_api_key,
127
+ dry_run=dry_run,
128
+ console=console,
129
+ # Only the shared-bearer policy reads API_KEY: never mint one it would not use.
130
+ mint_api_key=_preflight.effective_auth_policy(settings, chart_values) == "shared-bearer",
131
+ metrics_name=settings.metrics_secret_name,
132
+ )
133
+ if not _modes.is_dev_env(env):
134
+ for key in _preflight.dsn_keys(settings, chart_values):
135
+ dsn = plan.data.get(key) or ""
136
+ if (
137
+ dsn
138
+ and dsn != _apply.PENDING_PLACEHOLDER
139
+ and _preflight.dsn_without_tls(chart_values, dsn)
140
+ ):
141
+ console.print(
142
+ f" Warning: {_preflight.dsn_tls_warning(key)}", style="yellow", markup=False
143
+ )
144
+
145
+
146
+ @secrets_group.command("status")
147
+ @click.option("--env", "env", required=True, help="Target environment (dev, staging, prod).")
148
+ @click.option("--context", "context", default=None, help=_CONTEXT_HELP)
149
+ @click.option(
150
+ "--strict",
151
+ "strict",
152
+ is_flag=True,
153
+ help="Also exit 1 when an optional allow-listed key is missing.",
154
+ )
155
+ @click.option(
156
+ "--dry-run", "dry_run", is_flag=True, help="Print the kubectl command without running it."
157
+ )
158
+ def cmd_secrets_status(env: str, context: str | None, strict: bool, dry_run: bool) -> None:
159
+ """List which allow-listed keys are present in the app Secret (never values).
160
+
161
+ \b
162
+ Exit codes (usable as a CI or pre-deploy gate):
163
+ 0 the Secret holds every required key (optional ones may be missing)
164
+ 1 the Secret is missing, or a required key is (any key with --strict)
165
+ 2 kubectl failed (unreachable cluster, credentials, RBAC)
166
+ 3 configuration error (unknown environment or kube context, no manifest)
167
+ Required keys follow the environment's chart values: the model provider's key
168
+ (not for openai-compatible), API_KEY under shared-bearer, AUTH_JWT_SECRET under
169
+ jwt with an HS* algorithm, and POSTGRES_DSN or DATABASE_URI/REDIS_URI unless the
170
+ bundled subchart provides them.
171
+ """
172
+ console = Console()
173
+ settings = load_settings()
174
+ resolved, target = _resolve(settings, env, context)
175
+ _modes.announce(env, resolved, console=console)
176
+ _modes.require_known_context(env, resolved, dry_run=dry_run, console=console)
177
+ values = load_chart_values(settings.chart_dir, env)
178
+ # The env file `secrets apply` would read (never printed) tells whether the
179
+ # environment uses HS* JWTs, which adds AUTH_JWT_SECRET to the allow-list.
180
+ settings = _required.for_environment(settings, values, _default_env_file_values(env))
181
+ required = _required.required_keys(settings, values)
182
+ optional = [k for k in settings.secret_keys if k not in required]
183
+ present = _apply.secret_keys_present(
184
+ settings.secret_name, target, dry_run=dry_run, console=console
185
+ )
186
+ if dry_run:
187
+ return
188
+ if present is None:
189
+ console.print(
190
+ f"Secret {settings.secret_name} not found in namespace {target.namespace}.", style="red"
191
+ )
192
+ console.print(" Missing: " + ", ".join(settings.secret_keys))
193
+ raise SystemExit(1)
194
+ missing_required = [k for k in required if k not in present]
195
+ missing_optional = [k for k in optional if k not in present]
196
+ found = [k for k in settings.secret_keys if k in present]
197
+ extra = sorted(present - set(settings.secret_keys))
198
+ console.print(f"Secret {settings.secret_name} in {target.namespace}:")
199
+ console.print(" present: " + (", ".join(found) or "(none)"), style="green")
200
+ console.print(
201
+ " missing required: " + (", ".join(missing_required) or "(none)"),
202
+ style="red" if missing_required else "green",
203
+ )
204
+ console.print(
205
+ " missing optional: " + (", ".join(missing_optional) or "(none)"),
206
+ style="yellow" if missing_optional else "green",
207
+ )
208
+ if extra:
209
+ console.print(" not in the allow-list: " + ", ".join(extra), style="dim")
210
+ if missing_required or (strict and missing_optional):
211
+ raise SystemExit(1)
@@ -0,0 +1,13 @@
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
+ # http://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.
@@ -0,0 +1,221 @@
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
+ """Mirror globally-installed skills into Antigravity's skill directories.
17
+
18
+ Temporary workaround until `npx skills` fully supports Antigravity's IDE / CLI /
19
+ 2.0 skill paths. Today `npx skills ... -g` installs global skills into the
20
+ canonical ``~/.agents/skills`` store, but Antigravity does not read from there:
21
+ the IDE and Antigravity 2.0 read ``~/.gemini/config/skills`` and the Antigravity
22
+ CLI reads ``~/.gemini/antigravity-cli/skills``. This module bridges that gap by
23
+ linking each canonical skill into both locations so a global ``graph-agents-cli setup``
24
+ makes the skills visible to Antigravity. Once upstream installs to the correct
25
+ locations directly, this module can be removed.
26
+
27
+ Both locations are mirrored ourselves rather than relying on ``npx skills``,
28
+ whose global install is currently unreliable (vercel-labs/skills#1362).
29
+
30
+ Links are directory symlinks, falling back to directory junctions on Windows
31
+ (which work without elevation). Links auto-track the canonical store, so updates
32
+ to a skill are seen by Antigravity with no re-link needed.
33
+ """
34
+
35
+ from __future__ import annotations
36
+
37
+ import logging
38
+ import os
39
+ from pathlib import Path
40
+
41
+ # Canonical store that ``npx skills ... -g`` writes to.
42
+ _CANONICAL_SUBPATH = (".agents", "skills")
43
+
44
+ # Where Antigravity looks for global skills: IDE / 2.0 first, then the CLI.
45
+ _ANTIGRAVITY_SUBPATHS = (
46
+ (".gemini", "config", "skills"),
47
+ (".gemini", "antigravity-cli", "skills"),
48
+ )
49
+
50
+
51
+ def _display(path: Path, home: Path) -> str:
52
+ """Render ``path`` with the home directory collapsed to ``~`` for output."""
53
+ try:
54
+ return f"~/{path.relative_to(home)}"
55
+ except ValueError:
56
+ return str(path)
57
+
58
+
59
+ def _is_link(path: Path) -> bool:
60
+ """Return True if ``path`` is a symlink or a Windows directory junction.
61
+
62
+ ``Path.is_symlink()`` does not report junctions, so fall back to
63
+ ``os.readlink``, which succeeds for both and raises for real directories.
64
+ """
65
+ if path.is_symlink():
66
+ return True
67
+ try:
68
+ os.readlink(path)
69
+ return True
70
+ except OSError:
71
+ return False
72
+
73
+
74
+ def _link_target_into(entry: Path, source_root: Path) -> bool:
75
+ """Return True if the link at ``entry`` points somewhere under ``source_root``."""
76
+ try:
77
+ target_str = os.readlink(entry)
78
+ # Windows-specific path prefix handling:
79
+ # - "\??\" is the NT Object Manager namespace prefix, often returned by
80
+ # os.readlink for Windows junctions.
81
+ # - "\\?\" is the Win32 device namespace prefix, used for long paths.
82
+ # Python's pathlib.Path does not recognize these prefixes as absolute roots
83
+ # (e.g., Path("\??\C:\foo").is_absolute() may return False or behave incorrectly),
84
+ # so we strip the 4-character prefix to get a standard absolute path.
85
+ if target_str.startswith("\\??\\"):
86
+ target_str = target_str[4:]
87
+ elif target_str.startswith("\\\\?\\"):
88
+ target_str = target_str[4:]
89
+ target = Path(target_str)
90
+ except OSError:
91
+ return False
92
+ if not target.is_absolute():
93
+ target = entry.parent / target
94
+ try:
95
+ resolved_parent = target.parent.resolve()
96
+ resolved_source = source_root.resolve()
97
+ return resolved_source == resolved_parent or resolved_source in resolved_parent.parents
98
+ except OSError:
99
+ return False
100
+
101
+
102
+ def _link_dir(source: Path, link: Path) -> bool:
103
+ """Point ``link`` at ``source`` with a directory link (junction on Windows).
104
+
105
+ Idempotent: a link already resolving to ``source`` is left as-is. A stale or
106
+ mis-pointing link is replaced, but a real (non-link) file or directory is left
107
+ untouched and reported as a failure so user data is never clobbered.
108
+ """
109
+ try:
110
+ if link.exists() and os.path.realpath(link) == os.path.realpath(source):
111
+ return True
112
+ except OSError:
113
+ pass
114
+
115
+ if _is_link(link):
116
+ try:
117
+ link.unlink()
118
+ except OSError:
119
+ pass
120
+ elif link.exists():
121
+ return False
122
+
123
+ link.parent.mkdir(parents=True, exist_ok=True)
124
+
125
+ try:
126
+ os.symlink(source, link, target_is_directory=True)
127
+ return True
128
+ except (OSError, NotImplementedError):
129
+ pass
130
+
131
+ # Windows without symlink privilege: a junction works without elevation.
132
+ if os.name == "nt":
133
+ import subprocess
134
+
135
+ from graph_agents_cli._runner import run_resolved
136
+
137
+ try:
138
+ run_resolved(
139
+ ["cmd", "/c", "mklink", "/J", str(link), str(source)],
140
+ resolve_executable=False,
141
+ check=True,
142
+ capture_output=True,
143
+ text=True,
144
+ )
145
+ return True
146
+ except (OSError, subprocess.CalledProcessError):
147
+ pass
148
+
149
+ logging.warning(
150
+ "Could not link skill '%s' into %s "
151
+ "(on Windows, enable Developer Mode or run as administrator).",
152
+ source.name,
153
+ link.parent,
154
+ )
155
+ return False
156
+
157
+
158
+ def _prune_dead_links(target_root: Path, source_root: Path) -> None:
159
+ """Remove broken links under ``target_root`` that point into ``source_root``.
160
+
161
+ Keeps the mirror in sync when a skill is removed from the canonical store.
162
+ Only ever removes links (symlinks/junctions), never real files or directories.
163
+ """
164
+ if not target_root.is_dir():
165
+ return
166
+ for entry in target_root.iterdir():
167
+ if entry.exists() or not _link_target_into(entry, source_root):
168
+ continue
169
+ try:
170
+ entry.unlink()
171
+ except OSError as e:
172
+ logging.warning("Could not remove stale skill link %s: %s", entry, e)
173
+
174
+
175
+ def link_skills_for_antigravity(
176
+ *, home: Path | None = None, skills_dir: Path | None = None
177
+ ) -> list[str]:
178
+ """Link each canonical global skill into Antigravity's skill directories.
179
+
180
+ No-op (returns an empty list) when Antigravity/Gemini is not present
181
+ (no ``~/.gemini``) or when the canonical store is missing. Best-effort:
182
+ individual link failures are logged, never raised.
183
+
184
+ Returns a list of human-readable summary lines describing what was linked.
185
+ """
186
+ home = home or Path.home()
187
+
188
+ if not (home / ".gemini").is_dir():
189
+ return []
190
+
191
+ source_root = skills_dir or home.joinpath(*_CANONICAL_SUBPATH)
192
+ if not source_root.is_dir():
193
+ # This runs right after npx installs skills, so a missing canonical store
194
+ # means npx no longer writes there — surface it instead of silently
195
+ # no-op'ing (see module docstring).
196
+ logging.warning(
197
+ "Skills store %s not found; skills were not mirrored to Antigravity.",
198
+ source_root,
199
+ )
200
+ return []
201
+
202
+ skill_dirs = [
203
+ d for d in sorted(source_root.iterdir()) if d.is_dir() and (d / "SKILL.md").is_file()
204
+ ]
205
+ if not skill_dirs:
206
+ # Same expectation as above: npx just ran, so an empty store is surprising.
207
+ # Fall through so dead links from previously-linked skills still get pruned.
208
+ logging.warning("Skills store %s has no skills to mirror to Antigravity.", source_root)
209
+
210
+ summary: list[str] = []
211
+ for subpath in _ANTIGRAVITY_SUBPATHS:
212
+ target_root = home.joinpath(*subpath)
213
+ # Link each skill individually rather than pointing the whole target dir
214
+ # at the canonical store: ~/.gemini/.../skills is owned by Antigravity and
215
+ # may already hold skills from other sources. A top-level symlink would
216
+ # fail (dir exists) or shadow those; per-skill links coexist with them.
217
+ linked = sum(_link_dir(skill_dir, target_root / skill_dir.name) for skill_dir in skill_dirs)
218
+ _prune_dead_links(target_root, source_root)
219
+ if linked:
220
+ summary.append(f"Linked {linked} skill(s) into {_display(target_root, home)}")
221
+ return summary