docagent-cli 0.0.35__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 (300) hide show
  1. docagent_cli/__init__.py +36 -0
  2. docagent_cli/__main__.py +6 -0
  3. docagent_cli/_ask_user_types.py +90 -0
  4. docagent_cli/_cli_context.py +27 -0
  5. docagent_cli/_debug.py +52 -0
  6. docagent_cli/_env_vars.py +56 -0
  7. docagent_cli/_server_config.py +352 -0
  8. docagent_cli/_session_stats.py +114 -0
  9. docagent_cli/_testing_models.py +144 -0
  10. docagent_cli/_version.py +17 -0
  11. docagent_cli/agent.py +1193 -0
  12. docagent_cli/app.py +4979 -0
  13. docagent_cli/app.tcss +283 -0
  14. docagent_cli/ask_user.py +301 -0
  15. docagent_cli/built_in_skills/__init__.py +5 -0
  16. docagent_cli/built_in_skills/doc-coauthoring/SKILL.md +375 -0
  17. docagent_cli/built_in_skills/docx/LICENSE.txt +30 -0
  18. docagent_cli/built_in_skills/docx/SKILL.md +590 -0
  19. docagent_cli/built_in_skills/docx/scripts/__init__.py +1 -0
  20. docagent_cli/built_in_skills/docx/scripts/accept_changes.py +135 -0
  21. docagent_cli/built_in_skills/docx/scripts/comment.py +318 -0
  22. docagent_cli/built_in_skills/docx/scripts/office/helpers/__init__.py +0 -0
  23. docagent_cli/built_in_skills/docx/scripts/office/helpers/merge_runs.py +199 -0
  24. docagent_cli/built_in_skills/docx/scripts/office/helpers/simplify_redlines.py +197 -0
  25. docagent_cli/built_in_skills/docx/scripts/office/pack.py +159 -0
  26. docagent_cli/built_in_skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-chart.xsd +1499 -0
  27. docagent_cli/built_in_skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-chartDrawing.xsd +146 -0
  28. docagent_cli/built_in_skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-diagram.xsd +1085 -0
  29. docagent_cli/built_in_skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-lockedCanvas.xsd +11 -0
  30. docagent_cli/built_in_skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-main.xsd +3081 -0
  31. docagent_cli/built_in_skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-picture.xsd +23 -0
  32. docagent_cli/built_in_skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-spreadsheetDrawing.xsd +185 -0
  33. docagent_cli/built_in_skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-wordprocessingDrawing.xsd +287 -0
  34. docagent_cli/built_in_skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/pml.xsd +1676 -0
  35. docagent_cli/built_in_skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-additionalCharacteristics.xsd +28 -0
  36. docagent_cli/built_in_skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-bibliography.xsd +144 -0
  37. docagent_cli/built_in_skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-commonSimpleTypes.xsd +174 -0
  38. docagent_cli/built_in_skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-customXmlDataProperties.xsd +25 -0
  39. docagent_cli/built_in_skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-customXmlSchemaProperties.xsd +18 -0
  40. docagent_cli/built_in_skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesCustom.xsd +59 -0
  41. docagent_cli/built_in_skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesExtended.xsd +56 -0
  42. docagent_cli/built_in_skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesVariantTypes.xsd +195 -0
  43. docagent_cli/built_in_skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-math.xsd +582 -0
  44. docagent_cli/built_in_skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-relationshipReference.xsd +25 -0
  45. docagent_cli/built_in_skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/sml.xsd +4439 -0
  46. docagent_cli/built_in_skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-main.xsd +570 -0
  47. docagent_cli/built_in_skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-officeDrawing.xsd +509 -0
  48. docagent_cli/built_in_skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-presentationDrawing.xsd +12 -0
  49. docagent_cli/built_in_skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-spreadsheetDrawing.xsd +108 -0
  50. docagent_cli/built_in_skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-wordprocessingDrawing.xsd +96 -0
  51. docagent_cli/built_in_skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/wml.xsd +3646 -0
  52. docagent_cli/built_in_skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/xml.xsd +116 -0
  53. docagent_cli/built_in_skills/docx/scripts/office/schemas/ecma/fouth-edition/opc-contentTypes.xsd +42 -0
  54. docagent_cli/built_in_skills/docx/scripts/office/schemas/ecma/fouth-edition/opc-coreProperties.xsd +50 -0
  55. docagent_cli/built_in_skills/docx/scripts/office/schemas/ecma/fouth-edition/opc-digSig.xsd +49 -0
  56. docagent_cli/built_in_skills/docx/scripts/office/schemas/ecma/fouth-edition/opc-relationships.xsd +33 -0
  57. docagent_cli/built_in_skills/docx/scripts/office/schemas/mce/mc.xsd +75 -0
  58. docagent_cli/built_in_skills/docx/scripts/office/schemas/microsoft/wml-2010.xsd +560 -0
  59. docagent_cli/built_in_skills/docx/scripts/office/schemas/microsoft/wml-2012.xsd +67 -0
  60. docagent_cli/built_in_skills/docx/scripts/office/schemas/microsoft/wml-2018.xsd +14 -0
  61. docagent_cli/built_in_skills/docx/scripts/office/schemas/microsoft/wml-cex-2018.xsd +20 -0
  62. docagent_cli/built_in_skills/docx/scripts/office/schemas/microsoft/wml-cid-2016.xsd +13 -0
  63. docagent_cli/built_in_skills/docx/scripts/office/schemas/microsoft/wml-sdtdatahash-2020.xsd +4 -0
  64. docagent_cli/built_in_skills/docx/scripts/office/schemas/microsoft/wml-symex-2015.xsd +8 -0
  65. docagent_cli/built_in_skills/docx/scripts/office/soffice.py +183 -0
  66. docagent_cli/built_in_skills/docx/scripts/office/unpack.py +132 -0
  67. docagent_cli/built_in_skills/docx/scripts/office/validate.py +111 -0
  68. docagent_cli/built_in_skills/docx/scripts/office/validators/__init__.py +15 -0
  69. docagent_cli/built_in_skills/docx/scripts/office/validators/base.py +847 -0
  70. docagent_cli/built_in_skills/docx/scripts/office/validators/docx.py +446 -0
  71. docagent_cli/built_in_skills/docx/scripts/office/validators/pptx.py +275 -0
  72. docagent_cli/built_in_skills/docx/scripts/office/validators/redlining.py +247 -0
  73. docagent_cli/built_in_skills/docx/scripts/templates/comments.xml +3 -0
  74. docagent_cli/built_in_skills/docx/scripts/templates/commentsExtended.xml +3 -0
  75. docagent_cli/built_in_skills/docx/scripts/templates/commentsExtensible.xml +3 -0
  76. docagent_cli/built_in_skills/docx/scripts/templates/commentsIds.xml +3 -0
  77. docagent_cli/built_in_skills/docx/scripts/templates/people.xml +3 -0
  78. docagent_cli/built_in_skills/pdf/LICENSE.txt +30 -0
  79. docagent_cli/built_in_skills/pdf/SKILL.md +314 -0
  80. docagent_cli/built_in_skills/pdf/forms.md +294 -0
  81. docagent_cli/built_in_skills/pdf/reference.md +612 -0
  82. docagent_cli/built_in_skills/pdf/scripts/check_bounding_boxes.py +65 -0
  83. docagent_cli/built_in_skills/pdf/scripts/check_fillable_fields.py +11 -0
  84. docagent_cli/built_in_skills/pdf/scripts/convert_pdf_to_images.py +33 -0
  85. docagent_cli/built_in_skills/pdf/scripts/create_validation_image.py +37 -0
  86. docagent_cli/built_in_skills/pdf/scripts/extract_form_field_info.py +122 -0
  87. docagent_cli/built_in_skills/pdf/scripts/extract_form_structure.py +115 -0
  88. docagent_cli/built_in_skills/pdf/scripts/fill_fillable_fields.py +98 -0
  89. docagent_cli/built_in_skills/pdf/scripts/fill_pdf_form_with_annotations.py +107 -0
  90. docagent_cli/built_in_skills/pptx/LICENSE.txt +30 -0
  91. docagent_cli/built_in_skills/pptx/SKILL.md +232 -0
  92. docagent_cli/built_in_skills/pptx/editing.md +205 -0
  93. docagent_cli/built_in_skills/pptx/pptxgenjs.md +420 -0
  94. docagent_cli/built_in_skills/pptx/scripts/__init__.py +0 -0
  95. docagent_cli/built_in_skills/pptx/scripts/add_slide.py +195 -0
  96. docagent_cli/built_in_skills/pptx/scripts/clean.py +286 -0
  97. docagent_cli/built_in_skills/pptx/scripts/office/helpers/__init__.py +0 -0
  98. docagent_cli/built_in_skills/pptx/scripts/office/helpers/merge_runs.py +199 -0
  99. docagent_cli/built_in_skills/pptx/scripts/office/helpers/simplify_redlines.py +197 -0
  100. docagent_cli/built_in_skills/pptx/scripts/office/pack.py +159 -0
  101. docagent_cli/built_in_skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-chart.xsd +1499 -0
  102. docagent_cli/built_in_skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-chartDrawing.xsd +146 -0
  103. docagent_cli/built_in_skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-diagram.xsd +1085 -0
  104. docagent_cli/built_in_skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-lockedCanvas.xsd +11 -0
  105. docagent_cli/built_in_skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-main.xsd +3081 -0
  106. docagent_cli/built_in_skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-picture.xsd +23 -0
  107. docagent_cli/built_in_skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-spreadsheetDrawing.xsd +185 -0
  108. docagent_cli/built_in_skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-wordprocessingDrawing.xsd +287 -0
  109. docagent_cli/built_in_skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/pml.xsd +1676 -0
  110. docagent_cli/built_in_skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-additionalCharacteristics.xsd +28 -0
  111. docagent_cli/built_in_skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-bibliography.xsd +144 -0
  112. docagent_cli/built_in_skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-commonSimpleTypes.xsd +174 -0
  113. docagent_cli/built_in_skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-customXmlDataProperties.xsd +25 -0
  114. docagent_cli/built_in_skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-customXmlSchemaProperties.xsd +18 -0
  115. docagent_cli/built_in_skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesCustom.xsd +59 -0
  116. docagent_cli/built_in_skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesExtended.xsd +56 -0
  117. docagent_cli/built_in_skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesVariantTypes.xsd +195 -0
  118. docagent_cli/built_in_skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-math.xsd +582 -0
  119. docagent_cli/built_in_skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-relationshipReference.xsd +25 -0
  120. docagent_cli/built_in_skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/sml.xsd +4439 -0
  121. docagent_cli/built_in_skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-main.xsd +570 -0
  122. docagent_cli/built_in_skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-officeDrawing.xsd +509 -0
  123. docagent_cli/built_in_skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-presentationDrawing.xsd +12 -0
  124. docagent_cli/built_in_skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-spreadsheetDrawing.xsd +108 -0
  125. docagent_cli/built_in_skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-wordprocessingDrawing.xsd +96 -0
  126. docagent_cli/built_in_skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/wml.xsd +3646 -0
  127. docagent_cli/built_in_skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/xml.xsd +116 -0
  128. docagent_cli/built_in_skills/pptx/scripts/office/schemas/ecma/fouth-edition/opc-contentTypes.xsd +42 -0
  129. docagent_cli/built_in_skills/pptx/scripts/office/schemas/ecma/fouth-edition/opc-coreProperties.xsd +50 -0
  130. docagent_cli/built_in_skills/pptx/scripts/office/schemas/ecma/fouth-edition/opc-digSig.xsd +49 -0
  131. docagent_cli/built_in_skills/pptx/scripts/office/schemas/ecma/fouth-edition/opc-relationships.xsd +33 -0
  132. docagent_cli/built_in_skills/pptx/scripts/office/schemas/mce/mc.xsd +75 -0
  133. docagent_cli/built_in_skills/pptx/scripts/office/schemas/microsoft/wml-2010.xsd +560 -0
  134. docagent_cli/built_in_skills/pptx/scripts/office/schemas/microsoft/wml-2012.xsd +67 -0
  135. docagent_cli/built_in_skills/pptx/scripts/office/schemas/microsoft/wml-2018.xsd +14 -0
  136. docagent_cli/built_in_skills/pptx/scripts/office/schemas/microsoft/wml-cex-2018.xsd +20 -0
  137. docagent_cli/built_in_skills/pptx/scripts/office/schemas/microsoft/wml-cid-2016.xsd +13 -0
  138. docagent_cli/built_in_skills/pptx/scripts/office/schemas/microsoft/wml-sdtdatahash-2020.xsd +4 -0
  139. docagent_cli/built_in_skills/pptx/scripts/office/schemas/microsoft/wml-symex-2015.xsd +8 -0
  140. docagent_cli/built_in_skills/pptx/scripts/office/soffice.py +183 -0
  141. docagent_cli/built_in_skills/pptx/scripts/office/unpack.py +132 -0
  142. docagent_cli/built_in_skills/pptx/scripts/office/validate.py +111 -0
  143. docagent_cli/built_in_skills/pptx/scripts/office/validators/__init__.py +15 -0
  144. docagent_cli/built_in_skills/pptx/scripts/office/validators/base.py +847 -0
  145. docagent_cli/built_in_skills/pptx/scripts/office/validators/docx.py +446 -0
  146. docagent_cli/built_in_skills/pptx/scripts/office/validators/pptx.py +275 -0
  147. docagent_cli/built_in_skills/pptx/scripts/office/validators/redlining.py +247 -0
  148. docagent_cli/built_in_skills/pptx/scripts/thumbnail.py +289 -0
  149. docagent_cli/built_in_skills/remember/SKILL.md +118 -0
  150. docagent_cli/built_in_skills/skill-creator/LICENSE.txt +202 -0
  151. docagent_cli/built_in_skills/skill-creator/SKILL.md +485 -0
  152. docagent_cli/built_in_skills/skill-creator/agents/analyzer.md +274 -0
  153. docagent_cli/built_in_skills/skill-creator/agents/comparator.md +202 -0
  154. docagent_cli/built_in_skills/skill-creator/agents/grader.md +223 -0
  155. docagent_cli/built_in_skills/skill-creator/assets/eval_review.html +146 -0
  156. docagent_cli/built_in_skills/skill-creator/eval-viewer/generate_review.py +471 -0
  157. docagent_cli/built_in_skills/skill-creator/eval-viewer/viewer.html +1325 -0
  158. docagent_cli/built_in_skills/skill-creator/references/schemas.md +430 -0
  159. docagent_cli/built_in_skills/skill-creator/scripts/__init__.py +0 -0
  160. docagent_cli/built_in_skills/skill-creator/scripts/aggregate_benchmark.py +401 -0
  161. docagent_cli/built_in_skills/skill-creator/scripts/generate_report.py +326 -0
  162. docagent_cli/built_in_skills/skill-creator/scripts/improve_description.py +247 -0
  163. docagent_cli/built_in_skills/skill-creator/scripts/package_skill.py +136 -0
  164. docagent_cli/built_in_skills/skill-creator/scripts/quick_validate.py +103 -0
  165. docagent_cli/built_in_skills/skill-creator/scripts/run_eval.py +310 -0
  166. docagent_cli/built_in_skills/skill-creator/scripts/run_loop.py +328 -0
  167. docagent_cli/built_in_skills/skill-creator/scripts/utils.py +47 -0
  168. docagent_cli/built_in_skills/theme-factory/LICENSE.txt +202 -0
  169. docagent_cli/built_in_skills/theme-factory/SKILL.md +59 -0
  170. docagent_cli/built_in_skills/theme-factory/theme-showcase.pdf +0 -0
  171. docagent_cli/built_in_skills/theme-factory/themes/arctic-frost.md +19 -0
  172. docagent_cli/built_in_skills/theme-factory/themes/botanical-garden.md +19 -0
  173. docagent_cli/built_in_skills/theme-factory/themes/desert-rose.md +19 -0
  174. docagent_cli/built_in_skills/theme-factory/themes/forest-canopy.md +19 -0
  175. docagent_cli/built_in_skills/theme-factory/themes/golden-hour.md +19 -0
  176. docagent_cli/built_in_skills/theme-factory/themes/midnight-galaxy.md +19 -0
  177. docagent_cli/built_in_skills/theme-factory/themes/modern-minimalist.md +19 -0
  178. docagent_cli/built_in_skills/theme-factory/themes/ocean-depths.md +19 -0
  179. docagent_cli/built_in_skills/theme-factory/themes/sunset-boulevard.md +19 -0
  180. docagent_cli/built_in_skills/theme-factory/themes/tech-innovation.md +19 -0
  181. docagent_cli/built_in_skills/xlsx/LICENSE.txt +30 -0
  182. docagent_cli/built_in_skills/xlsx/SKILL.md +292 -0
  183. docagent_cli/built_in_skills/xlsx/scripts/office/helpers/__init__.py +0 -0
  184. docagent_cli/built_in_skills/xlsx/scripts/office/helpers/merge_runs.py +199 -0
  185. docagent_cli/built_in_skills/xlsx/scripts/office/helpers/simplify_redlines.py +197 -0
  186. docagent_cli/built_in_skills/xlsx/scripts/office/pack.py +159 -0
  187. docagent_cli/built_in_skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-chart.xsd +1499 -0
  188. docagent_cli/built_in_skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-chartDrawing.xsd +146 -0
  189. docagent_cli/built_in_skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-diagram.xsd +1085 -0
  190. docagent_cli/built_in_skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-lockedCanvas.xsd +11 -0
  191. docagent_cli/built_in_skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-main.xsd +3081 -0
  192. docagent_cli/built_in_skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-picture.xsd +23 -0
  193. docagent_cli/built_in_skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-spreadsheetDrawing.xsd +185 -0
  194. docagent_cli/built_in_skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-wordprocessingDrawing.xsd +287 -0
  195. docagent_cli/built_in_skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/pml.xsd +1676 -0
  196. docagent_cli/built_in_skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-additionalCharacteristics.xsd +28 -0
  197. docagent_cli/built_in_skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-bibliography.xsd +144 -0
  198. docagent_cli/built_in_skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-commonSimpleTypes.xsd +174 -0
  199. docagent_cli/built_in_skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-customXmlDataProperties.xsd +25 -0
  200. docagent_cli/built_in_skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-customXmlSchemaProperties.xsd +18 -0
  201. docagent_cli/built_in_skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesCustom.xsd +59 -0
  202. docagent_cli/built_in_skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesExtended.xsd +56 -0
  203. docagent_cli/built_in_skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesVariantTypes.xsd +195 -0
  204. docagent_cli/built_in_skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-math.xsd +582 -0
  205. docagent_cli/built_in_skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-relationshipReference.xsd +25 -0
  206. docagent_cli/built_in_skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/sml.xsd +4439 -0
  207. docagent_cli/built_in_skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-main.xsd +570 -0
  208. docagent_cli/built_in_skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-officeDrawing.xsd +509 -0
  209. docagent_cli/built_in_skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-presentationDrawing.xsd +12 -0
  210. docagent_cli/built_in_skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-spreadsheetDrawing.xsd +108 -0
  211. docagent_cli/built_in_skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-wordprocessingDrawing.xsd +96 -0
  212. docagent_cli/built_in_skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/wml.xsd +3646 -0
  213. docagent_cli/built_in_skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/xml.xsd +116 -0
  214. docagent_cli/built_in_skills/xlsx/scripts/office/schemas/ecma/fouth-edition/opc-contentTypes.xsd +42 -0
  215. docagent_cli/built_in_skills/xlsx/scripts/office/schemas/ecma/fouth-edition/opc-coreProperties.xsd +50 -0
  216. docagent_cli/built_in_skills/xlsx/scripts/office/schemas/ecma/fouth-edition/opc-digSig.xsd +49 -0
  217. docagent_cli/built_in_skills/xlsx/scripts/office/schemas/ecma/fouth-edition/opc-relationships.xsd +33 -0
  218. docagent_cli/built_in_skills/xlsx/scripts/office/schemas/mce/mc.xsd +75 -0
  219. docagent_cli/built_in_skills/xlsx/scripts/office/schemas/microsoft/wml-2010.xsd +560 -0
  220. docagent_cli/built_in_skills/xlsx/scripts/office/schemas/microsoft/wml-2012.xsd +67 -0
  221. docagent_cli/built_in_skills/xlsx/scripts/office/schemas/microsoft/wml-2018.xsd +14 -0
  222. docagent_cli/built_in_skills/xlsx/scripts/office/schemas/microsoft/wml-cex-2018.xsd +20 -0
  223. docagent_cli/built_in_skills/xlsx/scripts/office/schemas/microsoft/wml-cid-2016.xsd +13 -0
  224. docagent_cli/built_in_skills/xlsx/scripts/office/schemas/microsoft/wml-sdtdatahash-2020.xsd +4 -0
  225. docagent_cli/built_in_skills/xlsx/scripts/office/schemas/microsoft/wml-symex-2015.xsd +8 -0
  226. docagent_cli/built_in_skills/xlsx/scripts/office/soffice.py +183 -0
  227. docagent_cli/built_in_skills/xlsx/scripts/office/unpack.py +132 -0
  228. docagent_cli/built_in_skills/xlsx/scripts/office/validate.py +111 -0
  229. docagent_cli/built_in_skills/xlsx/scripts/office/validators/__init__.py +15 -0
  230. docagent_cli/built_in_skills/xlsx/scripts/office/validators/base.py +847 -0
  231. docagent_cli/built_in_skills/xlsx/scripts/office/validators/docx.py +446 -0
  232. docagent_cli/built_in_skills/xlsx/scripts/office/validators/pptx.py +275 -0
  233. docagent_cli/built_in_skills/xlsx/scripts/office/validators/redlining.py +247 -0
  234. docagent_cli/built_in_skills/xlsx/scripts/recalc.py +184 -0
  235. docagent_cli/clipboard.py +128 -0
  236. docagent_cli/command_registry.py +284 -0
  237. docagent_cli/config.py +2418 -0
  238. docagent_cli/configurable_model.py +162 -0
  239. docagent_cli/default_agent_prompt.md +12 -0
  240. docagent_cli/editor.py +142 -0
  241. docagent_cli/file_ops.py +473 -0
  242. docagent_cli/formatting.py +28 -0
  243. docagent_cli/hooks.py +206 -0
  244. docagent_cli/input.py +787 -0
  245. docagent_cli/integrations/__init__.py +1 -0
  246. docagent_cli/integrations/sandbox_factory.py +873 -0
  247. docagent_cli/integrations/sandbox_provider.py +71 -0
  248. docagent_cli/local_context.py +718 -0
  249. docagent_cli/main.py +1641 -0
  250. docagent_cli/mcp_tools.py +707 -0
  251. docagent_cli/mcp_trust.py +168 -0
  252. docagent_cli/media_utils.py +478 -0
  253. docagent_cli/model_config.py +1620 -0
  254. docagent_cli/non_interactive.py +948 -0
  255. docagent_cli/offload.py +371 -0
  256. docagent_cli/output.py +69 -0
  257. docagent_cli/project_utils.py +188 -0
  258. docagent_cli/py.typed +0 -0
  259. docagent_cli/remote_client.py +515 -0
  260. docagent_cli/server.py +520 -0
  261. docagent_cli/server_graph.py +196 -0
  262. docagent_cli/server_manager.py +365 -0
  263. docagent_cli/sessions.py +1262 -0
  264. docagent_cli/skills/__init__.py +18 -0
  265. docagent_cli/skills/commands.py +1090 -0
  266. docagent_cli/skills/load.py +192 -0
  267. docagent_cli/subagents.py +173 -0
  268. docagent_cli/system_prompt.md +247 -0
  269. docagent_cli/textual_adapter.py +1352 -0
  270. docagent_cli/theme.py +842 -0
  271. docagent_cli/token_state.py +31 -0
  272. docagent_cli/tool_display.py +298 -0
  273. docagent_cli/tools.py +236 -0
  274. docagent_cli/ui.py +420 -0
  275. docagent_cli/unicode_security.py +516 -0
  276. docagent_cli/update_check.py +454 -0
  277. docagent_cli/widgets/__init__.py +9 -0
  278. docagent_cli/widgets/_links.py +63 -0
  279. docagent_cli/widgets/approval.py +442 -0
  280. docagent_cli/widgets/ask_user.py +398 -0
  281. docagent_cli/widgets/autocomplete.py +691 -0
  282. docagent_cli/widgets/chat_input.py +1827 -0
  283. docagent_cli/widgets/diff.py +248 -0
  284. docagent_cli/widgets/history.py +188 -0
  285. docagent_cli/widgets/loading.py +177 -0
  286. docagent_cli/widgets/mcp_viewer.py +362 -0
  287. docagent_cli/widgets/message_store.py +675 -0
  288. docagent_cli/widgets/messages.py +1751 -0
  289. docagent_cli/widgets/model_selector.py +964 -0
  290. docagent_cli/widgets/status.py +372 -0
  291. docagent_cli/widgets/theme_selector.py +164 -0
  292. docagent_cli/widgets/thread_selector.py +1905 -0
  293. docagent_cli/widgets/tool_renderers.py +148 -0
  294. docagent_cli/widgets/tool_widgets.py +274 -0
  295. docagent_cli/widgets/welcome.py +339 -0
  296. docagent_cli-0.0.35.data/data/docagent_cli/default_agent_prompt.md +12 -0
  297. docagent_cli-0.0.35.dist-info/METADATA +200 -0
  298. docagent_cli-0.0.35.dist-info/RECORD +300 -0
  299. docagent_cli-0.0.35.dist-info/WHEEL +4 -0
  300. docagent_cli-0.0.35.dist-info/entry_points.txt +3 -0
