alloy-runtime-cli 0.1.0__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 (451) hide show
  1. alloy_runtime_cli-0.1.0.dist-info/METADATA +61 -0
  2. alloy_runtime_cli-0.1.0.dist-info/RECORD +451 -0
  3. alloy_runtime_cli-0.1.0.dist-info/WHEEL +5 -0
  4. alloy_runtime_cli-0.1.0.dist-info/entry_points.txt +2 -0
  5. alloy_runtime_cli-0.1.0.dist-info/top_level.txt +1 -0
  6. cli/__init__.py +0 -0
  7. cli/commands/__init__.py +0 -0
  8. cli/commands/admin/__init__.py +0 -0
  9. cli/commands/admin/bootstrap_command.py +118 -0
  10. cli/commands/admin/credentials/__init__.py +0 -0
  11. cli/commands/admin/credentials/create/__init__.py +0 -0
  12. cli/commands/admin/credentials/create/command.py +148 -0
  13. cli/commands/admin/credentials/create/presenter.py +16 -0
  14. cli/commands/admin/credentials/grant/__init__.py +0 -0
  15. cli/commands/admin/credentials/grant/command.py +119 -0
  16. cli/commands/admin/credentials/grant/fields.py +33 -0
  17. cli/commands/admin/credentials/grant/presenter.py +23 -0
  18. cli/commands/agents/__init__.py +0 -0
  19. cli/commands/agents/create/__init__.py +0 -0
  20. cli/commands/agents/create/command.py +475 -0
  21. cli/commands/agents/create/fields.py +64 -0
  22. cli/commands/agents/create/presenter.py +68 -0
  23. cli/commands/agents/delete/__init__.py +0 -0
  24. cli/commands/agents/delete/command.py +47 -0
  25. cli/commands/agents/delete/presenter.py +16 -0
  26. cli/commands/agents/get/command.py +37 -0
  27. cli/commands/agents/get/presenter.py +32 -0
  28. cli/commands/agents/list/__init__.py +1 -0
  29. cli/commands/agents/list/command.py +54 -0
  30. cli/commands/agents/list/presenter.py +82 -0
  31. cli/commands/agents/update/__init__.py +0 -0
  32. cli/commands/agents/update/command.py +435 -0
  33. cli/commands/agents/update/fields.py +40 -0
  34. cli/commands/agents/update/presenter.py +68 -0
  35. cli/commands/audio/__init__.py +0 -0
  36. cli/commands/audio/transcribe/__init__.py +0 -0
  37. cli/commands/audio/transcribe/command.py +144 -0
  38. cli/commands/audio/transcribe/presenter.py +15 -0
  39. cli/commands/auth/__init__.py +0 -0
  40. cli/commands/auth/login/__init__.py +0 -0
  41. cli/commands/auth/login/command.py +80 -0
  42. cli/commands/auth/signup/__init__.py +0 -0
  43. cli/commands/auth/signup/command.py +115 -0
  44. cli/commands/billing/__init__.py +1 -0
  45. cli/commands/billing/costs/__init__.py +1 -0
  46. cli/commands/billing/costs/by_agent/__init__.py +1 -0
  47. cli/commands/billing/costs/by_agent/command.py +57 -0
  48. cli/commands/billing/costs/by_agent/presenter.py +81 -0
  49. cli/commands/billing/costs/by_model/__init__.py +1 -0
  50. cli/commands/billing/costs/by_model/command.py +57 -0
  51. cli/commands/billing/costs/by_model/presenter.py +80 -0
  52. cli/commands/billing/costs/daily/__init__.py +1 -0
  53. cli/commands/billing/costs/daily/command.py +55 -0
  54. cli/commands/billing/costs/daily/presenter.py +75 -0
  55. cli/commands/billing/costs/summary/__init__.py +1 -0
  56. cli/commands/billing/costs/summary/command.py +57 -0
  57. cli/commands/billing/costs/summary/presenter.py +42 -0
  58. cli/commands/billing/projects/__init__.py +1 -0
  59. cli/commands/billing/projects/create/__init__.py +1 -0
  60. cli/commands/billing/projects/create/command.py +60 -0
  61. cli/commands/billing/projects/create/presenter.py +26 -0
  62. cli/commands/billing/projects/get/__init__.py +1 -0
  63. cli/commands/billing/projects/get/command.py +33 -0
  64. cli/commands/billing/projects/get/presenter.py +32 -0
  65. cli/commands/billing/projects/list/__init__.py +1 -0
  66. cli/commands/billing/projects/list/command.py +40 -0
  67. cli/commands/billing/projects/list/presenter.py +57 -0
  68. cli/commands/content/__init__.py +1 -0
  69. cli/commands/content/delete/__init__.py +0 -0
  70. cli/commands/content/delete/command.py +49 -0
  71. cli/commands/content/delete/presenter.py +18 -0
  72. cli/commands/content/edit/__init__.py +1 -0
  73. cli/commands/content/edit/command.py +155 -0
  74. cli/commands/content/edit/editor.py +150 -0
  75. cli/commands/content/edit/presenter.py +146 -0
  76. cli/commands/content/get/__init__.py +1 -0
  77. cli/commands/content/get/command.py +39 -0
  78. cli/commands/content/get/presenter.py +176 -0
  79. cli/commands/content/list/__init__.py +1 -0
  80. cli/commands/content/list/command.py +347 -0
  81. cli/commands/content/list/export_formatters.py +409 -0
  82. cli/commands/content/list/export_handler.py +165 -0
  83. cli/commands/content/list/presenter.py +190 -0
  84. cli/commands/credentials/__init__.py +0 -0
  85. cli/commands/credentials/create/__init__.py +0 -0
  86. cli/commands/credentials/create/command.py +165 -0
  87. cli/commands/credentials/create/fields.py +38 -0
  88. cli/commands/credentials/create/presenter.py +20 -0
  89. cli/commands/credentials/update/__init__.py +0 -0
  90. cli/commands/credentials/update/command.py +53 -0
  91. cli/commands/credentials/update/fields.py +71 -0
  92. cli/commands/credentials/update/presenter.py +16 -0
  93. cli/commands/flag_utils.py +366 -0
  94. cli/commands/generate/__init__.py +0 -0
  95. cli/commands/generate/cancel/__init__.py +1 -0
  96. cli/commands/generate/cancel/command.py +44 -0
  97. cli/commands/generate/cancel/presenter.py +26 -0
  98. cli/commands/generate/status/__init__.py +1 -0
  99. cli/commands/generate/status/command.py +58 -0
  100. cli/commands/generate/status/presenter.py +78 -0
  101. cli/commands/generate/text/__init__.py +0 -0
  102. cli/commands/generate/text/command.py +1325 -0
  103. cli/commands/generate/text/concurrent_renderer.py +355 -0
  104. cli/commands/generate/text/presenter.py +287 -0
  105. cli/commands/generate/text/stream_renderer.py +129 -0
  106. cli/commands/knowledge/__init__.py +0 -0
  107. cli/commands/knowledge/collections/__init__.py +0 -0
  108. cli/commands/knowledge/collections/cluster/__init__.py +0 -0
  109. cli/commands/knowledge/collections/cluster/command.py +64 -0
  110. cli/commands/knowledge/collections/cluster/presenter.py +74 -0
  111. cli/commands/knowledge/collections/cluster_status/__init__.py +0 -0
  112. cli/commands/knowledge/collections/cluster_status/command.py +46 -0
  113. cli/commands/knowledge/collections/cluster_status/presenter.py +10 -0
  114. cli/commands/knowledge/collections/create/__init__.py +0 -0
  115. cli/commands/knowledge/collections/create/command.py +137 -0
  116. cli/commands/knowledge/collections/create/presenter.py +38 -0
  117. cli/commands/knowledge/collections/delete/__init__.py +1 -0
  118. cli/commands/knowledge/collections/delete/command.py +47 -0
  119. cli/commands/knowledge/collections/delete/presenter.py +20 -0
  120. cli/commands/knowledge/collections/get/__init__.py +1 -0
  121. cli/commands/knowledge/collections/get/command.py +30 -0
  122. cli/commands/knowledge/collections/get/presenter.py +44 -0
  123. cli/commands/knowledge/collections/list/__init__.py +1 -0
  124. cli/commands/knowledge/collections/list/command.py +41 -0
  125. cli/commands/knowledge/collections/list/presenter.py +68 -0
  126. cli/commands/knowledge/collections/update/__init__.py +0 -0
  127. cli/commands/knowledge/collections/update/command.py +97 -0
  128. cli/commands/knowledge/collections/update/presenter.py +42 -0
  129. cli/commands/knowledge/documents/__init__.py +0 -0
  130. cli/commands/knowledge/documents/bulk_metadata/__init__.py +0 -0
  131. cli/commands/knowledge/documents/bulk_metadata/command.py +119 -0
  132. cli/commands/knowledge/documents/bulk_metadata/presenter.py +36 -0
  133. cli/commands/knowledge/documents/delete/__init__.py +0 -0
  134. cli/commands/knowledge/documents/delete/command.py +47 -0
  135. cli/commands/knowledge/documents/delete/presenter.py +20 -0
  136. cli/commands/knowledge/documents/get/__init__.py +0 -0
  137. cli/commands/knowledge/documents/get/command.py +39 -0
  138. cli/commands/knowledge/documents/get/presenter.py +78 -0
  139. cli/commands/knowledge/documents/ingest/__init__.py +0 -0
  140. cli/commands/knowledge/documents/ingest/command.py +222 -0
  141. cli/commands/knowledge/documents/ingest/presenter.py +41 -0
  142. cli/commands/knowledge/documents/list/__init__.py +0 -0
  143. cli/commands/knowledge/documents/list/command.py +69 -0
  144. cli/commands/knowledge/documents/list/presenter.py +86 -0
  145. cli/commands/knowledge/documents/reingest/__init__.py +0 -0
  146. cli/commands/knowledge/documents/reingest/command.py +102 -0
  147. cli/commands/knowledge/documents/reingest/presenter.py +70 -0
  148. cli/commands/knowledge/documents/update/__init__.py +0 -0
  149. cli/commands/knowledge/documents/update/command.py +85 -0
  150. cli/commands/knowledge/documents/update/presenter.py +37 -0
  151. cli/commands/knowledge/recover/__init__.py +0 -0
  152. cli/commands/knowledge/recover/command.py +46 -0
  153. cli/commands/knowledge/recover/presenter.py +79 -0
  154. cli/commands/knowledge/search/__init__.py +0 -0
  155. cli/commands/knowledge/search/command.py +218 -0
  156. cli/commands/knowledge/search/presenter.py +111 -0
  157. cli/commands/knowledge/synthesis/__init__.py +0 -0
  158. cli/commands/knowledge/synthesis/create/__init__.py +0 -0
  159. cli/commands/knowledge/synthesis/create/command.py +127 -0
  160. cli/commands/knowledge/synthesis/create/presenter.py +33 -0
  161. cli/commands/knowledge/synthesis/delete/__init__.py +0 -0
  162. cli/commands/knowledge/synthesis/delete/command.py +53 -0
  163. cli/commands/knowledge/synthesis/delete/presenter.py +31 -0
  164. cli/commands/knowledge/synthesis/get/__init__.py +0 -0
  165. cli/commands/knowledge/synthesis/get/command.py +55 -0
  166. cli/commands/knowledge/synthesis/get/presenter.py +114 -0
  167. cli/commands/knowledge/synthesis/list/__init__.py +0 -0
  168. cli/commands/knowledge/synthesis/list/command.py +132 -0
  169. cli/commands/knowledge/synthesis/list/presenter.py +84 -0
  170. cli/commands/knowledge/synthesis/refresh/__init__.py +0 -0
  171. cli/commands/knowledge/synthesis/refresh/command.py +42 -0
  172. cli/commands/knowledge/synthesis/refresh/presenter.py +33 -0
  173. cli/commands/knowledge/synthesis/update/__init__.py +0 -0
  174. cli/commands/knowledge/synthesis/update/command.py +76 -0
  175. cli/commands/knowledge/synthesis/update/presenter.py +41 -0
  176. cli/commands/models/__init__.py +0 -0
  177. cli/commands/models/list/__init__.py +0 -0
  178. cli/commands/models/list/command.py +84 -0
  179. cli/commands/models/list/presenter.py +114 -0
  180. cli/commands/organizations/__init__.py +0 -0
  181. cli/commands/organizations/create/command.py +32 -0
  182. cli/commands/organizations/create/presenter.py +9 -0
  183. cli/commands/pipelines/__init__.py +1 -0
  184. cli/commands/pipelines/approvals/__init__.py +1 -0
  185. cli/commands/pipelines/approvals/decide_command.py +77 -0
  186. cli/commands/pipelines/approvals/get_command.py +44 -0
  187. cli/commands/pipelines/approvals/presenter.py +56 -0
  188. cli/commands/pipelines/costs/__init__.py +1 -0
  189. cli/commands/pipelines/costs/command.py +57 -0
  190. cli/commands/pipelines/costs/daily_command.py +54 -0
  191. cli/commands/pipelines/costs/daily_presenter.py +59 -0
  192. cli/commands/pipelines/costs/presenter.py +37 -0
  193. cli/commands/pipelines/create/__init__.py +1 -0
  194. cli/commands/pipelines/create/command.py +103 -0
  195. cli/commands/pipelines/create/presenter.py +22 -0
  196. cli/commands/pipelines/env_vars/__init__.py +1 -0
  197. cli/commands/pipelines/env_vars/command.py +51 -0
  198. cli/commands/pipelines/env_vars/presenter.py +16 -0
  199. cli/commands/pipelines/execute/__init__.py +1 -0
  200. cli/commands/pipelines/execute/command.py +142 -0
  201. cli/commands/pipelines/execute/presenter.py +47 -0
  202. cli/commands/pipelines/executions/__init__.py +1 -0
  203. cli/commands/pipelines/executions/costs/__init__.py +1 -0
  204. cli/commands/pipelines/executions/costs/command.py +48 -0
  205. cli/commands/pipelines/executions/costs/presenter.py +29 -0
  206. cli/commands/pipelines/executions/costs_by_model/__init__.py +1 -0
  207. cli/commands/pipelines/executions/costs_by_model/command.py +50 -0
  208. cli/commands/pipelines/executions/costs_by_model/presenter.py +78 -0
  209. cli/commands/pipelines/executions/costs_by_step/__init__.py +1 -0
  210. cli/commands/pipelines/executions/costs_by_step/command.py +50 -0
  211. cli/commands/pipelines/executions/costs_by_step/presenter.py +72 -0
  212. cli/commands/pipelines/executions/get_command.py +38 -0
  213. cli/commands/pipelines/executions/list_command.py +123 -0
  214. cli/commands/pipelines/executions/presenter.py +131 -0
  215. cli/commands/pipelines/executions/rerun_command.py +41 -0
  216. cli/commands/pipelines/executions/update/__init__.py +1 -0
  217. cli/commands/pipelines/executions/update/command.py +110 -0
  218. cli/commands/pipelines/executions/update/presenter.py +28 -0
  219. cli/commands/pipelines/get/__init__.py +1 -0
  220. cli/commands/pipelines/get/command.py +33 -0
  221. cli/commands/pipelines/get/presenter.py +48 -0
  222. cli/commands/pipelines/list/__init__.py +1 -0
  223. cli/commands/pipelines/list/command.py +53 -0
  224. cli/commands/pipelines/list/presenter.py +66 -0
  225. cli/commands/pipelines/schedules/__init__.py +1 -0
  226. cli/commands/pipelines/schedules/create_command.py +119 -0
  227. cli/commands/pipelines/schedules/create_presenter.py +35 -0
  228. cli/commands/pipelines/schedules/delete_command.py +52 -0
  229. cli/commands/pipelines/schedules/env_vars_command.py +59 -0
  230. cli/commands/pipelines/schedules/env_vars_presenter.py +16 -0
  231. cli/commands/pipelines/schedules/get_command.py +38 -0
  232. cli/commands/pipelines/schedules/list_command.py +33 -0
  233. cli/commands/pipelines/schedules/once_command.py +90 -0
  234. cli/commands/pipelines/schedules/once_presenter.py +30 -0
  235. cli/commands/pipelines/schedules/presenter.py +104 -0
  236. cli/commands/pipelines/schedules/update_command.py +139 -0
  237. cli/commands/pipelines/schedules/update_presenter.py +29 -0
  238. cli/commands/render/__init__.py +0 -0
  239. cli/commands/render/html_to_image/__init__.py +0 -0
  240. cli/commands/render/html_to_image/command.py +170 -0
  241. cli/commands/schemas/__init__.py +0 -0
  242. cli/commands/schemas/create/__init__.py +0 -0
  243. cli/commands/schemas/create/command.py +122 -0
  244. cli/commands/schemas/create/presenter.py +53 -0
  245. cli/commands/schemas/delete/command.py +45 -0
  246. cli/commands/schemas/delete/presenter.py +9 -0
  247. cli/commands/schemas/get/__init__.py +0 -0
  248. cli/commands/schemas/get/command.py +56 -0
  249. cli/commands/schemas/get/presenter.py +129 -0
  250. cli/commands/schemas/list/__init__.py +0 -0
  251. cli/commands/schemas/list/command.py +64 -0
  252. cli/commands/schemas/list/presenter.py +133 -0
  253. cli/commands/schemas/update/__init__.py +0 -0
  254. cli/commands/schemas/update/command.py +369 -0
  255. cli/commands/schemas/update/presenter.py +53 -0
  256. cli/commands/sessions/__init__.py +1 -0
  257. cli/commands/sessions/delete/__init__.py +1 -0
  258. cli/commands/sessions/delete/command.py +47 -0
  259. cli/commands/sessions/delete/presenter.py +10 -0
  260. cli/commands/sessions/get/__init__.py +1 -0
  261. cli/commands/sessions/get/command.py +42 -0
  262. cli/commands/sessions/get/presenter.py +59 -0
  263. cli/commands/sessions/list/__init__.py +1 -0
  264. cli/commands/sessions/list/command.py +61 -0
  265. cli/commands/sessions/list/presenter.py +68 -0
  266. cli/commands/sessions/messages/__init__.py +1 -0
  267. cli/commands/sessions/messages/command.py +78 -0
  268. cli/commands/sessions/messages/presenter.py +79 -0
  269. cli/commands/shared_flags.py +500 -0
  270. cli/commands/sync/__init__.py +0 -0
  271. cli/commands/sync/command.py +45 -0
  272. cli/commands/sync/presenter.py +49 -0
  273. cli/commands/tags/__init__.py +1 -0
  274. cli/commands/tags/create/__init__.py +1 -0
  275. cli/commands/tags/create/command.py +60 -0
  276. cli/commands/tags/delete/__init__.py +1 -0
  277. cli/commands/tags/delete/command.py +47 -0
  278. cli/commands/tags/delete/presenter.py +10 -0
  279. cli/commands/tags/get/command.py +31 -0
  280. cli/commands/tags/get/presenter.py +23 -0
  281. cli/commands/tags/list/__init__.py +1 -0
  282. cli/commands/tags/list/command.py +52 -0
  283. cli/commands/tags/list/presenter.py +49 -0
  284. cli/commands/tags/update/command.py +64 -0
  285. cli/commands/tags/update/presenter.py +9 -0
  286. cli/commands/templates/__init__.py +0 -0
  287. cli/commands/templates/create/__init__.py +0 -0
  288. cli/commands/templates/create/command.py +152 -0
  289. cli/commands/templates/create/presenter.py +86 -0
  290. cli/commands/templates/delete/__init__.py +0 -0
  291. cli/commands/templates/delete/command.py +47 -0
  292. cli/commands/templates/delete/presenter.py +16 -0
  293. cli/commands/templates/get/__init__.py +0 -0
  294. cli/commands/templates/get/command.py +52 -0
  295. cli/commands/templates/get/presenter.py +233 -0
  296. cli/commands/templates/get_by_version/command.py +32 -0
  297. cli/commands/templates/get_by_version/presenter.py +30 -0
  298. cli/commands/templates/list/__init__.py +1 -0
  299. cli/commands/templates/list/command.py +102 -0
  300. cli/commands/templates/list/presenter.py +93 -0
  301. cli/commands/templates/render/__init__.py +0 -0
  302. cli/commands/templates/render/command.py +115 -0
  303. cli/commands/templates/render/presenter.py +276 -0
  304. cli/commands/templates/update/__init__.py +0 -0
  305. cli/commands/templates/update/command.py +199 -0
  306. cli/commands/templates/update/presenter.py +94 -0
  307. cli/commands/templates/version/__init__.py +1 -0
  308. cli/commands/templates/version/command.py +116 -0
  309. cli/commands/templates/version/presenter.py +100 -0
  310. cli/commands/tool_configs/__init__.py +0 -0
  311. cli/commands/tool_configs/create/__init__.py +0 -0
  312. cli/commands/tool_configs/create/command.py +118 -0
  313. cli/commands/tool_configs/create/presenter.py +53 -0
  314. cli/commands/tool_configs/delete/__init__.py +0 -0
  315. cli/commands/tool_configs/delete/command.py +47 -0
  316. cli/commands/tool_configs/delete/presenter.py +18 -0
  317. cli/commands/tool_configs/get/__init__.py +0 -0
  318. cli/commands/tool_configs/get/command.py +31 -0
  319. cli/commands/tool_configs/get/presenter.py +62 -0
  320. cli/commands/tool_configs/list/__init__.py +0 -0
  321. cli/commands/tool_configs/list/command.py +59 -0
  322. cli/commands/tool_configs/list/presenter.py +60 -0
  323. cli/commands/tool_configs/update/__init__.py +0 -0
  324. cli/commands/tool_configs/update/command.py +128 -0
  325. cli/commands/tool_configs/update/presenter.py +53 -0
  326. cli/commands/tools/__init__.py +1 -0
  327. cli/commands/tools/get/__init__.py +1 -0
  328. cli/commands/tools/get/command.py +42 -0
  329. cli/commands/tools/get/presenter.py +45 -0
  330. cli/commands/tools/list/__init__.py +1 -0
  331. cli/commands/tools/list/command.py +56 -0
  332. cli/commands/tools/list/presenter.py +44 -0
  333. cli/commands/users/__init__.py +0 -0
  334. cli/commands/users/create/command.py +53 -0
  335. cli/commands/users/create/presenter.py +9 -0
  336. cli/commands/whoami/__init__.py +0 -0
  337. cli/commands/whoami/command.py +42 -0
  338. cli/infrastructure/__init__.py +0 -0
  339. cli/infrastructure/auth_storage.py +71 -0
  340. cli/infrastructure/client_factory.py +36 -0
  341. cli/infrastructure/command.py +75 -0
  342. cli/infrastructure/config.py +188 -0
  343. cli/infrastructure/console.py +27 -0
  344. cli/infrastructure/editor.py +138 -0
  345. cli/infrastructure/error_display.py +178 -0
  346. cli/infrastructure/field_extractor.py +360 -0
  347. cli/infrastructure/file_content.py +210 -0
  348. cli/infrastructure/filter_parser.py +256 -0
  349. cli/infrastructure/formatters/__init__.py +0 -0
  350. cli/infrastructure/formatters/base.py +99 -0
  351. cli/infrastructure/formatters/compact_formatter.py +245 -0
  352. cli/infrastructure/formatters/json_formatter.py +84 -0
  353. cli/infrastructure/formatters/lines_formatter.py +102 -0
  354. cli/infrastructure/formatting/__init__.py +0 -0
  355. cli/infrastructure/formatting/fields.py +193 -0
  356. cli/infrastructure/forms/__init__.py +0 -0
  357. cli/infrastructure/forms/agent_picker.py +123 -0
  358. cli/infrastructure/forms/agent_tool_editor.py +384 -0
  359. cli/infrastructure/forms/agent_tools_manager.py +212 -0
  360. cli/infrastructure/forms/base_picker.py +469 -0
  361. cli/infrastructure/forms/components.py +126 -0
  362. cli/infrastructure/forms/json_schema_builder.py +149 -0
  363. cli/infrastructure/forms/model_picker.py +134 -0
  364. cli/infrastructure/forms/parsers.py +173 -0
  365. cli/infrastructure/forms/resolution_modal.py +302 -0
  366. cli/infrastructure/forms/schema_picker.py +137 -0
  367. cli/infrastructure/forms/tag_management_modal.py +103 -0
  368. cli/infrastructure/forms/tag_picker.py +207 -0
  369. cli/infrastructure/forms/template_picker.py +131 -0
  370. cli/infrastructure/forms/tool_config_picker.py +130 -0
  371. cli/infrastructure/forms/tool_picker.py +103 -0
  372. cli/infrastructure/injection/__init__.py +0 -0
  373. cli/infrastructure/injection/parser.py +302 -0
  374. cli/infrastructure/injection/resolver.py +399 -0
  375. cli/infrastructure/kv_parser.py +130 -0
  376. cli/infrastructure/local_storage.py +227 -0
  377. cli/infrastructure/macro_parser.py +215 -0
  378. cli/infrastructure/output.py +192 -0
  379. cli/infrastructure/provider_setup.py +81 -0
  380. cli/infrastructure/renderers/__init__.py +0 -0
  381. cli/infrastructure/renderers/entity_renderer.py +77 -0
  382. cli/infrastructure/renderers/list_renderer.py +114 -0
  383. cli/infrastructure/scope_utils.py +47 -0
  384. cli/infrastructure/spinner.py +101 -0
  385. cli/infrastructure/tui/__init__.py +0 -0
  386. cli/infrastructure/tui/clipboard.py +41 -0
  387. cli/infrastructure/tui/formatters.py +105 -0
  388. cli/infrastructure/tui/preview.py +14 -0
  389. cli/infrastructure/tui/selectable.py +198 -0
  390. cli/infrastructure/validation/__init__.py +0 -0
  391. cli/infrastructure/validation/tag_validation.py +74 -0
  392. cli/main.py +759 -0
  393. cli/tui/__init__.py +0 -0
  394. cli/tui/app.py +199 -0
  395. cli/tui/app_store.py +73 -0
  396. cli/tui/chat/__init__.py +0 -0
  397. cli/tui/chat/commands/__init__.py +0 -0
  398. cli/tui/chat/commands/base.py +65 -0
  399. cli/tui/chat/commands/create_session.py +135 -0
  400. cli/tui/chat/commands/load_session.py +119 -0
  401. cli/tui/chat/commands/regenerate.py +120 -0
  402. cli/tui/chat/commands/reload_session.py +63 -0
  403. cli/tui/chat/commands/send_message.py +190 -0
  404. cli/tui/chat/commands/undo.py +66 -0
  405. cli/tui/chat/editor.py +71 -0
  406. cli/tui/chat/messages.py +223 -0
  407. cli/tui/chat/pane.py +141 -0
  408. cli/tui/chat/renderers/__init__.py +0 -0
  409. cli/tui/chat/renderers/base.py +72 -0
  410. cli/tui/chat/renderers/markdown.py +250 -0
  411. cli/tui/chat/renderers/plain.py +83 -0
  412. cli/tui/chat/screen.py +1155 -0
  413. cli/tui/chat/services/__init__.py +0 -0
  414. cli/tui/chat/services/injection.py +386 -0
  415. cli/tui/chat/services/name_generator.py +256 -0
  416. cli/tui/chat/slash_commands.py +424 -0
  417. cli/tui/chat/store.py +280 -0
  418. cli/tui/chat/types.py +220 -0
  419. cli/tui/chat/widgets/__init__.py +0 -0
  420. cli/tui/chat/widgets/chat_header.py +75 -0
  421. cli/tui/chat/widgets/chat_input.py +362 -0
  422. cli/tui/chat/widgets/injection_popup.py +161 -0
  423. cli/tui/chat/widgets/message_display.py +287 -0
  424. cli/tui/chat/widgets/session_sidebar.py +214 -0
  425. cli/tui/chat/widgets/welcome_screen.py +290 -0
  426. cli/tui/screens/__init__.py +0 -0
  427. cli/tui/screens/agents.py +344 -0
  428. cli/tui/screens/base.py +301 -0
  429. cli/tui/screens/content.py +508 -0
  430. cli/tui/screens/dashboard.py +89 -0
  431. cli/tui/screens/models.py +96 -0
  432. cli/tui/screens/nav_screen.py +186 -0
  433. cli/tui/screens/schemas.py +522 -0
  434. cli/tui/screens/templates.py +734 -0
  435. cli/tui/screens/tool_configs.py +335 -0
  436. cli/tui/styles/__init__.py +0 -0
  437. cli/tui/widgets/__init__.py +0 -0
  438. cli/tui/widgets/agent_create_modal.py +139 -0
  439. cli/tui/widgets/agent_form_modal.py +659 -0
  440. cli/tui/widgets/agent_update_modal.py +299 -0
  441. cli/tui/widgets/base_form_modal.py +77 -0
  442. cli/tui/widgets/confirm_modal.py +75 -0
  443. cli/tui/widgets/help_modal.py +145 -0
  444. cli/tui/widgets/new_session_modal.py +328 -0
  445. cli/tui/widgets/schema_create_modal.py +271 -0
  446. cli/tui/widgets/schema_update_modal.py +188 -0
  447. cli/tui/widgets/status_footer.py +147 -0
  448. cli/tui/widgets/template_create_modal.py +502 -0
  449. cli/tui/widgets/template_update_modal.py +308 -0
  450. cli/tui/widgets/tool_config_create_modal.py +216 -0
  451. cli/tui/widgets/tool_config_update_modal.py +208 -0
