@ngocsangairvds/vsaf 4.1.9 → 4.1.10

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 (1053) hide show
  1. package/package.json +1 -1
  2. package/packages/cli/dist/commands/install.d.ts +1 -1
  3. package/packages/cli/dist/commands/install.d.ts.map +1 -1
  4. package/packages/cli/dist/commands/install.js +25 -13
  5. package/packages/cli/dist/commands/install.js.map +1 -1
  6. package/packages/cli/dist/commands/skill.d.ts.map +1 -1
  7. package/packages/cli/dist/commands/skill.js +5 -1
  8. package/packages/cli/dist/commands/skill.js.map +1 -1
  9. package/skills/vds-skill/install-deps.mjs +238 -0
  10. package/skills/vds-skill/pack.yaml +8 -0
  11. package/skills/vds-skill/runtime/.claude/phase7-CLOSURE.md +100 -0
  12. package/skills/vds-skill/runtime/.dockerignore +62 -0
  13. package/skills/vds-skill/runtime/.github/ISSUE_TEMPLATE/cli-change.md +92 -0
  14. package/skills/vds-skill/runtime/.github/PULL_REQUEST_TEMPLATE/pull_request_template.md +48 -0
  15. package/skills/vds-skill/runtime/.github/workflows/chaos-smoke.yml +266 -0
  16. package/skills/vds-skill/runtime/.github/workflows/confluence-sync.yml +44 -0
  17. package/skills/vds-skill/runtime/.github/workflows/docs-confluence-evidence.yml +170 -0
  18. package/skills/vds-skill/runtime/.github/workflows/docs-quality.yml +59 -0
  19. package/skills/vds-skill/runtime/.github/workflows/lint-and-test.yml +90 -0
  20. package/skills/vds-skill/runtime/.github/workflows/scheduler-load-smoke.yml +104 -0
  21. package/skills/vds-skill/runtime/.github/workflows/telegram-bridge-ci.yml +131 -0
  22. package/skills/vds-skill/runtime/.graphifyignore +29 -0
  23. package/skills/vds-skill/runtime/.importlinter +86 -0
  24. package/skills/vds-skill/runtime/.mcp.json +11 -0
  25. package/skills/vds-skill/runtime/.pre-commit-config.yaml +62 -0
  26. package/skills/vds-skill/runtime/.ruffignore +3 -0
  27. package/skills/vds-skill/runtime/AGENTS.md +250 -0
  28. package/skills/vds-skill/runtime/AGENTS.vi.md +92 -0
  29. package/skills/vds-skill/runtime/ECOSYSTEM-CHANGELOG.md +52 -0
  30. package/skills/vds-skill/runtime/ECOSYSTEM-DOCS.md +602 -0
  31. package/skills/vds-skill/runtime/ECOSYSTEM_ALIGNMENT.md +133 -0
  32. package/skills/vds-skill/runtime/Makefile +119 -0
  33. package/skills/vds-skill/runtime/README.md +103 -0
  34. package/skills/vds-skill/runtime/bitbucket_manifest_mapping.toml +34 -0
  35. package/skills/vds-skill/runtime/bitbucket_orchestrator/ARCHITECTURE_ANALYSIS.md +258 -0
  36. package/skills/vds-skill/runtime/bitbucket_orchestrator/BITBUCKET_API_PRACTICES.md +393 -0
  37. package/skills/vds-skill/runtime/bitbucket_orchestrator/EVALUATION_REPORT.md +61 -0
  38. package/skills/vds-skill/runtime/bitbucket_orchestrator/FEATURES.md +908 -0
  39. package/skills/vds-skill/runtime/bitbucket_orchestrator/README.md +817 -0
  40. package/skills/vds-skill/runtime/bitbucket_orchestrator/pyproject.toml +49 -0
  41. package/skills/vds-skill/runtime/bitbucket_orchestrator/src/vds_bitbucket_orchestrator/__init__.py +50 -0
  42. package/skills/vds-skill/runtime/bitbucket_orchestrator/src/vds_bitbucket_orchestrator/async_client.py +641 -0
  43. package/skills/vds-skill/runtime/bitbucket_orchestrator/src/vds_bitbucket_orchestrator/cli.py +2271 -0
  44. package/skills/vds-skill/runtime/bitbucket_orchestrator/src/vds_bitbucket_orchestrator/client.py +2693 -0
  45. package/skills/vds-skill/runtime/bitbucket_orchestrator/src/vds_bitbucket_orchestrator/config.py +186 -0
  46. package/skills/vds-skill/runtime/bitbucket_orchestrator/src/vds_bitbucket_orchestrator/errors.py +34 -0
  47. package/skills/vds-skill/runtime/bitbucket_orchestrator/src/vds_bitbucket_orchestrator/factory.py +185 -0
  48. package/skills/vds-skill/runtime/bitbucket_orchestrator/src/vds_bitbucket_orchestrator/parsers.py +113 -0
  49. package/skills/vds-skill/runtime/bitbucket_orchestrator/src/vds_bitbucket_orchestrator/protocols.py +244 -0
  50. package/skills/vds-skill/runtime/bitbucket_orchestrator/src/vds_bitbucket_orchestrator/repo_ops.py +325 -0
  51. package/skills/vds-skill/runtime/bitbucket_orchestrator/tests/__init__.py +8 -0
  52. package/skills/vds-skill/runtime/bitbucket_orchestrator/tests/conftest.py +65 -0
  53. package/skills/vds-skill/runtime/bitbucket_orchestrator/tests/test_advanced_search.py +155 -0
  54. package/skills/vds-skill/runtime/bitbucket_orchestrator/tests/test_async_client.py +505 -0
  55. package/skills/vds-skill/runtime/bitbucket_orchestrator/tests/test_branch_permissions.py +172 -0
  56. package/skills/vds-skill/runtime/bitbucket_orchestrator/tests/test_cli.py +113 -0
  57. package/skills/vds-skill/runtime/bitbucket_orchestrator/tests/test_cli_archive.py +122 -0
  58. package/skills/vds-skill/runtime/bitbucket_orchestrator/tests/test_cli_clone.py +131 -0
  59. package/skills/vds-skill/runtime/bitbucket_orchestrator/tests/test_client.py +207 -0
  60. package/skills/vds-skill/runtime/bitbucket_orchestrator/tests/test_client_archive.py +73 -0
  61. package/skills/vds-skill/runtime/bitbucket_orchestrator/tests/test_client_branch_conditions.py +101 -0
  62. package/skills/vds-skill/runtime/bitbucket_orchestrator/tests/test_client_code_advanced.py +180 -0
  63. package/skills/vds-skill/runtime/bitbucket_orchestrator/tests/test_client_code_file.py +33 -0
  64. package/skills/vds-skill/runtime/bitbucket_orchestrator/tests/test_client_deployment_environments.py +193 -0
  65. package/skills/vds-skill/runtime/bitbucket_orchestrator/tests/test_client_issues.py +163 -0
  66. package/skills/vds-skill/runtime/bitbucket_orchestrator/tests/test_client_pipelines_advanced.py +171 -0
  67. package/skills/vds-skill/runtime/bitbucket_orchestrator/tests/test_client_pr_blockers.py +118 -0
  68. package/skills/vds-skill/runtime/bitbucket_orchestrator/tests/test_client_repository_variables.py +155 -0
  69. package/skills/vds-skill/runtime/bitbucket_orchestrator/tests/test_code.py +98 -0
  70. package/skills/vds-skill/runtime/bitbucket_orchestrator/tests/test_code_advanced.py +279 -0
  71. package/skills/vds-skill/runtime/bitbucket_orchestrator/tests/test_code_insights.py +334 -0
  72. package/skills/vds-skill/runtime/bitbucket_orchestrator/tests/test_conditions.py +149 -0
  73. package/skills/vds-skill/runtime/bitbucket_orchestrator/tests/test_config.py +297 -0
  74. package/skills/vds-skill/runtime/bitbucket_orchestrator/tests/test_deployment_env.py +352 -0
  75. package/skills/vds-skill/runtime/bitbucket_orchestrator/tests/test_errors.py +67 -0
  76. package/skills/vds-skill/runtime/bitbucket_orchestrator/tests/test_factory.py +352 -0
  77. package/skills/vds-skill/runtime/bitbucket_orchestrator/tests/test_fork_operations.py +203 -0
  78. package/skills/vds-skill/runtime/bitbucket_orchestrator/tests/test_issue_cli.py +262 -0
  79. package/skills/vds-skill/runtime/bitbucket_orchestrator/tests/test_pipeline_advanced.py +265 -0
  80. package/skills/vds-skill/runtime/bitbucket_orchestrator/tests/test_pr_blocker.py +206 -0
  81. package/skills/vds-skill/runtime/bitbucket_orchestrator/tests/test_protocols.py +336 -0
  82. package/skills/vds-skill/runtime/bitbucket_orchestrator/tests/test_repo_ops_archive.py +169 -0
  83. package/skills/vds-skill/runtime/bitbucket_orchestrator/tests/test_repo_ops_clone.py +115 -0
  84. package/skills/vds-skill/runtime/bitbucket_orchestrator/tests/test_repo_ops_parsing.py +149 -0
  85. package/skills/vds-skill/runtime/bitbucket_orchestrator/tests/test_repo_settings.py +336 -0
  86. package/skills/vds-skill/runtime/bitbucket_orchestrator/tests/test_repo_variables.py +266 -0
  87. package/skills/vds-skill/runtime/bitbucket_orchestrator/tests/test_webhooks.py +188 -0
  88. package/skills/vds-skill/runtime/bitbucket_orchestrator/tests/test_workspace.py +234 -0
  89. package/skills/vds-skill/runtime/bitbucket_orchestrator/tests/unit/__init__.py +0 -0
  90. package/skills/vds-skill/runtime/bitbucket_orchestrator/tests/unit/test_parsers.py +254 -0
  91. package/skills/vds-skill/runtime/brd_orchestrator/README.md +29 -0
  92. package/skills/vds-skill/runtime/brd_orchestrator/pyproject.toml +63 -0
  93. package/skills/vds-skill/runtime/brd_orchestrator/src/vds_brd_orchestrator/__init__.py +17 -0
  94. package/skills/vds-skill/runtime/brd_orchestrator/src/vds_brd_orchestrator/cli.py +187 -0
  95. package/skills/vds-skill/runtime/brd_orchestrator/src/vds_brd_orchestrator/validator.py +121 -0
  96. package/skills/vds-skill/runtime/brd_orchestrator/tests/__init__.py +0 -0
  97. package/skills/vds-skill/runtime/brd_orchestrator/tests/test_cli.py +62 -0
  98. package/skills/vds-skill/runtime/brd_orchestrator/tests/test_validator.py +33 -0
  99. package/skills/vds-skill/runtime/code/code_evidence_pack.json +435 -0
  100. package/skills/vds-skill/runtime/confluence_orchestrator/Dockerfile +19 -0
  101. package/skills/vds-skill/runtime/confluence_orchestrator/README.md +479 -0
  102. package/skills/vds-skill/runtime/confluence_orchestrator/SYNC_SCRIPTS.md +127 -0
  103. package/skills/vds-skill/runtime/confluence_orchestrator/SYNC_STANDARDIZATION.md +108 -0
  104. package/skills/vds-skill/runtime/confluence_orchestrator/pyproject.toml +50 -0
  105. package/skills/vds-skill/runtime/confluence_orchestrator/src/confluence_orchestrator/__init__.py +56 -0
  106. package/skills/vds-skill/runtime/confluence_orchestrator/src/confluence_orchestrator/async_client.py +100 -0
  107. package/skills/vds-skill/runtime/confluence_orchestrator/src/confluence_orchestrator/cli.py +3160 -0
  108. package/skills/vds-skill/runtime/confluence_orchestrator/src/confluence_orchestrator/config.py +215 -0
  109. package/skills/vds-skill/runtime/confluence_orchestrator/src/confluence_orchestrator/content.py +368 -0
  110. package/skills/vds-skill/runtime/confluence_orchestrator/src/confluence_orchestrator/content_v2.py +144 -0
  111. package/skills/vds-skill/runtime/confluence_orchestrator/src/confluence_orchestrator/crawl_tree.py +1833 -0
  112. package/skills/vds-skill/runtime/confluence_orchestrator/src/confluence_orchestrator/errors.py +44 -0
  113. package/skills/vds-skill/runtime/confluence_orchestrator/src/confluence_orchestrator/eventing.py +111 -0
  114. package/skills/vds-skill/runtime/confluence_orchestrator/src/confluence_orchestrator/http.py +1850 -0
  115. package/skills/vds-skill/runtime/confluence_orchestrator/src/confluence_orchestrator/orchestration.py +166 -0
  116. package/skills/vds-skill/runtime/confluence_orchestrator/src/confluence_orchestrator/protocols.py +61 -0
  117. package/skills/vds-skill/runtime/confluence_orchestrator/src/confluence_orchestrator/reporting.py +78 -0
  118. package/skills/vds-skill/runtime/confluence_orchestrator/src/confluence_orchestrator/tree.py +122 -0
  119. package/skills/vds-skill/runtime/confluence_orchestrator/src/confluence_orchestrator/tree_copier.py +431 -0
  120. package/skills/vds-skill/runtime/confluence_orchestrator/sync_pdfs_from_markdown.py +203 -0
  121. package/skills/vds-skill/runtime/confluence_orchestrator/sync_pdfs_to_confluence.py +299 -0
  122. package/skills/vds-skill/runtime/confluence_orchestrator/sync_png_attachments.py +299 -0
  123. package/skills/vds-skill/runtime/confluence_orchestrator/tests/conftest.py +46 -0
  124. package/skills/vds-skill/runtime/confluence_orchestrator/tests/test_advanced_content.py +252 -0
  125. package/skills/vds-skill/runtime/confluence_orchestrator/tests/test_advanced_search.py +193 -0
  126. package/skills/vds-skill/runtime/confluence_orchestrator/tests/test_async_client.py +104 -0
  127. package/skills/vds-skill/runtime/confluence_orchestrator/tests/test_cache_management.py +246 -0
  128. package/skills/vds-skill/runtime/confluence_orchestrator/tests/test_cli.py +716 -0
  129. package/skills/vds-skill/runtime/confluence_orchestrator/tests/test_config.py +130 -0
  130. package/skills/vds-skill/runtime/confluence_orchestrator/tests/test_content.py +192 -0
  131. package/skills/vds-skill/runtime/confluence_orchestrator/tests/test_content_flags.py +27 -0
  132. package/skills/vds-skill/runtime/confluence_orchestrator/tests/test_content_labels.py +94 -0
  133. package/skills/vds-skill/runtime/confluence_orchestrator/tests/test_crawl_tree.py +2252 -0
  134. package/skills/vds-skill/runtime/confluence_orchestrator/tests/test_degraded_write_safety.py +176 -0
  135. package/skills/vds-skill/runtime/confluence_orchestrator/tests/test_draft_management.py +225 -0
  136. package/skills/vds-skill/runtime/confluence_orchestrator/tests/test_errors.py +75 -0
  137. package/skills/vds-skill/runtime/confluence_orchestrator/tests/test_eventing.py +73 -0
  138. package/skills/vds-skill/runtime/confluence_orchestrator/tests/test_eventing_chaos.py +37 -0
  139. package/skills/vds-skill/runtime/confluence_orchestrator/tests/test_eventing_rate_limit.py +44 -0
  140. package/skills/vds-skill/runtime/confluence_orchestrator/tests/test_eventing_timeout.py +49 -0
  141. package/skills/vds-skill/runtime/confluence_orchestrator/tests/test_export.py +231 -0
  142. package/skills/vds-skill/runtime/confluence_orchestrator/tests/test_history.py +217 -0
  143. package/skills/vds-skill/runtime/confluence_orchestrator/tests/test_http.py +375 -0
  144. package/skills/vds-skill/runtime/confluence_orchestrator/tests/test_orchestration.py +93 -0
  145. package/skills/vds-skill/runtime/confluence_orchestrator/tests/test_reporting.py +24 -0
  146. package/skills/vds-skill/runtime/confluence_orchestrator/tests/test_search_cql.py +36 -0
  147. package/skills/vds-skill/runtime/confluence_orchestrator/tests/test_space_management.py +236 -0
  148. package/skills/vds-skill/runtime/confluence_orchestrator/tests/test_space_permissions.py +384 -0
  149. package/skills/vds-skill/runtime/confluence_orchestrator/tests/test_tree_copier.py +644 -0
  150. package/skills/vds-skill/runtime/confluence_orchestrator/tests/test_tree_copier_remap.py +289 -0
  151. package/skills/vds-skill/runtime/confluence_orchestrator/tests/test_user_group_management.py +387 -0
  152. package/skills/vds-skill/runtime/diagram_generator/README.md +663 -0
  153. package/skills/vds-skill/runtime/diagram_generator/ci_validate.sh +16 -0
  154. package/skills/vds-skill/runtime/diagram_generator/docs-nttc/projects/INSURANCE/analysis/current-state/insurance-claim-business/insurance-claim-business-component.png +0 -0
  155. package/skills/vds-skill/runtime/diagram_generator/docs-nttc/projects/INSURANCE/analysis/current-state/insurance-claim-business/insurance-claim-business-component.puml +23 -0
  156. package/skills/vds-skill/runtime/diagram_generator/docs-nttc/projects/INSURANCE/analysis/current-state/insurance-claim-business/insurance-claim-business-sequence.png +0 -0
  157. package/skills/vds-skill/runtime/diagram_generator/docs-nttc/projects/INSURANCE/analysis/current-state/insurance-claim-business/insurance-claim-business-sequence.puml +21 -0
  158. package/skills/vds-skill/runtime/diagram_generator/docs-nttc/projects/INSURANCE/analysis/current-state/insurance-claim-business/insurance-claim-business-usecase.png +0 -0
  159. package/skills/vds-skill/runtime/diagram_generator/docs-nttc/projects/INSURANCE/analysis/current-state/insurance-claim-business/insurance-claim-business-usecase.puml +14 -0
  160. package/skills/vds-skill/runtime/diagram_generator/examples/github-actions-validate.yml +39 -0
  161. package/skills/vds-skill/runtime/diagram_generator/generate_all_diagrams.py +827 -0
  162. package/skills/vds-skill/runtime/diagram_generator/generate_insurance_c4_diagrams.py +261 -0
  163. package/skills/vds-skill/runtime/diagram_generator/generate_insurance_c4_quick.py +486 -0
  164. package/skills/vds-skill/runtime/diagram_generator/pyproject.toml +28 -0
  165. package/skills/vds-skill/runtime/diagram_generator/render_png.py +59 -0
  166. package/skills/vds-skill/runtime/diagram_generator/src/vds_diagram_generator/__init__.py +3 -0
  167. package/skills/vds-skill/runtime/diagram_generator/src/vds_diagram_generator/cli.py +50 -0
  168. package/skills/vds-skill/runtime/diagram_generator/test_c4_hierarchical.py +142 -0
  169. package/skills/vds-skill/runtime/diagram_generator/test_c4_quick.py +131 -0
  170. package/skills/vds-skill/runtime/diagram_generator/tests/__init__.py +0 -0
  171. package/skills/vds-skill/runtime/diagram_generator/tests/test_analyzer_completeness.py +260 -0
  172. package/skills/vds-skill/runtime/diagram_generator/tests/test_c4_syntax_correctness.py +138 -0
  173. package/skills/vds-skill/runtime/diagram_generator/tests/test_component_coverage.py +182 -0
  174. package/skills/vds-skill/runtime/diagram_generator/tests/test_mermaid_output.py +80 -0
  175. package/skills/vds-skill/runtime/diagram_generator/tests/test_png_generation.py +112 -0
  176. package/skills/vds-skill/runtime/diagram_generator/tests/test_scenario_templates.py +15 -0
  177. package/skills/vds-skill/runtime/diagram_generator/tests/test_sequence_accuracy.py +93 -0
  178. package/skills/vds-skill/runtime/diagram_generator/tests/test_structurizr_export.py +177 -0
  179. package/skills/vds-skill/runtime/diagram_generator/tests/test_style_consistency.py +174 -0
  180. package/skills/vds-skill/runtime/diagram_generator/tests/test_usecase_generator.py +201 -0
  181. package/skills/vds-skill/runtime/diagram_generator/tests/test_usecase_integration.py +124 -0
  182. package/skills/vds-skill/runtime/docker/.dockerignore +38 -0
  183. package/skills/vds-skill/runtime/docker/ADR.md +392 -0
  184. package/skills/vds-skill/runtime/docker/Dockerfile +68 -0
  185. package/skills/vds-skill/runtime/docker/MIGRATION.md +453 -0
  186. package/skills/vds-skill/runtime/docker/README.md +347 -0
  187. package/skills/vds-skill/runtime/docker/ROLLBACK.md +596 -0
  188. package/skills/vds-skill/runtime/docker/compose.phase2-verification.yml +31 -0
  189. package/skills/vds-skill/runtime/docker/docker-compose.cli.yml +206 -0
  190. package/skills/vds-skill/runtime/docker/docker-compose.infra.yml +276 -0
  191. package/skills/vds-skill/runtime/docker/docker-compose.services.yml +425 -0
  192. package/skills/vds-skill/runtime/docker/infrastructure/init-schemas.sql +177 -0
  193. package/skills/vds-skill/runtime/docker/infrastructure/pgbouncer/pgbouncer.ini +75 -0
  194. package/skills/vds-skill/runtime/docker/infrastructure/pgbouncer/userlist.txt +50 -0
  195. package/skills/vds-skill/runtime/docker/infrastructure/pgbouncer/userlist.txt.template +36 -0
  196. package/skills/vds-skill/runtime/docs/.confluence-evidence/.gitkeep +0 -0
  197. package/skills/vds-skill/runtime/docs/.confluence-evidence/PREFLIGHT.md +132 -0
  198. package/skills/vds-skill/runtime/docs/.confluence-evidence/README.md +84 -0
  199. package/skills/vds-skill/runtime/docs/.confluence.yaml +157 -0
  200. package/skills/vds-skill/runtime/docs/.freshness.yaml +54 -0
  201. package/skills/vds-skill/runtime/docs/README.md +235 -0
  202. package/skills/vds-skill/runtime/docs/agents/README.md +33 -0
  203. package/skills/vds-skill/runtime/docs/agents/explanation/data-flow.md +132 -0
  204. package/skills/vds-skill/runtime/docs/agents/explanation/development-roadmap.md +49 -0
  205. package/skills/vds-skill/runtime/docs/agents/explanation/features-overview.md +62 -0
  206. package/skills/vds-skill/runtime/docs/agents/explanation/index.md +36 -0
  207. package/skills/vds-skill/runtime/docs/agents/explanation/runtime-verification-and-gap-reporting.md +127 -0
  208. package/skills/vds-skill/runtime/docs/agents/explanation/system-architecture.md +139 -0
  209. package/skills/vds-skill/runtime/docs/agents/explanation/whats-new.md +75 -0
  210. package/skills/vds-skill/runtime/docs/agents/explanation/who-ecosystem-introduction.md +65 -0
  211. package/skills/vds-skill/runtime/docs/agents/explanation/who-ecosystem-model.md +41 -0
  212. package/skills/vds-skill/runtime/docs/agents/how-to/02-using-vds-ai-memory.md +98 -0
  213. package/skills/vds-skill/runtime/docs/agents/how-to/03-memory-cross-agent.md +241 -0
  214. package/skills/vds-skill/runtime/docs/agents/how-to/04-using-progress-reports.md +240 -0
  215. package/skills/vds-skill/runtime/docs/agents/how-to/08-semantic-search.md +46 -0
  216. package/skills/vds-skill/runtime/docs/agents/how-to/apply-phase3-migration.md +148 -0
  217. package/skills/vds-skill/runtime/docs/agents/how-to/choose-the-right-command-or-skill.md +34 -0
  218. package/skills/vds-skill/runtime/docs/agents/how-to/contribute-new-orchestrator.md +149 -0
  219. package/skills/vds-skill/runtime/docs/agents/how-to/decision-tree.md +63 -0
  220. package/skills/vds-skill/runtime/docs/agents/how-to/first-audit-run.md +83 -0
  221. package/skills/vds-skill/runtime/docs/agents/how-to/index.md +49 -0
  222. package/skills/vds-skill/runtime/docs/agents/how-to/install-and-bootstrap-who-scripts-and-skills.md +314 -0
  223. package/skills/vds-skill/runtime/docs/agents/how-to/orchestrator-workflows/analytics-pipeline-workflow.md +165 -0
  224. package/skills/vds-skill/runtime/docs/agents/how-to/orchestrator-workflows/code-quality-gate-workflow.md +138 -0
  225. package/skills/vds-skill/runtime/docs/agents/how-to/orchestrator-workflows/confluence-bitbucket-sync-workflow.md +130 -0
  226. package/skills/vds-skill/runtime/docs/agents/how-to/orchestrator-workflows/document-delivery-workflow.md +142 -0
  227. package/skills/vds-skill/runtime/docs/agents/how-to/orchestrator-workflows/memory-progress-workflow.md +140 -0
  228. package/skills/vds-skill/runtime/docs/agents/how-to/orchestrator-workflows/research-spec-audit-workflow.md +135 -0
  229. package/skills/vds-skill/runtime/docs/agents/how-to/phase131-all-project-preparation.md +211 -0
  230. package/skills/vds-skill/runtime/docs/agents/how-to/phase131-bounded-parallel-analysis.md +123 -0
  231. package/skills/vds-skill/runtime/docs/agents/how-to/phase131-confluence-upload-recovery.md +204 -0
  232. package/skills/vds-skill/runtime/docs/agents/how-to/phase132-department-preparation.md +144 -0
  233. package/skills/vds-skill/runtime/docs/agents/how-to/run-ecosystem-daily-report.md +213 -0
  234. package/skills/vds-skill/runtime/docs/agents/how-to/tips-and-tricks.md +138 -0
  235. package/skills/vds-skill/runtime/docs/agents/how-to/troubleshooting-guide.md +221 -0
  236. package/skills/vds-skill/runtime/docs/agents/reference/agent-operational-contract.md +162 -0
  237. package/skills/vds-skill/runtime/docs/agents/reference/alignment-phase179-report.md +144 -0
  238. package/skills/vds-skill/runtime/docs/agents/reference/audit-triage-playbook.md +256 -0
  239. package/skills/vds-skill/runtime/docs/agents/reference/backup-restore.md +132 -0
  240. package/skills/vds-skill/runtime/docs/agents/reference/bitbucket-orchestrator.md +56 -0
  241. package/skills/vds-skill/runtime/docs/agents/reference/brd-orchestrator.md +52 -0
  242. package/skills/vds-skill/runtime/docs/agents/reference/capability-coverage-review.md +51 -0
  243. package/skills/vds-skill/runtime/docs/agents/reference/ci-workflows.md +98 -0
  244. package/skills/vds-skill/runtime/docs/agents/reference/circular-dependency-orchestrator.md +55 -0
  245. package/skills/vds-skill/runtime/docs/agents/reference/cli-commands.md +583 -0
  246. package/skills/vds-skill/runtime/docs/agents/reference/cli-development-standards.md +41 -0
  247. package/skills/vds-skill/runtime/docs/agents/reference/cli-help-matrix.md +84 -0
  248. package/skills/vds-skill/runtime/docs/agents/reference/common-errors.md +126 -0
  249. package/skills/vds-skill/runtime/docs/agents/reference/configuration-reference.md +128 -0
  250. package/skills/vds-skill/runtime/docs/agents/reference/confluence-orchestrator.md +56 -0
  251. package/skills/vds-skill/runtime/docs/agents/reference/confluence-sync-target.md +111 -0
  252. package/skills/vds-skill/runtime/docs/agents/reference/confluence-sync.md +46 -0
  253. package/skills/vds-skill/runtime/docs/agents/reference/db-query-orchestrator.md +93 -0
  254. package/skills/vds-skill/runtime/docs/agents/reference/diagrams-orchestrator.md +52 -0
  255. package/skills/vds-skill/runtime/docs/agents/reference/ecosystem-daily-report.md +229 -0
  256. package/skills/vds-skill/runtime/docs/agents/reference/elastic-orchestrator.md +57 -0
  257. package/skills/vds-skill/runtime/docs/agents/reference/env-git-helper.md +216 -0
  258. package/skills/vds-skill/runtime/docs/agents/reference/evolution-orchestrator.md +113 -0
  259. package/skills/vds-skill/runtime/docs/agents/reference/excel-orchestrator.md +51 -0
  260. package/skills/vds-skill/runtime/docs/agents/reference/git-orchestrator.md +62 -0
  261. package/skills/vds-skill/runtime/docs/agents/reference/google-sheets-orchestrator.md +51 -0
  262. package/skills/vds-skill/runtime/docs/agents/reference/grafana-orchestrator.md +52 -0
  263. package/skills/vds-skill/runtime/docs/agents/reference/hexagonal-orchestrator.md +64 -0
  264. package/skills/vds-skill/runtime/docs/agents/reference/index.md +36 -0
  265. package/skills/vds-skill/runtime/docs/agents/reference/infrastructure-v2.15.md +67 -0
  266. package/skills/vds-skill/runtime/docs/agents/reference/intellij-orchestrator.md +50 -0
  267. package/skills/vds-skill/runtime/docs/agents/reference/jira-orchestrator.md +60 -0
  268. package/skills/vds-skill/runtime/docs/agents/reference/links-orchestrator.md +57 -0
  269. package/skills/vds-skill/runtime/docs/agents/reference/lint-cli.md +99 -0
  270. package/skills/vds-skill/runtime/docs/agents/reference/lsp-orchestrator.md +51 -0
  271. package/skills/vds-skill/runtime/docs/agents/reference/markdown-orchestrator.md +53 -0
  272. package/skills/vds-skill/runtime/docs/agents/reference/mcp-orchestrator.md +88 -0
  273. package/skills/vds-skill/runtime/docs/agents/reference/memory-orchestrator.md +53 -0
  274. package/skills/vds-skill/runtime/docs/agents/reference/metabase-orchestrator.md +51 -0
  275. package/skills/vds-skill/runtime/docs/agents/reference/migration-playbook.md +71 -0
  276. package/skills/vds-skill/runtime/docs/agents/reference/multi-agent-orchestrator.md +52 -0
  277. package/skills/vds-skill/runtime/docs/agents/reference/openapi-orchestrator.md +57 -0
  278. package/skills/vds-skill/runtime/docs/agents/reference/orchestrator-architecture.md +194 -0
  279. package/skills/vds-skill/runtime/docs/agents/reference/orchestrator-comparison-matrix.md +79 -0
  280. package/skills/vds-skill/runtime/docs/agents/reference/orchestrator-index.md +73 -0
  281. package/skills/vds-skill/runtime/docs/agents/reference/pdf-orchestrator.md +57 -0
  282. package/skills/vds-skill/runtime/docs/agents/reference/portable-paths-and-config.md +0 -0
  283. package/skills/vds-skill/runtime/docs/agents/reference/portable-paths-validation-matrix.md +129 -0
  284. package/skills/vds-skill/runtime/docs/agents/reference/progress-orchestrator.md +51 -0
  285. package/skills/vds-skill/runtime/docs/agents/reference/progress-report-cli.md +215 -0
  286. package/skills/vds-skill/runtime/docs/agents/reference/public-interface-orchestrator.md +73 -0
  287. package/skills/vds-skill/runtime/docs/agents/reference/research-orchestrator.md +53 -0
  288. package/skills/vds-skill/runtime/docs/agents/reference/schema-orchestrator.md +57 -0
  289. package/skills/vds-skill/runtime/docs/agents/reference/search-tools.md +34 -0
  290. package/skills/vds-skill/runtime/docs/agents/reference/skills-commands.md +256 -0
  291. package/skills/vds-skill/runtime/docs/agents/reference/skills-reference.md +32 -0
  292. package/skills/vds-skill/runtime/docs/agents/reference/sonarqube-orchestrator.md +62 -0
  293. package/skills/vds-skill/runtime/docs/agents/reference/spec-orchestrator.md +56 -0
  294. package/skills/vds-skill/runtime/docs/agents/reference/structure-orchestrator.md +69 -0
  295. package/skills/vds-skill/runtime/docs/agents/reference/system-requirements.md +76 -0
  296. package/skills/vds-skill/runtime/docs/agents/reference/tasks-orchestrator.md +53 -0
  297. package/skills/vds-skill/runtime/docs/agents/reference/validation-and-sync-notes.md +54 -0
  298. package/skills/vds-skill/runtime/docs/agents/reference/vds-ai-memory-api.md +51 -0
  299. package/skills/vds-skill/runtime/docs/agents/reference/vds-cli-reference.md +34 -0
  300. package/skills/vds-skill/runtime/docs/agents/reference/who-capability-inventory.md +96 -0
  301. package/skills/vds-skill/runtime/docs/agents/reference/who-capability-routing-matrix.md +14 -0
  302. package/skills/vds-skill/runtime/docs/agents/tutorials/feature-progression-guide.md +112 -0
  303. package/skills/vds-skill/runtime/docs/agents/tutorials/index.md +36 -0
  304. package/skills/vds-skill/runtime/docs/agents/tutorials/quick-start.md +50 -0
  305. package/skills/vds-skill/runtime/docs/agents/tutorials/who-skills-and-scripts-onboarding.md +47 -0
  306. package/skills/vds-skill/runtime/docs/agents/tutorials/zero-to-productive-developer.md +339 -0
  307. package/skills/vds-skill/runtime/docs/confluence/IMPLEMENTATION-SUMMARY.md +78 -0
  308. package/skills/vds-skill/runtime/docs/confluence/SYNC-GUIDE.md +47 -0
  309. package/skills/vds-skill/runtime/docs/deployment/offline-docker-image-load.md +59 -0
  310. package/skills/vds-skill/runtime/docs/evolution-auto-run-rollout.md +325 -0
  311. package/skills/vds-skill/runtime/docs/evolution-loop-deep-integration.md +496 -0
  312. package/skills/vds-skill/runtime/docs/evolution-loop-integration-guide.md +359 -0
  313. package/skills/vds-skill/runtime/docs/openspace-schema-snapshot.md +73 -0
  314. package/skills/vds-skill/runtime/docs/operations/sla-mttr-policy.md +44 -0
  315. package/skills/vds-skill/runtime/docs/p0-closure-evidence/SUMMARY.md +58 -0
  316. package/skills/vds-skill/runtime/docs/p4-closure-evidence/.gitkeep +0 -0
  317. package/skills/vds-skill/runtime/docs/p4-closure-evidence/smoke-20260427T024137Z-b95b586b.json +15 -0
  318. package/skills/vds-skill/runtime/docs/p8-preflight-evidence/alembic-and-runtime-advisory-locks.md +45 -0
  319. package/skills/vds-skill/runtime/docs/p8-preflight-evidence/dbos-listen-notify.md +54 -0
  320. package/skills/vds-skill/runtime/docs/p8-preflight-evidence/pgbouncer-search-path-empirical.md +110 -0
  321. package/skills/vds-skill/runtime/docs/p8-preflight-evidence/pgvector-set-local-audit.md +51 -0
  322. package/skills/vds-skill/runtime/docs/p8-preflight-evidence/topology-decision-session-mode.md +57 -0
  323. package/skills/vds-skill/runtime/docs/phases/CHANGELOG.md +103 -0
  324. package/skills/vds-skill/runtime/docs/phases/PHASE_125_COMPLETION_AND_MERGE.md +212 -0
  325. package/skills/vds-skill/runtime/docs/phases/phase125/IMPLEMENTATION_REPORT.md +227 -0
  326. package/skills/vds-skill/runtime/docs/phases/phase125/TSK-125.10-11-implementation-summary.md +196 -0
  327. package/skills/vds-skill/runtime/docs/phases/phase125/profile-patch-ollama-local-anthropic.md +122 -0
  328. package/skills/vds-skill/runtime/docs/phases/phase125_completion_summary.md +369 -0
  329. package/skills/vds-skill/runtime/docs/phases/phase125_llm_analysis_skill.md +164 -0
  330. package/skills/vds-skill/runtime/docs/phases/phase125_merge_complete.md +147 -0
  331. package/skills/vds-skill/runtime/docs/phases/phase125_skill_runtime_closure_20260321.md +91 -0
  332. package/skills/vds-skill/runtime/docs/phases/phase2-portable-paths/closure-handoff-summary-2026-03-23.md +290 -0
  333. package/skills/vds-skill/runtime/docs/phases/phase2-portable-paths/remaining-risk-register-2026-03-25.md +143 -0
  334. package/skills/vds-skill/runtime/docs/phases/phase2-portable-paths/verification-evidence-2026-03-23.md +135 -0
  335. package/skills/vds-skill/runtime/docs/v0-sunset-known-issues.md +88 -0
  336. package/skills/vds-skill/runtime/docs/vi/TRANSLATION-BACKLOG.md +72 -0
  337. package/skills/vds-skill/runtime/docs/vi/agents/README.md +41 -0
  338. package/skills/vds-skill/runtime/docs/vi/agents/explanation/features-overview.md +29 -0
  339. package/skills/vds-skill/runtime/docs/vi/agents/explanation/index.md +14 -0
  340. package/skills/vds-skill/runtime/docs/vi/agents/explanation/runtime-verification-and-gap-reporting.md +129 -0
  341. package/skills/vds-skill/runtime/docs/vi/agents/explanation/whats-new.md +37 -0
  342. package/skills/vds-skill/runtime/docs/vi/agents/explanation/who-ecosystem-introduction.md +21 -0
  343. package/skills/vds-skill/runtime/docs/vi/agents/explanation/who-ecosystem-model.md +36 -0
  344. package/skills/vds-skill/runtime/docs/vi/agents/how-to/02-using-vds-ai-memory.md +100 -0
  345. package/skills/vds-skill/runtime/docs/vi/agents/how-to/03-memory-cross-agent.md +243 -0
  346. package/skills/vds-skill/runtime/docs/vi/agents/how-to/04-using-progress-reports.md +242 -0
  347. package/skills/vds-skill/runtime/docs/vi/agents/how-to/08-semantic-search.md +16 -0
  348. package/skills/vds-skill/runtime/docs/vi/agents/how-to/choose-the-right-command-or-skill.md +36 -0
  349. package/skills/vds-skill/runtime/docs/vi/agents/how-to/decision-tree.md +77 -0
  350. package/skills/vds-skill/runtime/docs/vi/agents/how-to/first-audit-run.md +85 -0
  351. package/skills/vds-skill/runtime/docs/vi/agents/how-to/index.md +21 -0
  352. package/skills/vds-skill/runtime/docs/vi/agents/how-to/install-and-bootstrap-who-scripts-and-skills.md +156 -0
  353. package/skills/vds-skill/runtime/docs/vi/agents/how-to/orchestrator-workflows/analytics-pipeline-workflow.md +174 -0
  354. package/skills/vds-skill/runtime/docs/vi/agents/how-to/orchestrator-workflows/code-quality-gate-workflow.md +147 -0
  355. package/skills/vds-skill/runtime/docs/vi/agents/how-to/orchestrator-workflows/confluence-bitbucket-sync-workflow.md +139 -0
  356. package/skills/vds-skill/runtime/docs/vi/agents/how-to/orchestrator-workflows/document-delivery-workflow.md +151 -0
  357. package/skills/vds-skill/runtime/docs/vi/agents/how-to/orchestrator-workflows/memory-progress-workflow.md +149 -0
  358. package/skills/vds-skill/runtime/docs/vi/agents/how-to/orchestrator-workflows/research-spec-audit-workflow.md +144 -0
  359. package/skills/vds-skill/runtime/docs/vi/agents/how-to/phase131-all-project-preparation.md +213 -0
  360. package/skills/vds-skill/runtime/docs/vi/agents/how-to/phase131-bounded-parallel-analysis.md +125 -0
  361. package/skills/vds-skill/runtime/docs/vi/agents/how-to/phase131-confluence-upload-recovery.md +206 -0
  362. package/skills/vds-skill/runtime/docs/vi/agents/how-to/phase132-department-preparation.md +146 -0
  363. package/skills/vds-skill/runtime/docs/vi/agents/how-to/tips-and-tricks.md +34 -0
  364. package/skills/vds-skill/runtime/docs/vi/agents/how-to/troubleshooting-guide.md +36 -0
  365. package/skills/vds-skill/runtime/docs/vi/agents/reference/agent-operational-contract.md +98 -0
  366. package/skills/vds-skill/runtime/docs/vi/agents/reference/audit-triage-playbook.md +258 -0
  367. package/skills/vds-skill/runtime/docs/vi/agents/reference/bitbucket-orchestrator.md +30 -0
  368. package/skills/vds-skill/runtime/docs/vi/agents/reference/brd-orchestrator.md +29 -0
  369. package/skills/vds-skill/runtime/docs/vi/agents/reference/capability-coverage-review.md +46 -0
  370. package/skills/vds-skill/runtime/docs/vi/agents/reference/circular-dependency-orchestrator.md +29 -0
  371. package/skills/vds-skill/runtime/docs/vi/agents/reference/cli-commands.md +409 -0
  372. package/skills/vds-skill/runtime/docs/vi/agents/reference/cli-development-standards.md +19 -0
  373. package/skills/vds-skill/runtime/docs/vi/agents/reference/cli-help-matrix.md +71 -0
  374. package/skills/vds-skill/runtime/docs/vi/agents/reference/common-errors.md +133 -0
  375. package/skills/vds-skill/runtime/docs/vi/agents/reference/configuration-reference.md +25 -0
  376. package/skills/vds-skill/runtime/docs/vi/agents/reference/confluence-orchestrator.md +30 -0
  377. package/skills/vds-skill/runtime/docs/vi/agents/reference/db-query-orchestrator.md +34 -0
  378. package/skills/vds-skill/runtime/docs/vi/agents/reference/diagrams-orchestrator.md +31 -0
  379. package/skills/vds-skill/runtime/docs/vi/agents/reference/elastic-orchestrator.md +30 -0
  380. package/skills/vds-skill/runtime/docs/vi/agents/reference/evolution-orchestrator.md +31 -0
  381. package/skills/vds-skill/runtime/docs/vi/agents/reference/excel-orchestrator.md +60 -0
  382. package/skills/vds-skill/runtime/docs/vi/agents/reference/git-orchestrator.md +31 -0
  383. package/skills/vds-skill/runtime/docs/vi/agents/reference/google-sheets-orchestrator.md +60 -0
  384. package/skills/vds-skill/runtime/docs/vi/agents/reference/grafana-orchestrator.md +30 -0
  385. package/skills/vds-skill/runtime/docs/vi/agents/reference/hexagonal-orchestrator.md +73 -0
  386. package/skills/vds-skill/runtime/docs/vi/agents/reference/index.md +25 -0
  387. package/skills/vds-skill/runtime/docs/vi/agents/reference/intellij-orchestrator.md +59 -0
  388. package/skills/vds-skill/runtime/docs/vi/agents/reference/jira-orchestrator.md +32 -0
  389. package/skills/vds-skill/runtime/docs/vi/agents/reference/links-orchestrator.md +66 -0
  390. package/skills/vds-skill/runtime/docs/vi/agents/reference/lsp-orchestrator.md +60 -0
  391. package/skills/vds-skill/runtime/docs/vi/agents/reference/markdown-orchestrator.md +62 -0
  392. package/skills/vds-skill/runtime/docs/vi/agents/reference/mcp-orchestrator.md +34 -0
  393. package/skills/vds-skill/runtime/docs/vi/agents/reference/memory-orchestrator.md +45 -0
  394. package/skills/vds-skill/runtime/docs/vi/agents/reference/metabase-orchestrator.md +30 -0
  395. package/skills/vds-skill/runtime/docs/vi/agents/reference/multi-agent-orchestrator.md +61 -0
  396. package/skills/vds-skill/runtime/docs/vi/agents/reference/openapi-orchestrator.md +66 -0
  397. package/skills/vds-skill/runtime/docs/vi/agents/reference/orchestrator-architecture.md +24 -0
  398. package/skills/vds-skill/runtime/docs/vi/agents/reference/orchestrator-index.md +73 -0
  399. package/skills/vds-skill/runtime/docs/vi/agents/reference/pdf-orchestrator.md +30 -0
  400. package/skills/vds-skill/runtime/docs/vi/agents/reference/portable-paths-and-config.md +123 -0
  401. package/skills/vds-skill/runtime/docs/vi/agents/reference/portable-paths-validation-matrix.md +131 -0
  402. package/skills/vds-skill/runtime/docs/vi/agents/reference/progress-orchestrator.md +43 -0
  403. package/skills/vds-skill/runtime/docs/vi/agents/reference/progress-report-cli.md +217 -0
  404. package/skills/vds-skill/runtime/docs/vi/agents/reference/public-interface-orchestrator.md +82 -0
  405. package/skills/vds-skill/runtime/docs/vi/agents/reference/research-orchestrator.md +45 -0
  406. package/skills/vds-skill/runtime/docs/vi/agents/reference/schema-orchestrator.md +66 -0
  407. package/skills/vds-skill/runtime/docs/vi/agents/reference/search-tools.md +19 -0
  408. package/skills/vds-skill/runtime/docs/vi/agents/reference/skills-reference.md +27 -0
  409. package/skills/vds-skill/runtime/docs/vi/agents/reference/sonarqube-orchestrator.md +71 -0
  410. package/skills/vds-skill/runtime/docs/vi/agents/reference/spec-orchestrator.md +56 -0
  411. package/skills/vds-skill/runtime/docs/vi/agents/reference/structure-orchestrator.md +78 -0
  412. package/skills/vds-skill/runtime/docs/vi/agents/reference/system-requirements.md +30 -0
  413. package/skills/vds-skill/runtime/docs/vi/agents/reference/tasks-orchestrator.md +45 -0
  414. package/skills/vds-skill/runtime/docs/vi/agents/reference/validation-and-sync-notes.md +26 -0
  415. package/skills/vds-skill/runtime/docs/vi/agents/reference/vds-ai-memory-api.md +53 -0
  416. package/skills/vds-skill/runtime/docs/vi/agents/reference/vds-cli-reference.md +34 -0
  417. package/skills/vds-skill/runtime/docs/vi/agents/reference/who-capability-inventory.md +98 -0
  418. package/skills/vds-skill/runtime/docs/vi/agents/reference/who-capability-routing-matrix.md +16 -0
  419. package/skills/vds-skill/runtime/docs/vi/agents/tutorials/feature-progression-guide.md +124 -0
  420. package/skills/vds-skill/runtime/docs/vi/agents/tutorials/index.md +13 -0
  421. package/skills/vds-skill/runtime/docs/vi/agents/tutorials/quick-start.md +30 -0
  422. package/skills/vds-skill/runtime/docs/vi/agents/tutorials/who-skills-and-scripts-onboarding.md +42 -0
  423. package/skills/vds-skill/runtime/docs/vi/agents/tutorials/zero-to-productive-developer.md +137 -0
  424. package/skills/vds-skill/runtime/elastic_orchestrator/README.md +450 -0
  425. package/skills/vds-skill/runtime/elastic_orchestrator/pyproject.toml +97 -0
  426. package/skills/vds-skill/runtime/elastic_orchestrator/src/vds_elastic_orchestrator/__init__.py +81 -0
  427. package/skills/vds-skill/runtime/elastic_orchestrator/src/vds_elastic_orchestrator/cli.py +652 -0
  428. package/skills/vds-skill/runtime/elastic_orchestrator/src/vds_elastic_orchestrator/client.py +743 -0
  429. package/skills/vds-skill/runtime/elastic_orchestrator/src/vds_elastic_orchestrator/config.py +208 -0
  430. package/skills/vds-skill/runtime/elastic_orchestrator/src/vds_elastic_orchestrator/errors.py +34 -0
  431. package/skills/vds-skill/runtime/elastic_orchestrator/src/vds_elastic_orchestrator/py.typed +0 -0
  432. package/skills/vds-skill/runtime/elastic_orchestrator/tests/__init__.py +0 -0
  433. package/skills/vds-skill/runtime/elastic_orchestrator/tests/conftest.py +227 -0
  434. package/skills/vds-skill/runtime/elastic_orchestrator/tests/test_client.py +990 -0
  435. package/skills/vds-skill/runtime/elastic_orchestrator/tests/test_config.py +268 -0
  436. package/skills/vds-skill/runtime/elastic_orchestrator/tests/test_e2e_verification.py +272 -0
  437. package/skills/vds-skill/runtime/elastic_orchestrator/tests/test_errors.py +78 -0
  438. package/skills/vds-skill/runtime/excel_orchestrator/README.md +288 -0
  439. package/skills/vds-skill/runtime/excel_orchestrator/RESEARCH_BASED_UPDATES_REPORT.md +261 -0
  440. package/skills/vds-skill/runtime/excel_orchestrator/add_essential_missing_effort.py +255 -0
  441. package/skills/vds-skill/runtime/excel_orchestrator/adjust_effort_complexity.py +184 -0
  442. package/skills/vds-skill/runtime/excel_orchestrator/brd_analysis_and_task_breakdown.py +632 -0
  443. package/skills/vds-skill/runtime/excel_orchestrator/brd_analysis_comprehensive.py +1029 -0
  444. package/skills/vds-skill/runtime/excel_orchestrator/check_overlaps_and_brd_coverage.py +570 -0
  445. package/skills/vds-skill/runtime/excel_orchestrator/clean_remarks_column.py +127 -0
  446. package/skills/vds-skill/runtime/excel_orchestrator/comprehensive_brd_check.py +322 -0
  447. package/skills/vds-skill/runtime/excel_orchestrator/create_buffered_summary.py +119 -0
  448. package/skills/vds-skill/runtime/excel_orchestrator/create_service_totals_sheet.py +118 -0
  449. package/skills/vds-skill/runtime/excel_orchestrator/examples/basic_operations.py +85 -0
  450. package/skills/vds-skill/runtime/excel_orchestrator/expand_all_tasks.py +341 -0
  451. package/skills/vds-skill/runtime/excel_orchestrator/expand_tasks.py +304 -0
  452. package/skills/vds-skill/runtime/excel_orchestrator/fill_brd_references.py +347 -0
  453. package/skills/vds-skill/runtime/excel_orchestrator/fill_remarks_and_colors.py +132 -0
  454. package/skills/vds-skill/runtime/excel_orchestrator/finalize_brd_and_cleanup.py +295 -0
  455. package/skills/vds-skill/runtime/excel_orchestrator/finalize_brd_coverage.py +327 -0
  456. package/skills/vds-skill/runtime/excel_orchestrator/fix_all_formulas.py +99 -0
  457. package/skills/vds-skill/runtime/excel_orchestrator/fix_detail_presentation.py +113 -0
  458. package/skills/vds-skill/runtime/excel_orchestrator/fix_presentation_and_effort.py +116 -0
  459. package/skills/vds-skill/runtime/excel_orchestrator/fix_presentation_consistency.py +231 -0
  460. package/skills/vds-skill/runtime/excel_orchestrator/fix_remarks_matching.py +179 -0
  461. package/skills/vds-skill/runtime/excel_orchestrator/group_tasks_by_service_id.py +210 -0
  462. package/skills/vds-skill/runtime/excel_orchestrator/increase_brd_coverage.py +497 -0
  463. package/skills/vds-skill/runtime/excel_orchestrator/increase_effort_complexity.py +155 -0
  464. package/skills/vds-skill/runtime/excel_orchestrator/organize_and_deduplicate.py +273 -0
  465. package/skills/vds-skill/runtime/excel_orchestrator/pyproject.toml +64 -0
  466. package/skills/vds-skill/runtime/excel_orchestrator/rebuild_all_formulas.py +146 -0
  467. package/skills/vds-skill/runtime/excel_orchestrator/remove_base_multiplier_and_check_duplicates.py +310 -0
  468. package/skills/vds-skill/runtime/excel_orchestrator/remove_duplicate_brd_tasks.py +137 -0
  469. package/skills/vds-skill/runtime/excel_orchestrator/research_based_updates.py +457 -0
  470. package/skills/vds-skill/runtime/excel_orchestrator/restore_e_values.py +172 -0
  471. package/skills/vds-skill/runtime/excel_orchestrator/src/vds_excel_orchestrator/__init__.py +5 -0
  472. package/skills/vds-skill/runtime/excel_orchestrator/src/vds_excel_orchestrator/cli.py +746 -0
  473. package/skills/vds-skill/runtime/excel_orchestrator/src/vds_excel_orchestrator/config.py +74 -0
  474. package/skills/vds-skill/runtime/excel_orchestrator/src/vds_excel_orchestrator/converters.py +226 -0
  475. package/skills/vds-skill/runtime/excel_orchestrator/src/vds_excel_orchestrator/errors.py +88 -0
  476. package/skills/vds-skill/runtime/excel_orchestrator/src/vds_excel_orchestrator/excel_client.py +443 -0
  477. package/skills/vds-skill/runtime/excel_orchestrator/src/vds_excel_orchestrator/formatters.py +211 -0
  478. package/skills/vds-skill/runtime/excel_orchestrator/src/vds_excel_orchestrator/logging.py +57 -0
  479. package/skills/vds-skill/runtime/excel_orchestrator/src/vds_excel_orchestrator/source_contract.py +29 -0
  480. package/skills/vds-skill/runtime/excel_orchestrator/src/vds_excel_orchestrator/target_state_status.py +837 -0
  481. package/skills/vds-skill/runtime/excel_orchestrator/src/vds_excel_orchestrator/ulnc_alignment.py +1291 -0
  482. package/skills/vds-skill/runtime/excel_orchestrator/src/vds_excel_orchestrator/validators.py +164 -0
  483. package/skills/vds-skill/runtime/excel_orchestrator/sync_detail_and_total_sheets.py +211 -0
  484. package/skills/vds-skill/runtime/excel_orchestrator/tests/__init__.py +1 -0
  485. package/skills/vds-skill/runtime/excel_orchestrator/tests/conftest.py +36 -0
  486. package/skills/vds-skill/runtime/excel_orchestrator/tests/test_cli.py +383 -0
  487. package/skills/vds-skill/runtime/excel_orchestrator/tests/test_excel_client.py +129 -0
  488. package/skills/vds-skill/runtime/excel_orchestrator/tests/test_ulnc_alignment.py +373 -0
  489. package/skills/vds-skill/runtime/excel_orchestrator/tests/test_validators.py +64 -0
  490. package/skills/vds-skill/runtime/excel_orchestrator/update_api_database_effort.py +261 -0
  491. package/skills/vds-skill/runtime/excel_orchestrator/update_buffers_inline.py +115 -0
  492. package/skills/vds-skill/runtime/excel_orchestrator/update_complex_services_and_add_new.py +336 -0
  493. package/skills/vds-skill/runtime/excel_orchestrator/update_responsibility_and_fix_rows.py +208 -0
  494. package/skills/vds-skill/runtime/excel_orchestrator/update_task_breakdown_vietnamese.py +309 -0
  495. package/skills/vds-skill/runtime/excel_orchestrator/update_vietnamese_and_responsibility.py +415 -0
  496. package/skills/vds-skill/runtime/excel_orchestrator/verify_brd_coverage_comprehensive.py +401 -0
  497. package/skills/vds-skill/runtime/git_orchestrator/ENHANCEMENT_SUMMARY.md +119 -0
  498. package/skills/vds-skill/runtime/git_orchestrator/README.md +286 -0
  499. package/skills/vds-skill/runtime/git_orchestrator/VERIFICATION_REPORT.md +152 -0
  500. package/skills/vds-skill/runtime/git_orchestrator/pyproject.toml +37 -0
  501. package/skills/vds-skill/runtime/git_orchestrator/src/vds_git_orchestrator/__init__.py +30 -0
  502. package/skills/vds-skill/runtime/git_orchestrator/src/vds_git_orchestrator/__main__.py +4 -0
  503. package/skills/vds-skill/runtime/git_orchestrator/src/vds_git_orchestrator/branch_probe.py +271 -0
  504. package/skills/vds-skill/runtime/git_orchestrator/src/vds_git_orchestrator/cli.py +892 -0
  505. package/skills/vds-skill/runtime/git_orchestrator/src/vds_git_orchestrator/logging_config.py +63 -0
  506. package/skills/vds-skill/runtime/git_orchestrator/src/vds_git_orchestrator/manifest.py +249 -0
  507. package/skills/vds-skill/runtime/git_orchestrator/src/vds_git_orchestrator/orchestrator.py +1647 -0
  508. package/skills/vds-skill/runtime/git_orchestrator/src/vds_git_orchestrator/protocols.py +35 -0
  509. package/skills/vds-skill/runtime/git_orchestrator/src/vds_git_orchestrator/reporting.py +55 -0
  510. package/skills/vds-skill/runtime/git_orchestrator/tests/__init__.py +0 -0
  511. package/skills/vds-skill/runtime/git_orchestrator/tests/test_cli_settings.py +19 -0
  512. package/skills/vds-skill/runtime/git_orchestrator/tests/test_integration.py +79 -0
  513. package/skills/vds-skill/runtime/git_orchestrator/tests/test_manifest.py +79 -0
  514. package/skills/vds-skill/runtime/git_orchestrator/tests/test_orchestrator.py +207 -0
  515. package/skills/vds-skill/runtime/git_orchestrator/tests/test_public_api.py +235 -0
  516. package/skills/vds-skill/runtime/git_orchestrator/tests/test_resilience.py +343 -0
  517. package/skills/vds-skill/runtime/git_orchestrator/tests/unit/__init__.py +0 -0
  518. package/skills/vds-skill/runtime/git_orchestrator/tests/unit/test_branch_probe.py +327 -0
  519. package/skills/vds-skill/runtime/git_orchestrator/tests/unit/test_protocols.py +132 -0
  520. package/skills/vds-skill/runtime/google_sheets_orchestrator/README.md +241 -0
  521. package/skills/vds-skill/runtime/google_sheets_orchestrator/pyproject.toml +45 -0
  522. package/skills/vds-skill/runtime/google_sheets_orchestrator/src/vds_google_sheets_orchestrator/__init__.py +69 -0
  523. package/skills/vds-skill/runtime/google_sheets_orchestrator/src/vds_google_sheets_orchestrator/cli.py +568 -0
  524. package/skills/vds-skill/runtime/google_sheets_orchestrator/src/vds_google_sheets_orchestrator/client.py +186 -0
  525. package/skills/vds-skill/runtime/google_sheets_orchestrator/src/vds_google_sheets_orchestrator/config.py +46 -0
  526. package/skills/vds-skill/runtime/google_sheets_orchestrator/src/vds_google_sheets_orchestrator/errors.py +41 -0
  527. package/skills/vds-skill/runtime/google_sheets_orchestrator/tests/__init__.py +1 -0
  528. package/skills/vds-skill/runtime/google_sheets_orchestrator/tests/conftest.py +1 -0
  529. package/skills/vds-skill/runtime/google_sheets_orchestrator/tests/unit/__init__.py +1 -0
  530. package/skills/vds-skill/runtime/google_sheets_orchestrator/tests/unit/test_cli.py +212 -0
  531. package/skills/vds-skill/runtime/google_sheets_orchestrator/tests/unit/test_client.py +24 -0
  532. package/skills/vds-skill/runtime/google_sheets_orchestrator/tests/unit/test_config.py +16 -0
  533. package/skills/vds-skill/runtime/grafana_orchestrator/README.md +572 -0
  534. package/skills/vds-skill/runtime/grafana_orchestrator/pyproject.toml +102 -0
  535. package/skills/vds-skill/runtime/grafana_orchestrator/src/vds_grafana_orchestrator/__init__.py +78 -0
  536. package/skills/vds-skill/runtime/grafana_orchestrator/src/vds_grafana_orchestrator/cli.py +455 -0
  537. package/skills/vds-skill/runtime/grafana_orchestrator/src/vds_grafana_orchestrator/client.py +700 -0
  538. package/skills/vds-skill/runtime/grafana_orchestrator/src/vds_grafana_orchestrator/config.py +243 -0
  539. package/skills/vds-skill/runtime/grafana_orchestrator/src/vds_grafana_orchestrator/errors.py +34 -0
  540. package/skills/vds-skill/runtime/grafana_orchestrator/src/vds_grafana_orchestrator/py.typed +1 -0
  541. package/skills/vds-skill/runtime/grafana_orchestrator/tests/__init__.py +1 -0
  542. package/skills/vds-skill/runtime/grafana_orchestrator/tests/conftest.py +308 -0
  543. package/skills/vds-skill/runtime/grafana_orchestrator/tests/test_client.py +458 -0
  544. package/skills/vds-skill/runtime/grafana_orchestrator/tests/test_config.py +203 -0
  545. package/skills/vds-skill/runtime/grafana_orchestrator/tests/test_errors.py +78 -0
  546. package/skills/vds-skill/runtime/jira_orchestrator/README.md +864 -0
  547. package/skills/vds-skill/runtime/jira_orchestrator/pyproject.toml +43 -0
  548. package/skills/vds-skill/runtime/jira_orchestrator/src/vds_jira_orchestrator/__init__.py +65 -0
  549. package/skills/vds-skill/runtime/jira_orchestrator/src/vds_jira_orchestrator/adapter.py +1685 -0
  550. package/skills/vds-skill/runtime/jira_orchestrator/src/vds_jira_orchestrator/cli.py +2806 -0
  551. package/skills/vds-skill/runtime/jira_orchestrator/src/vds_jira_orchestrator/config.py +168 -0
  552. package/skills/vds-skill/runtime/jira_orchestrator/src/vds_jira_orchestrator/errors.py +34 -0
  553. package/skills/vds-skill/runtime/jira_orchestrator/src/vds_jira_orchestrator/reporting.py +66 -0
  554. package/skills/vds-skill/runtime/jira_orchestrator/tests/__init__.py +1 -0
  555. package/skills/vds-skill/runtime/jira_orchestrator/tests/conftest.py +86 -0
  556. package/skills/vds-skill/runtime/jira_orchestrator/tests/test_adapter_agile_list_payloads.py +54 -0
  557. package/skills/vds-skill/runtime/jira_orchestrator/tests/test_adapter_bulk_operations.py +91 -0
  558. package/skills/vds-skill/runtime/jira_orchestrator/tests/test_adapter_components.py +56 -0
  559. package/skills/vds-skill/runtime/jira_orchestrator/tests/test_adapter_createmeta.py +45 -0
  560. package/skills/vds-skill/runtime/jira_orchestrator/tests/test_adapter_dashboard.py +119 -0
  561. package/skills/vds-skill/runtime/jira_orchestrator/tests/test_adapter_issue_properties.py +53 -0
  562. package/skills/vds-skill/runtime/jira_orchestrator/tests/test_adapter_permissions_compat.py +41 -0
  563. package/skills/vds-skill/runtime/jira_orchestrator/tests/test_adapter_reindex.py +42 -0
  564. package/skills/vds-skill/runtime/jira_orchestrator/tests/test_adapter_remote_links.py +75 -0
  565. package/skills/vds-skill/runtime/jira_orchestrator/tests/test_adapter_transitions.py +90 -0
  566. package/skills/vds-skill/runtime/jira_orchestrator/tests/test_adapter_user_management.py +116 -0
  567. package/skills/vds-skill/runtime/jira_orchestrator/tests/test_adapter_version_management.py +181 -0
  568. package/skills/vds-skill/runtime/jira_orchestrator/tests/test_adapter_watchers.py +43 -0
  569. package/skills/vds-skill/runtime/jira_orchestrator/tests/test_advanced_search.py +179 -0
  570. package/skills/vds-skill/runtime/jira_orchestrator/tests/test_agile.py +304 -0
  571. package/skills/vds-skill/runtime/jira_orchestrator/tests/test_application_properties.py +243 -0
  572. package/skills/vds-skill/runtime/jira_orchestrator/tests/test_backlog.py +91 -0
  573. package/skills/vds-skill/runtime/jira_orchestrator/tests/test_bulk_operations.py +403 -0
  574. package/skills/vds-skill/runtime/jira_orchestrator/tests/test_cli.py +108 -0
  575. package/skills/vds-skill/runtime/jira_orchestrator/tests/test_components.py +119 -0
  576. package/skills/vds-skill/runtime/jira_orchestrator/tests/test_config.py +166 -0
  577. package/skills/vds-skill/runtime/jira_orchestrator/tests/test_dashboard.py +122 -0
  578. package/skills/vds-skill/runtime/jira_orchestrator/tests/test_discover_fields.py +207 -0
  579. package/skills/vds-skill/runtime/jira_orchestrator/tests/test_errors.py +72 -0
  580. package/skills/vds-skill/runtime/jira_orchestrator/tests/test_filter_management.py +411 -0
  581. package/skills/vds-skill/runtime/jira_orchestrator/tests/test_issue_archiving.py +179 -0
  582. package/skills/vds-skill/runtime/jira_orchestrator/tests/test_issue_links.py +257 -0
  583. package/skills/vds-skill/runtime/jira_orchestrator/tests/test_issue_properties.py +189 -0
  584. package/skills/vds-skill/runtime/jira_orchestrator/tests/test_link_types.py +407 -0
  585. package/skills/vds-skill/runtime/jira_orchestrator/tests/test_parse_set.py +37 -0
  586. package/skills/vds-skill/runtime/jira_orchestrator/tests/test_permissions.py +343 -0
  587. package/skills/vds-skill/runtime/jira_orchestrator/tests/test_reindex.py +81 -0
  588. package/skills/vds-skill/runtime/jira_orchestrator/tests/test_remote_links.py +269 -0
  589. package/skills/vds-skill/runtime/jira_orchestrator/tests/test_security_schemes.py +202 -0
  590. package/skills/vds-skill/runtime/jira_orchestrator/tests/test_transitions_changelog.py +109 -0
  591. package/skills/vds-skill/runtime/jira_orchestrator/tests/test_user_management.py +246 -0
  592. package/skills/vds-skill/runtime/jira_orchestrator/tests/test_version_management.py +503 -0
  593. package/skills/vds-skill/runtime/jira_orchestrator/tests/test_watchers.py +116 -0
  594. package/skills/vds-skill/runtime/jira_orchestrator/tests/test_worklog.py +243 -0
  595. package/skills/vds-skill/runtime/jira_viettelmoney_orchestrator/README.md +864 -0
  596. package/skills/vds-skill/runtime/jira_viettelmoney_orchestrator/pyproject.toml +43 -0
  597. package/skills/vds-skill/runtime/jira_viettelmoney_orchestrator/src/vds_jira_viettelmoney_orchestrator/__init__.py +65 -0
  598. package/skills/vds-skill/runtime/jira_viettelmoney_orchestrator/src/vds_jira_viettelmoney_orchestrator/adapter.py +1689 -0
  599. package/skills/vds-skill/runtime/jira_viettelmoney_orchestrator/src/vds_jira_viettelmoney_orchestrator/cli.py +2799 -0
  600. package/skills/vds-skill/runtime/jira_viettelmoney_orchestrator/src/vds_jira_viettelmoney_orchestrator/config.py +137 -0
  601. package/skills/vds-skill/runtime/jira_viettelmoney_orchestrator/src/vds_jira_viettelmoney_orchestrator/errors.py +34 -0
  602. package/skills/vds-skill/runtime/jira_viettelmoney_orchestrator/src/vds_jira_viettelmoney_orchestrator/reporting.py +65 -0
  603. package/skills/vds-skill/runtime/jira_viettelmoney_orchestrator/tests/__init__.py +1 -0
  604. package/skills/vds-skill/runtime/jira_viettelmoney_orchestrator/tests/conftest.py +86 -0
  605. package/skills/vds-skill/runtime/jira_viettelmoney_orchestrator/tests/test_adapter_agile_list_payloads.py +54 -0
  606. package/skills/vds-skill/runtime/jira_viettelmoney_orchestrator/tests/test_adapter_bulk_operations.py +101 -0
  607. package/skills/vds-skill/runtime/jira_viettelmoney_orchestrator/tests/test_adapter_components.py +64 -0
  608. package/skills/vds-skill/runtime/jira_viettelmoney_orchestrator/tests/test_adapter_createmeta.py +45 -0
  609. package/skills/vds-skill/runtime/jira_viettelmoney_orchestrator/tests/test_adapter_dashboard.py +135 -0
  610. package/skills/vds-skill/runtime/jira_viettelmoney_orchestrator/tests/test_adapter_issue_properties.py +63 -0
  611. package/skills/vds-skill/runtime/jira_viettelmoney_orchestrator/tests/test_adapter_permissions_compat.py +42 -0
  612. package/skills/vds-skill/runtime/jira_viettelmoney_orchestrator/tests/test_adapter_reindex.py +42 -0
  613. package/skills/vds-skill/runtime/jira_viettelmoney_orchestrator/tests/test_adapter_remote_links.py +89 -0
  614. package/skills/vds-skill/runtime/jira_viettelmoney_orchestrator/tests/test_adapter_transitions.py +91 -0
  615. package/skills/vds-skill/runtime/jira_viettelmoney_orchestrator/tests/test_adapter_user_management.py +130 -0
  616. package/skills/vds-skill/runtime/jira_viettelmoney_orchestrator/tests/test_adapter_version_management.py +189 -0
  617. package/skills/vds-skill/runtime/jira_viettelmoney_orchestrator/tests/test_adapter_watchers.py +49 -0
  618. package/skills/vds-skill/runtime/jira_viettelmoney_orchestrator/tests/test_advanced_search.py +213 -0
  619. package/skills/vds-skill/runtime/jira_viettelmoney_orchestrator/tests/test_agile.py +334 -0
  620. package/skills/vds-skill/runtime/jira_viettelmoney_orchestrator/tests/test_application_properties.py +261 -0
  621. package/skills/vds-skill/runtime/jira_viettelmoney_orchestrator/tests/test_backlog.py +91 -0
  622. package/skills/vds-skill/runtime/jira_viettelmoney_orchestrator/tests/test_bulk_operations.py +443 -0
  623. package/skills/vds-skill/runtime/jira_viettelmoney_orchestrator/tests/test_cli.py +106 -0
  624. package/skills/vds-skill/runtime/jira_viettelmoney_orchestrator/tests/test_components.py +133 -0
  625. package/skills/vds-skill/runtime/jira_viettelmoney_orchestrator/tests/test_config.py +166 -0
  626. package/skills/vds-skill/runtime/jira_viettelmoney_orchestrator/tests/test_dashboard.py +130 -0
  627. package/skills/vds-skill/runtime/jira_viettelmoney_orchestrator/tests/test_discover_fields.py +207 -0
  628. package/skills/vds-skill/runtime/jira_viettelmoney_orchestrator/tests/test_errors.py +61 -0
  629. package/skills/vds-skill/runtime/jira_viettelmoney_orchestrator/tests/test_filter_management.py +478 -0
  630. package/skills/vds-skill/runtime/jira_viettelmoney_orchestrator/tests/test_issue_archiving.py +181 -0
  631. package/skills/vds-skill/runtime/jira_viettelmoney_orchestrator/tests/test_issue_links.py +257 -0
  632. package/skills/vds-skill/runtime/jira_viettelmoney_orchestrator/tests/test_issue_properties.py +203 -0
  633. package/skills/vds-skill/runtime/jira_viettelmoney_orchestrator/tests/test_link_types.py +426 -0
  634. package/skills/vds-skill/runtime/jira_viettelmoney_orchestrator/tests/test_parse_set.py +37 -0
  635. package/skills/vds-skill/runtime/jira_viettelmoney_orchestrator/tests/test_permissions.py +358 -0
  636. package/skills/vds-skill/runtime/jira_viettelmoney_orchestrator/tests/test_reindex.py +81 -0
  637. package/skills/vds-skill/runtime/jira_viettelmoney_orchestrator/tests/test_remote_links.py +292 -0
  638. package/skills/vds-skill/runtime/jira_viettelmoney_orchestrator/tests/test_security_schemes.py +218 -0
  639. package/skills/vds-skill/runtime/jira_viettelmoney_orchestrator/tests/test_transitions_changelog.py +121 -0
  640. package/skills/vds-skill/runtime/jira_viettelmoney_orchestrator/tests/test_user_management.py +283 -0
  641. package/skills/vds-skill/runtime/jira_viettelmoney_orchestrator/tests/test_version_management.py +561 -0
  642. package/skills/vds-skill/runtime/jira_viettelmoney_orchestrator/tests/test_watchers.py +128 -0
  643. package/skills/vds-skill/runtime/jira_viettelmoney_orchestrator/tests/test_worklog.py +265 -0
  644. package/skills/vds-skill/runtime/llms.txt +159 -0
  645. package/skills/vds-skill/runtime/markdown_orchestrator/README.md +72 -0
  646. package/skills/vds-skill/runtime/markdown_orchestrator/pyproject.toml +39 -0
  647. package/skills/vds-skill/runtime/markdown_orchestrator/src/vds_markdown_orchestrator/__init__.py +5 -0
  648. package/skills/vds-skill/runtime/markdown_orchestrator/src/vds_markdown_orchestrator/cli.py +102 -0
  649. package/skills/vds-skill/runtime/mcp_server/Dockerfile +63 -0
  650. package/skills/vds-skill/runtime/mcp_server/README.md +140 -0
  651. package/skills/vds-skill/runtime/mcp_server/pyproject.toml +41 -0
  652. package/skills/vds-skill/runtime/mcp_server/src/vds_mcp_server/__init__.py +3 -0
  653. package/skills/vds-skill/runtime/mcp_server/src/vds_mcp_server/config.py +36 -0
  654. package/skills/vds-skill/runtime/mcp_server/src/vds_mcp_server/server.py +111 -0
  655. package/skills/vds-skill/runtime/mcp_server/src/vds_mcp_server/tools/__init__.py +15 -0
  656. package/skills/vds-skill/runtime/mcp_server/src/vds_mcp_server/tools/bitbucket_tools.py +47 -0
  657. package/skills/vds-skill/runtime/mcp_server/src/vds_mcp_server/tools/confluence_tools.py +53 -0
  658. package/skills/vds-skill/runtime/mcp_server/src/vds_mcp_server/tools/git_tools.py +71 -0
  659. package/skills/vds-skill/runtime/mcp_server/src/vds_mcp_server/tools/jira_tools.py +63 -0
  660. package/skills/vds-skill/runtime/mcp_server/src/vds_mcp_server/tools/vidp_tools.py +64 -0
  661. package/skills/vds-skill/runtime/mcp_server/tests/__init__.py +1 -0
  662. package/skills/vds-skill/runtime/mcp_server/tests/conftest.py +31 -0
  663. package/skills/vds-skill/runtime/mcp_server/tests/unit/__init__.py +1 -0
  664. package/skills/vds-skill/runtime/mcp_server/tests/unit/test_bitbucket_tools.py +28 -0
  665. package/skills/vds-skill/runtime/mcp_server/tests/unit/test_confluence_tools.py +28 -0
  666. package/skills/vds-skill/runtime/mcp_server/tests/unit/test_git_tools.py +35 -0
  667. package/skills/vds-skill/runtime/mcp_server/tests/unit/test_jira_tools.py +35 -0
  668. package/skills/vds-skill/runtime/mcp_server/tests/verification/__init__.py +6 -0
  669. package/skills/vds-skill/runtime/mcp_server/tests/verification/conftest.py +51 -0
  670. package/skills/vds-skill/runtime/mcp_server/tests/verification/test_mcp_confluence_tools.py +40 -0
  671. package/skills/vds-skill/runtime/mcp_server/tests/verification/test_mcp_jira_tools.py +39 -0
  672. package/skills/vds-skill/runtime/mcp_server/tests/verification/test_mcp_tool_registration.py +50 -0
  673. package/skills/vds-skill/runtime/pdf_orchestrator/.dockerignore +93 -0
  674. package/skills/vds-skill/runtime/pdf_orchestrator/.env.example +40 -0
  675. package/skills/vds-skill/runtime/pdf_orchestrator/.ruff_rules.py +350 -0
  676. package/skills/vds-skill/runtime/pdf_orchestrator/.yamllint.yml +43 -0
  677. package/skills/vds-skill/runtime/pdf_orchestrator/DEVELOPMENT_PLAN.md +80 -0
  678. package/skills/vds-skill/runtime/pdf_orchestrator/Dockerfile +87 -0
  679. package/skills/vds-skill/runtime/pdf_orchestrator/README.md +608 -0
  680. package/skills/vds-skill/runtime/pdf_orchestrator/cli_verification_test/test.md +6 -0
  681. package/skills/vds-skill/runtime/pdf_orchestrator/cli_verification_test/test.pdf +0 -0
  682. package/skills/vds-skill/runtime/pdf_orchestrator/config/alertmanager.yml +83 -0
  683. package/skills/vds-skill/runtime/pdf_orchestrator/config/prometheus.prod.yml +98 -0
  684. package/skills/vds-skill/runtime/pdf_orchestrator/config/prometheus.yml +40 -0
  685. package/skills/vds-skill/runtime/pdf_orchestrator/config/redis.conf +78 -0
  686. package/skills/vds-skill/runtime/pdf_orchestrator/docs/COMPETITIVE_ANALYSIS_REPORT.md +309 -0
  687. package/skills/vds-skill/runtime/pdf_orchestrator/docs/FEATURES_GUIDE.md +518 -0
  688. package/skills/vds-skill/runtime/pdf_orchestrator/docs/MULTI_USER_DEPLOYMENT_GUIDE.md +615 -0
  689. package/skills/vds-skill/runtime/pdf_orchestrator/docs/USER_GUIDE.md +829 -0
  690. package/skills/vds-skill/runtime/pdf_orchestrator/pyproject.toml +87 -0
  691. package/skills/vds-skill/runtime/pdf_orchestrator/pytest.ini +71 -0
  692. package/skills/vds-skill/runtime/pdf_orchestrator/ruff.toml +6 -0
  693. package/skills/vds-skill/runtime/pdf_orchestrator/scripts/debug_security_report.py +59 -0
  694. package/skills/vds-skill/runtime/pdf_orchestrator/scripts/demo_library_selector.py +109 -0
  695. package/skills/vds-skill/runtime/pdf_orchestrator/scripts/generate_project_stats.py +52 -0
  696. package/skills/vds-skill/runtime/pdf_orchestrator/scripts/generate_styled_pdf.py +95 -0
  697. package/skills/vds-skill/runtime/pdf_orchestrator/scripts/migrate_render_pdfs.py +285 -0
  698. package/skills/vds-skill/runtime/pdf_orchestrator/scripts/setup_team.bat +283 -0
  699. package/skills/vds-skill/runtime/pdf_orchestrator/scripts/setup_team.sh +324 -0
  700. package/skills/vds-skill/runtime/pdf_orchestrator/src/vds_pdf_orchestrator/__init__.py +5 -0
  701. package/skills/vds-skill/runtime/pdf_orchestrator/src/vds_pdf_orchestrator/cli.py +542 -0
  702. package/skills/vds-skill/runtime/pdf_orchestrator/src/vds_pdf_orchestrator/config.py +33 -0
  703. package/skills/vds-skill/runtime/pdf_orchestrator/tests/README.md +650 -0
  704. package/skills/vds-skill/runtime/pdf_orchestrator/tests/__init__.py +0 -0
  705. package/skills/vds-skill/runtime/pdf_orchestrator/tests/conftest.py +520 -0
  706. package/skills/vds-skill/runtime/pdf_orchestrator/tests/requirements.txt +51 -0
  707. package/skills/vds-skill/runtime/pdf_orchestrator/tests/run_tests.py +659 -0
  708. package/skills/vds-skill/runtime/pdf_orchestrator/tests/test_config.py +36 -0
  709. package/skills/vds-skill/runtime/platform_core/pyproject.toml +49 -0
  710. package/skills/vds-skill/runtime/platform_core/src/vds_platform_core/__init__.py +16 -0
  711. package/skills/vds-skill/runtime/platform_core/src/vds_platform_core/alembic/__init__.py +18 -0
  712. package/skills/vds-skill/runtime/platform_core/src/vds_platform_core/alembic/runtime.py +139 -0
  713. package/skills/vds-skill/runtime/platform_core/src/vds_platform_core/config.py +88 -0
  714. package/skills/vds-skill/runtime/platform_core/src/vds_platform_core/credentials.py +40 -0
  715. package/skills/vds-skill/runtime/platform_core/src/vds_platform_core/env.py +24 -0
  716. package/skills/vds-skill/runtime/platform_core/src/vds_platform_core/errors.py +127 -0
  717. package/skills/vds-skill/runtime/platform_core/src/vds_platform_core/http/__init__.py +18 -0
  718. package/skills/vds-skill/runtime/platform_core/src/vds_platform_core/http/auth.py +32 -0
  719. package/skills/vds-skill/runtime/platform_core/src/vds_platform_core/http/errors.py +47 -0
  720. package/skills/vds-skill/runtime/platform_core/src/vds_platform_core/http/pagination.py +65 -0
  721. package/skills/vds-skill/runtime/platform_core/src/vds_platform_core/http/retry.py +62 -0
  722. package/skills/vds-skill/runtime/platform_core/src/vds_platform_core/http/stack.py +61 -0
  723. package/skills/vds-skill/runtime/platform_core/src/vds_platform_core/logging.py +132 -0
  724. package/skills/vds-skill/runtime/platform_core/src/vds_platform_core/protocols.py +77 -0
  725. package/skills/vds-skill/runtime/platform_core/src/vds_platform_core/serialization.py +80 -0
  726. package/skills/vds-skill/runtime/platform_core/src/vds_platform_core/severity.py +175 -0
  727. package/skills/vds-skill/runtime/platform_core/tests/__init__.py +0 -0
  728. package/skills/vds-skill/runtime/platform_core/tests/conftest.py +1 -0
  729. package/skills/vds-skill/runtime/platform_core/tests/test_alembic_runtime.py +300 -0
  730. package/skills/vds-skill/runtime/platform_core/tests/test_auth.py +84 -0
  731. package/skills/vds-skill/runtime/platform_core/tests/test_config.py +83 -0
  732. package/skills/vds-skill/runtime/platform_core/tests/test_credentials.py +73 -0
  733. package/skills/vds-skill/runtime/platform_core/tests/test_env.py +56 -0
  734. package/skills/vds-skill/runtime/platform_core/tests/test_errors.py +201 -0
  735. package/skills/vds-skill/runtime/platform_core/tests/test_errors_http.py +74 -0
  736. package/skills/vds-skill/runtime/platform_core/tests/test_http_settings.py +116 -0
  737. package/skills/vds-skill/runtime/platform_core/tests/test_logging.py +148 -0
  738. package/skills/vds-skill/runtime/platform_core/tests/test_pagination.py +153 -0
  739. package/skills/vds-skill/runtime/platform_core/tests/test_protocols.py +132 -0
  740. package/skills/vds-skill/runtime/platform_core/tests/test_retry.py +151 -0
  741. package/skills/vds-skill/runtime/platform_core/tests/test_serialization.py +92 -0
  742. package/skills/vds-skill/runtime/platform_core/tests/test_severity.py +178 -0
  743. package/skills/vds-skill/runtime/platform_core/tests/test_stack.py +130 -0
  744. package/skills/vds-skill/runtime/platform_core/uv.lock +341 -0
  745. package/skills/vds-skill/runtime/pyproject.toml +145 -0
  746. package/skills/vds-skill/runtime/pyrightconfig.json +82 -0
  747. package/skills/vds-skill/runtime/repo-manifest.yaml +380 -0
  748. package/skills/vds-skill/runtime/repo-manifest.yaml.example +25 -0
  749. package/skills/vds-skill/runtime/ruff.toml +100 -0
  750. package/skills/vds-skill/runtime/scripts/BRD-Validation-API.postman_collection.json +706 -0
  751. package/skills/vds-skill/runtime/scripts/BRD-Validation-README.md +308 -0
  752. package/skills/vds-skill/runtime/scripts/README.md +271 -0
  753. package/skills/vds-skill/runtime/scripts/_validate_alias_phase2.py +137 -0
  754. package/skills/vds-skill/runtime/scripts/audit-cli-patterns.sh +135 -0
  755. package/skills/vds-skill/runtime/scripts/audit-dashboard.sh +525 -0
  756. package/skills/vds-skill/runtime/scripts/backup.sh +123 -0
  757. package/skills/vds-skill/runtime/scripts/bootstrap_uv.sh +69 -0
  758. package/skills/vds-skill/runtime/scripts/brd-validation-environment.json +51 -0
  759. package/skills/vds-skill/runtime/scripts/brd-validation-test-results.json +13023 -0
  760. package/skills/vds-skill/runtime/scripts/brd_coverage_report.json +276 -0
  761. package/skills/vds-skill/runtime/scripts/check-future-annotations.py +22 -0
  762. package/skills/vds-skill/runtime/scripts/check-invalid-symlinks.py +183 -0
  763. package/skills/vds-skill/runtime/scripts/check-no-debug-markers.py +21 -0
  764. package/skills/vds-skill/runtime/scripts/check-no-unittest.py +21 -0
  765. package/skills/vds-skill/runtime/scripts/ci/assert_no_openspace_commits.sh +37 -0
  766. package/skills/vds-skill/runtime/scripts/ci/verify_branch_protection.sh +64 -0
  767. package/skills/vds-skill/runtime/scripts/closure/phase1_check.sh +483 -0
  768. package/skills/vds-skill/runtime/scripts/closure/phase2_check.sh +500 -0
  769. package/skills/vds-skill/runtime/scripts/create_memory_session.py +36 -0
  770. package/skills/vds-skill/runtime/scripts/deploy-bootstrap.sh +201 -0
  771. package/skills/vds-skill/runtime/scripts/deployment/load_docker_images_offline.sh +90 -0
  772. package/skills/vds-skill/runtime/scripts/dev/cli_smoke.sh +259 -0
  773. package/skills/vds-skill/runtime/scripts/final_completion_report.md +139 -0
  774. package/skills/vds-skill/runtime/scripts/folder_structure_report.json +321 -0
  775. package/skills/vds-skill/runtime/scripts/generate_completion_report.py +132 -0
  776. package/skills/vds-skill/runtime/scripts/generate_intellij_modules.py +154 -0
  777. package/skills/vds-skill/runtime/scripts/init-pgbouncer-userlist.sh +154 -0
  778. package/skills/vds-skill/runtime/scripts/link_integrity_report.json +807 -0
  779. package/skills/vds-skill/runtime/scripts/move_audit_artifact_pages.py +252 -0
  780. package/skills/vds-skill/runtime/scripts/move_audit_artifact_pages_rest.py +165 -0
  781. package/skills/vds-skill/runtime/scripts/move_wrong_dept_pages.py +235 -0
  782. package/skills/vds-skill/runtime/scripts/openspace_bootstrap.sh +56 -0
  783. package/skills/vds-skill/runtime/scripts/openspace_common.sh +75 -0
  784. package/skills/vds-skill/runtime/scripts/openspace_doctor.sh +61 -0
  785. package/skills/vds-skill/runtime/scripts/openspace_sync_shadow.sh +65 -0
  786. package/skills/vds-skill/runtime/scripts/phase7-baseline.sh +77 -0
  787. package/skills/vds-skill/runtime/scripts/preflight/env_check.sh +102 -0
  788. package/skills/vds-skill/runtime/scripts/repair_autopay_reports.sh +173 -0
  789. package/skills/vds-skill/runtime/scripts/rollback_drill.sh +659 -0
  790. package/skills/vds-skill/runtime/scripts/run-audit-in-tmux.sh +286 -0
  791. package/skills/vds-skill/runtime/scripts/run-department-audit.sh +495 -0
  792. package/skills/vds-skill/runtime/scripts/run-project-audit.sh +267 -0
  793. package/skills/vds-skill/runtime/scripts/save_intellij_memories.py +112 -0
  794. package/skills/vds-skill/runtime/scripts/save_memories_to_vds_ai.py +81 -0
  795. package/skills/vds-skill/runtime/scripts/save_memories_vds_style.py +133 -0
  796. package/skills/vds-skill/runtime/scripts/search_intellij_memories.py +48 -0
  797. package/skills/vds-skill/runtime/scripts/setup_intellij_workspace.py +71 -0
  798. package/skills/vds-skill/runtime/scripts/smoke-test-deploy.sh +137 -0
  799. package/skills/vds-skill/runtime/scripts/smoke_deploy_lib.py +205 -0
  800. package/skills/vds-skill/runtime/scripts/target-state-automation/README.md +89 -0
  801. package/skills/vds-skill/runtime/scripts/target-state-automation/confluence_sync_coordinator.sh +27 -0
  802. package/skills/vds-skill/runtime/scripts/target-state-automation/coordination.sh +114 -0
  803. package/skills/vds-skill/runtime/scripts/target-state-automation/diagram_coordinator.sh +25 -0
  804. package/skills/vds-skill/runtime/scripts/target-state-automation/docs_root.sh +22 -0
  805. package/skills/vds-skill/runtime/scripts/target-state-automation/generate_diagrams.sh +22 -0
  806. package/skills/vds-skill/runtime/scripts/target-state-automation/markdown_coordinator.sh +25 -0
  807. package/skills/vds-skill/runtime/scripts/target-state-automation/progress_dashboard.sh +17 -0
  808. package/skills/vds-skill/runtime/scripts/target-state-automation/schema_coordinator.sh +25 -0
  809. package/skills/vds-skill/runtime/scripts/target-state-automation/sync_confluence.sh +30 -0
  810. package/skills/vds-skill/runtime/scripts/target-state-automation/update_dependencies.sh +19 -0
  811. package/skills/vds-skill/runtime/scripts/target-state-automation/validate_links.sh +86 -0
  812. package/skills/vds-skill/runtime/scripts/target-state-automation/validate_markdown.sh +52 -0
  813. package/skills/vds-skill/runtime/scripts/target-state-automation/validate_schemas.sh +26 -0
  814. package/skills/vds-skill/runtime/scripts/target-state-automation/validate_structure.sh +98 -0
  815. package/skills/vds-skill/runtime/scripts/tests/__init__.py +1 -0
  816. package/skills/vds-skill/runtime/scripts/tests/test_dockerfile_correctness.py +815 -0
  817. package/skills/vds-skill/runtime/scripts/tests/test_makefile_loadouts.py +560 -0
  818. package/skills/vds-skill/runtime/scripts/tests/test_smoke_deploy.py +313 -0
  819. package/skills/vds-skill/runtime/scripts/tests/test_verify_alembic.py +581 -0
  820. package/skills/vds-skill/runtime/scripts/tests/test_verify_infra_topology.py +254 -0
  821. package/skills/vds-skill/runtime/scripts/update_modules_xml.py +194 -0
  822. package/skills/vds-skill/runtime/scripts/uv-workspace-alignment-verification-2026-03-25.md +128 -0
  823. package/skills/vds-skill/runtime/scripts/uv-workspace-alignment-verification-2026-04-18.md +100 -0
  824. package/skills/vds-skill/runtime/scripts/validate-cli-standardization.sh +188 -0
  825. package/skills/vds-skill/runtime/scripts/validate_brd_coverage.py +197 -0
  826. package/skills/vds-skill/runtime/scripts/validate_folder_structure.py +234 -0
  827. package/skills/vds-skill/runtime/scripts/validate_link_integrity.py +274 -0
  828. package/skills/vds-skill/runtime/scripts/vami017-caller-compat-report.md +62 -0
  829. package/skills/vds-skill/runtime/scripts/vami017-phase-b-scaffold-notes.md +79 -0
  830. package/skills/vds-skill/runtime/scripts/vds_sh_helpers.sh +180 -0
  831. package/skills/vds-skill/runtime/scripts/verification/phase2_portable_paths_ubuntu_docker.sh +26 -0
  832. package/skills/vds-skill/runtime/scripts/verify-infra-topology.py +868 -0
  833. package/skills/vds-skill/runtime/scripts/verify-memory-cli-e2e.sh +598 -0
  834. package/skills/vds-skill/runtime/scripts/verify-worktree-features.sh +306 -0
  835. package/skills/vds-skill/runtime/scripts/worktree-add.sh +128 -0
  836. package/skills/vds-skill/runtime/scripts/worktree-remove.sh +112 -0
  837. package/skills/vds-skill/runtime/scripts/worktree_compose.sh +269 -0
  838. package/skills/vds-skill/runtime/scripts/worktree_uv.sh +77 -0
  839. package/skills/vds-skill/runtime/sonarqube_orchestrator/IMPLEMENTATION_AUDIT.md +376 -0
  840. package/skills/vds-skill/runtime/sonarqube_orchestrator/README.md +507 -0
  841. package/skills/vds-skill/runtime/sonarqube_orchestrator/pyproject.toml +106 -0
  842. package/skills/vds-skill/runtime/sonarqube_orchestrator/scripts/ensure_symlink.sh +38 -0
  843. package/skills/vds-skill/runtime/sonarqube_orchestrator/src/vds_sonarqube_orchestrator/__init__.py +164 -0
  844. package/skills/vds-skill/runtime/sonarqube_orchestrator/src/vds_sonarqube_orchestrator/batch.py +212 -0
  845. package/skills/vds-skill/runtime/sonarqube_orchestrator/src/vds_sonarqube_orchestrator/cli.py +1407 -0
  846. package/skills/vds-skill/runtime/sonarqube_orchestrator/src/vds_sonarqube_orchestrator/client.py +608 -0
  847. package/skills/vds-skill/runtime/sonarqube_orchestrator/src/vds_sonarqube_orchestrator/config.py +260 -0
  848. package/skills/vds-skill/runtime/sonarqube_orchestrator/src/vds_sonarqube_orchestrator/diff.py +220 -0
  849. package/skills/vds-skill/runtime/sonarqube_orchestrator/src/vds_sonarqube_orchestrator/errors.py +34 -0
  850. package/skills/vds-skill/runtime/sonarqube_orchestrator/src/vds_sonarqube_orchestrator/external_sca.py +932 -0
  851. package/skills/vds-skill/runtime/sonarqube_orchestrator/src/vds_sonarqube_orchestrator/portfolio.py +225 -0
  852. package/skills/vds-skill/runtime/sonarqube_orchestrator/src/vds_sonarqube_orchestrator/pr.py +505 -0
  853. package/skills/vds-skill/runtime/sonarqube_orchestrator/src/vds_sonarqube_orchestrator/reports.py +342 -0
  854. package/skills/vds-skill/runtime/sonarqube_orchestrator/src/vds_sonarqube_orchestrator/scanner.py +351 -0
  855. package/skills/vds-skill/runtime/sonarqube_orchestrator/src/vds_sonarqube_orchestrator/webhooks.py +269 -0
  856. package/skills/vds-skill/runtime/sonarqube_orchestrator/tests/__init__.py +0 -0
  857. package/skills/vds-skill/runtime/sonarqube_orchestrator/tests/conftest.py +134 -0
  858. package/skills/vds-skill/runtime/sonarqube_orchestrator/tests/test_batch.py +419 -0
  859. package/skills/vds-skill/runtime/sonarqube_orchestrator/tests/test_config.py +145 -0
  860. package/skills/vds-skill/runtime/sonarqube_orchestrator/tests/test_errors.py +78 -0
  861. package/skills/vds-skill/runtime/sonarqube_orchestrator/tests/test_external_sca.py +466 -0
  862. package/skills/vds-skill/runtime/sonarqube_orchestrator/tests/test_pr.py +471 -0
  863. package/skills/vds-skill/runtime/sonarqube_orchestrator/tests/test_reports.py +511 -0
  864. package/skills/vds-skill/runtime/sonarqube_orchestrator/tests/test_webhooks.py +660 -0
  865. package/skills/vds-skill/runtime/uv.lock +5046 -0
  866. package/skills/vds-skill/runtime/vds_agent_core/CHANGELOG.md +36 -0
  867. package/skills/vds-skill/runtime/vds_agent_core/README.md +453 -0
  868. package/skills/vds-skill/runtime/vds_agent_core/docs/PHASE9A_ASSESSMENT.md +50 -0
  869. package/skills/vds-skill/runtime/vds_agent_core/docs/embedding.md +468 -0
  870. package/skills/vds-skill/runtime/vds_agent_core/pyproject.toml +51 -0
  871. package/skills/vds-skill/runtime/vds_agent_core/src/vds_agent_core/__init__.py +29 -0
  872. package/skills/vds-skill/runtime/vds_agent_core/src/vds_agent_core/agents/__init__.py +26 -0
  873. package/skills/vds-skill/runtime/vds_agent_core/src/vds_agent_core/agents/hooks.py +119 -0
  874. package/skills/vds-skill/runtime/vds_agent_core/src/vds_agent_core/agents/loop.py +864 -0
  875. package/skills/vds-skill/runtime/vds_agent_core/src/vds_agent_core/agents/tools.py +41 -0
  876. package/skills/vds-skill/runtime/vds_agent_core/src/vds_agent_core/config.py +252 -0
  877. package/skills/vds-skill/runtime/vds_agent_core/src/vds_agent_core/llm/__init__.py +55 -0
  878. package/skills/vds-skill/runtime/vds_agent_core/src/vds_agent_core/llm/_cascade.py +143 -0
  879. package/skills/vds-skill/runtime/vds_agent_core/src/vds_agent_core/llm/budget.py +353 -0
  880. package/skills/vds-skill/runtime/vds_agent_core/src/vds_agent_core/llm/cache.py +373 -0
  881. package/skills/vds-skill/runtime/vds_agent_core/src/vds_agent_core/llm/embedding.py +815 -0
  882. package/skills/vds-skill/runtime/vds_agent_core/src/vds_agent_core/llm/provider.py +173 -0
  883. package/skills/vds-skill/runtime/vds_agent_core/src/vds_agent_core/llm/schemas.py +45 -0
  884. package/skills/vds-skill/runtime/vds_agent_core/src/vds_agent_core/observability/__init__.py +77 -0
  885. package/skills/vds-skill/runtime/vds_agent_core/src/vds_agent_core/observability/decorators.py +258 -0
  886. package/skills/vds-skill/runtime/vds_agent_core/src/vds_agent_core/observability/jsonl_exporter.py +236 -0
  887. package/skills/vds-skill/runtime/vds_agent_core/src/vds_agent_core/observability/tracer.py +497 -0
  888. package/skills/vds-skill/runtime/vds_agent_core/src/vds_agent_core/profiles.py +2015 -0
  889. package/skills/vds-skill/runtime/vds_agent_core/src/vds_agent_core/runtime/__init__.py +0 -0
  890. package/skills/vds-skill/runtime/vds_agent_core/src/vds_agent_core/runtime/agent_id.py +60 -0
  891. package/skills/vds-skill/runtime/vds_agent_core/src/vds_agent_core/security/__init__.py +13 -0
  892. package/skills/vds-skill/runtime/vds_agent_core/src/vds_agent_core/security/credentials.py +106 -0
  893. package/skills/vds-skill/runtime/vds_agent_core/src/vds_agent_core/skills/__init__.py +1 -0
  894. package/skills/vds-skill/runtime/vds_agent_core/src/vds_agent_core/skills/executor.py +238 -0
  895. package/skills/vds-skill/runtime/vds_agent_core/src/vds_agent_core/skills/manager.py +381 -0
  896. package/skills/vds-skill/runtime/vds_agent_core/src/vds_agent_core/skills/policy.py +568 -0
  897. package/skills/vds-skill/runtime/vds_agent_core/src/vds_agent_core/workflows/__init__.py +19 -0
  898. package/skills/vds-skill/runtime/vds_agent_core/src/vds_agent_core/workflows/langgraph_runner.py +102 -0
  899. package/skills/vds-skill/runtime/vds_agent_core/src/vds_agent_core/workflows/protocols.py +81 -0
  900. package/skills/vds-skill/runtime/vds_agent_core/tests/__init__.py +0 -0
  901. package/skills/vds-skill/runtime/vds_agent_core/tests/conftest.py +62 -0
  902. package/skills/vds-skill/runtime/vds_agent_core/tests/integration/__init__.py +0 -0
  903. package/skills/vds-skill/runtime/vds_agent_core/tests/integration/test_audit_loop_hooks_integration.py +135 -0
  904. package/skills/vds-skill/runtime/vds_agent_core/tests/integration/test_audit_observability_integration.py +246 -0
  905. package/skills/vds-skill/runtime/vds_agent_core/tests/integration/test_public_api_stability.py +91 -0
  906. package/skills/vds-skill/runtime/vds_agent_core/tests/unit/__init__.py +0 -0
  907. package/skills/vds-skill/runtime/vds_agent_core/tests/unit/llm/__init__.py +0 -0
  908. package/skills/vds-skill/runtime/vds_agent_core/tests/unit/llm/test_call_site_parallelism.py +30 -0
  909. package/skills/vds-skill/runtime/vds_agent_core/tests/unit/llm/test_dimension_guardrail.py +25 -0
  910. package/skills/vds-skill/runtime/vds_agent_core/tests/unit/llm/test_drop_in_provider_extensibility.py +76 -0
  911. package/skills/vds-skill/runtime/vds_agent_core/tests/unit/llm/test_embedding.py +393 -0
  912. package/skills/vds-skill/runtime/vds_agent_core/tests/unit/llm/test_embedding_cache.py +302 -0
  913. package/skills/vds-skill/runtime/vds_agent_core/tests/unit/llm/test_embedding_extra.py +696 -0
  914. package/skills/vds-skill/runtime/vds_agent_core/tests/unit/llm/test_embedding_subclass.py +49 -0
  915. package/skills/vds-skill/runtime/vds_agent_core/tests/unit/llm/test_no_provider_leakage_in_env.py +34 -0
  916. package/skills/vds-skill/runtime/vds_agent_core/tests/unit/llm/test_provider_auto_route.py +48 -0
  917. package/skills/vds-skill/runtime/vds_agent_core/tests/unit/llm/test_runtime_log_clean.py +111 -0
  918. package/skills/vds-skill/runtime/vds_agent_core/tests/unit/llm/test_w7_logic_fixes.py +219 -0
  919. package/skills/vds-skill/runtime/vds_agent_core/tests/unit/profiles/__init__.py +0 -0
  920. package/skills/vds-skill/runtime/vds_agent_core/tests/unit/profiles/test_embedding_block_parser.py +194 -0
  921. package/skills/vds-skill/runtime/vds_agent_core/tests/unit/profiles/test_env_resolver_allowlist.py +141 -0
  922. package/skills/vds-skill/runtime/vds_agent_core/tests/unit/profiles/test_profile_authorization.py +158 -0
  923. package/skills/vds-skill/runtime/vds_agent_core/tests/unit/profiles/test_profiles_w3_extra.py +547 -0
  924. package/skills/vds-skill/runtime/vds_agent_core/tests/unit/profiles/test_real_audit_profile_compat.py +129 -0
  925. package/skills/vds-skill/runtime/vds_agent_core/tests/unit/runtime/__init__.py +0 -0
  926. package/skills/vds-skill/runtime/vds_agent_core/tests/unit/runtime/test_for_agent.py +322 -0
  927. package/skills/vds-skill/runtime/vds_agent_core/tests/unit/runtime/test_w9_cascade_edges.py +369 -0
  928. package/skills/vds-skill/runtime/vds_agent_core/tests/unit/security/__init__.py +0 -0
  929. package/skills/vds-skill/runtime/vds_agent_core/tests/unit/security/test_credentials.py +132 -0
  930. package/skills/vds-skill/runtime/vds_agent_core/tests/unit/test_agent_loop.py +663 -0
  931. package/skills/vds-skill/runtime/vds_agent_core/tests/unit/test_agent_loop_coverage.py +429 -0
  932. package/skills/vds-skill/runtime/vds_agent_core/tests/unit/test_agents_hooks_defaults.py +22 -0
  933. package/skills/vds-skill/runtime/vds_agent_core/tests/unit/test_budget.py +155 -0
  934. package/skills/vds-skill/runtime/vds_agent_core/tests/unit/test_budget_coverage.py +264 -0
  935. package/skills/vds-skill/runtime/vds_agent_core/tests/unit/test_budget_tracking_only.py +71 -0
  936. package/skills/vds-skill/runtime/vds_agent_core/tests/unit/test_cache.py +251 -0
  937. package/skills/vds-skill/runtime/vds_agent_core/tests/unit/test_cache_context.py +62 -0
  938. package/skills/vds-skill/runtime/vds_agent_core/tests/unit/test_config.py +155 -0
  939. package/skills/vds-skill/runtime/vds_agent_core/tests/unit/test_langgraph_runner.py +45 -0
  940. package/skills/vds-skill/runtime/vds_agent_core/tests/unit/test_langgraph_runner_coverage.py +98 -0
  941. package/skills/vds-skill/runtime/vds_agent_core/tests/unit/test_llm_cache_deep.py +113 -0
  942. package/skills/vds-skill/runtime/vds_agent_core/tests/unit/test_observability_decorators.py +697 -0
  943. package/skills/vds-skill/runtime/vds_agent_core/tests/unit/test_observability_hooks.py +217 -0
  944. package/skills/vds-skill/runtime/vds_agent_core/tests/unit/test_observability_jsonl_exporter.py +542 -0
  945. package/skills/vds-skill/runtime/vds_agent_core/tests/unit/test_observability_jsonl_wiring.py +313 -0
  946. package/skills/vds-skill/runtime/vds_agent_core/tests/unit/test_observability_tracer.py +896 -0
  947. package/skills/vds-skill/runtime/vds_agent_core/tests/unit/test_profiles.py +1571 -0
  948. package/skills/vds-skill/runtime/vds_agent_core/tests/unit/test_profiles_coverage.py +444 -0
  949. package/skills/vds-skill/runtime/vds_agent_core/tests/unit/test_provider.py +316 -0
  950. package/skills/vds-skill/runtime/vds_agent_core/tests/unit/test_schemas.py +63 -0
  951. package/skills/vds-skill/runtime/vds_agent_core/tests/unit/test_skill_executor.py +297 -0
  952. package/skills/vds-skill/runtime/vds_agent_core/tests/unit/test_skill_manager.py +370 -0
  953. package/skills/vds-skill/runtime/vds_agent_core/tests/unit/test_skill_manager_coverage.py +364 -0
  954. package/skills/vds-skill/runtime/vds_agent_core/tests/unit/test_skill_policy.py +402 -0
  955. package/skills/vds-skill/runtime/vds_agent_core/tests/unit/test_skill_rubric.py +47 -0
  956. package/skills/vds-skill/runtime/vds_agent_core/tests/unit/test_tools.py +51 -0
  957. package/skills/vds-skill/runtime/vds_agent_core/tests/unit/test_workflow_protocols.py +136 -0
  958. package/skills/vds-skill/runtime/vds_cli/README.md +201 -0
  959. package/skills/vds-skill/runtime/vds_cli/VERIFICATION_REPORT.md +41 -0
  960. package/skills/vds-skill/runtime/vds_cli/pyproject.toml +50 -0
  961. package/skills/vds-skill/runtime/vds_cli/src/vds_cli/__init__.py +3 -0
  962. package/skills/vds-skill/runtime/vds_cli/src/vds_cli/assets/git-credential-helper.py +235 -0
  963. package/skills/vds-skill/runtime/vds_cli/src/vds_cli/cli.py +1126 -0
  964. package/skills/vds-skill/runtime/vds_cli/src/vds_cli/commands/__init__.py +1 -0
  965. package/skills/vds-skill/runtime/vds_cli/src/vds_cli/commands/lint_cli.py +389 -0
  966. package/skills/vds-skill/runtime/vds_cli/src/vds_cli/confluence_sync.py +846 -0
  967. package/skills/vds-skill/runtime/vds_cli/src/vds_cli/docs/consumption/__init__.py +7 -0
  968. package/skills/vds-skill/runtime/vds_cli/src/vds_cli/docs/consumption/funnel.py +105 -0
  969. package/skills/vds-skill/runtime/vds_cli/src/vds_cli/docs/consumption/scanner.py +211 -0
  970. package/skills/vds-skill/runtime/vds_cli/src/vds_cli/docs/freshness/report.py +90 -0
  971. package/skills/vds-skill/runtime/vds_cli/src/vds_cli/docs/types.py +27 -0
  972. package/skills/vds-skill/runtime/vds_cli/src/vds_cli/docs_cmd.py +672 -0
  973. package/skills/vds-skill/runtime/vds_cli/src/vds_cli/docs_metrics.py +75 -0
  974. package/skills/vds-skill/runtime/vds_cli/src/vds_cli/docs_sync.py +1171 -0
  975. package/skills/vds-skill/runtime/vds_cli/src/vds_cli/ecosystem/__init__.py +39 -0
  976. package/skills/vds-skill/runtime/vds_cli/src/vds_cli/ecosystem/report.py +439 -0
  977. package/skills/vds-skill/runtime/vds_cli/src/vds_cli/ecosystem_docs.py +164 -0
  978. package/skills/vds-skill/runtime/vds_cli/src/vds_cli/env.py +111 -0
  979. package/skills/vds-skill/runtime/vds_cli/src/vds_cli/env_git_helper.py +281 -0
  980. package/skills/vds-skill/runtime/vds_cli/src/vds_cli/google_sheets_orchestrator/__init__.py +3 -0
  981. package/skills/vds-skill/runtime/vds_cli/src/vds_cli/google_sheets_orchestrator/google_sheets_orchestrator.py +173 -0
  982. package/skills/vds-skill/runtime/vds_cli/src/vds_cli/router.py +232 -0
  983. package/skills/vds-skill/runtime/vds_cli/src/vds_cli/skills_cmd.py +274 -0
  984. package/skills/vds-skill/runtime/vds_cli/src/vds_cli/sync_api.py +613 -0
  985. package/skills/vds-skill/runtime/vds_cli/src/vds_cli/sync_service.py +283 -0
  986. package/skills/vds-skill/runtime/vds_cli/tests/conftest.py +62 -0
  987. package/skills/vds-skill/runtime/vds_cli/tests/test_env_git_helper_lifecycle.py +261 -0
  988. package/skills/vds-skill/runtime/vds_cli/tests/test_git_credential_helper.py +240 -0
  989. package/skills/vds-skill/runtime/vds_cli/tests/test_router_help_proxy.py +43 -0
  990. package/skills/vds-skill/runtime/vds_cli/tests/unit/test_cli.py +241 -0
  991. package/skills/vds-skill/runtime/vds_cli/tests/unit/test_cli_DOC004.py +110 -0
  992. package/skills/vds-skill/runtime/vds_cli/tests/unit/test_confluence_sync.py +315 -0
  993. package/skills/vds-skill/runtime/vds_cli/tests/unit/test_confluence_sync_wave7.py +375 -0
  994. package/skills/vds-skill/runtime/vds_cli/tests/unit/test_consumption_funnel.py +106 -0
  995. package/skills/vds-skill/runtime/vds_cli/tests/unit/test_consumption_scanner.py +144 -0
  996. package/skills/vds-skill/runtime/vds_cli/tests/unit/test_docs_cmd.py +89 -0
  997. package/skills/vds-skill/runtime/vds_cli/tests/unit/test_docs_cmd_wave8.py +161 -0
  998. package/skills/vds-skill/runtime/vds_cli/tests/unit/test_docs_metrics.py +16 -0
  999. package/skills/vds-skill/runtime/vds_cli/tests/unit/test_docs_quality_score.py +61 -0
  1000. package/skills/vds-skill/runtime/vds_cli/tests/unit/test_docs_sync.py +417 -0
  1001. package/skills/vds-skill/runtime/vds_cli/tests/unit/test_ecosystem_cli_dashboard.py +667 -0
  1002. package/skills/vds-skill/runtime/vds_cli/tests/unit/test_ecosystem_cli_dashboard_rendering.py +143 -0
  1003. package/skills/vds-skill/runtime/vds_cli/tests/unit/test_ecosystem_docs.py +63 -0
  1004. package/skills/vds-skill/runtime/vds_cli/tests/unit/test_env.py +85 -0
  1005. package/skills/vds-skill/runtime/vds_cli/tests/unit/test_freshness_report.py +125 -0
  1006. package/skills/vds-skill/runtime/vds_cli/tests/unit/test_lint_cli.py +224 -0
  1007. package/skills/vds-skill/runtime/vds_cli/tests/unit/test_router.py +101 -0
  1008. package/skills/vds-skill/runtime/vds_cli/tests/unit/test_skills_cmd.py +419 -0
  1009. package/skills/vds-skill/runtime/vds_cli/tests/unit/test_sync_api.py +357 -0
  1010. package/skills/vds-skill/runtime/vds_cli/tests/unit/test_sync_service.py +170 -0
  1011. package/skills/vds-skill/runtime/vds_cli/tests/verification/conftest.py +51 -0
  1012. package/skills/vds-skill/runtime/vds_cli/tests/verification/test_bitbucket_real.py +32 -0
  1013. package/skills/vds-skill/runtime/vds_cli/tests/verification/test_confluence_real.py +32 -0
  1014. package/skills/vds-skill/runtime/vds_cli/tests/verification/test_jira_real.py +40 -0
  1015. package/skills/vds-skill/runtime/vds_cli_common/README.md +190 -0
  1016. package/skills/vds-skill/runtime/vds_cli_common/pyproject.toml +96 -0
  1017. package/skills/vds-skill/runtime/vds_cli_common/src/vds_cli_common/__init__.py +36 -0
  1018. package/skills/vds-skill/runtime/vds_cli_common/src/vds_cli_common/app.py +55 -0
  1019. package/skills/vds-skill/runtime/vds_cli_common/src/vds_cli_common/completers.py +139 -0
  1020. package/skills/vds-skill/runtime/vds_cli_common/src/vds_cli_common/context.py +201 -0
  1021. package/skills/vds-skill/runtime/vds_cli_common/src/vds_cli_common/env.py +163 -0
  1022. package/skills/vds-skill/runtime/vds_cli_common/src/vds_cli_common/errors.py +440 -0
  1023. package/skills/vds-skill/runtime/vds_cli_common/src/vds_cli_common/output.py +284 -0
  1024. package/skills/vds-skill/runtime/vds_cli_common/src/vds_cli_common/paths.py +78 -0
  1025. package/skills/vds-skill/runtime/vds_cli_common/src/vds_cli_common/testing.py +211 -0
  1026. package/skills/vds-skill/runtime/vds_cli_common/src/vds_cli_common/version.py +85 -0
  1027. package/skills/vds-skill/runtime/vds_cli_common/tests/__init__.py +0 -0
  1028. package/skills/vds-skill/runtime/vds_cli_common/tests/test_app.py +126 -0
  1029. package/skills/vds-skill/runtime/vds_cli_common/tests/test_completers.py +148 -0
  1030. package/skills/vds-skill/runtime/vds_cli_common/tests/test_context.py +192 -0
  1031. package/skills/vds-skill/runtime/vds_cli_common/tests/test_env.py +235 -0
  1032. package/skills/vds-skill/runtime/vds_cli_common/tests/test_errors.py +275 -0
  1033. package/skills/vds-skill/runtime/vds_cli_common/tests/test_output.py +229 -0
  1034. package/skills/vds-skill/runtime/vds_cli_common/tests/test_paths.py +61 -0
  1035. package/skills/vds-skill/runtime/vds_cli_common/tests/test_testing.py +138 -0
  1036. package/skills/vds-skill/runtime/vds_cli_common/tests/test_version.py +64 -0
  1037. package/skills/vds-skill/runtime/vidp_orchestrator/README.md +31 -0
  1038. package/skills/vds-skill/runtime/vidp_orchestrator/pyproject.toml +50 -0
  1039. package/skills/vds-skill/runtime/vidp_orchestrator/src/vds_vidp_orchestrator/__init__.py +26 -0
  1040. package/skills/vds-skill/runtime/vidp_orchestrator/src/vds_vidp_orchestrator/cli.py +246 -0
  1041. package/skills/vds-skill/runtime/vidp_orchestrator/src/vds_vidp_orchestrator/client.py +104 -0
  1042. package/skills/vds-skill/runtime/vidp_orchestrator/src/vds_vidp_orchestrator/config.py +82 -0
  1043. package/skills/vds-skill/runtime/vidp_orchestrator/src/vds_vidp_orchestrator/workflows.json +3 -0
  1044. package/skills/vds-skill/runtime/vidp_orchestrator/src/vds_vidp_orchestrator/workflows.py +130 -0
  1045. package/skills/vds-skill/vds-scripts-skill/SKILL.md +129 -0
  1046. package/skills/vds-skill/vds-scripts-skill/references/audit-commands.md +171 -0
  1047. package/skills/vds-skill/vds-scripts-skill/references/capability-index.md +34 -0
  1048. package/skills/vds-skill/vds-scripts-skill/references/development-commands.md +12 -0
  1049. package/skills/vds-skill/vds-scripts-skill/references/google-sheets.md +71 -0
  1050. package/skills/vds-skill/vds-scripts-skill/references/integration-commands.md +17 -0
  1051. package/skills/vds-skill/vds-scripts-skill/references/platform-bootstrap.md +25 -0
  1052. package/skills/vds-skill/vds-scripts-skill/references/specialist-routing.md +14 -0
  1053. package/skills/vds-skill/vds-scripts-skill/references/validation-commands.md +15 -0
