keboola-cli 0.63.4__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (306) hide show
  1. keboola_agent_cli/__init__.py +34 -0
  2. keboola_agent_cli/__main__.py +5 -0
  3. keboola_agent_cli/_ui_dist/assets/arc-DhFYIddx.js +2 -0
  4. keboola_agent_cli/_ui_dist/assets/arc-DhFYIddx.js.map +1 -0
  5. keboola_agent_cli/_ui_dist/assets/architecture-7EHR7CIX-hNCijx_H.js +1 -0
  6. keboola_agent_cli/_ui_dist/assets/architectureDiagram-3BPJPVTR-C6hUlprM.js +37 -0
  7. keboola_agent_cli/_ui_dist/assets/architectureDiagram-3BPJPVTR-C6hUlprM.js.map +1 -0
  8. keboola_agent_cli/_ui_dist/assets/array-BifhSqXX.js +2 -0
  9. keboola_agent_cli/_ui_dist/assets/array-BifhSqXX.js.map +1 -0
  10. keboola_agent_cli/_ui_dist/assets/blockDiagram-GPEHLZMM-DC7qY9i4.js +133 -0
  11. keboola_agent_cli/_ui_dist/assets/blockDiagram-GPEHLZMM-DC7qY9i4.js.map +1 -0
  12. keboola_agent_cli/_ui_dist/assets/c4Diagram-AAUBKEIU-5Lh44evt.js +11 -0
  13. keboola_agent_cli/_ui_dist/assets/c4Diagram-AAUBKEIU-5Lh44evt.js.map +1 -0
  14. keboola_agent_cli/_ui_dist/assets/channel-DBMrXlxx.js +2 -0
  15. keboola_agent_cli/_ui_dist/assets/channel-DBMrXlxx.js.map +1 -0
  16. keboola_agent_cli/_ui_dist/assets/chunk-2J33WTMH-Coy82EBh.js +2 -0
  17. keboola_agent_cli/_ui_dist/assets/chunk-2J33WTMH-Coy82EBh.js.map +1 -0
  18. keboola_agent_cli/_ui_dist/assets/chunk-3OPIFGDE-BQC5CRHI.js +63 -0
  19. keboola_agent_cli/_ui_dist/assets/chunk-3OPIFGDE-BQC5CRHI.js.map +1 -0
  20. keboola_agent_cli/_ui_dist/assets/chunk-4BX2VUAB-DUuEt70o.js +2 -0
  21. keboola_agent_cli/_ui_dist/assets/chunk-4BX2VUAB-DUuEt70o.js.map +1 -0
  22. keboola_agent_cli/_ui_dist/assets/chunk-55IACEB6-BvR-6chF.js +2 -0
  23. keboola_agent_cli/_ui_dist/assets/chunk-55IACEB6-BvR-6chF.js.map +1 -0
  24. keboola_agent_cli/_ui_dist/assets/chunk-5ZQYHXKU-BjcTN7ul.js +3 -0
  25. keboola_agent_cli/_ui_dist/assets/chunk-5ZQYHXKU-BjcTN7ul.js.map +1 -0
  26. keboola_agent_cli/_ui_dist/assets/chunk-727SXJPM-C0zxqqRN.js +207 -0
  27. keboola_agent_cli/_ui_dist/assets/chunk-727SXJPM-C0zxqqRN.js.map +1 -0
  28. keboola_agent_cli/_ui_dist/assets/chunk-AQP2D5EJ-CXf7rIlZ.js +232 -0
  29. keboola_agent_cli/_ui_dist/assets/chunk-AQP2D5EJ-CXf7rIlZ.js.map +1 -0
  30. keboola_agent_cli/_ui_dist/assets/chunk-BSJP7CBP-Oj_FO9Q7.js +2 -0
  31. keboola_agent_cli/_ui_dist/assets/chunk-BSJP7CBP-Oj_FO9Q7.js.map +1 -0
  32. keboola_agent_cli/_ui_dist/assets/chunk-CSCIHK7Q-CcTsLrFc.js +124 -0
  33. keboola_agent_cli/_ui_dist/assets/chunk-CSCIHK7Q-CcTsLrFc.js.map +1 -0
  34. keboola_agent_cli/_ui_dist/assets/chunk-FMBD7UC4-FH-zLkkW.js +16 -0
  35. keboola_agent_cli/_ui_dist/assets/chunk-FMBD7UC4-FH-zLkkW.js.map +1 -0
  36. keboola_agent_cli/_ui_dist/assets/chunk-L5ZTLDWV-B1Ky_e7O.js +2 -0
  37. keboola_agent_cli/_ui_dist/assets/chunk-L5ZTLDWV-B1Ky_e7O.js.map +1 -0
  38. keboola_agent_cli/_ui_dist/assets/chunk-ND2GUHAM-BHz1rpbm.js +2 -0
  39. keboola_agent_cli/_ui_dist/assets/chunk-ND2GUHAM-BHz1rpbm.js.map +1 -0
  40. keboola_agent_cli/_ui_dist/assets/chunk-NNHCCRGN-DlpIbxXb.js +160 -0
  41. keboola_agent_cli/_ui_dist/assets/chunk-NNHCCRGN-DlpIbxXb.js.map +1 -0
  42. keboola_agent_cli/_ui_dist/assets/chunk-NZK2D7GU-tnrSoegS.js +2 -0
  43. keboola_agent_cli/_ui_dist/assets/chunk-NZK2D7GU-tnrSoegS.js.map +1 -0
  44. keboola_agent_cli/_ui_dist/assets/chunk-O5CBEL6O-DxxqDH0l.js +71 -0
  45. keboola_agent_cli/_ui_dist/assets/chunk-O5CBEL6O-DxxqDH0l.js.map +1 -0
  46. keboola_agent_cli/_ui_dist/assets/chunk-QZHKN3VN-CSjc2gjj.js +2 -0
  47. keboola_agent_cli/_ui_dist/assets/chunk-QZHKN3VN-CSjc2gjj.js.map +1 -0
  48. keboola_agent_cli/_ui_dist/assets/classDiagram-4FO5ZUOK-BuZcZu85.js +2 -0
  49. keboola_agent_cli/_ui_dist/assets/classDiagram-4FO5ZUOK-BuZcZu85.js.map +1 -0
  50. keboola_agent_cli/_ui_dist/assets/classDiagram-v2-Q7XG4LA2-BuZcZu85.js +2 -0
  51. keboola_agent_cli/_ui_dist/assets/classDiagram-v2-Q7XG4LA2-BuZcZu85.js.map +1 -0
  52. keboola_agent_cli/_ui_dist/assets/cose-bilkent-S5V4N54A-Y0L8LDMa.js +2 -0
  53. keboola_agent_cli/_ui_dist/assets/cose-bilkent-S5V4N54A-Y0L8LDMa.js.map +1 -0
  54. keboola_agent_cli/_ui_dist/assets/cytoscape.esm-C8YCVR3_.js +322 -0
  55. keboola_agent_cli/_ui_dist/assets/cytoscape.esm-C8YCVR3_.js.map +1 -0
  56. keboola_agent_cli/_ui_dist/assets/dagre-BM42HDAG-UZ-9BTqF.js +5 -0
  57. keboola_agent_cli/_ui_dist/assets/dagre-BM42HDAG-UZ-9BTqF.js.map +1 -0
  58. keboola_agent_cli/_ui_dist/assets/dagre-Bx709z4p.js +2 -0
  59. keboola_agent_cli/_ui_dist/assets/dagre-Bx709z4p.js.map +1 -0
  60. keboola_agent_cli/_ui_dist/assets/defaultLocale-C8Fc0cco.js +2 -0
  61. keboola_agent_cli/_ui_dist/assets/defaultLocale-C8Fc0cco.js.map +1 -0
  62. keboola_agent_cli/_ui_dist/assets/diagram-2AECGRRQ-DoDQ60wi.js +44 -0
  63. keboola_agent_cli/_ui_dist/assets/diagram-2AECGRRQ-DoDQ60wi.js.map +1 -0
  64. keboola_agent_cli/_ui_dist/assets/diagram-5GNKFQAL-CMGFxpUs.js +11 -0
  65. keboola_agent_cli/_ui_dist/assets/diagram-5GNKFQAL-CMGFxpUs.js.map +1 -0
  66. keboola_agent_cli/_ui_dist/assets/diagram-KO2AKTUF-1uGDa-Iu.js +4 -0
  67. keboola_agent_cli/_ui_dist/assets/diagram-KO2AKTUF-1uGDa-Iu.js.map +1 -0
  68. keboola_agent_cli/_ui_dist/assets/diagram-LMA3HP47-XtFH7B51.js +25 -0
  69. keboola_agent_cli/_ui_dist/assets/diagram-LMA3HP47-XtFH7B51.js.map +1 -0
  70. keboola_agent_cli/_ui_dist/assets/diagram-OG6HWLK6-B4_Te1T5.js +25 -0
  71. keboola_agent_cli/_ui_dist/assets/diagram-OG6HWLK6-B4_Te1T5.js.map +1 -0
  72. keboola_agent_cli/_ui_dist/assets/dist-Di6zmlv0.js +2 -0
  73. keboola_agent_cli/_ui_dist/assets/dist-Di6zmlv0.js.map +1 -0
  74. keboola_agent_cli/_ui_dist/assets/erDiagram-TEJ5UH35-NjQkrdFt.js +86 -0
  75. keboola_agent_cli/_ui_dist/assets/erDiagram-TEJ5UH35-NjQkrdFt.js.map +1 -0
  76. keboola_agent_cli/_ui_dist/assets/eventmodeling-FCH6USID-BrJMIks8.js +1 -0
  77. keboola_agent_cli/_ui_dist/assets/flowDiagram-I6XJVG4X-CIr8DWl7.js +163 -0
  78. keboola_agent_cli/_ui_dist/assets/flowDiagram-I6XJVG4X-CIr8DWl7.js.map +1 -0
  79. keboola_agent_cli/_ui_dist/assets/ganttDiagram-6RSMTGT7-C1VY_xbQ.js +293 -0
  80. keboola_agent_cli/_ui_dist/assets/ganttDiagram-6RSMTGT7-C1VY_xbQ.js.map +1 -0
  81. keboola_agent_cli/_ui_dist/assets/gitGraph-WXDBUCRP-COacYjo-.js +1 -0
  82. keboola_agent_cli/_ui_dist/assets/gitGraphDiagram-PVQCEYII-DQT8-kg2.js +107 -0
  83. keboola_agent_cli/_ui_dist/assets/gitGraphDiagram-PVQCEYII-DQT8-kg2.js.map +1 -0
  84. keboola_agent_cli/_ui_dist/assets/graphlib-B8gBHxth.js +2 -0
  85. keboola_agent_cli/_ui_dist/assets/graphlib-B8gBHxth.js.map +1 -0
  86. keboola_agent_cli/_ui_dist/assets/index-CMq50kkV.css +1 -0
  87. keboola_agent_cli/_ui_dist/assets/index-D8W97DAz.js +118 -0
  88. keboola_agent_cli/_ui_dist/assets/index-D8W97DAz.js.map +1 -0
  89. keboola_agent_cli/_ui_dist/assets/info-J43DQDTF-DdCTRIzU.js +1 -0
  90. keboola_agent_cli/_ui_dist/assets/infoDiagram-5YYISTIA-C77rsoTp.js +3 -0
  91. keboola_agent_cli/_ui_dist/assets/infoDiagram-5YYISTIA-C77rsoTp.js.map +1 -0
  92. keboola_agent_cli/_ui_dist/assets/init-D6jRqBbL.js +2 -0
  93. keboola_agent_cli/_ui_dist/assets/init-D6jRqBbL.js.map +1 -0
  94. keboola_agent_cli/_ui_dist/assets/ishikawaDiagram-YF4QCWOH-BcTbXaLy.js +71 -0
  95. keboola_agent_cli/_ui_dist/assets/ishikawaDiagram-YF4QCWOH-BcTbXaLy.js.map +1 -0
  96. keboola_agent_cli/_ui_dist/assets/journeyDiagram-JHISSGLW-BejeAJQ_.js +140 -0
  97. keboola_agent_cli/_ui_dist/assets/journeyDiagram-JHISSGLW-BejeAJQ_.js.map +1 -0
  98. keboola_agent_cli/_ui_dist/assets/kanban-definition-UN3LZRKU-BRNz_UrH.js +90 -0
  99. keboola_agent_cli/_ui_dist/assets/kanban-definition-UN3LZRKU-BRNz_UrH.js.map +1 -0
  100. keboola_agent_cli/_ui_dist/assets/katex-C4eR7coU.js +258 -0
  101. keboola_agent_cli/_ui_dist/assets/katex-C4eR7coU.js.map +1 -0
  102. keboola_agent_cli/_ui_dist/assets/line-CzAQKFbJ.js +2 -0
  103. keboola_agent_cli/_ui_dist/assets/line-CzAQKFbJ.js.map +1 -0
  104. keboola_agent_cli/_ui_dist/assets/linear-DUNFFdck.js +2 -0
  105. keboola_agent_cli/_ui_dist/assets/linear-DUNFFdck.js.map +1 -0
  106. keboola_agent_cli/_ui_dist/assets/mermaid-parser.core-CpuBOkFa.js +5 -0
  107. keboola_agent_cli/_ui_dist/assets/mermaid-parser.core-CpuBOkFa.js.map +1 -0
  108. keboola_agent_cli/_ui_dist/assets/mindmap-definition-RKZ34NQL-9EJQNjH0.js +97 -0
  109. keboola_agent_cli/_ui_dist/assets/mindmap-definition-RKZ34NQL-9EJQNjH0.js.map +1 -0
  110. keboola_agent_cli/_ui_dist/assets/ordinal-hYBb2elL.js +2 -0
  111. keboola_agent_cli/_ui_dist/assets/ordinal-hYBb2elL.js.map +1 -0
  112. keboola_agent_cli/_ui_dist/assets/packet-YPE3B663-DLiiw_B2.js +1 -0
  113. keboola_agent_cli/_ui_dist/assets/path-BWPyau1x.js +2 -0
  114. keboola_agent_cli/_ui_dist/assets/path-BWPyau1x.js.map +1 -0
  115. keboola_agent_cli/_ui_dist/assets/pie-LRSECV5Y-CRoO8G1g.js +1 -0
  116. keboola_agent_cli/_ui_dist/assets/pieDiagram-4H26LBE5-XH4cy6Cb.js +31 -0
  117. keboola_agent_cli/_ui_dist/assets/pieDiagram-4H26LBE5-XH4cy6Cb.js.map +1 -0
  118. keboola_agent_cli/_ui_dist/assets/quadrantDiagram-W4KKPZXB-fdhc93U8.js +8 -0
  119. keboola_agent_cli/_ui_dist/assets/quadrantDiagram-W4KKPZXB-fdhc93U8.js.map +1 -0
  120. keboola_agent_cli/_ui_dist/assets/radar-GUYGQ44K-DAlLVJHm.js +1 -0
  121. keboola_agent_cli/_ui_dist/assets/requirementDiagram-4Y6WPE33-a94eP3R9.js +85 -0
  122. keboola_agent_cli/_ui_dist/assets/requirementDiagram-4Y6WPE33-a94eP3R9.js.map +1 -0
  123. keboola_agent_cli/_ui_dist/assets/rough.esm-CSKSodPl.js +2 -0
  124. keboola_agent_cli/_ui_dist/assets/rough.esm-CSKSodPl.js.map +1 -0
  125. keboola_agent_cli/_ui_dist/assets/sankeyDiagram-5OEKKPKP-jcBa02sp.js +41 -0
  126. keboola_agent_cli/_ui_dist/assets/sankeyDiagram-5OEKKPKP-jcBa02sp.js.map +1 -0
  127. keboola_agent_cli/_ui_dist/assets/sequenceDiagram-3UESZ5HK-A5-GGM-e.js +163 -0
  128. keboola_agent_cli/_ui_dist/assets/sequenceDiagram-3UESZ5HK-A5-GGM-e.js.map +1 -0
  129. keboola_agent_cli/_ui_dist/assets/src-ZI-V_AF0.js +2 -0
  130. keboola_agent_cli/_ui_dist/assets/src-ZI-V_AF0.js.map +1 -0
  131. keboola_agent_cli/_ui_dist/assets/stateDiagram-AJRCARHV-BKAA5rqE.js +2 -0
  132. keboola_agent_cli/_ui_dist/assets/stateDiagram-AJRCARHV-BKAA5rqE.js.map +1 -0
  133. keboola_agent_cli/_ui_dist/assets/stateDiagram-v2-BHNVJYJU-DnJwJBsE.js +2 -0
  134. keboola_agent_cli/_ui_dist/assets/stateDiagram-v2-BHNVJYJU-DnJwJBsE.js.map +1 -0
  135. keboola_agent_cli/_ui_dist/assets/timeline-definition-PNZ67QCA-Cy39jp8b.js +121 -0
  136. keboola_agent_cli/_ui_dist/assets/timeline-definition-PNZ67QCA-Cy39jp8b.js.map +1 -0
  137. keboola_agent_cli/_ui_dist/assets/treeView-BLDUP644-DbLYl23-.js +1 -0
  138. keboola_agent_cli/_ui_dist/assets/treemap-LRROVOQU-Bp0eGlOt.js +1 -0
  139. keboola_agent_cli/_ui_dist/assets/vennDiagram-CIIHVFJN-BGECKubd.js +35 -0
  140. keboola_agent_cli/_ui_dist/assets/vennDiagram-CIIHVFJN-BGECKubd.js.map +1 -0
  141. keboola_agent_cli/_ui_dist/assets/wardley-L42UT6IY-D4yH4jqS.js +1 -0
  142. keboola_agent_cli/_ui_dist/assets/wardleyDiagram-YWT4CUSO-D6XRG3cZ.js +79 -0
  143. keboola_agent_cli/_ui_dist/assets/wardleyDiagram-YWT4CUSO-D6XRG3cZ.js.map +1 -0
  144. keboola_agent_cli/_ui_dist/assets/xychartDiagram-2RQKCTM6-DRre-pfZ.js +8 -0
  145. keboola_agent_cli/_ui_dist/assets/xychartDiagram-2RQKCTM6-DRre-pfZ.js.map +1 -0
  146. keboola_agent_cli/_ui_dist/index.html +50 -0
  147. keboola_agent_cli/ai_client.py +83 -0
  148. keboola_agent_cli/auto_update.py +550 -0
  149. keboola_agent_cli/changelog.py +1198 -0
  150. keboola_agent_cli/cli.py +448 -0
  151. keboola_agent_cli/client.py +3422 -0
  152. keboola_agent_cli/commands/__init__.py +0 -0
  153. keboola_agent_cli/commands/_data_app_git.py +343 -0
  154. keboola_agent_cli/commands/_helpers.py +377 -0
  155. keboola_agent_cli/commands/_metadata_input.py +49 -0
  156. keboola_agent_cli/commands/_semantic_layer_crud.py +632 -0
  157. keboola_agent_cli/commands/_semantic_layer_helpers.py +44 -0
  158. keboola_agent_cli/commands/_semantic_layer_reference_data.py +247 -0
  159. keboola_agent_cli/commands/agent.py +968 -0
  160. keboola_agent_cli/commands/branch.py +423 -0
  161. keboola_agent_cli/commands/changelog.py +168 -0
  162. keboola_agent_cli/commands/component.py +216 -0
  163. keboola_agent_cli/commands/config.py +2442 -0
  164. keboola_agent_cli/commands/context.py +1481 -0
  165. keboola_agent_cli/commands/data_app.py +1279 -0
  166. keboola_agent_cli/commands/dev_portal.py +584 -0
  167. keboola_agent_cli/commands/doctor.py +37 -0
  168. keboola_agent_cli/commands/encrypt.py +145 -0
  169. keboola_agent_cli/commands/feature.py +311 -0
  170. keboola_agent_cli/commands/flow.py +948 -0
  171. keboola_agent_cli/commands/http_client.py +157 -0
  172. keboola_agent_cli/commands/init.py +279 -0
  173. keboola_agent_cli/commands/job.py +661 -0
  174. keboola_agent_cli/commands/kai.py +301 -0
  175. keboola_agent_cli/commands/lineage.py +1464 -0
  176. keboola_agent_cli/commands/org.py +292 -0
  177. keboola_agent_cli/commands/permissions.py +360 -0
  178. keboola_agent_cli/commands/project.py +1192 -0
  179. keboola_agent_cli/commands/repl.py +243 -0
  180. keboola_agent_cli/commands/schedule.py +340 -0
  181. keboola_agent_cli/commands/search.py +178 -0
  182. keboola_agent_cli/commands/semantic_layer.py +939 -0
  183. keboola_agent_cli/commands/serve.py +272 -0
  184. keboola_agent_cli/commands/sharing.py +340 -0
  185. keboola_agent_cli/commands/storage.py +2630 -0
  186. keboola_agent_cli/commands/stream.py +266 -0
  187. keboola_agent_cli/commands/sync.py +1277 -0
  188. keboola_agent_cli/commands/tool.py +206 -0
  189. keboola_agent_cli/commands/version.py +186 -0
  190. keboola_agent_cli/commands/workspace.py +635 -0
  191. keboola_agent_cli/config_store.py +582 -0
  192. keboola_agent_cli/constants.py +528 -0
  193. keboola_agent_cli/data_science_client.py +342 -0
  194. keboola_agent_cli/dev_portal_client.py +323 -0
  195. keboola_agent_cli/errors.py +248 -0
  196. keboola_agent_cli/http_base.py +315 -0
  197. keboola_agent_cli/json_utils.py +126 -0
  198. keboola_agent_cli/lib.py +536 -0
  199. keboola_agent_cli/manage_client.py +324 -0
  200. keboola_agent_cli/metastore_client.py +214 -0
  201. keboola_agent_cli/models.py +427 -0
  202. keboola_agent_cli/output.py +1084 -0
  203. keboola_agent_cli/permissions.py +469 -0
  204. keboola_agent_cli/py.typed +3 -0
  205. keboola_agent_cli/result_models.py +271 -0
  206. keboola_agent_cli/server/__init__.py +34 -0
  207. keboola_agent_cli/server/agent_runner.py +1289 -0
  208. keboola_agent_cli/server/agents_store.py +325 -0
  209. keboola_agent_cli/server/app.py +764 -0
  210. keboola_agent_cli/server/auth.py +117 -0
  211. keboola_agent_cli/server/dependencies.py +149 -0
  212. keboola_agent_cli/server/pricing.py +303 -0
  213. keboola_agent_cli/server/routers/__init__.py +1 -0
  214. keboola_agent_cli/server/routers/agents.py +616 -0
  215. keboola_agent_cli/server/routers/ai_chat.py +129 -0
  216. keboola_agent_cli/server/routers/branches.py +133 -0
  217. keboola_agent_cli/server/routers/components.py +48 -0
  218. keboola_agent_cli/server/routers/configs.py +507 -0
  219. keboola_agent_cli/server/routers/data_apps.py +384 -0
  220. keboola_agent_cli/server/routers/dev_portal.py +67 -0
  221. keboola_agent_cli/server/routers/encrypt.py +35 -0
  222. keboola_agent_cli/server/routers/feature.py +179 -0
  223. keboola_agent_cli/server/routers/flows.py +204 -0
  224. keboola_agent_cli/server/routers/health.py +53 -0
  225. keboola_agent_cli/server/routers/jobs.py +175 -0
  226. keboola_agent_cli/server/routers/kai.py +80 -0
  227. keboola_agent_cli/server/routers/lineage.py +226 -0
  228. keboola_agent_cli/server/routers/mcp.py +70 -0
  229. keboola_agent_cli/server/routers/members.py +170 -0
  230. keboola_agent_cli/server/routers/org.py +96 -0
  231. keboola_agent_cli/server/routers/projects.py +106 -0
  232. keboola_agent_cli/server/routers/schedules.py +54 -0
  233. keboola_agent_cli/server/routers/search.py +30 -0
  234. keboola_agent_cli/server/routers/semantic_layer.py +650 -0
  235. keboola_agent_cli/server/routers/sharing.py +86 -0
  236. keboola_agent_cli/server/routers/storage.py +574 -0
  237. keboola_agent_cli/server/routers/stream.py +100 -0
  238. keboola_agent_cli/server/routers/workspaces.py +302 -0
  239. keboola_agent_cli/server/run_broadcaster.py +329 -0
  240. keboola_agent_cli/server/sse.py +25 -0
  241. keboola_agent_cli/services/__init__.py +0 -0
  242. keboola_agent_cli/services/_encryption.py +217 -0
  243. keboola_agent_cli/services/_semantic_layer_cascade.py +147 -0
  244. keboola_agent_cli/services/_semantic_layer_crud.py +382 -0
  245. keboola_agent_cli/services/_semantic_layer_internals.py +1078 -0
  246. keboola_agent_cli/services/_semantic_layer_lookup.py +181 -0
  247. keboola_agent_cli/services/_semantic_layer_reference_data.py +217 -0
  248. keboola_agent_cli/services/_sync_bindings.py +456 -0
  249. keboola_agent_cli/services/_sync_branch.py +191 -0
  250. keboola_agent_cli/services/_sync_bulk.py +228 -0
  251. keboola_agent_cli/services/_sync_clone.py +163 -0
  252. keboola_agent_cli/services/_sync_models.py +97 -0
  253. keboola_agent_cli/services/_sync_push_ops.py +369 -0
  254. keboola_agent_cli/services/_sync_storage.py +376 -0
  255. keboola_agent_cli/services/_sync_writeback.py +167 -0
  256. keboola_agent_cli/services/agent_service.py +458 -0
  257. keboola_agent_cli/services/base.py +175 -0
  258. keboola_agent_cli/services/branch_service.py +588 -0
  259. keboola_agent_cli/services/component_service.py +694 -0
  260. keboola_agent_cli/services/config_service.py +2099 -0
  261. keboola_agent_cli/services/data_app_git_service.py +224 -0
  262. keboola_agent_cli/services/data_app_service.py +2082 -0
  263. keboola_agent_cli/services/deep_lineage_service.py +1322 -0
  264. keboola_agent_cli/services/dev_portal_service.py +345 -0
  265. keboola_agent_cli/services/doctor_service.py +445 -0
  266. keboola_agent_cli/services/encrypt_service.py +87 -0
  267. keboola_agent_cli/services/feature_service.py +268 -0
  268. keboola_agent_cli/services/flow_service.py +769 -0
  269. keboola_agent_cli/services/flow_validation.py +188 -0
  270. keboola_agent_cli/services/http_forwarder_service.py +236 -0
  271. keboola_agent_cli/services/job_idempotency_store.py +285 -0
  272. keboola_agent_cli/services/job_service.py +797 -0
  273. keboola_agent_cli/services/kai_service.py +367 -0
  274. keboola_agent_cli/services/lineage_service.py +274 -0
  275. keboola_agent_cli/services/mcp_service.py +1498 -0
  276. keboola_agent_cli/services/mcp_transport.py +259 -0
  277. keboola_agent_cli/services/member_service.py +593 -0
  278. keboola_agent_cli/services/org_service.py +619 -0
  279. keboola_agent_cli/services/project_service.py +947 -0
  280. keboola_agent_cli/services/repo_validate_service.py +767 -0
  281. keboola_agent_cli/services/schedule_service.py +731 -0
  282. keboola_agent_cli/services/search_service.py +331 -0
  283. keboola_agent_cli/services/semantic_layer_service.py +1497 -0
  284. keboola_agent_cli/services/sharing_service.py +307 -0
  285. keboola_agent_cli/services/storage_service.py +2524 -0
  286. keboola_agent_cli/services/stream_service.py +395 -0
  287. keboola_agent_cli/services/sync_service.py +2244 -0
  288. keboola_agent_cli/services/variables_service.py +447 -0
  289. keboola_agent_cli/services/version_service.py +1038 -0
  290. keboola_agent_cli/services/workspace_service.py +1103 -0
  291. keboola_agent_cli/stream_client.py +217 -0
  292. keboola_agent_cli/sync/__init__.py +1 -0
  293. keboola_agent_cli/sync/branch_mapping.py +174 -0
  294. keboola_agent_cli/sync/clone.py +211 -0
  295. keboola_agent_cli/sync/code_extraction.py +655 -0
  296. keboola_agent_cli/sync/config_format.py +290 -0
  297. keboola_agent_cli/sync/diff_engine.py +566 -0
  298. keboola_agent_cli/sync/git_utils.py +93 -0
  299. keboola_agent_cli/sync/manifest.py +162 -0
  300. keboola_agent_cli/sync/naming.py +90 -0
  301. keboola_agent_cli/sync/secrets.py +62 -0
  302. keboola_agent_cli/sync/sql_split.py +134 -0
  303. keboola_cli-0.63.4.dist-info/METADATA +308 -0
  304. keboola_cli-0.63.4.dist-info/RECORD +306 -0
  305. keboola_cli-0.63.4.dist-info/WHEEL +4 -0
  306. keboola_cli-0.63.4.dist-info/entry_points.txt +2 -0