@@ -0,0 +1,473 @@
1
+ """Helpers for tracking file operations and computing diffs for CLI display."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import difflib
6
+ import logging
7
+ from dataclasses import dataclass, field
8
+ from pathlib import Path
9
+ from typing import TYPE_CHECKING, Any, Literal
10
+
11
+ logger = logging.getLogger(__name__)
12
+
13
+ if TYPE_CHECKING:
14
+ from deepagents.backends.protocol import BackendProtocol
15
+
16
+ FileOpStatus = Literal["pending", "success", "error"]
17
+
18
+
19
+ @dataclass
20
+ class ApprovalPreview:
21
+ """Data used to render HITL previews."""
22
+
23
+ title: str
24
+ details: list[str]
25
+ diff: str | None = None
26
+ diff_title: str | None = None
27
+ error: str | None = None
28
+
29
+
30
+ def _safe_read(path: Path) -> str | None:
31
+ """Read file content, returning None on failure.
32
+
33
+ Returns:
34
+ File content as string, or None if reading fails.
35
+ """
36
+ try:
37
+ return path.read_text(encoding="utf-8")
38
+ except (OSError, UnicodeDecodeError) as e:
39
+ logger.debug("Failed to read file %s: %s", path, e)
40
+ return None
41
+
42
+
43
+ def _count_lines(text: str) -> int:
44
+ """Count lines in text, treating empty strings as zero lines.
45
+
46
+ Returns:
47
+ Number of lines in the text.
48
+ """
49
+ if not text:
50
+ return 0
51
+ return len(text.splitlines())
52
+
53
+
54
+ def compute_unified_diff(
55
+ before: str,
56
+ after: str,
57
+ display_path: str,
58
+ *,
59
+ max_lines: int | None = 800,
60
+ context_lines: int = 3,
61
+ ) -> str | None:
62
+ """Compute a unified diff between before and after content.
63
+
64
+ Args:
65
+ before: Original content
66
+ after: New content
67
+ display_path: Path for display in diff headers
68
+ max_lines: Maximum number of diff lines (None for unlimited)
69
+ context_lines: Number of context lines around changes (default 3)
70
+
71
+ Returns:
72
+ Unified diff string or None if no changes
73
+ """
74
+ before_lines = before.splitlines()
75
+ after_lines = after.splitlines()
76
+ diff_lines = list(
77
+ difflib.unified_diff(
78
+ before_lines,
79
+ after_lines,
80
+ fromfile=f"{display_path} (before)",
81
+ tofile=f"{display_path} (after)",
82
+ lineterm="",
83
+ n=context_lines,
84
+ )
85
+ )
86
+ if not diff_lines:
87
+ return None
88
+ if max_lines is not None and len(diff_lines) > max_lines:
89
+ truncated = diff_lines[: max_lines - 1]
90
+ truncated.append("...")
91
+ return "\n".join(truncated)
92
+ return "\n".join(diff_lines)
93
+
94
+
95
+ @dataclass
96
+ class FileOpMetrics:
97
+ """Line and byte level metrics for a file operation."""
98
+
99
+ lines_read: int = 0
100
+ start_line: int | None = None
101
+ end_line: int | None = None
102
+ lines_written: int = 0
103
+ lines_added: int = 0
104
+ lines_removed: int = 0
105
+ bytes_written: int = 0
106
+
107
+
108
+ @dataclass
109
+ class FileOperationRecord:
110
+ """Track a single filesystem tool call."""
111
+
112
+ tool_name: str
113
+ display_path: str
114
+ physical_path: Path | None
115
+ tool_call_id: str | None
116
+ args: dict[str, Any] = field(default_factory=dict)
117
+ status: FileOpStatus = "pending"
118
+ error: str | None = None
119
+ metrics: FileOpMetrics = field(default_factory=FileOpMetrics)
120
+ diff: str | None = None
121
+ before_content: str | None = None
122
+ after_content: str | None = None
123
+ read_output: str | None = None
124
+ hitl_approved: bool = False
125
+
126
+
127
+ def resolve_physical_path(
128
+ path_str: str | None, assistant_id: str | None
129
+ ) -> Path | None:
130
+ """Convert a virtual/relative path to a physical filesystem path.
131
+
132
+ Returns:
133
+ Resolved physical Path, or None if path is empty or resolution fails.
134
+ """
135
+ if not path_str:
136
+ return None
137
+ try:
138
+ if assistant_id and path_str.startswith("/memories/"):
139
+ from docagent_cli.config import settings
140
+
141
+ agent_dir = settings.get_agent_dir(assistant_id)
142
+ suffix = path_str.removeprefix("/memories/").lstrip("/")
143
+ return (agent_dir / suffix).resolve()
144
+ path = Path(path_str)
145
+ if path.is_absolute():
146
+ return path
147
+ return (Path.cwd() / path).resolve()
148
+ except (OSError, ValueError):
149
+ return None
150
+
151
+
152
+ def format_display_path(path_str: str | None) -> str:
153
+ """Format a path for display.
154
+
155
+ Returns:
156
+ Formatted path string suitable for display.
157
+ """
158
+ if not path_str:
159
+ return "(unknown)"
160
+ try:
161
+ path = Path(path_str)
162
+ if path.is_absolute():
163
+ return path.name or str(path)
164
+ return str(path)
165
+ except (OSError, ValueError):
166
+ return str(path_str)
167
+
168
+
169
+ def build_approval_preview(
170
+ tool_name: str,
171
+ args: dict[str, Any],
172
+ assistant_id: str | None,
173
+ ) -> ApprovalPreview | None:
174
+ """Collect summary info and diff for HITL approvals.
175
+
176
+ Returns:
177
+ ApprovalPreview with diff and details, or None if tool not supported.
178
+ """
179
+ path_str = str(args.get("file_path") or args.get("path") or "")
180
+ display_path = format_display_path(path_str)
181
+ physical_path = resolve_physical_path(path_str, assistant_id)
182
+
183
+ if tool_name == "write_file":
184
+ content = str(args.get("content", ""))
185
+ before = (
186
+ _safe_read(physical_path)
187
+ if physical_path and physical_path.exists()
188
+ else ""
189
+ )
190
+ after = content
191
+ diff = compute_unified_diff(before or "", after, display_path, max_lines=100)
192
+ additions = 0
193
+ if diff:
194
+ additions = sum(
195
+ 1
196
+ for line in diff.splitlines()
197
+ if line.startswith("+") and not line.startswith("+++")
198
+ )
199
+ total_lines = _count_lines(after)
200
+ details = [
201
+ f"File: {path_str}",
202
+ "Action: Create new file"
203
+ + (" (overwrites existing content)" if before else ""),
204
+ f"Lines to write: {additions or total_lines}",
205
+ ]
206
+ return ApprovalPreview(
207
+ title=f"Write {display_path}",
208
+ details=details,
209
+ diff=diff,
210
+ diff_title=f"Diff {display_path}",
211
+ )
212
+
213
+ if tool_name == "edit_file":
214
+ if physical_path is None:
215
+ return ApprovalPreview(
216
+ title=f"Update {display_path}",
217
+ details=[f"File: {path_str}", "Action: Replace text"],
218
+ error="Unable to resolve file path.",
219
+ )
220
+ before = _safe_read(physical_path)
221
+ if before is None:
222
+ return ApprovalPreview(
223
+ title=f"Update {display_path}",
224
+ details=[f"File: {path_str}", "Action: Replace text"],
225
+ error="Unable to read current file contents.",
226
+ )
227
+ old_string = str(args.get("old_string", ""))
228
+ new_string = str(args.get("new_string", ""))
229
+ replace_all = bool(args.get("replace_all"))
230
+ from deepagents.backends.utils import perform_string_replacement
231
+
232
+ replacement = perform_string_replacement(
233
+ before, old_string, new_string, replace_all
234
+ )
235
+ if isinstance(replacement, str):
236
+ return ApprovalPreview(
237
+ title=f"Update {display_path}",
238
+ details=[f"File: {path_str}", "Action: Replace text"],
239
+ error=replacement,
240
+ )
241
+ after, occurrences = replacement
242
+ diff = compute_unified_diff(before, after, display_path, max_lines=None)
243
+ additions = 0
244
+ deletions = 0
245
+ if diff:
246
+ additions = sum(
247
+ 1
248
+ for line in diff.splitlines()
249
+ if line.startswith("+") and not line.startswith("+++")
250
+ )
251
+ deletions = sum(
252
+ 1
253
+ for line in diff.splitlines()
254
+ if line.startswith("-") and not line.startswith("---")
255
+ )
256
+ action = "all occurrences" if replace_all else "single occurrence"
257
+ details = [
258
+ f"File: {path_str}",
259
+ f"Action: Replace text ({action})",
260
+ f"Occurrences matched: {occurrences}",
261
+ f"Lines changed: +{additions} / -{deletions}",
262
+ ]
263
+ return ApprovalPreview(
264
+ title=f"Update {display_path}",
265
+ details=details,
266
+ diff=diff,
267
+ diff_title=f"Diff {display_path}",
268
+ )
269
+
270
+ return None
271
+
272
+
273
+ class FileOpTracker:
274
+ """Collect file operation metrics during a CLI interaction."""
275
+
276
+ def __init__(
277
+ self, *, assistant_id: str | None, backend: BackendProtocol | None = None
278
+ ) -> None:
279
+ """Initialize the tracker."""
280
+ self.assistant_id = assistant_id
281
+ self.backend = backend
282
+ self.active: dict[str | None, FileOperationRecord] = {}
283
+ self.completed: list[FileOperationRecord] = []
284
+
285
+ def start_operation(
286
+ self, tool_name: str, args: dict[str, Any], tool_call_id: str | None
287
+ ) -> None:
288
+ """Begin tracking a file operation.
289
+
290
+ Creates a record for the operation and, for write/edit operations,
291
+ captures the file's content before modification.
292
+ """
293
+ if tool_name not in {"read_file", "write_file", "edit_file"}:
294
+ return
295
+ path_str = str(args.get("file_path") or args.get("path") or "")
296
+ display_path = format_display_path(path_str)
297
+ record = FileOperationRecord(
298
+ tool_name=tool_name,
299
+ display_path=display_path,
300
+ physical_path=resolve_physical_path(path_str, self.assistant_id),
301
+ tool_call_id=tool_call_id,
302
+ args=args,
303
+ )
304
+ if tool_name in {"write_file", "edit_file"}:
305
+ if self.backend and path_str:
306
+ try:
307
+ responses = self.backend.download_files([path_str])
308
+ if (
309
+ responses
310
+ and responses[0].content is not None
311
+ and responses[0].error is None
312
+ ):
313
+ record.before_content = responses[0].content.decode("utf-8")
314
+ else:
315
+ record.before_content = ""
316
+ except (OSError, UnicodeDecodeError, AttributeError) as e:
317
+ logger.debug(
318
+ "Failed to read before_content for %s: %s", path_str, e
319
+ )
320
+ record.before_content = ""
321
+ elif record.physical_path:
322
+ record.before_content = _safe_read(record.physical_path) or ""
323
+ self.active[tool_call_id] = record
324
+
325
+ def complete_with_message(self, tool_message: Any) -> FileOperationRecord | None: # noqa: ANN401 # Tool message type is dynamic
326
+ """Complete a file operation with the tool message result.
327
+
328
+ Returns:
329
+ The completed FileOperationRecord, or None if no matching operation.
330
+ """
331
+ tool_call_id = getattr(tool_message, "tool_call_id", None)
332
+ record = self.active.get(tool_call_id)
333
+ if record is None:
334
+ return None
335
+
336
+ content = tool_message.content
337
+ if isinstance(content, list):
338
+ # Some tool messages may return list segments; join them for analysis.
339
+ joined = []
340
+ for item in content:
341
+ if isinstance(item, str):
342
+ joined.append(item)
343
+ else:
344
+ joined.append(str(item))
345
+ content_text = "\n".join(joined)
346
+ else:
347
+ content_text = str(content) if content is not None else ""
348
+
349
+ if getattr(
350
+ tool_message, "status", "success"
351
+ ) != "success" or content_text.lower().startswith("error"):
352
+ record.status = "error"
353
+ record.error = content_text
354
+ self._finalize(record)
355
+ return record
356
+
357
+ record.status = "success"
358
+
359
+ if record.tool_name == "read_file":
360
+ record.read_output = content_text
361
+ lines = _count_lines(content_text)
362
+ record.metrics.lines_read = lines
363
+ offset = record.args.get("offset")
364
+ limit = record.args.get("limit")
365
+ if isinstance(offset, int):
366
+ if offset > lines:
367
+ offset = 0
368
+ record.metrics.start_line = offset + 1
369
+ if lines:
370
+ record.metrics.end_line = offset + lines
371
+ elif lines:
372
+ record.metrics.start_line = 1
373
+ record.metrics.end_line = lines
374
+ if isinstance(limit, int) and lines > limit:
375
+ record.metrics.end_line = (record.metrics.start_line or 1) + limit - 1
376
+ else:
377
+ # For write/edit operations, read back from backend (or local filesystem)
378
+ self._populate_after_content(record)
379
+ if record.after_content is None:
380
+ record.status = "error"
381
+ record.error = "Could not read updated file content."
382
+ self._finalize(record)
383
+ return record
384
+ record.metrics.lines_written = _count_lines(record.after_content)
385
+ before_lines = _count_lines(record.before_content or "")
386
+ diff = compute_unified_diff(
387
+ record.before_content or "",
388
+ record.after_content,
389
+ record.display_path,
390
+ max_lines=100,
391
+ )
392
+ record.diff = diff
393
+ if diff:
394
+ additions = sum(
395
+ 1
396
+ for line in diff.splitlines()
397
+ if line.startswith("+") and not line.startswith("+++")
398
+ )
399
+ deletions = sum(
400
+ 1
401
+ for line in diff.splitlines()
402
+ if line.startswith("-") and not line.startswith("---")
403
+ )
404
+ record.metrics.lines_added = additions
405
+ record.metrics.lines_removed = deletions
406
+ elif record.tool_name == "write_file" and not (record.before_content or ""):
407
+ record.metrics.lines_added = record.metrics.lines_written
408
+ record.metrics.bytes_written = len(record.after_content.encode("utf-8"))
409
+ if (
410
+ record.diff is None
411
+ and (record.before_content or "") != record.after_content
412
+ ):
413
+ record.diff = compute_unified_diff(
414
+ record.before_content or "",
415
+ record.after_content,
416
+ record.display_path,
417
+ max_lines=100,
418
+ )
419
+ if record.diff is None and before_lines != record.metrics.lines_written:
420
+ record.metrics.lines_added = max(
421
+ record.metrics.lines_written - before_lines, 0
422
+ )
423
+
424
+ self._finalize(record)
425
+ return record
426
+
427
+ def mark_hitl_approved(self, tool_name: str, args: dict[str, Any]) -> None:
428
+ """Mark operations matching tool_name and file_path as HIL-approved."""
429
+ file_path = args.get("file_path") or args.get("path")
430
+ if not file_path:
431
+ return
432
+
433
+ # Mark all active records that match
434
+ for record in self.active.values():
435
+ if record.tool_name == tool_name:
436
+ record_path = record.args.get("file_path") or record.args.get("path")
437
+ if record_path == file_path:
438
+ record.hitl_approved = True
439
+
440
+ def _populate_after_content(self, record: FileOperationRecord) -> None:
441
+ # Use backend if available (works for any BackendProtocol implementation)
442
+ if self.backend:
443
+ try:
444
+ file_path = record.args.get("file_path") or record.args.get("path")
445
+ if file_path:
446
+ responses = self.backend.download_files([file_path])
447
+ if (
448
+ responses
449
+ and responses[0].content is not None
450
+ and responses[0].error is None
451
+ ):
452
+ record.after_content = responses[0].content.decode("utf-8")
453
+ else:
454
+ record.after_content = None
455
+ else:
456
+ record.after_content = None
457
+ except (OSError, UnicodeDecodeError, AttributeError) as e:
458
+ logger.debug(
459
+ "Failed to read after_content for %s: %s",
460
+ record.args.get("file_path") or record.args.get("path"),
461
+ e,
462
+ )
463
+ record.after_content = None
464
+ else:
465
+ # Fallback: direct filesystem read when no backend provided
466
+ if record.physical_path is None:
467
+ record.after_content = None
468
+ return
469
+ record.after_content = _safe_read(record.physical_path)
470
+
471
+ def _finalize(self, record: FileOperationRecord) -> None:
472
+ self.completed.append(record)
473
+ self.active.pop(record.tool_call_id, None)
@@ -0,0 +1,28 @@
1
+ """Lightweight text-formatting helpers.
2
+
3
+ Keep this module free of heavy dependencies so it can be imported anywhere
4
+ in the CLI without pulling in large frameworks.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+
10
+ def format_duration(seconds: float) -> str:
11
+ """Format a duration in seconds into a human-readable string.
12
+
13
+ Args:
14
+ seconds: Duration in seconds.
15
+
16
+ Returns:
17
+ Formatted string like `"5s"`, `"2.3s"`, `"5m 12s"`, or `"1h 23m 4s"`.
18
+ """
19
+ rounded = round(seconds, 1)
20
+ if rounded < 60: # noqa: PLR2004
21
+ if rounded % 1 == 0:
22
+ return f"{int(rounded)}s"
23
+ return f"{rounded:.1f}s"
24
+ minutes, secs = divmod(int(rounded), 60)
25
+ if minutes < 60: # noqa: PLR2004
26
+ return f"{minutes}m {secs}s"
27
+ hours, minutes = divmod(minutes, 60)
28
+ return f"{hours}h {minutes}m {secs}s"
docagent_cli/hooks.py ADDED
@@ -0,0 +1,206 @@
1
+ """Lightweight hook dispatch for external tool integration.
2
+
3
+ Loads hook configuration from `~/.docagent/hooks.json` and fires matching
4
+ commands with JSON payloads on stdin. Subprocess work is offloaded to a
5
+ background thread so the caller's event loop is never stalled. Failures are
6
+ logged but never bubble up to the caller.
7
+
8
+ Config format (`~/.docagent/hooks.json`):
9
+
10
+ ```json
11
+ {"hooks": [{"command": ["bash", "adapter.sh"], "events": ["session.start"]}]}
12
+ ```
13
+
14
+ If `events` is omitted or empty the hook receives **all** events.
15
+ """
16
+
17
+ from __future__ import annotations
18
+
19
+ import asyncio
20
+ import json
21
+ import logging
22
+ import subprocess # noqa: S404
23
+ from concurrent.futures import ThreadPoolExecutor
24
+ from typing import Any
25
+
26
+ logger = logging.getLogger(__name__)
27
+
28
+ _hooks_config: list[dict[str, Any]] | None = None
29
+ """Cached config — loaded lazily on first dispatch."""
30
+
31
+ _background_tasks: set[asyncio.Task[None]] = set()
32
+ """Strong references to fire-and-forget tasks to prevent GC."""
33
+
34
+
35
+ def _load_hooks() -> list[dict[str, Any]]:
36
+ """Load and cache hook definitions from the config file.
37
+
38
+ Returns:
39
+ An empty list when the file is missing or malformed so that normal
40
+ execution is never interrupted.
41
+ """
42
+ global _hooks_config # noqa: PLW0603
43
+ if _hooks_config is not None:
44
+ return _hooks_config
45
+
46
+ from docagent_cli.model_config import DEFAULT_CONFIG_DIR
47
+
48
+ hooks_path = DEFAULT_CONFIG_DIR / "hooks.json"
49
+
50
+ if not hooks_path.is_file():
51
+ _hooks_config = []
52
+ return _hooks_config
53
+
54
+ try:
55
+ data = json.loads(hooks_path.read_text())
56
+ if not isinstance(data, dict):
57
+ logger.warning(
58
+ "Hooks config at %s must be a JSON object, got %s",
59
+ hooks_path,
60
+ type(data).__name__,
61
+ )
62
+ _hooks_config = []
63
+ return _hooks_config
64
+ hooks = data.get("hooks", [])
65
+ if not isinstance(hooks, list):
66
+ logger.warning(
67
+ "Hooks config 'hooks' key at %s must be a list, got %s",
68
+ hooks_path,
69
+ type(hooks).__name__,
70
+ )
71
+ _hooks_config = []
72
+ return _hooks_config
73
+ _hooks_config = hooks
74
+ except (json.JSONDecodeError, OSError) as exc:
75
+ logger.warning("Failed to load hooks config from %s: %s", hooks_path, exc)
76
+ _hooks_config = []
77
+
78
+ return _hooks_config
79
+
80
+
81
+ def _run_single_hook(command: list[str], event: str, payload_bytes: bytes) -> None:
82
+ """Execute a single hook command, writing the JSON payload to its stdin.
83
+
84
+ Uses `subprocess.run` which automatically kills the child process on
85
+ timeout, preventing zombie/orphan process leaks.
86
+
87
+ Args:
88
+ command: The command and arguments to run.
89
+ event: Event name (for logging).
90
+ payload_bytes: JSON payload to write to the command's stdin.
91
+ """
92
+ try:
93
+ subprocess.run( # noqa: S603
94
+ command,
95
+ input=payload_bytes,
96
+ stdout=subprocess.DEVNULL,
97
+ stderr=subprocess.DEVNULL,
98
+ start_new_session=True,
99
+ timeout=5,
100
+ check=False,
101
+ )
102
+ except subprocess.TimeoutExpired:
103
+ logger.warning("Hook command timed out (>5s) for event %s: %s", event, command)
104
+ except (FileNotFoundError, PermissionError) as exc:
105
+ logger.warning("Hook command failed for event %s: %s — %s", event, command, exc)
106
+ except Exception:
107
+ logger.debug(
108
+ "Hook dispatch failed for event %s: %s",
109
+ event,
110
+ command,
111
+ exc_info=True,
112
+ )
113
+
114
+
115
+ def _dispatch_hook_sync(
116
+ event: str, payload_bytes: bytes, hooks: list[dict[str, Any]]
117
+ ) -> None:
118
+ """Dispatch matching hooks, running them concurrently via a thread pool.
119
+
120
+ Iterates over all configured hooks, skipping those whose event filter
121
+ does not match or whose `command` is missing/invalid. Matching hooks are
122
+ executed concurrently with a 5-second timeout per command. Errors are caught
123
+ per-hook and logged without propagating.
124
+
125
+ Args:
126
+ event: Dotted event name (e.g. `'session.start'`).
127
+ payload_bytes: JSON payload to write to each command's stdin.
128
+ hooks: List of hook definition dicts from the config file.
129
+ """
130
+ matching: list[list[str]] = []
131
+ for hook in hooks:
132
+ command = hook.get("command")
133
+ if not isinstance(command, list) or not command:
134
+ continue
135
+
136
+ events = hook.get("events")
137
+ # Empty/missing events list means "subscribe to everything".
138
+ if events and event not in events:
139
+ continue
140
+
141
+ matching.append(command)
142
+
143
+ if not matching:
144
+ return
145
+
146
+ if len(matching) == 1:
147
+ _run_single_hook(matching[0], event, payload_bytes)
148
+ return
149
+
150
+ with ThreadPoolExecutor(max_workers=len(matching)) as pool:
151
+ futures = [
152
+ pool.submit(_run_single_hook, cmd, event, payload_bytes) for cmd in matching
153
+ ]
154
+ for future in futures:
155
+ future.result()
156
+
157
+
158
+ async def dispatch_hook(event: str, payload: dict[str, Any]) -> None:
159
+ """Fire matching hook commands with `payload` serialized as JSON on stdin.
160
+
161
+ The `event` name is automatically injected into the payload under the
162
+ `"event"` key so callers don't need to duplicate it.
163
+
164
+ The blocking subprocess work is offloaded to a thread so the caller's
165
+ event loop is never stalled. Matching hooks run concurrently, each with
166
+ a 5-second timeout. Errors are logged and never propagated.
167
+
168
+ Args:
169
+ event: Dotted event name (e.g. `'session.start'`).
170
+ payload: Arbitrary JSON-serializable dict sent on the command's stdin.
171
+ """
172
+ try:
173
+ hooks = _load_hooks()
174
+ if not hooks:
175
+ return
176
+
177
+ payload_bytes = json.dumps({"event": event, **payload}).encode()
178
+ await asyncio.to_thread(_dispatch_hook_sync, event, payload_bytes, hooks)
179
+ except Exception:
180
+ logger.warning(
181
+ "Unexpected error in dispatch_hook for event %s",
182
+ event,
183
+ exc_info=True,
184
+ )
185
+
186
+
187
+ def dispatch_hook_fire_and_forget(event: str, payload: dict[str, Any]) -> None:
188
+ """Schedule `dispatch_hook` as a background task with a strong reference.
189
+
190
+ Use this instead of bare `create_task(dispatch_hook(...))` to prevent the
191
+ task from being garbage collected before completion.
192
+
193
+ Safe to call from sync code as long as an event loop is running.
194
+
195
+ Args:
196
+ event: Dotted event name (e.g. `'session.start'`).
197
+ payload: Arbitrary JSON-serializable dict sent on the command's stdin.
198
+ """
199
+ try:
200
+ loop = asyncio.get_running_loop()
201
+ except RuntimeError:
202
+ logger.debug("No running event loop; skipping hook for %s", event)
203
+ return
204
+ task = loop.create_task(dispatch_hook(event, payload))
205
+ _background_tasks.add(task)
206
+ task.add_done_callback(_background_tasks.discard)