@@ -0,0 +1,3160 @@
1
+ """Typer CLI wrapper for Confluence operations during the native Python implementations."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import logging
7
+ import os
8
+ import sys
9
+ from dataclasses import asdict, dataclass
10
+ from datetime import UTC, datetime
11
+ from pathlib import Path
12
+ from typing import Any, cast
13
+ from urllib.parse import urlparse
14
+
15
+ import structlog
16
+ import typer
17
+ from vds_platform_core.logging import configure_structlog
18
+
19
+ from .config import ConfluenceSettings, load_settings
20
+ from .content import ContentClient
21
+ from .content_v2 import ContentClientV2
22
+ from .crawl_tree import CrawlOptions, crawl_tree_to_disk
23
+ from .errors import VDSClientError
24
+ from .eventing import Poller
25
+ from .http import ConfluenceClient
26
+ from .orchestration import BatchRunner, WebhookClient
27
+ from .reporting import RunSummary, write_reports
28
+ from .tree_copier import CopyOptions, TreeCopier
29
+
30
+ # ---------------------------------------------------------------------------
31
+ # Typer setup & logging
32
+ # ---------------------------------------------------------------------------
33
+
34
+ app = typer.Typer(add_completion=False, help="Confluence operations orchestrator")
35
+ content_app = typer.Typer(help="Native Confluence API commands")
36
+ batch_app = typer.Typer(help="Batch polling utilities (REST v1)")
37
+ webhook_app = typer.Typer(help="Webhook management (REST v1)")
38
+ app.add_typer(content_app, name="content")
39
+ app.add_typer(batch_app, name="batch-native")
40
+ app.add_typer(webhook_app, name="webhook-native")
41
+
42
+ # Logger will be configured in main() callback
43
+ logger = logging.getLogger("confluence_cli")
44
+
45
+ # ---------------------------------------------------------------------------
46
+ # Paths and legacy script mapping
47
+ # ---------------------------------------------------------------------------
48
+
49
+ _SCRIPTS_DIR = Path(__file__).resolve().parents[3]
50
+ _DEFAULT_REPORT_DIR = (_SCRIPTS_DIR / "reports" / "confluence_runs").resolve()
51
+
52
+
53
+ @dataclass(slots=True)
54
+ class CLIContext:
55
+ report_dir: Path
56
+ markdown: bool
57
+ api_version: str
58
+ reports: bool
59
+
60
+
61
+ # ---------------------------------------------------------------------------
62
+ # Helper functions
63
+ # ---------------------------------------------------------------------------
64
+
65
+
66
+ def _normalise_server(server: str | None, settings: ConfluenceSettings) -> str:
67
+ return server.lower() if server else settings.default_server.lower()
68
+
69
+
70
+ def _infer_server_from_root_ref(root_ref: str, settings: ConfluenceSettings) -> str | None:
71
+ root_ref = (root_ref or "").strip()
72
+ if not root_ref:
73
+ return None
74
+
75
+ parsed = urlparse(root_ref)
76
+ if not parsed.scheme or not parsed.netloc:
77
+ return None
78
+
79
+ target = parsed.netloc.lower()
80
+ internal = urlparse(str(settings.internal_url)).netloc.lower()
81
+ external = urlparse(str(settings.external_url)).netloc.lower()
82
+
83
+ if target == internal:
84
+ return "internal"
85
+ if target == external:
86
+ return "external"
87
+ return None
88
+
89
+
90
+ def _build_env(server: str, settings: ConfluenceSettings) -> dict:
91
+ token = settings.token_for(server)
92
+ if not token and not (settings.username and settings.password):
93
+ raise VDSClientError(
94
+ f"Credentials for server '{server}' are missing. Provide VDS_USERNAME+VDS_PASSWORD as the primary auth mechanism or tokens "
95
+ "(INTERNAL_CONFLUENCE_TOKEN/EXTERNAL_CONFLUENCE_TOKEN) as fallback.",
96
+ context={"server": server},
97
+ )
98
+ env = os.environ.copy()
99
+ env.update(
100
+ {
101
+ "CONFLUENCE_SERVER": server,
102
+ "CONFLUENCE_TOKEN": token or "",
103
+ "CONFLUENCE_URL": str(settings.url_for(server)),
104
+ "INTERNAL_CONFLUENCE_TOKEN": settings.internal_token or "",
105
+ "EXTERNAL_CONFLUENCE_TOKEN": settings.external_token or "",
106
+ }
107
+ )
108
+ return env
109
+
110
+
111
+ def _build_http_client(settings: ConfluenceSettings, server: str) -> ConfluenceClient:
112
+ return ConfluenceClient(settings, server=server)
113
+
114
+
115
+ def _get_cli_ctx(ctx: typer.Context) -> CLIContext:
116
+ cli_ctx = ctx.obj
117
+ if not isinstance(cli_ctx, CLIContext):
118
+ raise RuntimeError("CLI context not initialised")
119
+ return cli_ctx
120
+
121
+
122
+ def _ensure_v1_api(_ctx: typer.Context, _command: str) -> None:
123
+ """Legacy command guard - redirects to native implementations."""
124
+ settings = load_settings(strict=False)
125
+ if settings is None:
126
+ typer.echo("Error: configuration not found", err=True)
127
+ raise typer.Exit(code=1) from None
128
+
129
+
130
+ def _build_content_client(
131
+ settings: ConfluenceSettings,
132
+ server: str,
133
+ api_version: str,
134
+ ):
135
+ http_client = _build_http_client(settings, server)
136
+ if not http_client.supports_api_version(api_version):
137
+ raise VDSClientError(
138
+ f"API version '{api_version}' is not supported for server '{server}'.",
139
+ context={"server": server, "api_version": api_version},
140
+ )
141
+ if api_version == "v1":
142
+ return ContentClient(http_client)
143
+ if api_version == "v2":
144
+ return ContentClientV2(http_client)
145
+ raise VDSClientError(f"Unsupported API version '{api_version}'")
146
+
147
+
148
+ def _record_summary(
149
+ ctx: typer.Context,
150
+ *,
151
+ command: str,
152
+ args: list[str],
153
+ server: str,
154
+ exit_code: int,
155
+ started_at: datetime,
156
+ finished_at: datetime,
157
+ ) -> None:
158
+ cli_ctx = _get_cli_ctx(ctx)
159
+ summary = RunSummary(
160
+ command=command,
161
+ args=args,
162
+ server=server,
163
+ exit_code=exit_code,
164
+ duration_ms=int((finished_at - started_at).total_seconds() * 1000),
165
+ started_at=started_at,
166
+ finished_at=finished_at,
167
+ )
168
+ if cli_ctx.reports:
169
+ write_reports(summary, base_dir=cli_ctx.report_dir, include_markdown=cli_ctx.markdown)
170
+
171
+
172
+ def _emit_json(payload: object) -> None:
173
+ text = json.dumps(payload, indent=2, sort_keys=True)
174
+ if sys.stdout.isatty():
175
+ typer.echo(text)
176
+ else:
177
+ sys.stdout.write(text + "\n")
178
+
179
+
180
+ def _expand_option(value: str | None) -> list[str] | None:
181
+ if not value:
182
+ return None
183
+ return [part.strip() for part in value.split(",") if part.strip()]
184
+
185
+
186
+ def _content_summary_args(
187
+ *, cql: str | None = None, limit: int | None = None, start: int | None = None, space: str | None = None
188
+ ) -> list[str]:
189
+ parts: list[str] = []
190
+ if cql is not None:
191
+ parts.append(cql)
192
+ if limit is not None:
193
+ parts.append(f"limit={limit}")
194
+ if start is not None:
195
+ parts.append(f"start={start}")
196
+ if space is not None:
197
+ parts.append(f"space={space}")
198
+ return parts
199
+
200
+
201
+ def _read_text_file(path: Path) -> str:
202
+ if not path.exists():
203
+ raise typer.BadParameter(f"File not found: {path}")
204
+ if path.is_dir():
205
+ raise typer.BadParameter(f"Expected file but found directory: {path}")
206
+ try:
207
+ return path.read_text(encoding="utf-8")
208
+ except OSError as exc:
209
+ raise typer.BadParameter(f"Unable to read file {path}: {exc}") from exc
210
+
211
+
212
+ def _parse_json_option(value: str | None, option_name: str) -> dict[str, Any] | None:
213
+ if value is None:
214
+ return None
215
+ try:
216
+ parsed = json.loads(value)
217
+ except json.JSONDecodeError as exc:
218
+ raise typer.BadParameter(f"Invalid JSON for {option_name}: {exc}") from exc
219
+ if not isinstance(parsed, dict):
220
+ raise typer.BadParameter(f"{option_name} must decode to a JSON object")
221
+ return parsed
222
+
223
+
224
+ def _parent_context(ctx: typer.Context) -> typer.Context:
225
+ return cast("typer.Context", ctx.parent or ctx)
226
+
227
+
228
+ # ---------------------------------------------------------------------------
229
+ # Typer callbacks & native commands
230
+ # ---------------------------------------------------------------------------
231
+
232
+
233
+ @app.callback()
234
+ def main(
235
+ ctx: typer.Context,
236
+ report_dir: Path | None = typer.Option(
237
+ None, help="Directory for run reports (default: <vds-scripts>/reports/confluence_runs)"
238
+ ),
239
+ markdown: bool = typer.Option(True, help="Emit Markdown summaries alongside JSON reports"),
240
+ reports: bool = typer.Option(True, "--reports/--no-reports", help="Persist run reports to disk"),
241
+ json_only: bool = typer.Option(
242
+ False,
243
+ "--json-only",
244
+ help="Emit JSON to stdout only (disables reports and markdown)",
245
+ ),
246
+ api_version: str = typer.Option("v1", "--api-version", "-A", help="Select REST API version (v1 or v2)"),
247
+ structured_logs: bool = typer.Option(
248
+ False, "--structured-logs/--no-structured-logs", help="Emit structured JSON logs to stderr"
249
+ ),
250
+ ) -> None:
251
+ resolved = report_dir.resolve() if report_dir else _DEFAULT_REPORT_DIR
252
+ version = api_version.lower()
253
+ if version not in {"v1", "v2"}:
254
+ raise typer.BadParameter("api-version must be one of: v1, v2")
255
+ if json_only:
256
+ markdown = False
257
+ reports = False
258
+
259
+ # Configure logging/structlog
260
+ if structured_logs or json_only:
261
+ configure_structlog(
262
+ json_logs=structured_logs, quiet=json_only and not structured_logs, cache_logger=structured_logs
263
+ )
264
+ if structured_logs:
265
+ global logger
266
+ logger = structlog.get_logger("confluence_cli") # type: ignore[assignment]
267
+
268
+ ctx.obj = CLIContext(report_dir=resolved, markdown=markdown, api_version=version, reports=reports)
269
+
270
+
271
+ @app.command("templates")
272
+ def cmd_templates(
273
+ ctx: typer.Context,
274
+ args: list[str] | None = typer.Argument(
275
+ None, help="Additional arguments for template operations", show_default=False, metavar="ARGS"
276
+ ),
277
+ server: str | None = typer.Option(None, "--server", "-s", help="internal|external"),
278
+ limit: int = typer.Option(20, "--limit", "-l", help="Number of templates to list"),
279
+ space_key: str | None = typer.Option(None, "--space", "-k", help="Space key filter"),
280
+ json_output: bool = typer.Option(False, "--json", help="Emit JSON payload"),
281
+ ) -> None:
282
+ """Native Python template management"""
283
+ parent_ctx = _parent_context(ctx)
284
+ started_at = datetime.now(UTC)
285
+
286
+ try:
287
+ settings = load_settings(strict=True)
288
+ server_key = _normalise_server(server, settings)
289
+ client = _build_content_client(settings, server_key, parent_ctx.obj.api_version)
290
+
291
+ # Use native Python implementation
292
+ if hasattr(client, "list_templates"):
293
+ result = client.list_templates(space_key=space_key)
294
+ else:
295
+ result = {"templates": [], "message": "Templates API not available for this server"}
296
+
297
+ if json_output:
298
+ _emit_json(result)
299
+ else:
300
+ templates_payload = []
301
+ if isinstance(result, dict):
302
+ templates_payload = (
303
+ result.get("blueprints")
304
+ or result.get("templates")
305
+ or result.get("results")
306
+ or result.get("contentTemplates")
307
+ or []
308
+ )
309
+ elif isinstance(result, list):
310
+ templates_payload = result
311
+
312
+ count = len(templates_payload)
313
+ typer.echo(f"Found {count} templates:")
314
+ for template in templates_payload[:limit]:
315
+ if isinstance(template, dict):
316
+ name = template.get("name", "Unknown")
317
+ template_id = template.get("templateId") or template.get("id") or template.get("uuid") or "Unknown"
318
+ else:
319
+ name = str(template)
320
+ template_id = "Unknown"
321
+ typer.echo(f" - {name} (ID: {template_id})")
322
+
323
+ exit_code = 0
324
+ finished_at = datetime.now(UTC)
325
+ _record_summary(
326
+ parent_ctx,
327
+ command="templates",
328
+ args=args or [],
329
+ server=server_key,
330
+ exit_code=exit_code,
331
+ started_at=started_at,
332
+ finished_at=finished_at,
333
+ )
334
+
335
+ if exit_code != 0:
336
+ raise typer.Exit(code=exit_code) from None
337
+
338
+ except Exception as exc:
339
+ logger.exception("templates_command_failed: %s", exc)
340
+ typer.echo(f"Error: {exc}", err=True)
341
+ raise typer.Exit(code=1) from None
342
+
343
+
344
+ @app.command("batch")
345
+ def cmd_batch(
346
+ _ctx: typer.Context,
347
+ _args: list[str] | None = typer.Argument(
348
+ None, help="Use 'confluence content batch --help' for available subcommands", show_default=False, metavar="ARGS"
349
+ ),
350
+ _server: str | None = typer.Option(None, "--server", "-s", help="internal|external"),
351
+ ) -> None:
352
+ """Legacy batch command - use 'confluence content batch' subcommands instead."""
353
+ typer.echo("Error: The 'batch' command is deprecated. Use 'confluence content batch' subcommands:", err=True)
354
+ typer.echo(" confluence content batch scan --cql '<query>' [options]", err=True)
355
+ typer.echo(" confluence content batch snapshot --cql '<query>' [options]", err=True)
356
+ raise typer.Exit(code=1) from None
357
+
358
+
359
+ @app.command("webhooks")
360
+ def cmd_webhooks(
361
+ ctx: typer.Context,
362
+ args: list[str] | None = typer.Argument(
363
+ None, help="Additional arguments for webhook operations", show_default=False, metavar="ARGS"
364
+ ),
365
+ server: str | None = typer.Option(None, "--server", "-s", help="internal|external"),
366
+ list_webhooks: bool = typer.Option(False, "--list", "-l", help="List existing webhooks"),
367
+ create_name: str | None = typer.Option(None, "--name", "-n", help="Webhook name"),
368
+ create_url: str | None = typer.Option(None, "--url", "-u", help="Webhook URL"),
369
+ create_event: str | None = typer.Option(None, "--event", "-e", help="Event type"),
370
+ json_output: bool = typer.Option(False, "--json", help="Emit JSON payload"),
371
+ ) -> None:
372
+ """Native Python webhook management"""
373
+ parent_ctx = _parent_context(ctx)
374
+ started_at = datetime.now(UTC)
375
+
376
+ server_key = "internal"
377
+ try:
378
+ settings = load_settings(strict=True)
379
+ server_key = _normalise_server(server, settings)
380
+ webhook_client = WebhookClient(_build_http_client(settings, server_key))
381
+
382
+ if list_webhooks or (not create_name and not create_url and not create_event):
383
+ # List webhooks
384
+ result = webhook_client.list()
385
+ if json_output or not sys.stdout.isatty():
386
+ _emit_json(result)
387
+ else:
388
+ result_payload = result if isinstance(result, dict) else {"webhooks": result}
389
+ webhooks_raw = result_payload.get("webhooks", [])
390
+ webhooks = (
391
+ [item for item in webhooks_raw if isinstance(item, dict)] if isinstance(webhooks_raw, list) else []
392
+ )
393
+ count = len(webhooks)
394
+ typer.echo(f"Found {count} webhooks:")
395
+ for webhook in webhooks:
396
+ name = webhook.get("name", "Unknown")
397
+ url = webhook.get("url", "Unknown")
398
+ events = webhook.get("events", [])
399
+ typer.echo(f" - {name}: {url} (events: {', '.join(events)})")
400
+
401
+ elif create_name and create_url and create_event:
402
+ # Create webhook
403
+ result = webhook_client.create(name=create_name, url=create_url, events=[create_event])
404
+ if json_output or not sys.stdout.isatty():
405
+ _emit_json(result)
406
+ else:
407
+ webhook_id = result.get("id", "Unknown")
408
+ typer.echo(f"Created webhook '{create_name}' with ID: {webhook_id}")
409
+
410
+ else:
411
+ typer.echo(
412
+ "Error: Either use --list to list webhooks or provide --name, --url, and --event to create", err=True
413
+ )
414
+ raise typer.Exit(code=1) from None
415
+
416
+ exit_code = 0
417
+ finished_at = datetime.now(UTC)
418
+ _record_summary(
419
+ parent_ctx,
420
+ command="webhooks",
421
+ args=args or [],
422
+ server=server_key,
423
+ exit_code=exit_code,
424
+ started_at=started_at,
425
+ finished_at=finished_at,
426
+ )
427
+
428
+ if exit_code != 0:
429
+ raise typer.Exit(code=exit_code) from None
430
+
431
+ except Exception as exc:
432
+ logger.exception("webhooks_command_failed: %s", exc)
433
+ typer.echo(f"Error: {exc}", err=True)
434
+ raise typer.Exit(code=1) from None
435
+
436
+
437
+ @app.command("search")
438
+ def cmd_search(
439
+ ctx: typer.Context,
440
+ args: list[str] | None = typer.Argument(
441
+ None, help="Additional arguments for CQL search", show_default=False, metavar="ARGS"
442
+ ),
443
+ server: str | None = typer.Option(None, "--server", "-s", help="internal|external"),
444
+ cql: str = typer.Option(..., "--cql", "-q", help="CQL query string"),
445
+ limit: int = typer.Option(25, "--limit", "-l", help="Number of results"),
446
+ start: int = typer.Option(0, "--start", help="Pagination start"),
447
+ expand: str | None = typer.Option(None, "--expand", help="Comma separated expansions"),
448
+ excerpt: str | None = typer.Option(None, "--excerpt", help="Excerpt strategy (e.g., 'highlighted', 'indexed')"),
449
+ advanced: bool = typer.Option(False, "--advanced", help="Use advanced CQL search with additional options"),
450
+ json_output: bool = typer.Option(False, "--json", help="Emit JSON payload"),
451
+ ) -> None:
452
+ """Native Python CQL search. Use --advanced for additional options like excerpt."""
453
+ parent_ctx = _parent_context(ctx)
454
+ started_at = datetime.now(UTC)
455
+ server_key = "internal"
456
+
457
+ try:
458
+ settings = load_settings(strict=True)
459
+ server_key = _normalise_server(server, settings)
460
+ client = _build_content_client(settings, server_key, parent_ctx.obj.api_version)
461
+
462
+ # Use native Python implementation
463
+ expand_list = _expand_option(expand) if expand else None
464
+ if advanced or excerpt:
465
+ result = client.cql_advanced(cql, limit=limit, start=start, expand=expand_list, excerpt=excerpt)
466
+ else:
467
+ result = client.search_cql(cql, limit=limit, start=start, expand=expand_list)
468
+
469
+ if json_output or not sys.stdout.isatty():
470
+ _emit_json(result)
471
+ else:
472
+ results = result.get("results", [])
473
+ count = len(results)
474
+ total = result.get("size", 0)
475
+ typer.echo(f"Found {count} of {total} results for CQL: {cql}")
476
+ for item in results:
477
+ title = item.get("title", "Unknown")
478
+ page_id = item.get("id", "Unknown")
479
+ space = item.get("space", {}).get("key", "Unknown")
480
+ typer.echo(f" - {title} (ID: {page_id}, Space: {space})")
481
+
482
+ exit_code = 0
483
+ finished_at = datetime.now(UTC)
484
+ _record_summary(
485
+ parent_ctx,
486
+ command="search",
487
+ args=args or [],
488
+ server=server_key,
489
+ exit_code=exit_code,
490
+ started_at=started_at,
491
+ finished_at=finished_at,
492
+ )
493
+
494
+ if exit_code != 0:
495
+ raise typer.Exit(code=exit_code) from None
496
+
497
+ except Exception as exc:
498
+ logger.exception("search_command_failed: %s", exc)
499
+
500
+
501
+ @content_app.command("child-pages")
502
+ def cmd_child_pages(
503
+ ctx: typer.Context,
504
+ page_id: str = typer.Argument(..., help="Parent page ID"),
505
+ expand: str | None = typer.Option(None, "--expand", help="Comma separated expansions"),
506
+ server: str | None = typer.Option(None, "--server", "-s", help="internal|external"),
507
+ json_output: bool = typer.Option(False, "--json", help="Emit JSON payload"),
508
+ ) -> None:
509
+ """Get child pages using the direct API endpoint (faster than CQL)."""
510
+ parent_ctx = _parent_context(ctx)
511
+ started_at = datetime.now(UTC)
512
+ exit_code = 0
513
+ server_key = "internal"
514
+
515
+ try:
516
+ settings = load_settings(strict=True)
517
+ server_key = _normalise_server(server, settings)
518
+ client = _build_content_client(settings, server_key, parent_ctx.obj.api_version)
519
+ expand_list = _expand_option(expand)
520
+
521
+ results = client.get_child_pages(page_id, expand=expand_list)
522
+
523
+ if json_output or not sys.stdout.isatty():
524
+ _emit_json(results)
525
+ else:
526
+ count = len(results)
527
+ typer.echo(f"Found {count} child pages for {page_id}:")
528
+ for item in results:
529
+ title = item.get("title", "Unknown")
530
+ child_id = item.get("id", "Unknown")
531
+ typer.echo(f" - {title} (ID: {child_id})")
532
+
533
+ except Exception as exc:
534
+ exit_code = 1
535
+ logger.exception("child_pages_failed: %s", exc)
536
+ typer.echo(f"Error: {exc}", err=True)
537
+ finally:
538
+ finished_at = datetime.now(UTC)
539
+ _record_summary(
540
+ parent_ctx,
541
+ command="child-pages",
542
+ args=[page_id],
543
+ server=server_key if "server_key" in locals() else "internal",
544
+ exit_code=exit_code,
545
+ started_at=started_at,
546
+ finished_at=finished_at,
547
+ )
548
+ if exit_code != 0:
549
+ raise typer.Exit(code=exit_code) from None
550
+
551
+
552
+ @app.command("search-by-space-type")
553
+ def cmd_search_by_space_type(
554
+ ctx: typer.Context,
555
+ server: str | None = typer.Option(None, "--server", "-s", help="internal|external"),
556
+ space_key: str | None = typer.Option(None, "--space", help="Space key to filter by"),
557
+ content_type: str | None = typer.Option(None, "--type", help="Content type (e.g., 'page', 'blogpost', 'comment')"),
558
+ limit: int = typer.Option(25, "--limit", "-l", help="Number of results"),
559
+ start: int = typer.Option(0, "--start", help="Pagination start"),
560
+ expand: str | None = typer.Option(None, "--expand", help="Comma separated expansions"),
561
+ json_output: bool = typer.Option(False, "--json", help="Emit JSON payload"),
562
+ ) -> None:
563
+ """Search by space and/or content type (helper that builds CQL automatically)."""
564
+ parent_ctx = _parent_context(ctx)
565
+ started_at = datetime.now(UTC)
566
+ exit_code = 0
567
+ server_key = "internal"
568
+
569
+ try:
570
+ if not space_key and not content_type:
571
+ raise typer.BadParameter("At least one of --space or --type must be provided")
572
+
573
+ settings = load_settings(strict=True)
574
+ server_key = _normalise_server(server, settings)
575
+ client = _build_content_client(settings, server_key, parent_ctx.obj.api_version)
576
+
577
+ expand_list = _expand_option(expand) if expand else None
578
+ result = client.search_by_space_and_type(
579
+ space_key=space_key,
580
+ content_type=content_type,
581
+ limit=limit,
582
+ start=start,
583
+ expand=expand_list,
584
+ )
585
+
586
+ if json_output or not sys.stdout.isatty():
587
+ _emit_json(result)
588
+ else:
589
+ results = result.get("results", [])
590
+ count = len(results)
591
+ total = result.get("size", 0)
592
+ typer.echo(f"Found {count} of {total} results")
593
+ for item in results:
594
+ title = item.get("title", "Unknown")
595
+ page_id = item.get("id", "Unknown")
596
+ space = item.get("space", {}).get("key", "Unknown")
597
+ item_type = item.get("type", "Unknown")
598
+ typer.echo(f" - {title} (ID: {page_id}, Space: {space}, Type: {item_type})")
599
+
600
+ exit_code = 0
601
+ except (typer.BadParameter, Exception) as exc:
602
+ exit_code = 1
603
+ logger.exception("search_by_space_type_failed: %s", exc)
604
+ typer.echo(f"Error: {exc}", err=True)
605
+ finally:
606
+ finished_at = datetime.now(UTC)
607
+ args_list: list[str] = []
608
+ if space_key:
609
+ args_list.append(f"space={space_key}")
610
+ if content_type:
611
+ args_list.append(f"type={content_type}")
612
+ _record_summary(
613
+ parent_ctx,
614
+ command="search-by-space-type",
615
+ args=args_list,
616
+ server=server_key,
617
+ exit_code=exit_code,
618
+ started_at=started_at,
619
+ finished_at=finished_at,
620
+ )
621
+ if exit_code != 0:
622
+ raise typer.Exit(code=exit_code) from None
623
+
624
+
625
+ @app.command("attachments")
626
+ def cmd_attachments(
627
+ ctx: typer.Context,
628
+ args: list[str] | None = typer.Argument(
629
+ None, help="Additional arguments for attachment operations", show_default=False, metavar="ARGS"
630
+ ),
631
+ server: str | None = typer.Option(None, "--server", "-s", help="internal|external"),
632
+ page_id: str | None = typer.Option(None, "--page", "-p", help="Page ID for attachments"),
633
+ list_attachments: bool = typer.Option(False, "--list", "-l", help="List attachments for page"),
634
+ upload_file: str | None = typer.Option(None, "--upload", "-f", help="File to upload"),
635
+ comment: str | None = typer.Option(None, "--comment", "-c", help="Upload comment"),
636
+ json_output: bool = typer.Option(False, "--json", help="Emit JSON payload"),
637
+ ) -> None:
638
+ """Native Python attachment management"""
639
+ parent_ctx = _parent_context(ctx)
640
+ started_at = datetime.now(UTC)
641
+
642
+ try:
643
+ settings = load_settings(strict=True)
644
+ server_key = _normalise_server(server, settings)
645
+ client = _build_content_client(settings, server_key, parent_ctx.obj.api_version)
646
+
647
+ if list_attachments or (page_id and not upload_file):
648
+ # List attachments
649
+ if not page_id:
650
+ typer.echo("Error: --page ID required for listing attachments", err=True)
651
+ raise typer.Exit(code=1) from None
652
+
653
+ result = client.list_attachments(page_id)
654
+ if json_output or not sys.stdout.isatty():
655
+ _emit_json(result)
656
+ else:
657
+ attachments = result.get("results", [])
658
+ count = len(attachments)
659
+ typer.echo(f"Found {count} attachments for page {page_id}:")
660
+ for attachment in attachments:
661
+ name = attachment.get("title", "Unknown")
662
+ size = attachment.get("size", 0)
663
+ typer.echo(f" - {name} ({size} bytes)")
664
+
665
+ elif upload_file and page_id:
666
+ # Upload attachment
667
+ file_path = Path(upload_file)
668
+ if not file_path.exists():
669
+ typer.echo(f"Error: File not found: {upload_file}", err=True)
670
+ raise typer.Exit(code=1) from None
671
+
672
+ result = client.upload_attachment(page_id, file_path, comment=comment)
673
+ if json_output or not sys.stdout.isatty():
674
+ _emit_json(result)
675
+ else:
676
+ attachment_id = result.get("id", "Unknown")
677
+ typer.echo(f"Uploaded attachment '{file_path.name}' with ID: {attachment_id}")
678
+
679
+ else:
680
+ typer.echo("Error: Either use --list with --page or provide --page and --upload", err=True)
681
+ raise typer.Exit(code=1) from None
682
+
683
+ exit_code = 0
684
+ finished_at = datetime.now(UTC)
685
+ _record_summary(
686
+ parent_ctx,
687
+ command="attachments",
688
+ args=args or [],
689
+ server=server_key,
690
+ exit_code=exit_code,
691
+ started_at=started_at,
692
+ finished_at=finished_at,
693
+ )
694
+
695
+ if exit_code != 0:
696
+ raise typer.Exit(code=exit_code) from None
697
+
698
+ except Exception as exc:
699
+ logger.exception("attachments_command_failed: %s", exc)
700
+ typer.echo(f"Error: {exc}", err=True)
701
+ raise typer.Exit(code=1) from None
702
+
703
+
704
+ @app.command("space-permissions")
705
+ def cmd_space_permissions(
706
+ ctx: typer.Context,
707
+ action: str = typer.Argument(..., help="Action: get, set, remove"),
708
+ space_key: str = typer.Option(..., "--space-key", "-k", help="Confluence space key"),
709
+ server: str | None = typer.Option(None, "--server", "-s", help="internal|external"),
710
+ user_key: str | None = typer.Option(None, "--user", "-u", help="User key for user-targeted operations"),
711
+ group_name: str | None = typer.Option(None, "--group", "-g", help="Group name for group-targeted operations"),
712
+ anonymous: bool = typer.Option(False, "--anonymous", help="Target anonymous permissions"),
713
+ operations: list[str] = typer.Option(
714
+ [],
715
+ "--operation",
716
+ "-o",
717
+ help="Space permission operation key (repeatable, e.g., administer, read)",
718
+ ),
719
+ permissions_file: Path | None = typer.Option(
720
+ None,
721
+ "--permissions-file",
722
+ help="JSON object/array with bulk permission definitions (target.type: user|group|anonymous, operations: [...])",
723
+ ),
724
+ yes: bool = typer.Option(False, "--yes", help="Confirm write operations"),
725
+ json_output: bool = typer.Option(False, "--json", help="Emit JSON payload"),
726
+ ) -> None:
727
+ """Manage Confluence space permissions."""
728
+
729
+ parent_ctx = _parent_context(ctx)
730
+ started_at = datetime.now(UTC)
731
+ exit_code = 0
732
+ result_payload: dict[str, Any] = {}
733
+ args_summary: list[str] = []
734
+ server_key: str | None = None
735
+ settings: ConfluenceSettings | None = None
736
+
737
+ try:
738
+ settings = load_settings(strict=True)
739
+ server_key = _normalise_server(server, settings)
740
+ client = _build_http_client(settings, server_key)
741
+
742
+ resolved_operations = operations or None
743
+ args_summary = [
744
+ action,
745
+ f"space={space_key}",
746
+ f"user={user_key}",
747
+ f"group={group_name}",
748
+ f"anonymous={anonymous}",
749
+ f"operations={operations}",
750
+ f"permissions_file={permissions_file}",
751
+ ]
752
+
753
+ if action == "get":
754
+ result = client.get_space_permissions(space_key)
755
+ result_payload = {"space": space_key, "permissions": result}
756
+
757
+ elif action == "set":
758
+ if not yes:
759
+ raise typer.BadParameter("--yes required for write operations")
760
+
761
+ targets = [bool(user_key), bool(group_name), anonymous, bool(permissions_file)]
762
+ if sum(1 for flag in targets if flag) != 1:
763
+ raise typer.BadParameter(
764
+ "Provide exactly one of --user, --group, --anonymous, or --permissions-file for set action"
765
+ )
766
+
767
+ if permissions_file:
768
+ file_content = _read_text_file(permissions_file)
769
+ try:
770
+ parsed = json.loads(file_content)
771
+ except json.JSONDecodeError as exc:
772
+ raise typer.BadParameter(f"Invalid JSON in permissions file: {exc}") from exc
773
+
774
+ if isinstance(parsed, dict):
775
+ items: list[dict[str, Any]] = [parsed]
776
+ elif isinstance(parsed, list):
777
+ if not all(isinstance(item, dict) for item in parsed):
778
+ raise typer.BadParameter("--permissions-file list entries must be JSON objects")
779
+ items = parsed # type: ignore[assignment]
780
+ else:
781
+ raise typer.BadParameter("--permissions-file must contain a JSON object or array")
782
+
783
+ result = client.set_permissions_to_multiple_items_for_space(space_key, items)
784
+ result_payload = {"space": space_key, "bulk": True, "items_processed": len(items), "result": result}
785
+
786
+ elif user_key:
787
+ if not resolved_operations:
788
+ raise typer.BadParameter("At least one --operation required when setting user permissions")
789
+ result = client.set_permissions_to_user_for_space(
790
+ space_key,
791
+ user_key,
792
+ operations=resolved_operations,
793
+ )
794
+ result_payload = {
795
+ "space": space_key,
796
+ "target": {"type": "user", "key": user_key},
797
+ "operations": resolved_operations,
798
+ "result": result,
799
+ }
800
+
801
+ elif group_name:
802
+ if not resolved_operations:
803
+ raise typer.BadParameter("At least one --operation required when setting group permissions")
804
+ result = client.set_permissions_to_group_for_space(
805
+ space_key,
806
+ group_name,
807
+ operations=resolved_operations,
808
+ )
809
+ result_payload = {
810
+ "space": space_key,
811
+ "target": {"type": "group", "name": group_name},
812
+ "operations": resolved_operations,
813
+ "result": result,
814
+ }
815
+
816
+ elif anonymous:
817
+ if not resolved_operations:
818
+ raise typer.BadParameter("At least one --operation required when setting anonymous permissions")
819
+ result = client.set_permissions_to_anonymous_for_space(
820
+ space_key,
821
+ operations=resolved_operations,
822
+ )
823
+ result_payload = {
824
+ "space": space_key,
825
+ "target": {"type": "anonymous"},
826
+ "operations": resolved_operations,
827
+ "result": result,
828
+ }
829
+
830
+ elif action == "remove":
831
+ if not yes:
832
+ raise typer.BadParameter("--yes required for write operations")
833
+
834
+ targets = [bool(user_key), bool(group_name), anonymous]
835
+ if sum(1 for flag in targets if flag) != 1:
836
+ raise typer.BadParameter("Provide exactly one of --user, --group, or --anonymous for remove action")
837
+
838
+ if user_key:
839
+ result = client.remove_permissions_from_user_for_space(space_key, user_key)
840
+ result_payload = {
841
+ "space": space_key,
842
+ "target": {"type": "user", "key": user_key},
843
+ "result": result,
844
+ }
845
+
846
+ elif group_name:
847
+ result = client.remove_permissions_from_group_for_space(space_key, group_name)
848
+ result_payload = {
849
+ "space": space_key,
850
+ "target": {"type": "group", "name": group_name},
851
+ "result": result,
852
+ }
853
+
854
+ elif anonymous:
855
+ result = client.remove_permissions_from_anonymous_for_space(space_key)
856
+ result_payload = {
857
+ "space": space_key,
858
+ "target": {"type": "anonymous"},
859
+ "result": result,
860
+ }
861
+
862
+ else:
863
+ raise typer.BadParameter(f"Unknown action: {action}")
864
+
865
+ if json_output or not sys.stdout.isatty():
866
+ _emit_json(result_payload)
867
+ else:
868
+ typer.echo(json.dumps(result_payload, indent=2, sort_keys=True))
869
+
870
+ except (VDSClientError, typer.BadParameter) as exc:
871
+ exit_code = 1
872
+ logger.exception("space_permissions_command_failed: %s", exc)
873
+ typer.echo(f"Error: {exc}", err=True)
874
+ except Exception as exc: # pragma: no cover - unexpected errors
875
+ exit_code = 1
876
+ logger.exception("space_permissions_command_unexpected")
877
+ typer.echo(f"Error: {exc}", err=True)
878
+ finally:
879
+ finished_at = datetime.now(UTC)
880
+ server_label = server_key or (server.lower() if server else "internal")
881
+ _record_summary(
882
+ parent_ctx,
883
+ command="space-permissions",
884
+ args=args_summary,
885
+ server=server_label,
886
+ exit_code=exit_code,
887
+ started_at=started_at,
888
+ finished_at=finished_at,
889
+ )
890
+ if exit_code != 0:
891
+ raise typer.Exit(code=exit_code) from None
892
+
893
+
894
+ @app.command("space-management")
895
+ def cmd_space_management(
896
+ ctx: typer.Context,
897
+ action: str = typer.Argument(..., help="Action: archive, trash-list, trash-remove"),
898
+ space_key: str = typer.Option(..., "--space-key", "-k", help="Confluence space key"),
899
+ server: str | None = typer.Option(None, "--server", "-s", help="internal|external"),
900
+ cursor: str | None = typer.Option(None, "--cursor", help="Pagination cursor for trash-list"),
901
+ limit: int = typer.Option(50, "--limit", help="Limit for trash-list (max 100)"),
902
+ expand: str | None = typer.Option(None, "--expand", help="Comma separated expansions for trash-list"),
903
+ yes: bool = typer.Option(False, "--yes", help="Confirm write operations"),
904
+ json_output: bool = typer.Option(False, "--json", help="Emit JSON payload"),
905
+ ) -> None:
906
+ """Manage Confluence space lifecycle operations."""
907
+
908
+ parent_ctx = _parent_context(ctx)
909
+ started_at = datetime.now(UTC)
910
+ exit_code = 0
911
+ result_payload: dict[str, Any] = {}
912
+ args_summary: list[str] = []
913
+ server_key: str | None = None
914
+ settings: ConfluenceSettings | None = None
915
+
916
+ try:
917
+ if limit < 1 or limit > 100:
918
+ raise typer.BadParameter("--limit must be between 1 and 100")
919
+
920
+ settings = load_settings(strict=True)
921
+ server_key = _normalise_server(server, settings)
922
+ client = _build_http_client(settings, server_key)
923
+
924
+ expand_value = expand if expand else None
925
+ args_summary = [
926
+ action,
927
+ f"space={space_key}",
928
+ f"cursor={cursor}",
929
+ f"limit={limit}",
930
+ f"expand={expand_value}",
931
+ ]
932
+
933
+ if action == "archive":
934
+ if not yes:
935
+ raise typer.BadParameter("--yes required for archive")
936
+ result = client.archive_space(space_key)
937
+ result_payload = {"space": space_key, "action": "archive", "result": result}
938
+
939
+ elif action == "trash-list":
940
+ result = client.get_trashed_contents_by_space(
941
+ space_key,
942
+ cursor=cursor,
943
+ expand=expand_value,
944
+ limit=limit,
945
+ )
946
+ result_payload = {
947
+ "space": space_key,
948
+ "action": "trash-list",
949
+ "result": result,
950
+ }
951
+
952
+ elif action == "trash-remove":
953
+ if not yes:
954
+ raise typer.BadParameter("--yes required for trash-remove")
955
+ result = client.remove_trashed_contents_by_space(space_key)
956
+ result_payload = {"space": space_key, "action": "trash-remove", "result": result}
957
+
958
+ else:
959
+ raise typer.BadParameter(f"Unknown action: {action}")
960
+
961
+ if json_output or not sys.stdout.isatty():
962
+ _emit_json(result_payload)
963
+ else:
964
+ typer.echo(json.dumps(result_payload, indent=2, sort_keys=True))
965
+
966
+ except (VDSClientError, typer.BadParameter) as exc:
967
+ exit_code = 1
968
+ logger.exception("space_management_failed: %s", exc)
969
+ typer.echo(f"Error: {exc}", err=True)
970
+ except Exception as exc: # pragma: no cover - unexpected errors
971
+ exit_code = 1
972
+ logger.exception("space_management_unexpected_error")
973
+ typer.echo(f"Error: {exc}", err=True)
974
+ finally:
975
+ finished_at = datetime.now(UTC)
976
+ server_label = server_key or (server.lower() if server else "internal")
977
+ _record_summary(
978
+ parent_ctx,
979
+ command="space-management",
980
+ args=args_summary,
981
+ server=server_label,
982
+ exit_code=exit_code,
983
+ started_at=started_at,
984
+ finished_at=finished_at,
985
+ )
986
+ if exit_code != 0:
987
+ raise typer.Exit(code=exit_code) from None
988
+
989
+
990
+ @app.command("group")
991
+ def cmd_group(
992
+ ctx: typer.Context,
993
+ action: str = typer.Argument(..., help="Action: list, members"),
994
+ group_name: str | None = typer.Option(None, "--group", "-g", help="Group name (for members)"),
995
+ server: str | None = typer.Option(None, "--server", "-s", help="internal|external"),
996
+ start: int = typer.Option(0, "--start", help="Start index for pagination"),
997
+ limit: int = typer.Option(1000, "--limit", help="Limit for pagination (max 1000)"),
998
+ json_output: bool = typer.Option(False, "--json", help="Emit JSON payload"),
999
+ ) -> None:
1000
+ """Manage Confluence groups."""
1001
+ parent_ctx = _parent_context(ctx)
1002
+ started_at = datetime.now(UTC)
1003
+ exit_code = 0
1004
+ result_payload: dict[str, Any] = {}
1005
+ args_summary: list[str] = []
1006
+ server_key: str | None = None
1007
+ settings: ConfluenceSettings | None = None
1008
+
1009
+ try:
1010
+ if limit < 1 or limit > 1000:
1011
+ raise typer.BadParameter("--limit must be between 1 and 1000")
1012
+
1013
+ settings = load_settings(strict=True)
1014
+ server_key = _normalise_server(server, settings)
1015
+ client = _build_http_client(settings, server_key)
1016
+
1017
+ args_summary = [action, f"start={start}", f"limit={limit}"]
1018
+
1019
+ if action == "list":
1020
+ result = client.get_all_groups(start=start, limit=limit)
1021
+ result_payload = {"count": len(result), "groups": result}
1022
+
1023
+ elif action == "members":
1024
+ if not group_name:
1025
+ raise typer.BadParameter("--group required for members action")
1026
+ args_summary.append(f"group={group_name}")
1027
+ result = client.get_group_members(group_name, start=start, limit=limit)
1028
+ result_payload = {"group": group_name, "count": len(result), "members": result}
1029
+
1030
+ else:
1031
+ raise typer.BadParameter(f"Unknown action: {action}")
1032
+
1033
+ if json_output or not sys.stdout.isatty():
1034
+ _emit_json(result_payload)
1035
+ else:
1036
+ typer.echo(json.dumps(result_payload, indent=2, sort_keys=True))
1037
+
1038
+ except (VDSClientError, typer.BadParameter) as exc:
1039
+ exit_code = 1
1040
+ logger.exception("group_failed: %s", exc)
1041
+ typer.echo(f"Error: {exc}", err=True)
1042
+ except Exception as exc: # pragma: no cover - unexpected errors
1043
+ exit_code = 1
1044
+ logger.exception("group_unexpected_error")
1045
+ typer.echo(f"Error: {exc}", err=True)
1046
+ finally:
1047
+ finished_at = datetime.now(UTC)
1048
+ server_label = server_key or (server.lower() if server else "internal")
1049
+ _record_summary(
1050
+ parent_ctx,
1051
+ command="group",
1052
+ args=args_summary,
1053
+ server=server_label,
1054
+ exit_code=exit_code,
1055
+ started_at=started_at,
1056
+ finished_at=finished_at,
1057
+ )
1058
+ if exit_code != 0:
1059
+ raise typer.Exit(code=exit_code) from None
1060
+
1061
+
1062
+ @app.command("user")
1063
+ def cmd_user(
1064
+ ctx: typer.Context,
1065
+ action: str = typer.Argument(..., help="Action: get, password, group-add, group-remove"),
1066
+ username: str | None = typer.Option(None, "--username", "-u", help="Username"),
1067
+ userkey: str | None = typer.Option(None, "--userkey", help="User key"),
1068
+ group_name: str | None = typer.Option(None, "--group", "-g", help="Group name (for group-add/group-remove)"),
1069
+ server: str | None = typer.Option(None, "--server", "-s", help="internal|external"),
1070
+ expand: str | None = typer.Option(None, "--expand", help="Comma-separated fields to expand (for get)"),
1071
+ new_password: str | None = typer.Option(None, "--new-password", help="New password (for password)"),
1072
+ yes: bool = typer.Option(False, "--yes", help="Confirm write operations"),
1073
+ json_output: bool = typer.Option(False, "--json", help="Emit JSON payload"),
1074
+ ) -> None:
1075
+ """Manage Confluence users."""
1076
+ parent_ctx = _parent_context(ctx)
1077
+ started_at = datetime.now(UTC)
1078
+ exit_code = 0
1079
+ result_payload: dict[str, Any] = {}
1080
+ args_summary: list[str] = []
1081
+ server_key: str | None = None
1082
+ settings: ConfluenceSettings | None = None
1083
+
1084
+ try:
1085
+ settings = load_settings(strict=True)
1086
+ server_key = _normalise_server(server, settings)
1087
+ client = _build_http_client(settings, server_key)
1088
+
1089
+ args_summary = [action]
1090
+
1091
+ if action == "get":
1092
+ if not username and not userkey:
1093
+ raise typer.BadParameter("Provide --username or --userkey for get action")
1094
+ if username and userkey:
1095
+ raise typer.BadParameter("Provide either --username or --userkey, not both")
1096
+ expand_value = expand if expand else None
1097
+ args_summary.append(f"username={username}" if username else f"userkey={userkey}")
1098
+ if expand_value:
1099
+ args_summary.append(f"expand={expand_value}")
1100
+
1101
+ if username:
1102
+ result = client.get_user_details_by_username(username, expand=expand_value)
1103
+ else:
1104
+ assert userkey is not None
1105
+ result = client.get_user_details_by_userkey(userkey, expand=expand_value)
1106
+ result_payload = {"user": result}
1107
+
1108
+ elif action == "password":
1109
+ if not yes:
1110
+ raise typer.BadParameter("--yes required for password change")
1111
+ if not username:
1112
+ raise typer.BadParameter("--username required for password action")
1113
+ if not new_password:
1114
+ raise typer.BadParameter("--new-password required for password action")
1115
+ args_summary.append(f"username={username}")
1116
+ client.change_user_password(username, new_password)
1117
+ result_payload = {"username": username, "status": "password_changed"}
1118
+
1119
+ elif action == "group-add":
1120
+ if not yes:
1121
+ raise typer.BadParameter("--yes required for group-add")
1122
+ if not username:
1123
+ raise typer.BadParameter("--username required for group-add")
1124
+ if not group_name:
1125
+ raise typer.BadParameter("--group required for group-add")
1126
+ args_summary.append(f"username={username}")
1127
+ args_summary.append(f"group={group_name}")
1128
+ client.add_user_to_group(username, group_name)
1129
+ result_payload = {"username": username, "group": group_name, "status": "added"}
1130
+
1131
+ elif action == "group-remove":
1132
+ if not yes:
1133
+ raise typer.BadParameter("--yes required for group-remove")
1134
+ if not username:
1135
+ raise typer.BadParameter("--username required for group-remove")
1136
+ if not group_name:
1137
+ raise typer.BadParameter("--group required for group-remove")
1138
+ args_summary.append(f"username={username}")
1139
+ args_summary.append(f"group={group_name}")
1140
+ client.remove_user_from_group(username, group_name)
1141
+ result_payload = {"username": username, "group": group_name, "status": "removed"}
1142
+
1143
+ else:
1144
+ raise typer.BadParameter(f"Unknown action: {action}")
1145
+
1146
+ if json_output or not sys.stdout.isatty():
1147
+ _emit_json(result_payload)
1148
+ else:
1149
+ typer.echo(json.dumps(result_payload, indent=2, sort_keys=True))
1150
+
1151
+ except (VDSClientError, typer.BadParameter) as exc:
1152
+ exit_code = 1
1153
+ logger.exception("user_failed: %s", exc)
1154
+ typer.echo(f"Error: {exc}", err=True)
1155
+ except Exception as exc: # pragma: no cover - unexpected errors
1156
+ exit_code = 1
1157
+ logger.exception("user_unexpected_error")
1158
+ typer.echo(f"Error: {exc}", err=True)
1159
+ finally:
1160
+ finished_at = datetime.now(UTC)
1161
+ server_label = server_key or (server.lower() if server else "internal")
1162
+ _record_summary(
1163
+ parent_ctx,
1164
+ command="user",
1165
+ args=args_summary,
1166
+ server=server_label,
1167
+ exit_code=exit_code,
1168
+ started_at=started_at,
1169
+ finished_at=finished_at,
1170
+ )
1171
+ if exit_code != 0:
1172
+ raise typer.Exit(code=exit_code) from None
1173
+
1174
+
1175
+ @app.command("export")
1176
+ def cmd_export(
1177
+ ctx: typer.Context,
1178
+ action: str = typer.Argument(..., help="Action: page, space"),
1179
+ page_id: str | None = typer.Option(None, "--page-id", help="Page ID (for page)"),
1180
+ space_key: str | None = typer.Option(None, "--space-key", "-k", help="Space key (for space)"),
1181
+ export_type: str | None = typer.Option(None, "--export-type", help="Export type: pdf, html, xml (for space)"),
1182
+ server: str | None = typer.Option(None, "--server", "-s", help="internal|external"),
1183
+ output: Path | None = typer.Option(None, "--output", "-o", help="Output file path (for page PDF)"),
1184
+ api_version: str | None = typer.Option(
1185
+ None, "--api-version", help="API version: cloud, server (for page, auto-detected if not specified)"
1186
+ ),
1187
+ json_output: bool = typer.Option(False, "--json", help="Emit JSON payload"),
1188
+ ) -> None:
1189
+ """Export Confluence pages or spaces."""
1190
+ parent_ctx = _parent_context(ctx)
1191
+ started_at = datetime.now(UTC)
1192
+ exit_code = 0
1193
+ result_payload: dict[str, Any] = {}
1194
+ args_summary: list[str] = []
1195
+ server_key: str | None = None
1196
+ settings: ConfluenceSettings | None = None
1197
+
1198
+ try:
1199
+ settings = load_settings(strict=True)
1200
+ server_key = _normalise_server(server, settings)
1201
+ client = _build_http_client(settings, server_key)
1202
+
1203
+ args_summary = [action]
1204
+
1205
+ if action == "page":
1206
+ if not page_id:
1207
+ raise typer.BadParameter("--page-id required for page action")
1208
+ args_summary.append(f"page_id={page_id}")
1209
+ if api_version:
1210
+ args_summary.append(f"api_version={api_version}")
1211
+
1212
+ pdf_bytes = client.export_page(page_id, api_version=api_version)
1213
+
1214
+ if output:
1215
+ output.write_bytes(pdf_bytes)
1216
+ result_payload = {"page_id": page_id, "output_file": str(output), "size_bytes": len(pdf_bytes)}
1217
+ typer.echo(f"Exported page {page_id} to {output} ({len(pdf_bytes)} bytes)")
1218
+ else:
1219
+ # Write to stdout as binary
1220
+ sys.stdout.buffer.write(pdf_bytes)
1221
+ result_payload = {"page_id": page_id, "size_bytes": len(pdf_bytes), "format": "pdf"}
1222
+
1223
+ elif action == "space":
1224
+ if not space_key:
1225
+ raise typer.BadParameter("--space-key required for space action")
1226
+ if not export_type:
1227
+ raise typer.BadParameter("--export-type required for space action")
1228
+ if export_type not in ("pdf", "html", "xml"):
1229
+ raise typer.BadParameter("--export-type must be one of: pdf, html, xml")
1230
+ args_summary.append(f"space_key={space_key}")
1231
+ args_summary.append(f"export_type={export_type}")
1232
+
1233
+ download_url = client.get_space_export(space_key, export_type)
1234
+ result_payload = {"space_key": space_key, "export_type": export_type, "download_url": download_url}
1235
+
1236
+ if json_output or not sys.stdout.isatty():
1237
+ _emit_json(result_payload)
1238
+ else:
1239
+ typer.echo(f"Space export URL: {download_url}")
1240
+
1241
+ else:
1242
+ raise typer.BadParameter(f"Unknown action: {action}")
1243
+
1244
+ # For page action, JSON output only if explicitly requested or output file specified
1245
+ if action == "page" and (json_output or output) and json_output:
1246
+ _emit_json(result_payload)
1247
+
1248
+ except (VDSClientError, typer.BadParameter) as exc:
1249
+ exit_code = 1
1250
+ logger.exception("export_failed: %s", exc)
1251
+ typer.echo(f"Error: {exc}", err=True)
1252
+ except Exception as exc: # pragma: no cover - unexpected errors
1253
+ exit_code = 1
1254
+ logger.exception("export_unexpected_error")
1255
+ typer.echo(f"Error: {exc}", err=True)
1256
+ finally:
1257
+ finished_at = datetime.now(UTC)
1258
+ server_label = server_key or (server.lower() if server else "internal")
1259
+ _record_summary(
1260
+ parent_ctx,
1261
+ command="export",
1262
+ args=args_summary,
1263
+ server=server_label,
1264
+ exit_code=exit_code,
1265
+ started_at=started_at,
1266
+ finished_at=finished_at,
1267
+ )
1268
+ if exit_code != 0:
1269
+ raise typer.Exit(code=exit_code) from None
1270
+
1271
+
1272
+ @app.command("crawl-tree")
1273
+ def cmd_crawl_tree(
1274
+ ctx: typer.Context,
1275
+ root: str = typer.Argument(..., help="Root page reference (numeric ID, ?pageId= URL, or /display/<SPACE>/<Title>)"),
1276
+ output_dir: Path = typer.Option(..., "--out", "-o", help="Output directory for downloaded pages/attachments"),
1277
+ depth: int = typer.Option(
1278
+ 3,
1279
+ "--depth",
1280
+ "--crawl-max-depth",
1281
+ min=0,
1282
+ max=20,
1283
+ help="Recursive child-page crawl depth (0 = only root).",
1284
+ ),
1285
+ include_attachments: bool = typer.Option(
1286
+ True,
1287
+ "--include-attachments/--no-include-attachments",
1288
+ help="Download attachments for each visited page",
1289
+ ),
1290
+ emit_corpus: bool = typer.Option(
1291
+ False,
1292
+ "--emit-corpus/--no-emit-corpus",
1293
+ help="Emit `corpus.jsonl` chunked by headings for LLM ingestion",
1294
+ ),
1295
+ emit_link_graph: bool = typer.Option(
1296
+ False,
1297
+ "--emit-link-graph/--no-emit-link-graph",
1298
+ help="Emit `link-graph.json` (normalized outbound links) for doc↔code alignment",
1299
+ ),
1300
+ emit_tables: bool = typer.Option(
1301
+ False,
1302
+ "--emit-tables",
1303
+ help="Emit `tables.jsonl` (extracted HTML tables) for downstream analysis",
1304
+ ),
1305
+ emit_mentions: bool = typer.Option(
1306
+ False,
1307
+ "--emit-mentions",
1308
+ help="Emit `mentions.jsonl` (cross-system refs: Confluence/Jira/Bitbucket/external/email) for downstream analysis",
1309
+ ),
1310
+ body_format: str = typer.Option(
1311
+ "view",
1312
+ "--body-format",
1313
+ help="Body representation to store/analyze: view|export_view|storage (default: view)",
1314
+ ),
1315
+ chunk_max_chars: int = typer.Option(
1316
+ 4000,
1317
+ "--chunk-max-chars",
1318
+ help="Max characters per corpus chunk (emit-corpus only). Oversized heading chunks are split into ordered subchunks (0 disables splitting).",
1319
+ ),
1320
+ cache_mode: str = typer.Option(
1321
+ "read-write",
1322
+ "--cache-mode",
1323
+ help="Compatibility cache mode flag (off|read|read-write|refresh) for downstream orchestrators.",
1324
+ ),
1325
+ cache_ttl_hours: int | None = typer.Option(
1326
+ None,
1327
+ "--cache-ttl-hours",
1328
+ min=1,
1329
+ help="Compatibility cache TTL (hours) for downstream orchestrators.",
1330
+ ),
1331
+ incremental: bool = typer.Option(
1332
+ True,
1333
+ "--incremental/--no-incremental",
1334
+ help="Compatibility incremental mode flag; enables resume semantics by default.",
1335
+ ),
1336
+ incremental_overlap_minutes: int = typer.Option(
1337
+ 5,
1338
+ "--incremental-overlap-minutes",
1339
+ min=0,
1340
+ help="Incremental crawl overlap window in minutes for last-modified high-water mark refresh.",
1341
+ ),
1342
+ full_reconciliation_interval_days: int = typer.Option(
1343
+ 7,
1344
+ "--full-reconciliation-interval-days",
1345
+ min=1,
1346
+ help="Interval in days for full reconciliation sweep when incremental mode is enabled.",
1347
+ ),
1348
+ content_root: Path | None = typer.Option(
1349
+ None,
1350
+ "--content-root",
1351
+ help="Compatibility content root for downstream orchestrators (diagnostic metadata only).",
1352
+ ),
1353
+ project_storage_key: str | None = typer.Option(
1354
+ None,
1355
+ "--project-storage-key",
1356
+ help="Compatibility project storage namespace key (diagnostic metadata only).",
1357
+ ),
1358
+ resume: bool = typer.Option(
1359
+ False,
1360
+ "--resume",
1361
+ help="Resume from previous crawl state; skip unchanged pages (TSK-188)",
1362
+ ),
1363
+ max_nodes: int | None = typer.Option(
1364
+ 500,
1365
+ "--max-nodes",
1366
+ "--crawl-max-nodes",
1367
+ min=1,
1368
+ help="Safety limit: max pages to process before stopping (default: 500).",
1369
+ ),
1370
+ page_concurrency: int = typer.Option(
1371
+ 5,
1372
+ "--page-concurrency",
1373
+ "--crawl-concurrency",
1374
+ min=1,
1375
+ help="Bounded page fetch concurrency (default: 5).",
1376
+ ),
1377
+ staged_mode: str = typer.Option(
1378
+ "off",
1379
+ "--staged-mode",
1380
+ "--staged-crawl-mode",
1381
+ help="Optional staged crawl mode: off|structure-first (default: off).",
1382
+ ),
1383
+ rate_limit_budget: int = typer.Option(
1384
+ 50000,
1385
+ "--rate-limit-budget",
1386
+ min=100,
1387
+ help="Hourly Confluence API points budget used for proactive crawl throttling.",
1388
+ ),
1389
+ bundle: bool = typer.Option(
1390
+ False,
1391
+ "--bundle",
1392
+ help="Create zip bundle of all artifacts for offline handoff (TSK-189)",
1393
+ ),
1394
+ manifest_path: Path | None = typer.Option(
1395
+ None,
1396
+ "--manifest",
1397
+ help="Optional manifest file path (default: <out>/crawl_manifest.json)",
1398
+ ),
1399
+ server: str | None = typer.Option(None, "--server", "-s", help="internal|external"),
1400
+ json_output: bool = typer.Option(False, "--json", help="Emit JSON payload"),
1401
+ ) -> None:
1402
+ """Crawl a Confluence page tree and download content + attachments to disk."""
1403
+ parent_ctx = _parent_context(ctx)
1404
+ started_at = datetime.now(UTC)
1405
+ exit_code = 0
1406
+ server_key: str | None = None
1407
+ result_payload: dict[str, Any] = {}
1408
+ args_summary: list[str] = []
1409
+
1410
+ try:
1411
+ settings = load_settings(strict=True)
1412
+ if server is None:
1413
+ inferred = _infer_server_from_root_ref(root, settings)
1414
+ if inferred:
1415
+ server = inferred
1416
+ server_key = _normalise_server(server, settings)
1417
+ client = _build_http_client(settings, server_key)
1418
+
1419
+ body_format = (body_format or "view").strip().lower().replace("-", "_")
1420
+ if body_format not in {"view", "export_view", "storage"}:
1421
+ raise typer.BadParameter("--body-format must be one of: view, export_view, storage")
1422
+ cache_mode = (cache_mode or "read-write").strip().lower()
1423
+ if cache_mode not in {"off", "read", "read-write", "refresh"}:
1424
+ raise typer.BadParameter("--cache-mode must be one of: off, read, read-write, refresh")
1425
+ staged_mode = (staged_mode or "off").strip().lower().replace("-", "_")
1426
+ if staged_mode not in {"off", "structure_first"}:
1427
+ raise typer.BadParameter("--staged-mode must be one of: off, structure-first")
1428
+ if chunk_max_chars < 0:
1429
+ raise typer.BadParameter("--chunk-max-chars must be >= 0")
1430
+ effective_resume = bool(resume or incremental)
1431
+
1432
+ args_summary = [
1433
+ root,
1434
+ f"depth={depth}",
1435
+ f"out={output_dir!s}",
1436
+ f"attachments={include_attachments}",
1437
+ f"body_format={body_format}",
1438
+ f"chunk_max_chars={chunk_max_chars}",
1439
+ f"resume={resume}",
1440
+ f"incremental={incremental}",
1441
+ f"incremental_overlap_minutes={incremental_overlap_minutes}",
1442
+ f"full_reconciliation_interval_days={full_reconciliation_interval_days}",
1443
+ f"effective_resume={effective_resume}",
1444
+ f"cache_mode={cache_mode}",
1445
+ f"cache_ttl_hours={cache_ttl_hours}",
1446
+ f"content_root={content_root}",
1447
+ f"project_storage_key={project_storage_key}",
1448
+ f"max_nodes={max_nodes}",
1449
+ f"page_concurrency={page_concurrency}",
1450
+ f"staged_mode={staged_mode}",
1451
+ f"rate_limit_budget={rate_limit_budget}",
1452
+ f"bundle={bundle}",
1453
+ ]
1454
+
1455
+ runtime_event_logger = logging.getLogger("confluence_cli.crawl_tree")
1456
+
1457
+ def _runtime_event_sink(event_name: str, payload: dict[str, Any]) -> None:
1458
+ runtime_event_logger.info(
1459
+ "crawl_tree_runtime_event %s %s",
1460
+ event_name,
1461
+ json.dumps(payload, ensure_ascii=False, sort_keys=True),
1462
+ )
1463
+
1464
+ result_payload = crawl_tree_to_disk(
1465
+ http=client,
1466
+ root_ref=root,
1467
+ output_dir=output_dir,
1468
+ options=CrawlOptions(
1469
+ max_depth=depth,
1470
+ include_attachments=include_attachments,
1471
+ emit_corpus=emit_corpus,
1472
+ emit_link_graph=emit_link_graph,
1473
+ emit_tables=emit_tables,
1474
+ emit_mentions=emit_mentions,
1475
+ body_format=body_format,
1476
+ chunk_max_chars=None if chunk_max_chars == 0 else chunk_max_chars,
1477
+ resume=effective_resume,
1478
+ max_nodes=max_nodes,
1479
+ page_concurrency=page_concurrency,
1480
+ staged_mode=staged_mode,
1481
+ incremental_cql=incremental,
1482
+ incremental_overlap_minutes=incremental_overlap_minutes,
1483
+ full_reconciliation_interval_days=full_reconciliation_interval_days,
1484
+ rate_limit_budget=rate_limit_budget,
1485
+ runtime_event_sink=_runtime_event_sink,
1486
+ bundle=bundle,
1487
+ ),
1488
+ manifest_path=manifest_path,
1489
+ )
1490
+ result_payload["cache"] = {
1491
+ "mode": cache_mode,
1492
+ "ttl_hours": cache_ttl_hours,
1493
+ "incremental": incremental,
1494
+ "incremental_overlap_minutes": incremental_overlap_minutes,
1495
+ "full_reconciliation_interval_days": full_reconciliation_interval_days,
1496
+ "effective_resume": effective_resume,
1497
+ "content_root": str(content_root) if content_root else None,
1498
+ "project_storage_key": project_storage_key,
1499
+ }
1500
+
1501
+ if json_output or not sys.stdout.isatty():
1502
+ _emit_json(result_payload)
1503
+ else:
1504
+ root_id = result_payload.get("root_page_id")
1505
+ pages = 1 + len(result_payload.get("child_pages") or [])
1506
+ atts = len(result_payload.get("attachments") or [])
1507
+ resume_state = result_payload.get("resume_state") or {}
1508
+ skipped = resume_state.get("skipped_unchanged", 0)
1509
+ typer.echo(f"Crawled {pages} page(s), downloaded {atts} attachment(s). Root: {root_id}.")
1510
+ if skipped > 0:
1511
+ typer.echo(f"Skipped {skipped} unchanged page(s) (resume mode).")
1512
+ if max_nodes and not resume_state.get("completed", True):
1513
+ typer.echo(f"Stopped at max_nodes={max_nodes}. Use --resume to continue.")
1514
+ typer.echo(f"Output: {result_payload.get('out_dir')}")
1515
+ if result_payload.get("bundle_path"):
1516
+ typer.echo(f"Bundle: {result_payload.get('bundle_path')}")
1517
+
1518
+ except (VDSClientError, typer.BadParameter) as exc:
1519
+ exit_code = 1
1520
+ logger.exception("crawl_tree_failed: %s", exc)
1521
+ typer.echo(f"Error: {exc}", err=True)
1522
+ except Exception as exc: # pragma: no cover - unexpected errors
1523
+ exit_code = 1
1524
+ logger.exception("crawl_tree_unexpected_error")
1525
+ typer.echo(f"Error: {exc}", err=True)
1526
+ finally:
1527
+ finished_at = datetime.now(UTC)
1528
+ server_label = server_key or (server.lower() if server else "internal")
1529
+ _record_summary(
1530
+ parent_ctx,
1531
+ command="crawl-tree",
1532
+ args=args_summary,
1533
+ server=server_label,
1534
+ exit_code=exit_code,
1535
+ started_at=started_at,
1536
+ finished_at=finished_at,
1537
+ )
1538
+ if exit_code != 0:
1539
+ raise typer.Exit(code=exit_code) from None
1540
+
1541
+
1542
+ @app.command("draft")
1543
+ def cmd_draft(
1544
+ ctx: typer.Context,
1545
+ action: str = typer.Argument(..., help="Action: get, list, remove"),
1546
+ page_id: str | None = typer.Option(None, "--page-id", help="Page ID (for get/remove)"),
1547
+ space_key: str | None = typer.Option(None, "--space-key", "-k", help="Space key (for list)"),
1548
+ server: str | None = typer.Option(None, "--server", "-s", help="internal|external"),
1549
+ limit: int = typer.Option(25, "--limit", help="Limit for list (max 100)"),
1550
+ yes: bool = typer.Option(False, "--yes", help="Confirm write operations (for remove)"),
1551
+ json_output: bool = typer.Option(False, "--json", help="Emit JSON payload"),
1552
+ ) -> None:
1553
+ """Manage Confluence draft pages (Server-only)."""
1554
+ parent_ctx = _parent_context(ctx)
1555
+ started_at = datetime.now(UTC)
1556
+ exit_code = 0
1557
+ result_payload: dict[str, Any] = {}
1558
+ args_summary: list[str] = []
1559
+ server_key: str | None = None
1560
+ settings: ConfluenceSettings | None = None
1561
+
1562
+ try:
1563
+ if limit < 1 or limit > 100:
1564
+ raise typer.BadParameter("--limit must be between 1 and 100")
1565
+
1566
+ settings = load_settings(strict=True)
1567
+ server_key = _normalise_server(server, settings)
1568
+ client = _build_http_client(settings, server_key)
1569
+
1570
+ args_summary = [action]
1571
+
1572
+ if action == "get":
1573
+ if not page_id:
1574
+ raise typer.BadParameter("--page-id required for get action")
1575
+ args_summary.append(f"page_id={page_id}")
1576
+ result = client.get_draft_page_by_id(page_id)
1577
+ result_payload = {"draft_page": result}
1578
+
1579
+ elif action == "list":
1580
+ if not space_key:
1581
+ raise typer.BadParameter("--space-key required for list action")
1582
+ args_summary.append(f"space_key={space_key}")
1583
+ args_summary.append(f"limit={limit}")
1584
+ result = client.get_all_draft_pages_from_space(space_key, limit=limit)
1585
+ result_payload = {"space": space_key, "count": len(result), "drafts": result}
1586
+
1587
+ elif action == "remove":
1588
+ if not yes:
1589
+ raise typer.BadParameter("--yes required for remove action")
1590
+ if not page_id:
1591
+ raise typer.BadParameter("--page-id required for remove action")
1592
+ args_summary.append(f"page_id={page_id}")
1593
+ client.remove_page_as_draft(page_id)
1594
+ result_payload = {"page_id": page_id, "status": "removed"}
1595
+
1596
+ else:
1597
+ raise typer.BadParameter(f"Unknown action: {action}")
1598
+
1599
+ if json_output or not sys.stdout.isatty():
1600
+ _emit_json(result_payload)
1601
+ else:
1602
+ typer.echo(json.dumps(result_payload, indent=2, sort_keys=True))
1603
+
1604
+ except (VDSClientError, typer.BadParameter) as exc:
1605
+ exit_code = 1
1606
+ logger.exception("draft_failed: %s", exc)
1607
+ typer.echo(f"Error: {exc}", err=True)
1608
+ except Exception as exc: # pragma: no cover - unexpected errors
1609
+ exit_code = 1
1610
+ logger.exception("draft_unexpected_error")
1611
+ typer.echo(f"Error: {exc}", err=True)
1612
+ finally:
1613
+ finished_at = datetime.now(UTC)
1614
+ server_label = server_key or (server.lower() if server else "internal")
1615
+ _record_summary(
1616
+ parent_ctx,
1617
+ command="draft",
1618
+ args=args_summary,
1619
+ server=server_label,
1620
+ exit_code=exit_code,
1621
+ started_at=started_at,
1622
+ finished_at=finished_at,
1623
+ )
1624
+ if exit_code != 0:
1625
+ raise typer.Exit(code=exit_code) from None
1626
+
1627
+
1628
+ @app.command("history")
1629
+ def cmd_history(
1630
+ ctx: typer.Context,
1631
+ action: str = typer.Argument(..., help="Action: get, version, remove"),
1632
+ page_id: str | None = typer.Option(None, "--page-id", help="Page ID"),
1633
+ version: int | None = typer.Option(None, "--version", help="Version number (for version/remove)"),
1634
+ server: str | None = typer.Option(None, "--server", "-s", help="internal|external"),
1635
+ yes: bool = typer.Option(False, "--yes", help="Confirm write operations (for remove)"),
1636
+ json_output: bool = typer.Option(False, "--json", help="Emit JSON payload"),
1637
+ ) -> None:
1638
+ """Manage Confluence page history and versioning."""
1639
+ parent_ctx = _parent_context(ctx)
1640
+ started_at = datetime.now(UTC)
1641
+ exit_code = 0
1642
+ result_payload: dict[str, Any] = {}
1643
+ args_summary: list[str] = []
1644
+ server_key: str | None = None
1645
+ settings: ConfluenceSettings | None = None
1646
+
1647
+ try:
1648
+ settings = load_settings(strict=True)
1649
+ server_key = _normalise_server(server, settings)
1650
+ client = _build_http_client(settings, server_key)
1651
+
1652
+ args_summary = [action]
1653
+
1654
+ if action == "get":
1655
+ if not page_id:
1656
+ raise typer.BadParameter("--page-id required for get action")
1657
+ args_summary.append(f"page_id={page_id}")
1658
+ result = client.history(page_id)
1659
+ result_payload = {"page_id": page_id, "history": result}
1660
+
1661
+ elif action == "version":
1662
+ if not page_id:
1663
+ raise typer.BadParameter("--page-id required for version action")
1664
+ if version is None:
1665
+ raise typer.BadParameter("--version required for version action")
1666
+ args_summary.append(f"page_id={page_id}")
1667
+ args_summary.append(f"version={version}")
1668
+ result = client.get_content_history_by_version_number(page_id, version)
1669
+ result_payload = {"page_id": page_id, "version": version, "content": result}
1670
+
1671
+ elif action == "remove":
1672
+ if not yes:
1673
+ raise typer.BadParameter("--yes required for remove action (experimental feature)")
1674
+ if not page_id:
1675
+ raise typer.BadParameter("--page-id required for remove action")
1676
+ if version is None:
1677
+ raise typer.BadParameter("--version required for remove action")
1678
+ args_summary.append(f"page_id={page_id}")
1679
+ args_summary.append(f"version={version}")
1680
+ typer.echo("Warning: remove_content_history is an experimental feature", err=True)
1681
+ client.remove_content_history(page_id, version)
1682
+ result_payload = {"page_id": page_id, "version": version, "status": "removed"}
1683
+
1684
+ else:
1685
+ raise typer.BadParameter(f"Unknown action: {action}")
1686
+
1687
+ if json_output or not sys.stdout.isatty():
1688
+ _emit_json(result_payload)
1689
+ else:
1690
+ typer.echo(json.dumps(result_payload, indent=2, sort_keys=True))
1691
+
1692
+ except (VDSClientError, typer.BadParameter) as exc:
1693
+ exit_code = 1
1694
+ logger.exception("history_failed: %s", exc)
1695
+ typer.echo(f"Error: {exc}", err=True)
1696
+ except Exception as exc: # pragma: no cover - unexpected errors
1697
+ exit_code = 1
1698
+ logger.exception("history_unexpected_error")
1699
+ typer.echo(f"Error: {exc}", err=True)
1700
+ finally:
1701
+ finished_at = datetime.now(UTC)
1702
+ server_label = server_key or (server.lower() if server else "internal")
1703
+ _record_summary(
1704
+ parent_ctx,
1705
+ command="history",
1706
+ args=args_summary,
1707
+ server=server_label,
1708
+ exit_code=exit_code,
1709
+ started_at=started_at,
1710
+ finished_at=finished_at,
1711
+ )
1712
+ if exit_code != 0:
1713
+ raise typer.Exit(code=exit_code) from None
1714
+
1715
+
1716
+ @app.command("cache")
1717
+ def cmd_cache(
1718
+ ctx: typer.Context,
1719
+ action: str = typer.Argument(..., help="Action: statistics, flush, size"),
1720
+ cache_name: str | None = typer.Option(None, "--cache-name", help="Cache name (for flush)"),
1721
+ server: str | None = typer.Option(None, "--server", "-s", help="internal|external"),
1722
+ yes: bool = typer.Option(False, "--yes", help="Confirm write operation (for flush)"),
1723
+ json_output: bool = typer.Option(False, "--json", help="Emit JSON payload"),
1724
+ ) -> None:
1725
+ """Manage Confluence cache (Server-only)."""
1726
+ parent_ctx = _parent_context(ctx)
1727
+ started_at = datetime.now(UTC)
1728
+ exit_code = 0
1729
+ result_payload: dict[str, Any] = {}
1730
+ args_summary: list[str] = []
1731
+ server_key: str | None = None
1732
+ settings: ConfluenceSettings | None = None
1733
+
1734
+ try:
1735
+ settings = load_settings(strict=True)
1736
+ server_key = _normalise_server(server, settings)
1737
+ client = _build_http_client(settings, server_key)
1738
+
1739
+ args_summary = [action]
1740
+
1741
+ if action == "statistics":
1742
+ result = client.get_cache_statistics()
1743
+ result_payload = {"cache_statistics": result}
1744
+ elif action == "flush":
1745
+ if not yes:
1746
+ raise typer.BadParameter("Refusing to flush cache without --yes")
1747
+ if cache_name:
1748
+ args_summary.append(f"cache_name={cache_name}")
1749
+ client.flush_cache(cache_name=cache_name)
1750
+ result_payload = {"status": "success", "cache_flushed": cache_name or "all"}
1751
+ elif action == "size":
1752
+ result = client.get_cache_size()
1753
+ result_payload = {"cache_size": result}
1754
+ else:
1755
+ raise typer.BadParameter(f"Unknown action: {action}")
1756
+
1757
+ if json_output or not sys.stdout.isatty():
1758
+ _emit_json(result_payload)
1759
+ else:
1760
+ typer.echo(json.dumps(result_payload, indent=2, sort_keys=True))
1761
+
1762
+ except (VDSClientError, typer.BadParameter) as exc:
1763
+ exit_code = 1
1764
+ logger.exception("cache_failed: %s", exc)
1765
+ typer.echo(f"Error: {exc}", err=True)
1766
+ except Exception as exc: # pragma: no cover - unexpected errors
1767
+ exit_code = 1
1768
+ logger.exception("cache_unexpected_error")
1769
+ typer.echo(f"Error: {exc}", err=True)
1770
+ finally:
1771
+ finished_at = datetime.now(UTC)
1772
+ server_label = server_key or (server.lower() if server else "internal")
1773
+ _record_summary(
1774
+ parent_ctx,
1775
+ command="cache",
1776
+ args=args_summary,
1777
+ server=server_label,
1778
+ exit_code=exit_code,
1779
+ started_at=started_at,
1780
+ finished_at=finished_at,
1781
+ )
1782
+ if exit_code != 0:
1783
+ raise typer.Exit(code=exit_code) from None
1784
+
1785
+
1786
+ @app.command("get")
1787
+ def cmd_get(
1788
+ ctx: typer.Context,
1789
+ page_id: str = typer.Argument(..., help="Confluence page ID"),
1790
+ expand: str | None = typer.Option(
1791
+ None, "--expand", "-e", help="Comma separated expansions (e.g., 'body.storage,version')"
1792
+ ),
1793
+ format: str | None = typer.Option(None, "--format", "-f", help="Output format (storage|editor|view)"),
1794
+ server: str | None = typer.Option(None, "--server", "-s", help="internal|external"),
1795
+ ) -> None:
1796
+ """Get Confluence content by page ID."""
1797
+ parent_ctx = _parent_context(ctx)
1798
+ _ensure_v1_api(parent_ctx, "get")
1799
+ started_at = datetime.now(UTC)
1800
+ settings = load_settings(strict=True)
1801
+ server_key = _normalise_server(server, settings)
1802
+
1803
+ try:
1804
+ client = ContentClientV2(_build_http_client(settings, server_key))
1805
+ content = client.get_content(page_id, expand=_expand_option(expand), format=format)
1806
+
1807
+ if content:
1808
+ _emit_json(content)
1809
+ else:
1810
+ typer.echo(f"Content not found: {page_id}", err=True)
1811
+ raise typer.Exit(code=1) from None
1812
+
1813
+ finished_at = datetime.now(UTC)
1814
+ _record_summary(
1815
+ parent_ctx,
1816
+ command="get",
1817
+ args=[page_id],
1818
+ server=server_key,
1819
+ exit_code=0,
1820
+ started_at=started_at,
1821
+ finished_at=finished_at,
1822
+ )
1823
+
1824
+ except Exception as exc:
1825
+ logger.exception("get_command_failed: %s", exc)
1826
+ typer.echo(f"Error: {exc}", err=True)
1827
+ raise typer.Exit(code=1) from None
1828
+
1829
+
1830
+ # ---------------------------------------------------------------------------
1831
+ # Native content commands
1832
+ # ---------------------------------------------------------------------------
1833
+
1834
+
1835
+ @batch_app.command("scan")
1836
+ def batch_scan(
1837
+ ctx: typer.Context,
1838
+ cql: str = typer.Option(..., "--cql", "-q", help="CQL query string"),
1839
+ limit: int = typer.Option(25, "--limit", help="Number of results per request"),
1840
+ max_results: int | None = typer.Option(None, "--max-results", help="Total results to fetch"),
1841
+ start: int = typer.Option(0, "--start", help="Pagination start offset"),
1842
+ expand: str | None = typer.Option(None, "--expand", help="Comma separated expansions"),
1843
+ server: str | None = typer.Option(None, "--server", "-s", help="internal|external"),
1844
+ json_output: bool = typer.Option(False, "--json", help="Emit JSON payload"),
1845
+ ) -> None:
1846
+ parent_ctx = _parent_context(ctx)
1847
+ _ensure_v1_api(parent_ctx, "batch.scan")
1848
+ started_at = datetime.now(UTC)
1849
+ settings = load_settings(strict=True)
1850
+ server_key = _normalise_server(server, settings)
1851
+ runner = BatchRunner(_build_http_client(settings, server_key), expand=_expand_option(expand))
1852
+ results: list[dict[str, Any]] = []
1853
+ for item in runner.scan(cql, limit=limit, max_results=max_results, start=start):
1854
+ results.append(item)
1855
+ metrics = runner.last_metrics
1856
+ payload: dict[str, Any] = {
1857
+ "cql": cql,
1858
+ "count": len(results),
1859
+ "limit": limit,
1860
+ "maxResults": max_results,
1861
+ "start": start,
1862
+ "results": results,
1863
+ }
1864
+ if metrics is not None:
1865
+ payload["metrics"] = {
1866
+ "requests": metrics.requests,
1867
+ "pagesProcessed": metrics.pages_processed,
1868
+ "durationSeconds": metrics.duration_seconds,
1869
+ }
1870
+ if json_output or not sys.stdout.isatty():
1871
+ _emit_json(payload)
1872
+ else:
1873
+ typer.echo(
1874
+ f"Scanned {payload['count']} results (requests={payload['metrics']['requests'] if 'metrics' in payload else '?'})"
1875
+ )
1876
+ finished_at = datetime.now(UTC)
1877
+ _record_summary(
1878
+ parent_ctx,
1879
+ command="batch.scan",
1880
+ args=[cql, f"limit={limit}", f"start={start}"],
1881
+ server=server_key,
1882
+ exit_code=0,
1883
+ started_at=started_at,
1884
+ finished_at=finished_at,
1885
+ )
1886
+
1887
+
1888
+ @batch_app.command("snapshot")
1889
+ def batch_snapshot(
1890
+ ctx: typer.Context,
1891
+ cql: str = typer.Option(..., "--cql", "-q", help="CQL query string"),
1892
+ limit: int = typer.Option(25, "--limit", help="Number of results per request"),
1893
+ max_results: int | None = typer.Option(None, "--max-results", help="Total results to fetch"),
1894
+ expand: str | None = typer.Option(None, "--expand", help="Comma separated expansions"),
1895
+ server: str | None = typer.Option(None, "--server", "-s", help="internal|external"),
1896
+ ) -> None:
1897
+ parent_ctx = _parent_context(ctx)
1898
+ _ensure_v1_api(parent_ctx, "batch.snapshot")
1899
+ started_at = datetime.now(UTC)
1900
+ settings = load_settings(strict=True)
1901
+ server_key = _normalise_server(server, settings)
1902
+ runner = BatchRunner(_build_http_client(settings, server_key))
1903
+ payload = runner.snapshot(cql, limit=limit, max_results=max_results, expand=_expand_option(expand))
1904
+ _emit_json(payload)
1905
+ finished_at = datetime.now(UTC)
1906
+ _record_summary(
1907
+ parent_ctx,
1908
+ command="batch.snapshot",
1909
+ args=[cql, f"limit={limit}"],
1910
+ server=server_key,
1911
+ exit_code=0,
1912
+ started_at=started_at,
1913
+ finished_at=finished_at,
1914
+ )
1915
+
1916
+
1917
+ @batch_app.command("poll")
1918
+ def batch_poll(
1919
+ ctx: typer.Context,
1920
+ cql: str = typer.Option(..., "--cql", "-q", help="Base CQL query (will be augmented with lastmodified filter)"),
1921
+ state_file: Path = typer.Option(..., "--state-file", help="Path to persistent cursor file (JSON)"),
1922
+ limit: int = typer.Option(25, "--limit", help="Page size per request"),
1923
+ max_results: int | None = typer.Option(None, "--max-results", help="Max results this run"),
1924
+ expand: str | None = typer.Option(
1925
+ "version", "--expand", help="Comma separated expansions (default includes 'version')"
1926
+ ),
1927
+ out_jsonl: Path | None = typer.Option(None, "--out-jsonl", help="Write results to JSONL (one object per line)"),
1928
+ server: str | None = typer.Option(None, "--server", "-s", help="internal|external"),
1929
+ ) -> None:
1930
+ parent_ctx = _parent_context(ctx)
1931
+ _ensure_v1_api(parent_ctx, "batch.poll")
1932
+ started_at = datetime.now(UTC)
1933
+ server_key = "internal"
1934
+ try:
1935
+ settings = load_settings(strict=True)
1936
+ server_key = _normalise_server(server, settings)
1937
+ client = _build_http_client(settings, server_key)
1938
+ poller = Poller(client)
1939
+ expand_list = _expand_option(expand) or ["version"]
1940
+ count = poller.poll_once(
1941
+ base_cql=cql,
1942
+ state_file=state_file,
1943
+ limit=limit,
1944
+ max_results=max_results,
1945
+ expand=expand_list,
1946
+ out_jsonl=out_jsonl,
1947
+ )
1948
+ typer.echo(f"Polled {count} items")
1949
+ except Exception as exc:
1950
+ logger.exception("batch.poll failed", extra={"cql": cql})
1951
+ typer.echo(f"Error: {exc}", err=True)
1952
+ raise typer.Exit(code=1) from None
1953
+ finally:
1954
+ finished_at = datetime.now(UTC)
1955
+ _record_summary(
1956
+ parent_ctx,
1957
+ command="batch.poll",
1958
+ args=[cql, f"limit={limit}", f"out={str(out_jsonl) if out_jsonl else '-'}"],
1959
+ server=server_key,
1960
+ exit_code=0,
1961
+ started_at=started_at,
1962
+ finished_at=finished_at,
1963
+ )
1964
+
1965
+
1966
+ @webhook_app.command("create")
1967
+ def webhook_create(
1968
+ ctx: typer.Context,
1969
+ name: str = typer.Option(..., "--name", "-n", help="Webhook name"),
1970
+ url: str = typer.Option(..., "--url", help="Destination URL"),
1971
+ event: list[str] = typer.Option(["page_created"], "--event", "-e", help="Event identifier"),
1972
+ active: bool = typer.Option(True, "--active/--inactive", help="Toggle webhook activation"),
1973
+ filters: str | None = typer.Option(None, "--filters", help="JSON object filters"),
1974
+ server: str | None = typer.Option(None, "--server", "-s", help="internal|external"),
1975
+ json_output: bool = typer.Option(False, "--json", help="Emit JSON payload"),
1976
+ ) -> None:
1977
+ parent_ctx = _parent_context(ctx)
1978
+ _ensure_v1_api(parent_ctx, "webhook.create")
1979
+ started_at = datetime.now(UTC)
1980
+ settings = load_settings(strict=True)
1981
+ server_key = _normalise_server(server, settings)
1982
+ client = WebhookClient(_build_http_client(settings, server_key))
1983
+ payload = client.create(
1984
+ name=name,
1985
+ url=url,
1986
+ events=event,
1987
+ active=active,
1988
+ filters=_parse_json_option(filters, "--filters"),
1989
+ )
1990
+ if json_output:
1991
+ _emit_json(payload)
1992
+ else:
1993
+ typer.echo(f"Created webhook {payload.get('id')}")
1994
+ finished_at = datetime.now(UTC)
1995
+ _record_summary(
1996
+ parent_ctx,
1997
+ command="webhook.create",
1998
+ args=[name, f"events={len(event)}"],
1999
+ server=server_key,
2000
+ exit_code=0,
2001
+ started_at=started_at,
2002
+ finished_at=finished_at,
2003
+ )
2004
+
2005
+
2006
+ @webhook_app.command("list")
2007
+ def webhook_list(
2008
+ ctx: typer.Context,
2009
+ server: str | None = typer.Option(None, "--server", "-s", help="internal|external"),
2010
+ json_output: bool = typer.Option(False, "--json", help="Emit JSON payload"),
2011
+ ) -> None:
2012
+ parent_ctx = _parent_context(ctx)
2013
+ _ensure_v1_api(parent_ctx, "webhook.list")
2014
+ started_at = datetime.now(UTC)
2015
+ settings = load_settings(strict=True)
2016
+ server_key = _normalise_server(server, settings)
2017
+ client = WebhookClient(_build_http_client(settings, server_key))
2018
+ payload = client.list()
2019
+ if json_output:
2020
+ _emit_json(payload)
2021
+ else:
2022
+ typer.echo(f"{len(payload)} webhooks")
2023
+ finished_at = datetime.now(UTC)
2024
+ _record_summary(
2025
+ parent_ctx,
2026
+ command="webhook.list",
2027
+ args=[f"count={len(payload)}"],
2028
+ server=server_key,
2029
+ exit_code=0,
2030
+ started_at=started_at,
2031
+ finished_at=finished_at,
2032
+ )
2033
+
2034
+
2035
+ @webhook_app.command("delete")
2036
+ def webhook_delete(
2037
+ ctx: typer.Context,
2038
+ webhook_id: str = typer.Argument(..., help="Webhook identifier"),
2039
+ server: str | None = typer.Option(None, "--server", "-s", help="internal|external"),
2040
+ ) -> None:
2041
+ parent_ctx = _parent_context(ctx)
2042
+ _ensure_v1_api(parent_ctx, "webhook.delete")
2043
+ started_at = datetime.now(UTC)
2044
+ settings = load_settings(strict=True)
2045
+ server_key = _normalise_server(server, settings)
2046
+ client = WebhookClient(_build_http_client(settings, server_key))
2047
+ client.delete(webhook_id)
2048
+ typer.echo(f"Deleted webhook {webhook_id}")
2049
+ finished_at = datetime.now(UTC)
2050
+ _record_summary(
2051
+ parent_ctx,
2052
+ command="webhook.delete",
2053
+ args=[webhook_id],
2054
+ server=server_key,
2055
+ exit_code=0,
2056
+ started_at=started_at,
2057
+ finished_at=finished_at,
2058
+ )
2059
+
2060
+
2061
+ @content_app.command("page")
2062
+ def content_page(
2063
+ ctx: typer.Context,
2064
+ page_id: str = typer.Argument(..., help="Confluence page ID"),
2065
+ expand: str | None = typer.Option(None, "--expand", help="Comma separated expansions"),
2066
+ server: str | None = typer.Option(None, "--server", "-s", help="internal|external"),
2067
+ ) -> None:
2068
+ """Fetch a Confluence page by ID."""
2069
+ parent_ctx = _parent_context(ctx)
2070
+ started_at = datetime.now(UTC)
2071
+ try:
2072
+ settings = load_settings(strict=True)
2073
+ server_key = _normalise_server(server, settings)
2074
+ cli_ctx = _get_cli_ctx(parent_ctx)
2075
+ client = _build_content_client(settings, server_key, cli_ctx.api_version)
2076
+ expand_list = _expand_option(expand)
2077
+ payload = client.get_page(page_id, expand=expand_list)
2078
+ _emit_json(payload)
2079
+ except Exception as exc:
2080
+ logger.exception("content.page failed", extra={"page_id": page_id})
2081
+ typer.echo(f"Error: {exc}", err=True)
2082
+ raise typer.Exit(code=1) from None
2083
+ else:
2084
+ finished_at = datetime.now(UTC)
2085
+ _record_summary(
2086
+ parent_ctx,
2087
+ command="content.page",
2088
+ args=[page_id],
2089
+ server=server_key,
2090
+ exit_code=0,
2091
+ started_at=started_at,
2092
+ finished_at=finished_at,
2093
+ )
2094
+
2095
+
2096
+ @content_app.command("search")
2097
+ def content_search(
2098
+ ctx: typer.Context,
2099
+ cql: str = typer.Argument(..., help="CQL query string"),
2100
+ limit: int = typer.Option(25, "--limit", help="Number of results"),
2101
+ start: int = typer.Option(0, "--start", help="Pagination start"),
2102
+ expand: str | None = typer.Option(None, "--expand", help="Comma separated expansions"),
2103
+ server: str | None = typer.Option(None, "--server", "-s", help="internal|external"),
2104
+ ) -> None:
2105
+ """Search Confluence pages by CQL query."""
2106
+ parent_ctx = _parent_context(ctx)
2107
+ started_at = datetime.now(UTC)
2108
+ exit_code = 0
2109
+ server_key: str = ""
2110
+ try:
2111
+ settings = load_settings(strict=True)
2112
+ server_key = _normalise_server(server, settings)
2113
+ cli_ctx = _get_cli_ctx(parent_ctx)
2114
+ client = _build_content_client(settings, server_key, cli_ctx.api_version)
2115
+ expand_list = _expand_option(expand)
2116
+ payload = client.search_cql(cql, limit=limit, start=start, expand=expand_list)
2117
+ _emit_json(payload)
2118
+ except Exception as exc:
2119
+ exit_code = 1
2120
+ logger.exception("content.search failed", extra={"cql": cql})
2121
+ # Provide targeted guidance for CQL auth failures
2122
+ from .errors import AuthError # local import to avoid top-level cycles
2123
+
2124
+ if isinstance(exc, AuthError):
2125
+ typer.echo("Error: Authentication failed for CQL search.", err=True)
2126
+ typer.echo("Hint: We default to Basic auth (VDS_USERNAME/PASSWORD). If using token, ensure:", err=True)
2127
+ typer.echo(" - Token has permission to use CQL (/rest/api/*search)", err=True)
2128
+ typer.echo(" - Or set VDS_USERNAME/VDS_PASSWORD to use Basic auth", err=True)
2129
+ typer.echo(
2130
+ " - Quick check: curl -u \"$VDS_USERNAME:$VDS_PASSWORD\" 'http://confluence.digital.vn/rest/api/search?cql=type=page&limit=1'",
2131
+ err=True,
2132
+ )
2133
+ else:
2134
+ typer.echo(f"Error: {exc}", err=True)
2135
+ finally:
2136
+ finished_at = datetime.now(UTC)
2137
+ _record_summary(
2138
+ parent_ctx,
2139
+ command="content.search",
2140
+ args=_content_summary_args(cql=cql, limit=limit, start=start),
2141
+ server=server_key,
2142
+ exit_code=exit_code,
2143
+ started_at=started_at,
2144
+ finished_at=finished_at,
2145
+ )
2146
+ if exit_code != 0:
2147
+ raise typer.Exit(code=exit_code) from None
2148
+
2149
+
2150
+ @content_app.command("templates")
2151
+ def content_templates(
2152
+ ctx: typer.Context,
2153
+ space_key: str | None = typer.Option(None, "--space", help="Space key"),
2154
+ server: str | None = typer.Option(None, "--server", "-s", help="internal|external"),
2155
+ ) -> None:
2156
+ """List available Confluence page templates."""
2157
+ parent_ctx = _parent_context(ctx)
2158
+ started_at = datetime.now(UTC)
2159
+ try:
2160
+ settings = load_settings(strict=True)
2161
+ server_key = _normalise_server(server, settings)
2162
+ cli_ctx = _get_cli_ctx(parent_ctx)
2163
+ client = _build_content_client(settings, server_key, cli_ctx.api_version)
2164
+ if hasattr(client, "list_templates"):
2165
+ payload = client.list_templates(space_key=space_key)
2166
+ else:
2167
+ payload = {"templates": [], "message": "Templates API not available for this server"}
2168
+ _emit_json(payload)
2169
+ except Exception as exc:
2170
+ logger.exception("content.templates failed", extra={"space": space_key})
2171
+ typer.echo(f"Error: {exc}", err=True)
2172
+ raise typer.Exit(code=1) from None
2173
+ else:
2174
+ finished_at = datetime.now(UTC)
2175
+ _record_summary(
2176
+ parent_ctx,
2177
+ command="content.templates",
2178
+ args=_content_summary_args(space=space_key or "*"),
2179
+ server=server_key,
2180
+ exit_code=0,
2181
+ started_at=started_at,
2182
+ finished_at=finished_at,
2183
+ )
2184
+
2185
+
2186
+ @content_app.command("create-page")
2187
+ def content_create_page(
2188
+ ctx: typer.Context,
2189
+ space_key: str = typer.Option(..., "--space", "-S", help="Space key to create the page in"),
2190
+ title: str = typer.Option(..., "--title", "-t", help="Page title"),
2191
+ body_file: Path = typer.Option(
2192
+ ..., "--body-file", exists=True, file_okay=True, dir_okay=False, help="Path to storage-format body"
2193
+ ),
2194
+ parent_id: str | None = typer.Option(None, "--parent", help="Ancestor page id"),
2195
+ representation: str = typer.Option(
2196
+ "storage", "--representation", help="Content representation (storage, wiki, view)"
2197
+ ),
2198
+ status: str = typer.Option("current", "--status", help="Target page status (current, draft)"),
2199
+ notify_watchers: bool = typer.Option(
2200
+ False, "--notify-watchers/--no-notify-watchers", help="Send notifications to watchers"
2201
+ ),
2202
+ server: str | None = typer.Option(None, "--server", "-s", help="internal|external"),
2203
+ ) -> None:
2204
+ """Create a new Confluence page."""
2205
+ parent_ctx = _parent_context(ctx)
2206
+ _ensure_v1_api(parent_ctx, "content.create-page")
2207
+ started_at = datetime.now(UTC)
2208
+ try:
2209
+ settings = load_settings(strict=True)
2210
+ server_key = _normalise_server(server, settings)
2211
+ client = ContentClient(_build_http_client(settings, server_key))
2212
+ body = _read_text_file(body_file)
2213
+ payload = client.create_page(
2214
+ space_key=space_key,
2215
+ title=title,
2216
+ body=body,
2217
+ parent_id=parent_id,
2218
+ representation=representation,
2219
+ status=status,
2220
+ notify_watchers=notify_watchers,
2221
+ )
2222
+ _emit_json(payload)
2223
+ except Exception as exc:
2224
+ logger.exception("content.create-page failed", extra={"space": space_key, "title": title})
2225
+ typer.echo(f"Error: {exc}", err=True)
2226
+ raise typer.Exit(code=1) from None
2227
+ else:
2228
+ finished_at = datetime.now(UTC)
2229
+ _record_summary(
2230
+ parent_ctx,
2231
+ command="content.create-page",
2232
+ args=[f"space={space_key}", f"title={title}"],
2233
+ server=server_key,
2234
+ exit_code=0,
2235
+ started_at=started_at,
2236
+ finished_at=finished_at,
2237
+ )
2238
+
2239
+
2240
+ @content_app.command("update-page")
2241
+ def content_update_page(
2242
+ ctx: typer.Context,
2243
+ page_id: str = typer.Argument(..., help="Confluence page ID"),
2244
+ title: str | None = typer.Option(None, "--title", help="New page title"),
2245
+ body_file: Path | None = typer.Option(
2246
+ None, "--body-file", exists=True, file_okay=True, dir_okay=False, help="Path to storage-format body"
2247
+ ),
2248
+ space_key: str | None = typer.Option(None, "--space", help="Move page to this space"),
2249
+ version: int | None = typer.Option(None, "--version", help="Explicit version number"),
2250
+ representation: str = typer.Option("storage", "--representation", help="Content representation for --body-file"),
2251
+ minor_edit: bool = typer.Option(False, "--minor-edit/--no-minor-edit", help="Mark version as minor edit"),
2252
+ message: str | None = typer.Option(None, "--message", help="Version comment"),
2253
+ status: str = typer.Option("current", "--status", help="Target page status"),
2254
+ notify_watchers: bool = typer.Option(
2255
+ False, "--notify-watchers/--no-notify-watchers", help="Send notifications to watchers"
2256
+ ),
2257
+ server: str | None = typer.Option(None, "--server", "-s", help="internal|external"),
2258
+ ) -> None:
2259
+ """Update an existing Confluence page."""
2260
+ parent_ctx = _parent_context(ctx)
2261
+ _ensure_v1_api(parent_ctx, "content.update-page")
2262
+ if body_file is None and title is None and space_key is None:
2263
+ raise typer.BadParameter("Provide at least one of --title, --body-file or --space")
2264
+ body = _read_text_file(body_file) if body_file else None
2265
+ started_at = datetime.now(UTC)
2266
+ try:
2267
+ settings = load_settings(strict=True)
2268
+ server_key = _normalise_server(server, settings)
2269
+ client = ContentClient(_build_http_client(settings, server_key))
2270
+ payload = client.update_page(
2271
+ page_id,
2272
+ title=title,
2273
+ body=body,
2274
+ space_key=space_key,
2275
+ representation=representation,
2276
+ version=version,
2277
+ minor_edit=minor_edit,
2278
+ message=message,
2279
+ status=status,
2280
+ notify_watchers=notify_watchers,
2281
+ )
2282
+ _emit_json(payload)
2283
+ except Exception as exc:
2284
+ logger.exception("content.update-page failed", extra={"page_id": page_id})
2285
+ typer.echo(f"Error: {exc}", err=True)
2286
+ raise typer.Exit(code=1) from None
2287
+ else:
2288
+ finished_at = datetime.now(UTC)
2289
+ args = [page_id]
2290
+ if version is not None:
2291
+ args.append(f"version={version}")
2292
+ if title is not None:
2293
+ args.append(f"title={title}")
2294
+ _record_summary(
2295
+ parent_ctx,
2296
+ command="content.update-page",
2297
+ args=args,
2298
+ server=server_key,
2299
+ exit_code=0,
2300
+ started_at=started_at,
2301
+ finished_at=finished_at,
2302
+ )
2303
+
2304
+
2305
+ @content_app.command("delete-page")
2306
+ def content_delete_page(
2307
+ ctx: typer.Context,
2308
+ page_id: str = typer.Argument(..., help="Confluence page ID"),
2309
+ status: str = typer.Option("current", "--status", help="Specify current or draft"),
2310
+ server: str | None = typer.Option(None, "--server", "-s", help="internal|external"),
2311
+ ) -> None:
2312
+ """Delete a Confluence page."""
2313
+ parent_ctx = _parent_context(ctx)
2314
+ _ensure_v1_api(parent_ctx, "content.delete-page")
2315
+ started_at = datetime.now(UTC)
2316
+ try:
2317
+ settings = load_settings(strict=True)
2318
+ server_key = _normalise_server(server, settings)
2319
+ client = ContentClient(_build_http_client(settings, server_key))
2320
+ client.delete_page(page_id, status=status)
2321
+ typer.echo(f"Deleted page {page_id} (status={status})")
2322
+ except Exception as exc:
2323
+ logger.exception("content.delete-page failed", extra={"page_id": page_id})
2324
+ typer.echo(f"Error: {exc}", err=True)
2325
+ raise typer.Exit(code=1) from None
2326
+ else:
2327
+ finished_at = datetime.now(UTC)
2328
+ _record_summary(
2329
+ parent_ctx,
2330
+ command="content.delete-page",
2331
+ args=[page_id, f"status={status}"],
2332
+ server=server_key,
2333
+ exit_code=0,
2334
+ started_at=started_at,
2335
+ finished_at=finished_at,
2336
+ )
2337
+
2338
+
2339
+ @content_app.command("add-attachment")
2340
+ def content_add_attachment(
2341
+ ctx: typer.Context,
2342
+ page_id: str = typer.Argument(..., help="Confluence page ID"),
2343
+ file_path: Path = typer.Option(..., "--file", exists=True, file_okay=True, dir_okay=False, help="File to upload"),
2344
+ filename: str | None = typer.Option(None, "--filename", help="Override uploaded filename"),
2345
+ content_type: str | None = typer.Option(None, "--content-type", help="MIME type for the attachment"),
2346
+ comment: str | None = typer.Option(None, "--comment", help="Attachment comment"),
2347
+ server: str | None = typer.Option(None, "--server", "-s", help="internal|external"),
2348
+ ) -> None:
2349
+ """Add an attachment to a Confluence page."""
2350
+ parent_ctx = _parent_context(ctx)
2351
+ _ensure_v1_api(parent_ctx, "content.add-attachment")
2352
+ started_at = datetime.now(UTC)
2353
+ try:
2354
+ settings = load_settings(strict=True)
2355
+ server_key = _normalise_server(server, settings)
2356
+ client = ContentClient(_build_http_client(settings, server_key))
2357
+ payload = client.upload_attachment(
2358
+ page_id,
2359
+ str(file_path),
2360
+ filename=filename,
2361
+ content_type=content_type,
2362
+ comment=comment,
2363
+ )
2364
+ _emit_json(payload)
2365
+ except Exception as exc:
2366
+ logger.exception("content.add-attachment failed", extra={"page_id": page_id, "file": str(file_path)})
2367
+ typer.echo(f"Error: {exc}", err=True)
2368
+ raise typer.Exit(code=1) from None
2369
+ else:
2370
+ finished_at = datetime.now(UTC)
2371
+ _record_summary(
2372
+ parent_ctx,
2373
+ command="content.add-attachment",
2374
+ args=[page_id, file_path.name],
2375
+ server=server_key,
2376
+ exit_code=0,
2377
+ started_at=started_at,
2378
+ finished_at=finished_at,
2379
+ )
2380
+
2381
+
2382
+ @content_app.command("download-attachment")
2383
+ def content_download_attachment(
2384
+ ctx: typer.Context,
2385
+ attachment_id: str = typer.Argument(..., help="Attachment ID"),
2386
+ output_path: Path = typer.Option(
2387
+ ..., "--out", "-o", file_okay=True, dir_okay=True, help="Output file path or directory"
2388
+ ),
2389
+ filename: str | None = typer.Option(None, "--filename", help="Override downloaded filename"),
2390
+ server: str | None = typer.Option(None, "--server", "-s", help="internal|external"),
2391
+ ) -> None:
2392
+ """Download a Confluence attachment by ID to a local file."""
2393
+ parent_ctx = _parent_context(ctx)
2394
+ _ensure_v1_api(parent_ctx, "content.download-attachment")
2395
+ started_at = datetime.now(UTC)
2396
+ try:
2397
+ settings = load_settings(strict=True)
2398
+ server_key = _normalise_server(server, settings)
2399
+ client = ContentClient(_build_http_client(settings, server_key))
2400
+ output = client.download_attachment(attachment_id, output_path, filename=filename)
2401
+ _emit_json({"attachment_id": attachment_id, "output_path": str(output)})
2402
+ except Exception as exc:
2403
+ logger.exception("content.download-attachment failed", extra={"attachment_id": attachment_id})
2404
+ typer.echo(f"Error: {exc}", err=True)
2405
+ raise typer.Exit(code=1) from None
2406
+ else:
2407
+ finished_at = datetime.now(UTC)
2408
+ _record_summary(
2409
+ parent_ctx,
2410
+ command="content.download-attachment",
2411
+ args=[attachment_id, str(output_path)],
2412
+ server=server_key,
2413
+ exit_code=0,
2414
+ started_at=started_at,
2415
+ finished_at=finished_at,
2416
+ )
2417
+
2418
+
2419
+ @content_app.command("add-comment")
2420
+ def content_add_comment(
2421
+ ctx: typer.Context,
2422
+ page_id: str = typer.Argument(..., help="Confluence page ID"),
2423
+ body: str | None = typer.Option(None, "--body", help="Comment body in Confluence storage format"),
2424
+ body_file: Path | None = typer.Option(
2425
+ None, "--body-file", exists=True, file_okay=True, dir_okay=False, help="Path to file containing comment body"
2426
+ ),
2427
+ parent_id: str | None = typer.Option(
2428
+ None,
2429
+ "--parent-id",
2430
+ help="Optional parent comment ID to create a threaded reply instead of a top-level comment",
2431
+ ),
2432
+ server: str | None = typer.Option(None, "--server", "-s", help="internal|external"),
2433
+ ) -> None:
2434
+ """Add a comment to a Confluence page."""
2435
+ parent_ctx = _parent_context(ctx)
2436
+ _ensure_v1_api(parent_ctx, "content.add-comment")
2437
+ started_at = datetime.now(UTC)
2438
+
2439
+ if bool(body) == bool(body_file):
2440
+ raise typer.BadParameter("Provide either --body or --body-file")
2441
+
2442
+ try:
2443
+ settings = load_settings(strict=True)
2444
+ server_key = _normalise_server(server, settings)
2445
+ client = ContentClient(_build_http_client(settings, server_key))
2446
+ if body is not None:
2447
+ comment_body = body
2448
+ else:
2449
+ assert body_file is not None
2450
+ comment_body = _read_text_file(body_file)
2451
+ payload = client.add_comment(page_id, comment_body, parent_id=parent_id)
2452
+ _emit_json(payload)
2453
+ except Exception as exc:
2454
+ logger.exception("content.add-comment failed", extra={"page_id": page_id})
2455
+ typer.echo(f"Error: {exc}", err=True)
2456
+ raise typer.Exit(code=1) from None
2457
+ else:
2458
+ finished_at = datetime.now(UTC)
2459
+ _record_summary(
2460
+ parent_ctx,
2461
+ command="content.add-comment",
2462
+ args=[page_id],
2463
+ server=server_key,
2464
+ exit_code=0,
2465
+ started_at=started_at,
2466
+ finished_at=finished_at,
2467
+ )
2468
+
2469
+
2470
+ @content_app.command("update-attachment")
2471
+ def content_update_attachment(
2472
+ ctx: typer.Context,
2473
+ page_id: str = typer.Argument(..., help="Confluence page ID"),
2474
+ attachment_id: str = typer.Argument(..., help="Attachment ID"),
2475
+ file_path: Path = typer.Option(..., "--file", exists=True, file_okay=True, dir_okay=False, help="Replacement file"),
2476
+ filename: str | None = typer.Option(None, "--filename", help="Override uploaded filename"),
2477
+ content_type: str | None = typer.Option(None, "--content-type", help="MIME type for the attachment"),
2478
+ comment: str | None = typer.Option(None, "--comment", help="Attachment comment"),
2479
+ server: str | None = typer.Option(None, "--server", "-s", help="internal|external"),
2480
+ ) -> None:
2481
+ """Update an existing attachment on a Confluence page."""
2482
+ parent_ctx = _parent_context(ctx)
2483
+ _ensure_v1_api(parent_ctx, "content.update-attachment")
2484
+ started_at = datetime.now(UTC)
2485
+ try:
2486
+ settings = load_settings(strict=True)
2487
+ server_key = _normalise_server(server, settings)
2488
+ client = ContentClient(_build_http_client(settings, server_key))
2489
+ payload = client.update_attachment(
2490
+ page_id,
2491
+ attachment_id,
2492
+ str(file_path),
2493
+ filename=filename,
2494
+ content_type=content_type,
2495
+ comment=comment,
2496
+ )
2497
+ _emit_json(payload)
2498
+ except Exception as exc:
2499
+ logger.exception("content.update-attachment failed", extra={"page_id": page_id, "attachment_id": attachment_id})
2500
+ typer.echo(f"Error: {exc}", err=True)
2501
+ raise typer.Exit(code=1) from None
2502
+ else:
2503
+ finished_at = datetime.now(UTC)
2504
+ _record_summary(
2505
+ parent_ctx,
2506
+ command="content.update-attachment",
2507
+ args=[page_id, attachment_id],
2508
+ server=server_key,
2509
+ exit_code=0,
2510
+ started_at=started_at,
2511
+ finished_at=finished_at,
2512
+ )
2513
+
2514
+
2515
+ @content_app.command("delete-attachment")
2516
+ def content_delete_attachment(
2517
+ ctx: typer.Context,
2518
+ page_id: str = typer.Argument(..., help="Confluence page ID"),
2519
+ attachment_id: str | None = typer.Argument(None, help="Attachment ID"),
2520
+ version: int | None = typer.Option(None, "--version", help="Delete a specific attachment version"),
2521
+ filename: str | None = typer.Option(None, "--filename", help="Attachment filename (delete all versions)"),
2522
+ server: str | None = typer.Option(None, "--server", "-s", help="internal|external"),
2523
+ ) -> None:
2524
+ """Delete an attachment from a Confluence page."""
2525
+ parent_ctx = _parent_context(ctx)
2526
+ _ensure_v1_api(parent_ctx, "content.delete-attachment")
2527
+ started_at = datetime.now(UTC)
2528
+ try:
2529
+ settings = load_settings(strict=True)
2530
+ server_key = _normalise_server(server, settings)
2531
+ if not attachment_id and not filename:
2532
+ raise typer.BadParameter("Provide either attachment_id or --filename")
2533
+
2534
+ client = ContentClient(_build_http_client(settings, server_key))
2535
+ client.delete_attachment(page_id, attachment_id=attachment_id, version=version, filename=filename)
2536
+ target = filename or attachment_id
2537
+ typer.echo(f"Deleted attachment {target} from page {page_id}")
2538
+ except Exception as exc:
2539
+ logger.exception("content.delete-attachment failed", extra={"page_id": page_id, "attachment_id": attachment_id})
2540
+ typer.echo(f"Error: {exc}", err=True)
2541
+ raise typer.Exit(code=1) from None
2542
+ else:
2543
+ finished_at = datetime.now(UTC)
2544
+ _record_summary(
2545
+ parent_ctx,
2546
+ command="content.delete-attachment",
2547
+ args=[
2548
+ page_id,
2549
+ filename or attachment_id or "<unspecified>",
2550
+ f"version={version}" if version is not None else "all_versions",
2551
+ ],
2552
+ server=server_key,
2553
+ exit_code=0,
2554
+ started_at=started_at,
2555
+ finished_at=finished_at,
2556
+ )
2557
+
2558
+
2559
+ @content_app.command("ancestors")
2560
+ def content_ancestors(
2561
+ ctx: typer.Context,
2562
+ page_id: str = typer.Argument(..., help="Confluence page ID"),
2563
+ server: str | None = typer.Option(None, "--server", "-s", help="internal|external"),
2564
+ json_output: bool = typer.Option(False, "--json", help="Emit JSON payload"),
2565
+ ) -> None:
2566
+ """Get page ancestors (parent pages)."""
2567
+ parent_ctx = _parent_context(ctx)
2568
+ started_at = datetime.now(UTC)
2569
+ exit_code = 0
2570
+ result_payload: dict[str, Any] = {}
2571
+ server_key: str | None = None
2572
+
2573
+ try:
2574
+ settings = load_settings(strict=True)
2575
+ server_key = _normalise_server(server, settings)
2576
+ client = _build_http_client(settings, server_key)
2577
+
2578
+ result = client.get_page_ancestors(page_id)
2579
+ result_payload = {"page_id": page_id, "count": len(result), "ancestors": result}
2580
+
2581
+ if json_output or not sys.stdout.isatty():
2582
+ _emit_json(result_payload)
2583
+ else:
2584
+ typer.echo(json.dumps(result_payload, indent=2, sort_keys=True))
2585
+
2586
+ except (VDSClientError, typer.BadParameter) as exc:
2587
+ exit_code = 1
2588
+ logger.exception("content.ancestors_failed: %s", exc)
2589
+ typer.echo(f"Error: {exc}", err=True)
2590
+ except Exception as exc: # pragma: no cover - unexpected errors
2591
+ exit_code = 1
2592
+ logger.exception("content.ancestors_unexpected_error")
2593
+ typer.echo(f"Error: {exc}", err=True)
2594
+ finally:
2595
+ finished_at = datetime.now(UTC)
2596
+ server_label = server_key or (server.lower() if server else "internal")
2597
+ _record_summary(
2598
+ parent_ctx,
2599
+ command="content.ancestors",
2600
+ args=[page_id],
2601
+ server=server_label,
2602
+ exit_code=exit_code,
2603
+ started_at=started_at,
2604
+ finished_at=finished_at,
2605
+ )
2606
+ if exit_code != 0:
2607
+ raise typer.Exit(code=exit_code) from None
2608
+
2609
+
2610
+ @content_app.command("move")
2611
+ def content_move(
2612
+ ctx: typer.Context,
2613
+ page_id: str = typer.Argument(..., help="Confluence page ID"),
2614
+ target_title: str = typer.Option(..., "--target-title", help="Target page title to move under"),
2615
+ position: str = typer.Option("append", "--position", help="Position: append, before, after"),
2616
+ server: str | None = typer.Option(None, "--server", "-s", help="internal|external"),
2617
+ yes: bool = typer.Option(False, "--yes", help="Confirm move operation"),
2618
+ json_output: bool = typer.Option(False, "--json", help="Emit JSON payload"),
2619
+ ) -> None:
2620
+ """Move page to a new location."""
2621
+ parent_ctx = _parent_context(ctx)
2622
+ started_at = datetime.now(UTC)
2623
+ exit_code = 0
2624
+ result_payload: dict[str, Any] = {}
2625
+ server_key: str | None = None
2626
+
2627
+ try:
2628
+ if not yes:
2629
+ raise typer.BadParameter("--yes required for move operation")
2630
+ if position not in ("append", "before", "after"):
2631
+ raise typer.BadParameter("--position must be one of: append, before, after")
2632
+
2633
+ settings = load_settings(strict=True)
2634
+ server_key = _normalise_server(server, settings)
2635
+ client = _build_http_client(settings, server_key)
2636
+
2637
+ result = client.move_page(page_id, target_title, position=position)
2638
+ result_payload = {"page_id": page_id, "target_title": target_title, "position": position, "result": result}
2639
+
2640
+ if json_output or not sys.stdout.isatty():
2641
+ _emit_json(result_payload)
2642
+ else:
2643
+ typer.echo(json.dumps(result_payload, indent=2, sort_keys=True))
2644
+
2645
+ except (VDSClientError, typer.BadParameter) as exc:
2646
+ exit_code = 1
2647
+ logger.exception("content.move_failed: %s", exc)
2648
+ typer.echo(f"Error: {exc}", err=True)
2649
+ except Exception as exc: # pragma: no cover - unexpected errors
2650
+ exit_code = 1
2651
+ logger.exception("content.move_unexpected_error")
2652
+ typer.echo(f"Error: {exc}", err=True)
2653
+ finally:
2654
+ finished_at = datetime.now(UTC)
2655
+ server_label = server_key or (server.lower() if server else "internal")
2656
+ _record_summary(
2657
+ parent_ctx,
2658
+ command="content.move",
2659
+ args=[page_id, f"target={target_title}", f"position={position}"],
2660
+ server=server_label,
2661
+ exit_code=exit_code,
2662
+ started_at=started_at,
2663
+ finished_at=finished_at,
2664
+ )
2665
+ if exit_code != 0:
2666
+ raise typer.Exit(code=exit_code) from None
2667
+
2668
+
2669
+ @content_app.command("tables")
2670
+ def content_tables(
2671
+ ctx: typer.Context,
2672
+ page_id: str = typer.Argument(..., help="Confluence page ID"),
2673
+ server: str | None = typer.Option(None, "--server", "-s", help="internal|external"),
2674
+ json_output: bool = typer.Option(False, "--json", help="Emit JSON payload"),
2675
+ ) -> None:
2676
+ """Extract tables from page."""
2677
+ parent_ctx = _parent_context(ctx)
2678
+ started_at = datetime.now(UTC)
2679
+ exit_code = 0
2680
+ result_payload: dict[str, Any] = {}
2681
+ server_key: str | None = None
2682
+
2683
+ try:
2684
+ settings = load_settings(strict=True)
2685
+ server_key = _normalise_server(server, settings)
2686
+ client = _build_http_client(settings, server_key)
2687
+
2688
+ result = client.get_tables_from_page(page_id)
2689
+ result_payload = {"page_id": page_id, "count": len(result), "tables": result}
2690
+
2691
+ if json_output or not sys.stdout.isatty():
2692
+ _emit_json(result_payload)
2693
+ else:
2694
+ typer.echo(json.dumps(result_payload, indent=2, sort_keys=True))
2695
+
2696
+ except (VDSClientError, typer.BadParameter) as exc:
2697
+ exit_code = 1
2698
+ logger.exception("content.tables_failed: %s", exc)
2699
+ typer.echo(f"Error: {exc}", err=True)
2700
+ except Exception as exc: # pragma: no cover - unexpected errors
2701
+ exit_code = 1
2702
+ logger.exception("content.tables_unexpected_error")
2703
+ typer.echo(f"Error: {exc}", err=True)
2704
+ finally:
2705
+ finished_at = datetime.now(UTC)
2706
+ server_label = server_key or (server.lower() if server else "internal")
2707
+ _record_summary(
2708
+ parent_ctx,
2709
+ command="content.tables",
2710
+ args=[page_id],
2711
+ server=server_label,
2712
+ exit_code=exit_code,
2713
+ started_at=started_at,
2714
+ finished_at=finished_at,
2715
+ )
2716
+ if exit_code != 0:
2717
+ raise typer.Exit(code=exit_code) from None
2718
+
2719
+
2720
+ @content_app.command("regex")
2721
+ def content_regex(
2722
+ ctx: typer.Context,
2723
+ page_id: str = typer.Argument(..., help="Confluence page ID"),
2724
+ pattern: str = typer.Option(..., "--pattern", help="Regex pattern to match"),
2725
+ server: str | None = typer.Option(None, "--server", "-s", help="internal|external"),
2726
+ json_output: bool = typer.Option(False, "--json", help="Emit JSON payload"),
2727
+ ) -> None:
2728
+ """Extract regex matches from page."""
2729
+ parent_ctx = _parent_context(ctx)
2730
+ started_at = datetime.now(UTC)
2731
+ exit_code = 0
2732
+ result_payload: dict[str, Any] = {}
2733
+ server_key: str | None = None
2734
+
2735
+ try:
2736
+ settings = load_settings(strict=True)
2737
+ server_key = _normalise_server(server, settings)
2738
+ client = _build_http_client(settings, server_key)
2739
+
2740
+ result = client.scrap_regex_from_page(page_id, pattern)
2741
+ result_payload = {"page_id": page_id, "pattern": pattern, "count": len(result), "matches": result}
2742
+
2743
+ if json_output or not sys.stdout.isatty():
2744
+ _emit_json(result_payload)
2745
+ else:
2746
+ typer.echo(json.dumps(result_payload, indent=2, sort_keys=True))
2747
+
2748
+ except (VDSClientError, typer.BadParameter) as exc:
2749
+ exit_code = 1
2750
+ logger.exception("content.regex_failed: %s", exc)
2751
+ typer.echo(f"Error: {exc}", err=True)
2752
+ except Exception as exc: # pragma: no cover - unexpected errors
2753
+ exit_code = 1
2754
+ logger.exception("content.regex_unexpected_error")
2755
+ typer.echo(f"Error: {exc}", err=True)
2756
+ finally:
2757
+ finished_at = datetime.now(UTC)
2758
+ server_label = server_key or (server.lower() if server else "internal")
2759
+ _record_summary(
2760
+ parent_ctx,
2761
+ command="content.regex",
2762
+ args=[page_id, f"pattern={pattern}"],
2763
+ server=server_label,
2764
+ exit_code=exit_code,
2765
+ started_at=started_at,
2766
+ finished_at=finished_at,
2767
+ )
2768
+ if exit_code != 0:
2769
+ raise typer.Exit(code=exit_code) from None
2770
+
2771
+
2772
+ @content_app.command("restrictions")
2773
+ def content_restrictions(
2774
+ ctx: typer.Context,
2775
+ page_id: str = typer.Argument(..., help="Confluence page ID"),
2776
+ server: str | None = typer.Option(None, "--server", "-s", help="internal|external"),
2777
+ json_output: bool = typer.Option(False, "--json", help="Emit JSON payload"),
2778
+ ) -> None:
2779
+ """Get all restrictions for content."""
2780
+ parent_ctx = _parent_context(ctx)
2781
+ started_at = datetime.now(UTC)
2782
+ exit_code = 0
2783
+ result_payload: dict[str, Any] = {}
2784
+ server_key: str | None = None
2785
+
2786
+ try:
2787
+ settings = load_settings(strict=True)
2788
+ server_key = _normalise_server(server, settings)
2789
+ client = _build_http_client(settings, server_key)
2790
+
2791
+ result = client.get_all_restrictions_for_content(page_id)
2792
+ result_payload = {"page_id": page_id, "restrictions": result}
2793
+
2794
+ if json_output or not sys.stdout.isatty():
2795
+ _emit_json(result_payload)
2796
+ else:
2797
+ typer.echo(json.dumps(result_payload, indent=2, sort_keys=True))
2798
+
2799
+ except (VDSClientError, typer.BadParameter) as exc:
2800
+ exit_code = 1
2801
+ logger.exception("content.restrictions_failed: %s", exc)
2802
+ typer.echo(f"Error: {exc}", err=True)
2803
+ except Exception as exc: # pragma: no cover - unexpected errors
2804
+ exit_code = 1
2805
+ logger.exception("content.restrictions_unexpected_error")
2806
+ typer.echo(f"Error: {exc}", err=True)
2807
+ finally:
2808
+ finished_at = datetime.now(UTC)
2809
+ server_label = server_key or (server.lower() if server else "internal")
2810
+ _record_summary(
2811
+ parent_ctx,
2812
+ command="content.restrictions",
2813
+ args=[page_id],
2814
+ server=server_label,
2815
+ exit_code=exit_code,
2816
+ started_at=started_at,
2817
+ finished_at=finished_at,
2818
+ )
2819
+ if exit_code != 0:
2820
+ raise typer.Exit(code=exit_code) from None
2821
+
2822
+
2823
+ @content_app.command("create-template")
2824
+ def content_create_template(
2825
+ ctx: typer.Context,
2826
+ name: str = typer.Option(..., "--name", "-n", help="Template name"),
2827
+ body_file: Path = typer.Option(
2828
+ ..., "--body-file", exists=True, file_okay=True, dir_okay=False, help="Path to storage-format body"
2829
+ ),
2830
+ template_type: str = typer.Option("page", "--type", help="Template type (page or other supported types)"),
2831
+ space_key: str | None = typer.Option(None, "--space", help="Limit template to space key"),
2832
+ description: str | None = typer.Option(None, "--description", help="Template description"),
2833
+ representation: str = typer.Option("storage", "--representation", help="Content representation"),
2834
+ server: str | None = typer.Option(None, "--server", "-s", help="internal|external"),
2835
+ ) -> None:
2836
+ """Create a new Confluence page template."""
2837
+ parent_ctx = _parent_context(ctx)
2838
+ _ensure_v1_api(parent_ctx, "content.create-template")
2839
+ started_at = datetime.now(UTC)
2840
+ try:
2841
+ settings = load_settings(strict=True)
2842
+ server_key = _normalise_server(server, settings)
2843
+ client = ContentClient(_build_http_client(settings, server_key))
2844
+ body = _read_text_file(body_file)
2845
+ payload = client.create_template(
2846
+ name=name,
2847
+ body=body,
2848
+ template_type=template_type,
2849
+ space_key=space_key,
2850
+ description=description,
2851
+ representation=representation,
2852
+ )
2853
+ _emit_json(payload)
2854
+ except Exception as exc:
2855
+ logger.exception("content.create-template failed", extra={"name": name, "space": space_key})
2856
+ typer.echo(f"Error: {exc}", err=True)
2857
+ raise typer.Exit(code=1) from None
2858
+ else:
2859
+ finished_at = datetime.now(UTC)
2860
+ args = [name, f"type={template_type}"]
2861
+ if space_key:
2862
+ args.append(f"space={space_key}")
2863
+ _record_summary(
2864
+ parent_ctx,
2865
+ command="content.create-template",
2866
+ args=args,
2867
+ server=server_key,
2868
+ exit_code=0,
2869
+ started_at=started_at,
2870
+ finished_at=finished_at,
2871
+ )
2872
+
2873
+
2874
+ @content_app.command("update-template")
2875
+ def content_update_template(
2876
+ ctx: typer.Context,
2877
+ template_id: str = typer.Argument(..., help="Template identifier"),
2878
+ name: str | None = typer.Option(None, "--name", help="Template name"),
2879
+ body_file: Path | None = typer.Option(
2880
+ None, "--body-file", exists=True, file_okay=True, dir_okay=False, help="Path to storage-format body"
2881
+ ),
2882
+ description: str | None = typer.Option(None, "--description", help="Template description"),
2883
+ template_type: str | None = typer.Option(None, "--type", help="Template type"),
2884
+ representation: str = typer.Option("storage", "--representation", help="Representation for --body-file"),
2885
+ server: str | None = typer.Option(None, "--server", "-s", help="internal|external"),
2886
+ ) -> None:
2887
+ """Update an existing Confluence page template."""
2888
+ parent_ctx = _parent_context(ctx)
2889
+ _ensure_v1_api(parent_ctx, "content.update-template")
2890
+ if name is None and body_file is None and description is None and template_type is None:
2891
+ raise typer.BadParameter("Provide at least one of --name, --body-file, --description, or --type")
2892
+ body = _read_text_file(body_file) if body_file else None
2893
+ started_at = datetime.now(UTC)
2894
+ try:
2895
+ settings = load_settings(strict=True)
2896
+ server_key = _normalise_server(server, settings)
2897
+ client = ContentClient(_build_http_client(settings, server_key))
2898
+ payload = client.update_template(
2899
+ template_id,
2900
+ name=name,
2901
+ body=body,
2902
+ description=description,
2903
+ representation=representation,
2904
+ template_type=template_type,
2905
+ )
2906
+ _emit_json(payload)
2907
+ except Exception as exc:
2908
+ logger.exception("content.update-template failed", extra={"template_id": template_id})
2909
+ typer.echo(f"Error: {exc}", err=True)
2910
+ raise typer.Exit(code=1) from None
2911
+ else:
2912
+ finished_at = datetime.now(UTC)
2913
+ args = [template_id]
2914
+ if name:
2915
+ args.append(f"name={name}")
2916
+ _record_summary(
2917
+ parent_ctx,
2918
+ command="content.update-template",
2919
+ args=args,
2920
+ server=server_key,
2921
+ exit_code=0,
2922
+ started_at=started_at,
2923
+ finished_at=finished_at,
2924
+ )
2925
+
2926
+
2927
+ @content_app.command("delete-template")
2928
+ def content_delete_template(
2929
+ ctx: typer.Context,
2930
+ template_id: str = typer.Argument(..., help="Template identifier"),
2931
+ server: str | None = typer.Option(None, "--server", "-s", help="internal|external"),
2932
+ ) -> None:
2933
+ """Delete a Confluence page template."""
2934
+ parent_ctx = _parent_context(ctx)
2935
+ _ensure_v1_api(parent_ctx, "content.delete-template")
2936
+ started_at = datetime.now(UTC)
2937
+ try:
2938
+ settings = load_settings(strict=True)
2939
+ server_key = _normalise_server(server, settings)
2940
+ client = ContentClient(_build_http_client(settings, server_key))
2941
+ client.delete_template(template_id)
2942
+ typer.echo(f"Deleted template {template_id}")
2943
+ except Exception as exc:
2944
+ logger.exception("content.delete-template failed", extra={"template_id": template_id})
2945
+ typer.echo(f"Error: {exc}", err=True)
2946
+ raise typer.Exit(code=1) from None
2947
+ else:
2948
+ finished_at = datetime.now(UTC)
2949
+ _record_summary(
2950
+ parent_ctx,
2951
+ command="content.delete-template",
2952
+ args=[template_id],
2953
+ server=server_key,
2954
+ exit_code=0,
2955
+ started_at=started_at,
2956
+ finished_at=finished_at,
2957
+ )
2958
+
2959
+
2960
+ # ---------------------------------------------------------------------------
2961
+ # Diagnostics
2962
+ # ---------------------------------------------------------------------------
2963
+
2964
+
2965
+ @app.command("env")
2966
+ def cmd_env(_ctx: typer.Context) -> None:
2967
+ """Show Confluence connection environment variable status."""
2968
+ settings = load_settings(strict=False)
2969
+ typer.echo(
2970
+ f"Default server: {settings.default_server}\n"
2971
+ f"Internal URL: {settings.internal_url}\n"
2972
+ f"External URL: {settings.external_url}\n"
2973
+ f"Internal token set: {bool(settings.internal_token)}\n"
2974
+ f"External token set: {bool(settings.external_token)}"
2975
+ )
2976
+
2977
+
2978
+ # ---------------------------------------------------------------------------
2979
+ # Sync Commands (delegate to standalone scripts)
2980
+ # ---------------------------------------------------------------------------
2981
+
2982
+ sync_app = typer.Typer(help="Sync attachments from local files to Confluence")
2983
+ app.add_typer(sync_app, name="sync")
2984
+
2985
+
2986
+ @sync_app.command("pngs")
2987
+ def cmd_sync_pngs(
2988
+ _ctx: typer.Context,
2989
+ page_id: str = typer.Argument(..., help="Confluence page ID"),
2990
+ directory: Path = typer.Option(..., "--dir", "-d", help="Directory containing PNG files"),
2991
+ server: str | None = typer.Option(None, "--server", "-s", help="internal|external"),
2992
+ pattern: str | None = typer.Option(None, "--pattern", "-p", help="Filter PNG files by pattern"),
2993
+ dry_run: bool = typer.Option(False, "--dry-run", help="Show what would be done without making changes"),
2994
+ comment: str | None = typer.Option(None, "--comment", "-c", help="Comment for attachment updates"),
2995
+ yes: bool = typer.Option(False, "--yes", "-y", help="Auto-confirm without prompting"),
2996
+ ) -> None:
2997
+ """Sync local PNG files with Confluence page attachments."""
2998
+ import subprocess
2999
+ import sys
3000
+
3001
+ # Project root for confluence_orchestrator (where sync scripts live)
3002
+ script_path = Path(__file__).resolve().parents[2] / "sync_png_attachments.py"
3003
+ if not script_path.exists():
3004
+ typer.echo(f"Error: Sync script not found: {script_path}", err=True)
3005
+ raise typer.Exit(code=1) from None
3006
+
3007
+ cmd = [
3008
+ sys.executable,
3009
+ str(script_path),
3010
+ "sync",
3011
+ page_id,
3012
+ "--dir",
3013
+ str(directory),
3014
+ ]
3015
+ if server:
3016
+ cmd.extend(["--server", server])
3017
+ if pattern:
3018
+ cmd.extend(["--pattern", pattern])
3019
+ if dry_run:
3020
+ cmd.append("--dry-run")
3021
+ if comment:
3022
+ cmd.extend(["--comment", comment])
3023
+ if yes:
3024
+ cmd.append("--yes")
3025
+
3026
+ result = subprocess.run(cmd, check=False)
3027
+ raise typer.Exit(code=result.returncode) from None
3028
+
3029
+
3030
+ @sync_app.command("pdfs")
3031
+ def cmd_sync_pdfs(
3032
+ _ctx: typer.Context,
3033
+ page_id: str = typer.Argument(..., help="Confluence page ID"),
3034
+ directory: Path = typer.Option(..., "--dir", "-d", help="Directory containing PDF files"),
3035
+ server: str | None = typer.Option(None, "--server", "-s", help="internal|external"),
3036
+ pattern: str | None = typer.Option(None, "--pattern", "-p", help="Filter PDF files by pattern"),
3037
+ dry_run: bool = typer.Option(False, "--dry-run", help="Show what would be done without making changes"),
3038
+ comment: str | None = typer.Option(None, "--comment", "-c", help="Comment for attachment updates"),
3039
+ yes: bool = typer.Option(False, "--yes", "-y", help="Auto-confirm without prompting"),
3040
+ ) -> None:
3041
+ """Sync local PDF files with Confluence page attachments.
3042
+
3043
+ This command delegates to the standalone sync_pdfs_to_confluence.py script.
3044
+ For markdown-to-PDF sync, use the PDF orchestrator: vds-cli pdf -- md2pdf
3045
+ """
3046
+ import subprocess
3047
+ import sys
3048
+
3049
+ # Look for script in confluence_orchestrator/ directory (standard location)
3050
+ script_path = Path(__file__).resolve().parents[2] / "sync_pdfs_to_confluence.py"
3051
+
3052
+ if not script_path.exists():
3053
+ typer.echo(
3054
+ f"Error: Sync script not found: {script_path}\n"
3055
+ "Note: Ensure sync_pdfs_to_confluence.py exists in confluence_orchestrator/ directory.",
3056
+ err=True,
3057
+ )
3058
+ raise typer.Exit(code=1) from None
3059
+
3060
+ cmd = [
3061
+ sys.executable,
3062
+ str(script_path),
3063
+ "sync",
3064
+ page_id,
3065
+ "--dir",
3066
+ str(directory),
3067
+ ]
3068
+ if server:
3069
+ cmd.extend(["--server", server])
3070
+ if pattern:
3071
+ cmd.extend(["--pattern", pattern])
3072
+ if dry_run:
3073
+ cmd.append("--dry-run")
3074
+ if comment:
3075
+ cmd.extend(["--comment", comment])
3076
+ if yes:
3077
+ cmd.append("--yes")
3078
+
3079
+ result = subprocess.run(cmd, check=False)
3080
+ raise typer.Exit(code=result.returncode) from None
3081
+
3082
+
3083
+ # ---------------------------------------------------------------------------
3084
+ # content copy-tree
3085
+ # ---------------------------------------------------------------------------
3086
+
3087
+
3088
+ @content_app.command("copy-tree")
3089
+ def content_copy_tree(
3090
+ ctx: typer.Context,
3091
+ source_page_id: str = typer.Argument(..., help="Source root page ID"),
3092
+ target_page_id: str = typer.Option(..., "--target-page-id", help="Target root page ID"),
3093
+ source_server: str = typer.Option(
3094
+ "external", "--source-server", help="Source Confluence server (internal|external)"
3095
+ ),
3096
+ target_server: str = typer.Option(
3097
+ "internal", "--target-server", help="Target Confluence server (internal|external)"
3098
+ ),
3099
+ source_space: str | None = typer.Option(
3100
+ None, "--source-space", help="Source space key (auto-detected from source page if omitted)"
3101
+ ),
3102
+ target_space: str = typer.Option(..., "--target-space", help="Target space key"),
3103
+ skip_attachments: bool = typer.Option(False, "--skip-attachments", help="Skip attachment copy"),
3104
+ skip_labels: bool = typer.Option(False, "--skip-labels", help="Skip label copy"),
3105
+ skip_link_remap: bool = typer.Option(False, "--skip-link-remap", help="Skip link remapping"),
3106
+ max_depth: int | None = typer.Option(None, "--max-depth", help="Max BFS depth (0=root only; max: 100)"),
3107
+ dry_run: bool = typer.Option(False, "--dry-run", help="Report what would be copied without writing"),
3108
+ ) -> None:
3109
+ """Copy a Confluence page tree from one space/server to another."""
3110
+ parent_ctx = _parent_context(ctx)
3111
+ if max_depth is not None and max_depth > 100:
3112
+ raise typer.BadParameter("--max-depth cannot exceed 100 (NFR-05)")
3113
+ started_at = datetime.now(UTC)
3114
+ source_server_key: str = ""
3115
+ try:
3116
+ settings = load_settings(strict=True)
3117
+ source_server_key = _normalise_server(source_server, settings)
3118
+ target_server_key = _normalise_server(target_server, settings)
3119
+ source_client = ContentClient(_build_http_client(settings, source_server_key))
3120
+ target_client = ContentClient(_build_http_client(settings, target_server_key))
3121
+ if source_space:
3122
+ resolved_source_space = source_space
3123
+ else:
3124
+ src_page = source_client.get_page(source_page_id, expand=["space"])
3125
+ space_info = src_page.get("space")
3126
+ if not isinstance(space_info, dict) or not space_info.get("key"):
3127
+ raise VDSClientError(
3128
+ f"Cannot determine source space key from page {source_page_id}. Pass --source-space explicitly.",
3129
+ context={"source_page_id": source_page_id},
3130
+ )
3131
+ resolved_source_space = str(space_info["key"])
3132
+ options = CopyOptions(
3133
+ skip_attachments=skip_attachments,
3134
+ skip_labels=skip_labels,
3135
+ skip_link_remap=skip_link_remap,
3136
+ dry_run=dry_run,
3137
+ max_depth=max_depth,
3138
+ )
3139
+ copier = TreeCopier(source_client, target_client, target_space, resolved_source_space, options)
3140
+ result = copier.copy_tree(source_page_id, target_page_id)
3141
+ _emit_json(asdict(result))
3142
+ except Exception as exc:
3143
+ logger.exception("content.copy-tree failed", extra={"source_page_id": source_page_id})
3144
+ typer.echo(f"Error: {exc}", err=True)
3145
+ raise typer.Exit(code=1) from None
3146
+ else:
3147
+ finished_at = datetime.now(UTC)
3148
+ _record_summary(
3149
+ parent_ctx,
3150
+ command="content.copy-tree",
3151
+ args=[source_page_id, f"target={target_page_id}", f"target_space={target_space}"],
3152
+ server=source_server_key,
3153
+ exit_code=0,
3154
+ started_at=started_at,
3155
+ finished_at=finished_at,
3156
+ )
3157
+
3158
+
3159
+ if __name__ == "__main__":
3160
+ app()