@@ -0,0 +1,1084 @@
1
+ """Output formatting with JSON and Rich dual mode support."""
2
+
3
+ import json
4
+ import sys
5
+ from collections.abc import Callable
6
+ from datetime import datetime
7
+ from typing import Any
8
+
9
+ from rich.console import Console
10
+ from rich.panel import Panel
11
+ from rich.table import Table
12
+ from rich.text import Text
13
+
14
+ from .models import ErrorResponse, SuccessResponse
15
+
16
+
17
+ class OutputFormatter:
18
+ """Formats CLI output as either JSON (for machines/agents) or Rich (for humans).
19
+
20
+ In JSON mode, all output goes to stdout as valid JSON.
21
+ In human mode, Rich console is used for formatted tables and panels.
22
+ """
23
+
24
+ def __init__(
25
+ self,
26
+ json_mode: bool = False,
27
+ no_color: bool = False,
28
+ verbose: bool = False,
29
+ ) -> None:
30
+ self.json_mode = json_mode
31
+ self.verbose = verbose
32
+ is_tty = hasattr(sys.stdout, "isatty") and sys.stdout.isatty()
33
+ force_terminal = None if is_tty and not no_color else False
34
+ self.console = Console(
35
+ no_color=no_color,
36
+ force_terminal=force_terminal,
37
+ )
38
+ self.err_console = Console(
39
+ stderr=True,
40
+ no_color=no_color,
41
+ force_terminal=force_terminal,
42
+ )
43
+
44
+ def output(
45
+ self, data: Any, human_formatter: Callable[[Console, Any], object] | None = None
46
+ ) -> None:
47
+ """Output data in the appropriate format.
48
+
49
+ Args:
50
+ data: The data to output. In JSON mode, this is serialized directly.
51
+ In human mode, it's passed to human_formatter.
52
+ human_formatter: A callable that takes (Console, data) and prints
53
+ human-friendly output. If None in human mode, prints repr.
54
+ """
55
+ if self.json_mode:
56
+ response = SuccessResponse(status="ok", data=data)
57
+ sys.stdout.write(response.model_dump_json(indent=2) + "\n")
58
+ else:
59
+ if human_formatter is not None:
60
+ human_formatter(self.console, data)
61
+ else:
62
+ self.console.print(data)
63
+
64
+ def error(
65
+ self,
66
+ message: str,
67
+ error_code: str = "ERROR",
68
+ project: str = "",
69
+ retryable: bool = False,
70
+ error_type: str = "",
71
+ details: dict | None = None,
72
+ ) -> None:
73
+ """Output an error message.
74
+
75
+ Args:
76
+ message: Human-readable error description.
77
+ error_code: Machine-readable error code.
78
+ project: Project alias related to the error.
79
+ retryable: Whether the operation can be retried.
80
+ error_type: Broad error category. If empty, derived from error_code.
81
+ details: Optional structured context (e.g. {"logTail": [...]}).
82
+ When empty/None, the field is omitted from JSON output so
83
+ consumers can key off presence.
84
+ """
85
+ if self.json_mode:
86
+ if not error_type:
87
+ from .errors import map_error_code_to_type
88
+
89
+ error_type = map_error_code_to_type(error_code)
90
+ err = ErrorResponse(
91
+ code=error_code,
92
+ error_type=error_type,
93
+ message=message,
94
+ project=project,
95
+ retryable=retryable,
96
+ details=details if details else None,
97
+ )
98
+ error_envelope = {
99
+ "status": "error",
100
+ "error": err.model_dump(exclude_none=True),
101
+ }
102
+ sys.stdout.write(json.dumps(error_envelope, indent=2) + "\n")
103
+ else:
104
+ self.err_console.print(f"[bold red]Error:[/bold red] {message}")
105
+
106
+ def success(self, message: str) -> None:
107
+ """Output a success message.
108
+
109
+ Args:
110
+ message: The success message to display.
111
+ """
112
+ if self.json_mode:
113
+ response = SuccessResponse(status="ok", data={"message": message})
114
+ sys.stdout.write(response.model_dump_json(indent=2) + "\n")
115
+ else:
116
+ self.console.print(f"[bold green]Success:[/bold green] {message}")
117
+
118
+ def warning(self, message: str) -> None:
119
+ """Output a warning message to stderr (human mode only).
120
+
121
+ In JSON mode, warnings are not printed separately -- they are
122
+ embedded in the structured response via the errors list.
123
+
124
+ Args:
125
+ message: The warning message to display.
126
+ """
127
+ if not self.json_mode:
128
+ self.err_console.print(f"[bold yellow]Warning:[/bold yellow] {message}")
129
+
130
+
131
+ def format_configs_table(console: Console, data: dict[str, Any]) -> None:
132
+ """Render a Rich table of configurations grouped by project alias.
133
+
134
+ Args:
135
+ console: Rich Console instance.
136
+ data: Dict with "configs" (list of config dicts) and "errors" (list of error dicts).
137
+ """
138
+ configs = data.get("configs", [])
139
+ errors = data.get("errors", [])
140
+
141
+ # Show per-project errors as warnings
142
+ for err in errors:
143
+ console.print(
144
+ f"[bold yellow]Warning:[/bold yellow] Project [bold]{err['project_alias']}[/bold]: "
145
+ f"{err['message']}"
146
+ )
147
+
148
+ if not configs:
149
+ if not errors:
150
+ console.print(
151
+ "No configurations found. Use [bold]kbagent project add[/bold] to connect a project first."
152
+ )
153
+ else:
154
+ console.print("No configurations retrieved (all projects failed).")
155
+ return
156
+
157
+ # Group configs by project alias
158
+ projects_order: list[str] = []
159
+ grouped: dict[str, list[dict[str, Any]]] = {}
160
+ for cfg in configs:
161
+ alias = cfg["project_alias"]
162
+ if alias not in grouped:
163
+ projects_order.append(alias)
164
+ grouped[alias] = []
165
+ grouped[alias].append(cfg)
166
+
167
+ for alias in projects_order:
168
+ project_configs = grouped[alias]
169
+ table = Table(title=f"Configurations - {alias}")
170
+ table.add_column("Component", style="bold cyan")
171
+ table.add_column("Type", style="dim")
172
+ table.add_column("Config ID", justify="right")
173
+ table.add_column("Config Name")
174
+ table.add_column("Folder", style="dim")
175
+ table.add_column("Last Modified", style="dim")
176
+ table.add_column("Modified By", style="dim")
177
+
178
+ for cfg in project_configs:
179
+ # Format last_modified to shorter form if present
180
+ last_mod = cfg.get("last_modified", "")
181
+ if last_mod and "T" in last_mod:
182
+ last_mod = last_mod.split("T")[0] # just date part
183
+
184
+ table.add_row(
185
+ cfg["component_id"],
186
+ cfg["component_type"],
187
+ cfg["config_id"],
188
+ cfg["config_name"],
189
+ cfg.get("folder", ""),
190
+ last_mod,
191
+ cfg.get("last_modified_by", ""),
192
+ )
193
+
194
+ console.print(table)
195
+ console.print()
196
+
197
+
198
+ def format_config_detail(console: Console, data: dict[str, Any]) -> None:
199
+ """Render detailed configuration information.
200
+
201
+ Args:
202
+ console: Rich Console instance.
203
+ data: Configuration detail dict from the API.
204
+ """
205
+ alias = data.get("project_alias", "unknown")
206
+ name = data.get("name", "Unknown")
207
+ config_id = data.get("id", "")
208
+ description = data.get("description", "")
209
+ component_id = data.get("component_id", data.get("componentId", ""))
210
+
211
+ header = f"Configuration Detail - {alias}"
212
+
213
+ lines = [
214
+ f"[bold]Name:[/bold] {name}",
215
+ f"[bold]Config ID:[/bold] {config_id}",
216
+ f"[bold]Component:[/bold] {component_id}",
217
+ ]
218
+ if description:
219
+ lines.append(f"[bold]Description:[/bold] {description}")
220
+
221
+ # Show configuration parameters if present
222
+ configuration = data.get("configuration", {})
223
+ if configuration:
224
+ config_str = json.dumps(configuration, indent=2)
225
+ lines.append(f"\n[bold]Configuration:[/bold]\n{config_str}")
226
+
227
+ # keboola.sandboxes annotation (issue #304 bod #3): make explicit that
228
+ # ``parameters.id`` is NOT the Storage workspace ID, and show the real
229
+ # mapping resolved by the command layer.
230
+ annotation = data.get("sandbox_annotation")
231
+ if annotation:
232
+ sb_id = annotation.get("sandbox_service_id")
233
+ ws_id = annotation.get("storage_workspace_id")
234
+ lines.append("\n[bold yellow]Sandbox / Workspace mapping:[/bold yellow]")
235
+ lines.append(
236
+ f" [dim]parameters.id (sandbox-service):[/dim] {sb_id if sb_id is not None else '[dim](absent)[/dim]'}"
237
+ )
238
+ if ws_id is not None:
239
+ lines.append(
240
+ f" [bold]Storage workspace ID:[/bold] [green]{ws_id}[/green] "
241
+ f"[dim](use with `kbagent workspace detail --workspace-id {ws_id}`)[/dim]"
242
+ )
243
+ else:
244
+ lines.append(
245
+ " [bold]Storage workspace ID:[/bold] [dim]none[/dim] "
246
+ "[yellow](no workspace currently backed by this config -- orphan sandbox)[/yellow]"
247
+ )
248
+ lines.append(f" [dim]{annotation.get('note', '')}[/dim]")
249
+
250
+ # Show rows if present
251
+ rows = data.get("rows", [])
252
+ if rows:
253
+ lines.append(f"\n[bold]Rows:[/bold] {len(rows)} row(s)")
254
+ for row in rows[:10]: # Show at most 10 rows
255
+ row_name = row.get("name", row.get("id", ""))
256
+ lines.append(f" - {row_name}")
257
+ if len(rows) > 10:
258
+ lines.append(f" ... and {len(rows) - 10} more")
259
+
260
+ panel = Panel("\n".join(lines), title=header, expand=False)
261
+ console.print(panel)
262
+
263
+
264
+ _JOB_STATUS_STYLES = {
265
+ "success": "bold green",
266
+ "error": "bold red",
267
+ "processing": "bold blue",
268
+ "cancelled": "bold yellow",
269
+ "terminated": "bold yellow",
270
+ }
271
+
272
+
273
+ def format_jobs_table(console: Console, data: dict[str, Any]) -> None:
274
+ """Render a Rich table of jobs grouped by project alias.
275
+
276
+ Args:
277
+ console: Rich Console instance.
278
+ data: Dict with "jobs" (list of job dicts) and "errors" (list of error dicts).
279
+ """
280
+ jobs = data.get("jobs", [])
281
+ errors = data.get("errors", [])
282
+
283
+ for err in errors:
284
+ console.print(
285
+ f"[bold yellow]Warning:[/bold yellow] Project [bold]{err['project_alias']}[/bold]: "
286
+ f"{err['message']}"
287
+ )
288
+
289
+ if not jobs:
290
+ if not errors:
291
+ console.print(
292
+ "No jobs found. Use [bold]kbagent project add[/bold] to connect a project first."
293
+ )
294
+ else:
295
+ console.print("No jobs retrieved (all projects failed).")
296
+ return
297
+
298
+ # Detect if jobs span multiple projects
299
+ project_aliases = {job.get("project_alias", "unknown") for job in jobs}
300
+ multi_project = len(project_aliases) > 1
301
+
302
+ table = Table(title="Jobs")
303
+ table.add_column("Project", style="bold magenta")
304
+ table.add_column("Job ID", justify="right")
305
+ table.add_column("Status")
306
+ table.add_column("Component", style="bold cyan")
307
+ table.add_column("Config ID", justify="right")
308
+ table.add_column("Created", style="dim")
309
+ table.add_column("Duration", justify="right")
310
+
311
+ # Track previous alias for visual grouping (show alias only on change)
312
+ prev_alias = None
313
+ for job in jobs:
314
+ alias = job.get("project_alias", "unknown")
315
+ status = job.get("status", "unknown")
316
+ style = _JOB_STATUS_STYLES.get(status, "")
317
+ status_display = f"[{style}]{status}[/{style}]" if style else status
318
+
319
+ duration = _format_duration(job)
320
+
321
+ # In multi-project mode, show alias only on first row of each group
322
+ if multi_project:
323
+ display_alias = alias if alias != prev_alias else ""
324
+ else:
325
+ display_alias = alias if prev_alias is None else ""
326
+ prev_alias = alias
327
+
328
+ table.add_row(
329
+ display_alias,
330
+ str(job.get("id", "")),
331
+ status_display,
332
+ job.get("component", ""),
333
+ str(job.get("configId", job.get("config_id", ""))),
334
+ job.get("createdTime", ""),
335
+ duration,
336
+ )
337
+
338
+ console.print(table)
339
+ console.print()
340
+
341
+
342
+ def _format_duration(job: dict[str, Any]) -> str:
343
+ """Format job duration from startTime/endTime or durationSeconds."""
344
+ duration_sec = job.get("durationSeconds")
345
+ if duration_sec is not None:
346
+ return _seconds_to_human(int(duration_sec))
347
+
348
+ start = job.get("startTime")
349
+ end = job.get("endTime")
350
+ if start and end:
351
+ try:
352
+ start_dt = datetime.fromisoformat(start)
353
+ end_dt = datetime.fromisoformat(end)
354
+ delta = int((end_dt - start_dt).total_seconds())
355
+ return _seconds_to_human(delta)
356
+ except (ValueError, TypeError):
357
+ pass
358
+
359
+ return "-"
360
+
361
+
362
+ def _seconds_to_human(seconds: int) -> str:
363
+ """Convert seconds to a human-readable duration string."""
364
+ if seconds < 60:
365
+ return f"{seconds}s"
366
+ minutes, secs = divmod(seconds, 60)
367
+ if minutes < 60:
368
+ return f"{minutes}m {secs}s"
369
+ hours, mins = divmod(minutes, 60)
370
+ return f"{hours}h {mins}m"
371
+
372
+
373
+ def format_job_detail(console: Console, data: dict[str, Any]) -> None:
374
+ """Render detailed job information as a Rich panel.
375
+
376
+ Args:
377
+ console: Rich Console instance.
378
+ data: Job detail dict from the Queue API with project_alias.
379
+ """
380
+ alias = data.get("project_alias", "unknown")
381
+ job_id = data.get("id", "")
382
+ status = data.get("status", "unknown")
383
+ style = _JOB_STATUS_STYLES.get(status, "")
384
+ status_display = f"[{style}]{status}[/{style}]" if style else status
385
+
386
+ lines = [
387
+ f"[bold]Job ID:[/bold] {job_id}",
388
+ f"[bold]Project:[/bold] {alias}",
389
+ f"[bold]Status:[/bold] {status_display}",
390
+ f"[bold]Component:[/bold] {data.get('component', '')}",
391
+ f"[bold]Config ID:[/bold] {data.get('config', data.get('configId', ''))}",
392
+ f"[bold]Mode:[/bold] {data.get('mode', '')}",
393
+ f"[bold]Type:[/bold] {data.get('type', '')}",
394
+ ]
395
+
396
+ # Timing
397
+ created = data.get("createdTime", "")
398
+ start = data.get("startTime", "")
399
+ end = data.get("endTime", "")
400
+ duration = _format_duration(data)
401
+
402
+ if created:
403
+ lines.append(f"[bold]Created:[/bold] {created}")
404
+ if start:
405
+ lines.append(f"[bold]Started:[/bold] {start}")
406
+ if end:
407
+ lines.append(f"[bold]Ended:[/bold] {end}")
408
+ lines.append(f"[bold]Duration:[/bold] {duration}")
409
+
410
+ # Branch and orchestration
411
+ branch_id = data.get("branchId")
412
+ if branch_id:
413
+ lines.append(f"[bold]Branch ID:[/bold] {branch_id}")
414
+ orch_job = data.get("orchestrationJobId")
415
+ if orch_job:
416
+ lines.append(f"[bold]Orchestration Job:[/bold] {orch_job}")
417
+
418
+ # URL
419
+ url = data.get("url", "")
420
+ if url:
421
+ lines.append(f"[bold]URL:[/bold] {url}")
422
+
423
+ # Result message
424
+ result = data.get("result", {})
425
+ if isinstance(result, dict):
426
+ message = result.get("message", "")
427
+ if message:
428
+ lines.append(f"\n[bold]Result Message:[/bold]\n{message}")
429
+
430
+ error_info = result.get("error", {})
431
+ if isinstance(error_info, dict) and error_info:
432
+ error_type = error_info.get("type", "")
433
+ if error_type:
434
+ lines.append(f"[bold]Error Type:[/bold] {error_type}")
435
+
436
+ panel = Panel("\n".join(lines), title=f"Job Detail - {job_id}", expand=False)
437
+ console.print(panel)
438
+
439
+
440
+ def _format_tool_params(schema: dict[str, Any]) -> str:
441
+ """Format inputSchema properties as a compact param string.
442
+
443
+ Required params are marked with *, optional are dim.
444
+ Example: "sql_query*, query_name*"
445
+ """
446
+ props = schema.get("properties", {})
447
+ required = set(schema.get("required", []))
448
+ if not props:
449
+ return "[dim](none)[/dim]"
450
+ parts = []
451
+ for name in props:
452
+ if name in required:
453
+ parts.append(f"[bold]{name}[/bold]*")
454
+ else:
455
+ parts.append(f"[dim]{name}[/dim]")
456
+ return ", ".join(parts)
457
+
458
+
459
+ def format_tools_table(console: Console, data: dict[str, Any]) -> None:
460
+ """Render a Rich table of MCP tools.
461
+
462
+ Args:
463
+ console: Rich Console instance.
464
+ data: Dict with "tools" list and "errors" list.
465
+ """
466
+ tools = data.get("tools", [])
467
+ errors = data.get("errors", [])
468
+
469
+ for err in errors:
470
+ console.print(
471
+ f"[bold yellow]Warning:[/bold yellow] Project [bold]{err['project_alias']}[/bold]: "
472
+ f"{err['message']}"
473
+ )
474
+
475
+ if not tools:
476
+ if not errors:
477
+ console.print(
478
+ "No MCP tools found. Ensure keboola-mcp-server is installed and a project is connected."
479
+ )
480
+ else:
481
+ console.print("No tools retrieved (all projects failed).")
482
+ return
483
+
484
+ table = Table(title="MCP Tools")
485
+ table.add_column("Tool Name", style="bold cyan")
486
+ table.add_column("Parameters")
487
+ table.add_column("Multi-Project", justify="center")
488
+ table.add_column("Description", max_width=60)
489
+
490
+ for tool in tools:
491
+ multi = "[green]yes[/green]" if tool.get("multi_project") else "[dim]no[/dim]"
492
+ params_str = _format_tool_params(tool.get("inputSchema", {}))
493
+ table.add_row(
494
+ tool["name"],
495
+ params_str,
496
+ multi,
497
+ tool.get("description", ""),
498
+ )
499
+
500
+ console.print(table)
501
+ console.print()
502
+
503
+
504
+ def _extract_result_text(result: dict[str, Any]) -> str:
505
+ """Extract a text representation of a tool result's content for comparison."""
506
+ parts = []
507
+ for item in result.get("content", []):
508
+ if isinstance(item, str):
509
+ parts.append(item)
510
+ elif isinstance(item, dict):
511
+ parts.append(json.dumps(item, sort_keys=True))
512
+ else:
513
+ parts.append(str(item))
514
+ return "\n".join(parts)
515
+
516
+
517
+ def format_tool_result(console: Console, data: dict[str, Any]) -> None:
518
+ """Render MCP tool call results as Rich panels.
519
+
520
+ When all results are errors with the same message (e.g. missing parameter),
521
+ consolidates them into a single error panel instead of repeating N times.
522
+
523
+ Args:
524
+ console: Rich Console instance.
525
+ data: Dict with "results" list and "errors" list.
526
+ """
527
+ results = data.get("results", [])
528
+ errors = data.get("errors", [])
529
+
530
+ for err in errors:
531
+ console.print(
532
+ f"[bold yellow]Warning:[/bold yellow] Project [bold]{err['project_alias']}[/bold]: "
533
+ f"{err['message']}"
534
+ )
535
+
536
+ if not results:
537
+ if not errors:
538
+ console.print("No results returned.")
539
+ else:
540
+ console.print("Tool call failed for all projects.")
541
+ return
542
+
543
+ # Detect all-same-error pattern: all results are errors with identical content
544
+ all_errors = all(r.get("isError", False) for r in results)
545
+ if all_errors and len(results) > 1:
546
+ unique_messages = {_extract_result_text(r) for r in results}
547
+ if len(unique_messages) == 1:
548
+ # Consolidate: show one error panel + count
549
+ affected = [r.get("project_alias", "unknown") for r in results]
550
+ content = results[0].get("content", [])
551
+
552
+ lines = [
553
+ f"[bold]Status:[/bold] [bold red]ERROR[/bold red] (same error across {len(results)} projects)",
554
+ ]
555
+ for item in content:
556
+ if isinstance(item, str):
557
+ lines.append(item)
558
+ elif isinstance(item, dict):
559
+ lines.append(json.dumps(item, indent=2))
560
+ else:
561
+ lines.append(str(item))
562
+
563
+ lines.append(f"\n[dim]Affected projects: {', '.join(affected)}[/dim]")
564
+
565
+ panel = Panel("\n".join(lines), title="Tool Error", expand=False)
566
+ console.print(panel)
567
+ return
568
+
569
+ # Normal rendering: one panel per result
570
+ for result in results:
571
+ alias = result.get("project_alias", "unknown")
572
+ is_error = result.get("isError", False)
573
+ content = result.get("content", [])
574
+
575
+ status_label = "[bold red]ERROR[/bold red]" if is_error else "[bold green]OK[/bold green]"
576
+ lines: list[str] = [f"[bold]Status:[/bold] {status_label}"]
577
+
578
+ for item in content:
579
+ if isinstance(item, str):
580
+ lines.append(item)
581
+ elif isinstance(item, dict):
582
+ lines.append(json.dumps(item, indent=2))
583
+ else:
584
+ lines.append(str(item))
585
+
586
+ panel = Panel("\n".join(lines), title=f"Result - {alias}", expand=False)
587
+ console.print(panel)
588
+
589
+
590
+ _SHARING_TYPE_STYLES = {
591
+ "organization": "bold cyan",
592
+ "organization-project": "bold green",
593
+ "data-science": "bold magenta",
594
+ }
595
+
596
+
597
+ def _project_label(alias: str, project_id: int, project_name: str) -> Text:
598
+ """Create a styled project label. Unknown projects shown as dimmed #id."""
599
+ if alias:
600
+ return Text(alias, style="bold")
601
+ label = Text(f"#{project_id}", style="dim")
602
+ if project_name:
603
+ label.append(f" ({project_name})", style="dim")
604
+ return label
605
+
606
+
607
+ def format_lineage_table(console: Console, data: dict[str, Any]) -> None:
608
+ """Render cross-project lineage data as Rich tables.
609
+
610
+ Args:
611
+ console: Rich Console instance.
612
+ data: Dict with "edges", "shared_buckets", "linked_buckets", "summary", "errors".
613
+ """
614
+ edges = data.get("edges", [])
615
+ shared_buckets = data.get("shared_buckets", [])
616
+ linked_buckets = data.get("linked_buckets", [])
617
+ summary = data.get("summary", {})
618
+ errors = data.get("errors", [])
619
+
620
+ # Show per-project errors
621
+ for err in errors:
622
+ console.print(
623
+ f"[bold yellow]Warning:[/bold yellow] Project [bold]{err['project_alias']}[/bold]: "
624
+ f"{err['message']}"
625
+ )
626
+
627
+ # Render summary
628
+ _render_lineage_summary(console, summary)
629
+
630
+ # Render edges table if available
631
+ if edges:
632
+ _render_edges_table(console, edges)
633
+ elif shared_buckets or linked_buckets:
634
+ # No edges but buckets exist -- show details
635
+ if shared_buckets:
636
+ _render_shared_buckets_table(console, shared_buckets)
637
+ if linked_buckets:
638
+ _render_linked_buckets_table(console, linked_buckets)
639
+ elif not errors:
640
+ console.print("\nNo bucket sharing detected across queried projects.")
641
+
642
+
643
+ def _render_lineage_summary(console: Console, summary: dict[str, Any]) -> None:
644
+ """Render a summary line for lineage results."""
645
+ shared = summary.get("total_shared_buckets", 0)
646
+ linked = summary.get("total_linked_buckets", 0)
647
+ edge_count = summary.get("total_edges", 0)
648
+ queried = summary.get("projects_queried", 0)
649
+
650
+ console.print(
651
+ f"\nFound [bold]{shared}[/bold] shared bucket(s) with "
652
+ f"[bold]{edge_count}[/bold] link(s) across "
653
+ f"[bold]{queried}[/bold] project(s)."
654
+ )
655
+ if linked:
656
+ console.print(f" [dim]{linked} linked bucket(s) detected.[/dim]")
657
+ console.print()
658
+
659
+
660
+ def _render_edges_table(console: Console, edges: list[dict[str, Any]]) -> None:
661
+ """Render the main edges table showing data flow between projects."""
662
+ table = Table(title="Data Flow Edges")
663
+ table.add_column("Source Project", style="bold magenta")
664
+ table.add_column("Source Bucket", style="cyan")
665
+ table.add_column("Sharing Type", style="dim")
666
+ table.add_column("Target Project", style="bold magenta")
667
+ table.add_column("Target Bucket", style="cyan")
668
+
669
+ for edge in edges:
670
+ source_label = _project_label(
671
+ edge.get("source_project_alias", ""),
672
+ edge.get("source_project_id", 0),
673
+ edge.get("source_project_name", ""),
674
+ )
675
+ target_label = _project_label(
676
+ edge.get("target_project_alias", ""),
677
+ edge.get("target_project_id", 0),
678
+ edge.get("target_project_name", ""),
679
+ )
680
+ sharing_type = edge.get("sharing_type", "")
681
+ style = _SHARING_TYPE_STYLES.get(sharing_type, "")
682
+ sharing_display = Text(sharing_type, style=style) if style else Text(sharing_type)
683
+
684
+ table.add_row(
685
+ source_label,
686
+ edge.get("source_bucket_id", ""),
687
+ sharing_display,
688
+ target_label,
689
+ edge.get("target_bucket_id", ""),
690
+ )
691
+
692
+ console.print(table)
693
+ console.print()
694
+
695
+
696
+ def _render_shared_buckets_table(console: Console, shared_buckets: list[dict[str, Any]]) -> None:
697
+ """Render a table of shared buckets (no linked targets found)."""
698
+ table = Table(title="Shared Buckets (no linked targets found)")
699
+ table.add_column("Project", style="bold magenta")
700
+ table.add_column("Bucket ID", style="cyan")
701
+ table.add_column("Bucket Name")
702
+ table.add_column("Sharing Type", style="dim")
703
+
704
+ for sb in shared_buckets:
705
+ table.add_row(
706
+ sb.get("project_alias", ""),
707
+ sb.get("bucket_id", ""),
708
+ sb.get("bucket_name", ""),
709
+ sb.get("sharing_type", ""),
710
+ )
711
+
712
+ console.print(table)
713
+ console.print()
714
+
715
+
716
+ def _render_linked_buckets_table(console: Console, linked_buckets: list[dict[str, Any]]) -> None:
717
+ """Render a table of linked buckets (incoming links)."""
718
+ table = Table(title="Linked Buckets (incoming)")
719
+ table.add_column("Project", style="bold magenta")
720
+ table.add_column("Bucket ID", style="cyan")
721
+ table.add_column("Source Bucket", style="dim")
722
+ table.add_column("Source Project", style="dim")
723
+ table.add_column("Read-only", justify="center")
724
+
725
+ for lb in linked_buckets:
726
+ readonly = "[green]yes[/green]" if lb.get("is_readonly") else "[dim]no[/dim]"
727
+ table.add_row(
728
+ lb.get("project_alias", ""),
729
+ lb.get("bucket_id", ""),
730
+ lb.get("source_bucket_id", ""),
731
+ lb.get("source_project_name", ""),
732
+ readonly,
733
+ )
734
+
735
+ console.print(table)
736
+ console.print()
737
+
738
+
739
+ def format_branches_table(console: Console, data: dict[str, Any]) -> None:
740
+ """Render a Rich table of development branches grouped by project alias.
741
+
742
+ Args:
743
+ console: Rich Console instance.
744
+ data: Dict with "branches" (list of branch dicts) and "errors" (list of error dicts).
745
+ """
746
+ branches = data.get("branches", [])
747
+ errors = data.get("errors", [])
748
+
749
+ for err in errors:
750
+ console.print(
751
+ f"[bold yellow]Warning:[/bold yellow] Project [bold]{err['project_alias']}[/bold]: "
752
+ f"{err['message']}"
753
+ )
754
+
755
+ if not branches:
756
+ if not errors:
757
+ console.print(
758
+ "No branches found. Use [bold]kbagent project add[/bold] to connect a project first."
759
+ )
760
+ else:
761
+ console.print("No branches retrieved (all projects failed).")
762
+ return
763
+
764
+ active_branches = data.get("active_branches", {})
765
+
766
+ table = Table(title="Development Branches")
767
+ table.add_column("Project", style="bold magenta")
768
+ table.add_column("Branch ID", justify="right")
769
+ table.add_column("Name", style="bold cyan")
770
+ table.add_column("Default", justify="center")
771
+ table.add_column("Active", justify="center")
772
+ table.add_column("Description", style="dim", max_width=40)
773
+ table.add_column("Created", style="dim")
774
+
775
+ prev_alias = None
776
+ for branch in branches:
777
+ alias = branch.get("project_alias", "unknown")
778
+ is_default = branch.get("isDefault", False)
779
+ default_display = "[green]yes[/green]" if is_default else "[dim]no[/dim]"
780
+
781
+ branch_id = branch.get("id")
782
+ active_id = active_branches.get(alias)
783
+ # Compare as int to handle potential type mismatch from API
784
+ is_active = (
785
+ branch_id is not None and active_id is not None and int(branch_id) == int(active_id)
786
+ )
787
+ active_display = "[bold green]>>>[/bold green]" if is_active else ""
788
+
789
+ display_alias = alias if alias != prev_alias else ""
790
+ prev_alias = alias
791
+
792
+ table.add_row(
793
+ display_alias,
794
+ str(branch.get("id", "")),
795
+ branch.get("name", ""),
796
+ default_display,
797
+ active_display,
798
+ branch.get("description", ""),
799
+ branch.get("created", ""),
800
+ )
801
+
802
+ console.print(table)
803
+ console.print()
804
+
805
+
806
+ def format_branch_metadata_table(console: Console, data: dict[str, Any]) -> None:
807
+ """Render branch metadata entries as a Rich table.
808
+
809
+ Args:
810
+ console: Rich Console instance.
811
+ data: Dict with project_alias, branch_id, and metadata (list of entries).
812
+ """
813
+ entries = data.get("metadata", [])
814
+ alias = data.get("project_alias", "unknown")
815
+ branch_id = data.get("branch_id", "default")
816
+
817
+ if not entries:
818
+ console.print(
819
+ f"No metadata on branch [bold]{branch_id}[/bold] of project "
820
+ f"[bold magenta]{alias}[/bold magenta]."
821
+ )
822
+ return
823
+
824
+ table = Table(title=f"Branch Metadata - project: {alias} - branch: {branch_id}")
825
+ table.add_column("ID", justify="right", style="dim")
826
+ table.add_column("Key", style="bold cyan")
827
+ table.add_column("Value", max_width=60)
828
+ table.add_column("Provider", style="dim")
829
+ table.add_column("Timestamp", style="dim")
830
+
831
+ for entry in entries:
832
+ value = str(entry.get("value", ""))
833
+ if "\n" in value:
834
+ first_line = value.splitlines()[0]
835
+ value = f"{first_line} [dim](+{len(value.splitlines()) - 1} more lines)[/dim]"
836
+ table.add_row(
837
+ str(entry.get("id", "")),
838
+ str(entry.get("key", "")),
839
+ value,
840
+ str(entry.get("provider", "")),
841
+ str(entry.get("timestamp", "")),
842
+ )
843
+
844
+ console.print(table)
845
+
846
+
847
+ def format_doctor_panel(console: Console, data: dict[str, Any]) -> None:
848
+ """Render doctor check results as a Rich panel with colored status indicators.
849
+
850
+ Args:
851
+ console: Rich Console instance.
852
+ data: Dict with "checks" list and "summary" dict from DoctorService.
853
+ """
854
+ status_icons = {
855
+ "pass": "[bold green]PASS[/bold green]",
856
+ "fail": "[bold red]FAIL[/bold red]",
857
+ "warn": "[bold yellow]WARN[/bold yellow]",
858
+ }
859
+
860
+ checks = data.get("checks", [])
861
+ lines = [
862
+ f" {status_icons.get(c['status'], '[dim]SKIP[/dim]')} {c['name']}: {c['message']}"
863
+ for c in checks
864
+ ]
865
+
866
+ summary = data.get("summary", {})
867
+ parts = [f"{summary.get('total', 0)} checks"]
868
+ if summary.get("passed"):
869
+ parts.append(f"[green]{summary['passed']} passed[/green]")
870
+ if summary.get("failed"):
871
+ parts.append(f"[red]{summary['failed']} failed[/red]")
872
+ if summary.get("warnings"):
873
+ parts.append(f"[yellow]{summary['warnings']} warnings[/yellow]")
874
+
875
+ lines.append("")
876
+ lines.append(f" Summary: {', '.join(parts)}")
877
+
878
+ console.print(Panel("\n".join(lines), title="kbagent doctor", expand=False))
879
+
880
+
881
+ def format_workspaces_table(console: Console, data: dict[str, Any]) -> None:
882
+ """Render a Rich table of workspaces grouped by project alias.
883
+
884
+ Args:
885
+ console: Rich Console instance.
886
+ data: Dict with "workspaces" and "errors" lists.
887
+ """
888
+ workspaces = data.get("workspaces", [])
889
+ errors = data.get("errors", [])
890
+
891
+ for err in errors:
892
+ console.print(
893
+ f"[bold yellow]Warning:[/bold yellow] Project [bold]{err['project_alias']}[/bold]: "
894
+ f"{err['message']}"
895
+ )
896
+
897
+ if not workspaces:
898
+ if not errors:
899
+ console.print(
900
+ "No workspaces found. Use [bold]kbagent workspace create[/bold] to create one."
901
+ )
902
+ else:
903
+ console.print("No workspaces retrieved (all projects failed).")
904
+ return
905
+
906
+ table = Table(title="Workspaces")
907
+ table.add_column("Project", style="bold magenta")
908
+ table.add_column("ID", justify="right")
909
+ table.add_column("Name", style="bold cyan")
910
+ table.add_column("Backend")
911
+ table.add_column("Schema")
912
+ # Login type + RO + QS surface the three Query-Service compatibility signals
913
+ # that were previously invisible to data-app developers (issue #304).
914
+ # Width-clamped because ``snowflake-service-keypair`` is the longest known
915
+ # value; clamp leaves room for the wider table on standard terminals.
916
+ table.add_column("Login Type", max_width=24)
917
+ table.add_column("RO", justify="center")
918
+ table.add_column("QS", justify="center")
919
+ table.add_column("Created", style="dim")
920
+
921
+ prev_alias = None
922
+ for ws in workspaces:
923
+ alias = ws.get("project_alias", "unknown")
924
+ display_alias = alias if alias != prev_alias else ""
925
+ prev_alias = alias
926
+
927
+ login_type = ws.get("login_type", "") or "[dim]?[/dim]"
928
+ ro_cell = "[green]yes[/green]" if ws.get("read_only") else "no"
929
+ if ws.get("qs_compatible") is True:
930
+ qs_cell = "[green]yes[/green]"
931
+ elif ws.get("login_type"):
932
+ # We have a loginType but it is not on the confirmed whitelist.
933
+ # Yellow rather than red: the policy varies per stack (see
934
+ # snowflake-legacy-service in constants.py) and we do not want to
935
+ # actively discourage a workspace that might in fact work.
936
+ qs_cell = "[yellow]?[/yellow]"
937
+ else:
938
+ qs_cell = "[dim]?[/dim]"
939
+
940
+ table.add_row(
941
+ display_alias,
942
+ str(ws.get("workspace_id", ws.get("id", ""))),
943
+ ws.get("name", ""),
944
+ ws.get("backend", ""),
945
+ ws.get("schema", ""),
946
+ login_type,
947
+ ro_cell,
948
+ qs_cell,
949
+ ws.get("created", ""),
950
+ )
951
+
952
+ console.print(table)
953
+ console.print(
954
+ "[dim]RO = read-only storage access. QS = Query Service compatible "
955
+ "(yes = confirmed whitelist, ? = loginType not on confirmed list, "
956
+ "may still work). See `kbagent workspace list --qs-compatible` to "
957
+ "filter to data-app-ready workspaces.[/dim]"
958
+ )
959
+ console.print()
960
+
961
+
962
+ def format_query_results(console: Console, data: dict[str, Any]) -> None:
963
+ """Render SQL query results.
964
+
965
+ Shows query status plus, per statement, a Rich table built from the
966
+ structured ``columns``/``rows`` returned by the fast inline path. Falls back
967
+ to a CSV preview when only ``csv_data`` is present (the ``--full`` export
968
+ path, which returns a CSV string without structured columns).
969
+
970
+ Args:
971
+ console: Rich Console instance.
972
+ data: Dict with query execution results.
973
+ """
974
+ alias = data.get("project_alias", "unknown")
975
+ workspace_id = data.get("workspace_id", "")
976
+ status = data.get("status", "unknown")
977
+
978
+ console.print(
979
+ Panel(
980
+ f"[bold]Project:[/bold] {alias}\n"
981
+ f"[bold]Workspace:[/bold] {workspace_id}\n"
982
+ f"[bold]Status:[/bold] {status}",
983
+ title=f"Query Results - Workspace {workspace_id}",
984
+ expand=False,
985
+ )
986
+ )
987
+
988
+ statements = data.get("statements", [])
989
+ for i, stmt in enumerate(statements):
990
+ console.print(
991
+ f"\n[bold]Statement {i + 1}:[/bold] "
992
+ f"{stmt.get('status', 'unknown')} ・ {stmt.get('rows_affected', 0)} rows"
993
+ )
994
+ _render_statement_result(console, stmt)
995
+
996
+
997
+ def _render_statement_result(console: Console, stmt: dict[str, Any]) -> None:
998
+ """Render a single statement's result set (structured table or CSV preview)."""
999
+ columns = stmt.get("columns")
1000
+ rows = stmt.get("rows")
1001
+ if columns and rows is not None:
1002
+ table = Table(show_lines=False)
1003
+ for col in columns:
1004
+ table.add_column(str(col.get("name", "")))
1005
+ for row in rows:
1006
+ table.add_row(*["" if value is None else str(value) for value in row])
1007
+ console.print(table)
1008
+ if stmt.get("truncated"):
1009
+ total = stmt.get("total_rows")
1010
+ shown = stmt.get("row_count", len(rows))
1011
+ suffix = f" of {total}" if total is not None else ""
1012
+ console.print(
1013
+ f" [dim]Showing first {shown}{suffix} rows. "
1014
+ f"Use --full for the complete result set.[/dim]"
1015
+ )
1016
+ return
1017
+
1018
+ # Fallback: --full export path returns a CSV string with no structured columns.
1019
+ csv_data = stmt.get("csv_data", "")
1020
+ if not csv_data:
1021
+ return
1022
+ csv_lines = csv_data.strip().split("\n")
1023
+ preview_count = min(len(csv_lines), 11) # header + 10 rows
1024
+ console.print(" [bold]Results:[/bold]")
1025
+ for csv_line in csv_lines[:preview_count]:
1026
+ console.print(f" {csv_line}")
1027
+ if len(csv_lines) > preview_count:
1028
+ console.print(f" ... ({len(csv_lines) - preview_count} more rows)")
1029
+
1030
+
1031
+ def format_search_results(console: Console, data: dict[str, Any]) -> None:
1032
+ """Render search results as a Rich table with match locations.
1033
+
1034
+ Args:
1035
+ console: Rich Console instance.
1036
+ data: Dict with "matches", "errors", and "stats".
1037
+ """
1038
+ matches = data.get("matches", [])
1039
+ errors = data.get("errors", [])
1040
+ stats = data.get("stats", {})
1041
+
1042
+ for err in errors:
1043
+ console.print(
1044
+ f"[bold yellow]Warning:[/bold yellow] Project [bold]{err['project_alias']}[/bold]: "
1045
+ f"{err['message']}"
1046
+ )
1047
+
1048
+ if not matches:
1049
+ console.print(
1050
+ f"No matches found. Searched {stats.get('configs_searched', 0)} "
1051
+ f"configurations across {stats.get('projects_searched', 0)} project(s)."
1052
+ )
1053
+ return
1054
+
1055
+ table = Table(title="Search Results")
1056
+ table.add_column("Project", style="bold cyan")
1057
+ table.add_column("Component", style="dim")
1058
+ table.add_column("Config ID", justify="right")
1059
+ table.add_column("Config Name")
1060
+ table.add_column("Hits", justify="right", style="bold yellow")
1061
+ table.add_column("Match Locations", style="dim", max_width=60)
1062
+
1063
+ for match in matches:
1064
+ locations = match.get("match_locations", [])
1065
+ # Show first 3 locations, truncate the rest
1066
+ display_locations = ", ".join(locations[:3])
1067
+ if len(locations) > 3:
1068
+ display_locations += f", ... (+{len(locations) - 3})"
1069
+
1070
+ table.add_row(
1071
+ match["project_alias"],
1072
+ match["component_id"],
1073
+ match["config_id"],
1074
+ match["config_name"],
1075
+ str(match.get("match_count", len(locations))),
1076
+ display_locations,
1077
+ )
1078
+
1079
+ console.print(table)
1080
+ console.print(
1081
+ f"\n[bold]{stats.get('matches_found', 0)}[/bold] matching config(s) "
1082
+ f"in {stats.get('configs_searched', 0)} searched "
1083
+ f"across {stats.get('projects_searched', 0)} project(s)."
1084
+ )