@@ -0,0 +1,155 @@
1
+ """Content edit command implementation."""
2
+
3
+ from pathlib import Path
4
+ from typing import Any
5
+ from uuid import UUID
6
+
7
+ import typer
8
+
9
+ from cli.commands.content.edit.editor import (
10
+ ContentValidationError,
11
+ format_content_for_editor,
12
+ open_editor_for_content,
13
+ parse_edited_content,
14
+ )
15
+ from cli.infrastructure.editor import EditorError
16
+ from cli.commands.content.edit.presenter import present_content_edit_success
17
+ from cli.commands.flag_utils import parse_tags, require_exclusive, validate_uuid
18
+ from cli.infrastructure.command import async_command, authenticated_client
19
+ from cli.infrastructure.error_display import display_error
20
+ from cli.infrastructure.output import OutputService
21
+ from alloy_runtime_types.dtos.content import UpdateContentPartRequest
22
+
23
+
24
+ def content_edit_command(
25
+ content_part_id: str = typer.Argument(
26
+ ...,
27
+ help="Content part UUID",
28
+ ),
29
+ tags: str | None = typer.Option(
30
+ None,
31
+ "-t",
32
+ "--tags",
33
+ help="Replace tags (comma-separated)",
34
+ ),
35
+ no_edit: bool = typer.Option(
36
+ False,
37
+ "--no-edit",
38
+ help="Skip editor, only update tags",
39
+ ),
40
+ content: str | None = typer.Option(
41
+ None,
42
+ "-c",
43
+ "--content",
44
+ help="Inline content replacement",
45
+ ),
46
+ content_file: Path | None = typer.Option(
47
+ None,
48
+ "-f",
49
+ "--file",
50
+ help="Read replacement from file",
51
+ exists=True,
52
+ readable=True,
53
+ ),
54
+ ) -> None:
55
+ """Edit a content part.
56
+
57
+ By default opens $EDITOR. Use --content or --file to bypass.
58
+
59
+ Examples:
60
+ ai content edit 019405e0-...
61
+ ai content edit <id> -t "email.final,reviewed"
62
+ ai content edit <id> --no-edit -t "archived"
63
+ ai content edit <id> -c "New content"
64
+ ai content edit <id> -f output.txt
65
+ """
66
+ content_uuid = validate_uuid(content_part_id, "content")
67
+
68
+ # Validate exclusive content modes
69
+ require_exclusive(
70
+ ("--no-edit", no_edit),
71
+ ("--content", content),
72
+ ("--file", content_file),
73
+ )
74
+
75
+ # Parse tags
76
+ tag_paths: list[str] | None = None
77
+ if tags is not None:
78
+ tag_paths = parse_tags(tags)
79
+
80
+ # Validate at least one operation
81
+ if no_edit and tag_paths is None:
82
+ raise typer.BadParameter("--tags required with --no-edit")
83
+
84
+ _execute_edit(
85
+ content_part_id=content_uuid,
86
+ tag_paths=tag_paths,
87
+ no_edit=no_edit,
88
+ inline_content=content,
89
+ content_file=content_file,
90
+ )
91
+
92
+
93
+ @async_command
94
+ async def _execute_edit(
95
+ content_part_id: UUID,
96
+ tag_paths: list[str] | None,
97
+ no_edit: bool,
98
+ inline_content: str | None,
99
+ content_file: Path | None,
100
+ ) -> None:
101
+ """Execute edit content part operation."""
102
+ async with authenticated_client() as (_config, client):
103
+ current = await client.get_content_part(content_part_id)
104
+
105
+ new_content_text: str | None = None
106
+ new_content_structured: dict[str, Any] | None = None
107
+ updated_content = False
108
+
109
+ if not no_edit:
110
+ try:
111
+ if inline_content is not None:
112
+ edited_content = inline_content
113
+ is_structured = current.content_structured is not None
114
+ elif content_file is not None:
115
+ edited_content = content_file.read_text()
116
+ is_structured = current.content_structured is not None
117
+ else:
118
+ initial_content, is_structured = format_content_for_editor(
119
+ current.content_text,
120
+ current.content_structured,
121
+ )
122
+ edited_content = open_editor_for_content(
123
+ initial_content, is_structured
124
+ )
125
+
126
+ if edited_content is None:
127
+ output = OutputService.get()
128
+ output.warning("Editor cancelled")
129
+ raise typer.Exit(code=1)
130
+
131
+ new_content_text, new_content_structured = parse_edited_content(
132
+ edited_content, is_structured
133
+ )
134
+ updated_content = True
135
+
136
+ except EditorError as e:
137
+ display_error(e)
138
+ raise typer.Exit(code=1)
139
+ except ContentValidationError as e:
140
+ display_error(e)
141
+ raise typer.Exit(code=1)
142
+
143
+ request = UpdateContentPartRequest(
144
+ content_text=new_content_text,
145
+ content_structured=new_content_structured,
146
+ tag_paths=tag_paths,
147
+ )
148
+
149
+ response = await client.update_content_part(content_part_id, request)
150
+
151
+ present_content_edit_success(
152
+ response,
153
+ updated_content=updated_content,
154
+ updated_tags=(tag_paths is not None),
155
+ )
@@ -0,0 +1,150 @@
1
+ """External editor integration for content part editing.
2
+
3
+ Opens the user's preferred $EDITOR to edit content,
4
+ with content-type-aware file extensions for syntax highlighting.
5
+ """
6
+
7
+ import json
8
+ import subprocess
9
+ import tempfile
10
+ from pathlib import Path
11
+ from typing import Any, cast
12
+
13
+ from cli.infrastructure.editor import get_editor
14
+
15
+
16
+ class ContentValidationError(Exception):
17
+ """Raised when edited content fails validation."""
18
+
19
+
20
+ __all__ = [
21
+ "ContentValidationError",
22
+ "open_editor_for_content",
23
+ "format_content_for_editor",
24
+ "parse_edited_content",
25
+ ]
26
+
27
+
28
+ def open_editor_for_content(
29
+ initial_content: str,
30
+ is_structured: bool,
31
+ ) -> str | None:
32
+ """Open the user's editor with content, using appropriate file extension.
33
+
34
+ Creates a temporary file with the right extension for syntax highlighting:
35
+ - .json for structured content
36
+ - .txt for text content
37
+
38
+ Args:
39
+ initial_content: Initial content to populate the file with
40
+ is_structured: Whether the content is JSON (structured) or plain text
41
+
42
+ Returns:
43
+ The edited content from the file, or None if the editor failed
44
+ or the user quit without saving.
45
+
46
+ Raises:
47
+ EditorError: If no editor is available
48
+ ContentValidationError: If JSON content is malformed after editing
49
+ """
50
+ editor = get_editor()
51
+
52
+ # Choose extension based on content type
53
+ suffix = ".json" if is_structured else ".txt"
54
+
55
+ # Create temp file with appropriate extension
56
+ with tempfile.NamedTemporaryFile(
57
+ mode="w",
58
+ suffix=suffix,
59
+ delete=False,
60
+ ) as f:
61
+ f.write(initial_content)
62
+ temp_path = Path(f.name)
63
+
64
+ try:
65
+ # Get file modification time before editing
66
+ mtime_before = temp_path.stat().st_mtime
67
+
68
+ # Open editor and wait for it to close
69
+ editor_parts = editor.split()
70
+ cmd = editor_parts + [str(temp_path)]
71
+
72
+ result = subprocess.call(cmd)
73
+
74
+ if result != 0:
75
+ return None
76
+
77
+ # Check if file was modified
78
+ mtime_after = temp_path.stat().st_mtime
79
+ if mtime_after == mtime_before:
80
+ # File wasn't modified - still return content in case
81
+ # user just didn't change anything
82
+ pass
83
+
84
+ # Read the edited content
85
+ content = temp_path.read_text()
86
+
87
+ # Validate JSON if structured content
88
+ if is_structured:
89
+ try:
90
+ json.loads(content)
91
+ except json.JSONDecodeError as e:
92
+ raise ContentValidationError(
93
+ f"Invalid JSON after editing: {e.msg} at line {e.lineno}"
94
+ )
95
+
96
+ return content
97
+
98
+ finally:
99
+ # Clean up temp file
100
+ try:
101
+ temp_path.unlink()
102
+ except OSError:
103
+ pass # Best effort cleanup
104
+
105
+
106
+ def format_content_for_editor(
107
+ content_text: str | None, content_structured: dict[str, Any] | None
108
+ ) -> tuple[str, bool]:
109
+ """Format content for editing in $EDITOR.
110
+
111
+ Args:
112
+ content_text: Plain text content (if text type)
113
+ content_structured: Structured JSON content (if structured type)
114
+
115
+ Returns:
116
+ Tuple of (formatted_content, is_structured)
117
+ """
118
+ if content_structured is not None:
119
+ # Format JSON with indentation for readability
120
+ return json.dumps(content_structured, indent=2, ensure_ascii=False), True
121
+ elif content_text is not None:
122
+ return content_text, False
123
+ else:
124
+ return "", False
125
+
126
+
127
+ def parse_edited_content(
128
+ edited_content: str,
129
+ is_structured: bool,
130
+ ) -> tuple[str | None, dict[str, Any] | None]:
131
+ """Parse edited content back to appropriate type.
132
+
133
+ Args:
134
+ edited_content: Content string from editor
135
+ is_structured: Whether content should be parsed as JSON
136
+
137
+ Returns:
138
+ Tuple of (content_text, content_structured) with appropriate field set
139
+
140
+ Raises:
141
+ ContentValidationError: If JSON parsing fails for structured content
142
+ """
143
+ if is_structured:
144
+ try:
145
+ parsed = json.loads(edited_content)
146
+ return None, cast(dict[str, Any], parsed)
147
+ except json.JSONDecodeError as e:
148
+ raise ContentValidationError(f"Invalid JSON: {e.msg} at line {e.lineno}")
149
+ else:
150
+ return edited_content, None
@@ -0,0 +1,146 @@
1
+ """Presenter for content edit command output."""
2
+
3
+ import json
4
+ import sys
5
+
6
+ from rich.console import Console
7
+ from rich.panel import Panel
8
+ from rich.table import Table
9
+
10
+ from cli.infrastructure.formatting.fields import (
11
+ format_cost,
12
+ format_datetime,
13
+ format_optional,
14
+ format_tokens,
15
+ format_uuid,
16
+ )
17
+ from alloy_runtime_types.dtos.content import UpdateContentPartResponse
18
+ from alloy_runtime_types.dtos.templates import TagResponse
19
+
20
+
21
+ def present_content_edit_success(
22
+ response: UpdateContentPartResponse, updated_content: bool, updated_tags: bool
23
+ ) -> None:
24
+ """Present content edit success with piping support.
25
+
26
+ Metadata goes to stderr (visible in terminal but not piped).
27
+ Content goes to stdout (for piping to other commands).
28
+
29
+ Args:
30
+ response: Updated content part response from server
31
+ updated_content: Whether content was updated
32
+ updated_tags: Whether tags were updated
33
+ """
34
+ # Create console for stderr output (metadata)
35
+ stderr_console = Console(file=sys.stderr, force_terminal=True)
36
+
37
+ # Build update summary
38
+ updates: list[str] = []
39
+ if updated_content:
40
+ updates.append("content")
41
+ if updated_tags:
42
+ updates.append("tags")
43
+ update_summary = " and ".join(updates) if updates else "no changes"
44
+
45
+ # Print success message to stderr
46
+ stderr_console.print(
47
+ f"[green]✓[/green] Content part updated ({update_summary}): {format_uuid(response.id, short=True)}"
48
+ )
49
+
50
+ # Build and display metadata table to stderr
51
+ _display_metadata(response, stderr_console)
52
+
53
+ # Print content to stdout for piping
54
+ _display_content(response)
55
+
56
+
57
+ def _display_metadata(response: UpdateContentPartResponse, console: Console) -> None:
58
+ """Display content part metadata to stderr.
59
+
60
+ Args:
61
+ response: Content part response
62
+ console: Console for stderr output
63
+ """
64
+ meta_table = Table(show_header=False, box=None, padding=(0, 1))
65
+ meta_table.add_column("Field", style="dim", width=20)
66
+ meta_table.add_column("Value")
67
+
68
+ # Basic info
69
+ meta_table.add_row("ID", str(response.id))
70
+ meta_table.add_row("Message ID", format_uuid(response.message_id, short=True))
71
+ meta_table.add_row("Part Index", str(response.part_index))
72
+ meta_table.add_row("Content Type", response.content_type_id)
73
+ meta_table.add_row(
74
+ "Storage Type",
75
+ "structured" if response.content_structured else "text",
76
+ )
77
+ meta_table.add_row("Created", format_datetime(response.created_at))
78
+
79
+ # Tags
80
+ meta_table.add_row("Tags", _format_tags(response.tags))
81
+
82
+ # Execution metadata
83
+ exec_meta = response.execution_metadata
84
+ meta_table.add_row("", "") # Separator
85
+ meta_table.add_row("[bold]Execution Info[/bold]", "")
86
+ meta_table.add_row(
87
+ "Execution ID",
88
+ format_uuid(exec_meta.agent_execution_id, short=True),
89
+ )
90
+ meta_table.add_row(
91
+ "Agent",
92
+ format_optional(exec_meta.agent_name, placeholder="(direct model call)"),
93
+ )
94
+ meta_table.add_row("Provider", exec_meta.provider_key)
95
+ meta_table.add_row("Model", exec_meta.provider_model_name)
96
+ meta_table.add_row(
97
+ "Cost",
98
+ format_optional(exec_meta.total_cost_usd, formatter=format_cost),
99
+ )
100
+ meta_table.add_row(
101
+ "Tokens",
102
+ format_optional(exec_meta.total_tokens, formatter=format_tokens),
103
+ )
104
+ meta_table.add_row("Queued At", format_datetime(exec_meta.queued_at))
105
+
106
+ meta_panel = Panel(meta_table, title="Content Part Updated", border_style="green")
107
+ console.print(meta_panel)
108
+
109
+
110
+ def _format_tags(tags: list[TagResponse]) -> str:
111
+ """Format tags list for display.
112
+
113
+ Args:
114
+ tags: List of tag responses
115
+
116
+ Returns:
117
+ Formatted tags string
118
+ """
119
+ if not tags:
120
+ return "(no tags)"
121
+
122
+ tag_paths = [t.tag_path for t in tags]
123
+ return ", ".join(tag_paths)
124
+
125
+
126
+ def _display_content(response: UpdateContentPartResponse) -> None:
127
+ """Display content to stdout for piping.
128
+
129
+ Text content is printed directly.
130
+ Structured content is formatted as indented JSON.
131
+
132
+ Args:
133
+ response: Content part response
134
+ """
135
+ if response.content_text is not None:
136
+ # Plain text content
137
+ print(response.content_text, file=sys.stdout)
138
+ elif response.content_structured is not None:
139
+ # Structured JSON content - pretty print
140
+ print(
141
+ json.dumps(response.content_structured, indent=2, ensure_ascii=False),
142
+ file=sys.stdout,
143
+ )
144
+ else:
145
+ # No content (shouldn't happen but handle gracefully)
146
+ print("(no content)", file=sys.stdout)
@@ -0,0 +1 @@
1
+
@@ -0,0 +1,39 @@
1
+ """Content get command implementation."""
2
+
3
+ from uuid import UUID
4
+
5
+ import typer
6
+
7
+ from cli.commands.content.get.presenter import present_content_part
8
+ from cli.commands.flag_utils import validate_uuid
9
+ from cli.infrastructure.command import async_command, authenticated_client
10
+
11
+
12
+ def content_get_command(
13
+ content_part_id: str = typer.Argument(
14
+ ...,
15
+ help="Content part UUID",
16
+ ),
17
+ ) -> None:
18
+ """Get a content part by UUID.
19
+
20
+ Output is piping-friendly:
21
+ - Content goes to stdout
22
+ - Metadata goes to stderr
23
+
24
+ Examples:
25
+ ai content get 019405e0-e000-7000-f100-000000000001
26
+ ai content get <id> | jq .
27
+ ai content get <id> > output.txt
28
+ """
29
+ content_uuid = validate_uuid(content_part_id, "content")
30
+ _execute_get(content_part_id=content_uuid)
31
+
32
+
33
+ @async_command
34
+ async def _execute_get(content_part_id: UUID) -> None:
35
+ """Execute get content part operation."""
36
+ async with authenticated_client() as (_config, client):
37
+ response = await client.get_content_part(content_part_id)
38
+
39
+ present_content_part(response)
@@ -0,0 +1,176 @@
1
+ """Presenter for content get command output."""
2
+
3
+ import json
4
+ import sys
5
+
6
+ from rich.console import Console
7
+ from rich.panel import Panel
8
+ from rich.table import Table
9
+
10
+ from cli.infrastructure.formatting.fields import (
11
+ format_cost,
12
+ format_datetime,
13
+ format_optional,
14
+ format_tokens,
15
+ format_uuid,
16
+ )
17
+ from alloy_runtime_types.dtos.content import ContentPartItemResponse
18
+ from alloy_runtime_types.dtos.templates import TagResponse
19
+
20
+
21
+ def present_content_part(response: ContentPartItemResponse) -> None:
22
+ """Present content part details with piping support.
23
+
24
+ Metadata goes to stderr (visible in terminal but not piped).
25
+ Content goes to stdout (for piping to other commands).
26
+
27
+ Args:
28
+ response: Content part response from server
29
+ """
30
+ # Write content to stdout FIRST - this is what gets piped
31
+ # Must happen before any stderr output to avoid blocking issues with vim/less
32
+ _display_content(response)
33
+
34
+ # Now write metadata to stderr (won't be piped)
35
+ # Wrap in try/except since stderr can also block when stdout is piped
36
+ try:
37
+ stderr_console = Console(file=sys.stderr)
38
+ stderr_console.print(
39
+ f"[green]✓[/green] Content part retrieved: {format_uuid(response.id, short=True)}"
40
+ )
41
+ _display_metadata(response, stderr_console)
42
+ except (BlockingIOError, BrokenPipeError):
43
+ # stderr blocked or closed - that's fine, content already went to stdout
44
+ pass
45
+
46
+
47
+ def _display_metadata(response: ContentPartItemResponse, console: Console) -> None:
48
+ """Display content part metadata to stderr.
49
+
50
+ Args:
51
+ response: Content part response
52
+ console: Console for stderr output
53
+ """
54
+ meta_table = Table(show_header=False, box=None, padding=(0, 1))
55
+ meta_table.add_column("Field", style="dim", width=20)
56
+ meta_table.add_column("Value")
57
+
58
+ # Basic info
59
+ meta_table.add_row("ID", str(response.id))
60
+ meta_table.add_row("Message ID", format_uuid(response.message_id, short=True))
61
+ meta_table.add_row("Part Index", str(response.part_index))
62
+ meta_table.add_row("Content Type", response.content_type_id)
63
+ meta_table.add_row(
64
+ "Storage Type",
65
+ "structured" if response.content_structured else "text",
66
+ )
67
+ meta_table.add_row("Created", format_datetime(response.created_at))
68
+
69
+ # Tags
70
+ meta_table.add_row("Tags", _format_tags(response.tags))
71
+
72
+ # Execution metadata
73
+ exec_meta = response.execution_metadata
74
+ meta_table.add_row("", "") # Separator
75
+ meta_table.add_row("[bold]Execution Info[/bold]", "")
76
+ meta_table.add_row(
77
+ "Execution ID",
78
+ format_uuid(exec_meta.agent_execution_id, short=True),
79
+ )
80
+ meta_table.add_row(
81
+ "Agent",
82
+ format_optional(exec_meta.agent_name, placeholder="(direct model call)"),
83
+ )
84
+ meta_table.add_row("Provider", exec_meta.provider_key)
85
+ meta_table.add_row("Model", exec_meta.provider_model_name)
86
+ meta_table.add_row(
87
+ "Cost",
88
+ format_optional(exec_meta.total_cost_usd, formatter=format_cost),
89
+ )
90
+ meta_table.add_row(
91
+ "Tokens",
92
+ format_optional(exec_meta.total_tokens, formatter=format_tokens),
93
+ )
94
+ meta_table.add_row("Queued At", format_datetime(exec_meta.queued_at))
95
+
96
+ meta_panel = Panel(meta_table, title="Content Part Details", border_style="blue")
97
+ console.print(meta_panel)
98
+
99
+
100
+ def _format_tags(tags: list[TagResponse]) -> str:
101
+ """Format tags list for display.
102
+
103
+ Args:
104
+ tags: List of tag responses
105
+
106
+ Returns:
107
+ Formatted tags string
108
+ """
109
+ if not tags:
110
+ return "(no tags)"
111
+
112
+ tag_paths = [t.tag_path for t in tags]
113
+ return ", ".join(tag_paths)
114
+
115
+
116
+ def _display_content(response: ContentPartItemResponse) -> None:
117
+ """Display content to stdout for piping.
118
+
119
+ Text content is printed directly.
120
+ Structured content is formatted as indented JSON.
121
+
122
+ Args:
123
+ response: Content part response
124
+ """
125
+ if response.content_text is not None:
126
+ content = response.content_text
127
+ elif response.content_structured is not None:
128
+ content = json.dumps(response.content_structured, indent=2, ensure_ascii=False)
129
+ else:
130
+ content = "(no content)"
131
+
132
+ _write_stdout_safe(content)
133
+
134
+
135
+ def _write_stdout_safe(content: str) -> None:
136
+ """Write content to stdout safely, handling non-blocking pipes.
137
+
138
+ When piping to programs like vim that don't immediately consume stdin,
139
+ the pipe can be set to non-blocking mode, causing BlockingIOError.
140
+ This function handles that by using a binary write with proper encoding.
141
+
142
+ Args:
143
+ content: Text content to write
144
+ """
145
+ # Check if stdout has a binary buffer (real stdout does, StringIO in tests doesn't)
146
+ stdout_bin = getattr(sys.stdout, "buffer", None)
147
+
148
+ if stdout_bin is None:
149
+ # Fallback for testing with StringIO or other text-mode streams
150
+ print(content)
151
+ return
152
+
153
+ # Encode the content
154
+ data = (content + "\n").encode("utf-8")
155
+
156
+ # Write in a loop to handle partial writes
157
+ written = 0
158
+ while written < len(data):
159
+ try:
160
+ n = stdout_bin.write(data[written:])
161
+ if n is None:
162
+ # Non-blocking mode returned None, flush and retry
163
+ stdout_bin.flush()
164
+ continue
165
+ written += n
166
+ except BlockingIOError:
167
+ # Pipe is full, flush and continue
168
+ stdout_bin.flush()
169
+ continue
170
+ except BrokenPipeError:
171
+ # Consumer closed the pipe (e.g., head -n 1)
172
+ # This is expected behavior, exit quietly
173
+ sys.stderr.close()
174
+ sys.exit(0)
175
+
176
+ stdout_bin.flush()
@@ -0,0 +1 @@
1
+