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,1620 @@
1
+ """Model configuration management.
2
+
3
+ Handles loading and saving model configuration from TOML files, providing a
4
+ structured way to define available models and providers.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import contextlib
10
+ import importlib.util
11
+ import logging
12
+ import os
13
+ import tempfile
14
+ import threading
15
+ import tomllib
16
+ from dataclasses import dataclass, field
17
+ from pathlib import Path
18
+ from types import MappingProxyType
19
+ from typing import TYPE_CHECKING, Any, NamedTuple, TypedDict
20
+
21
+ import tomli_w
22
+
23
+ if TYPE_CHECKING:
24
+ from collections.abc import Mapping
25
+
26
+ logger = logging.getLogger(__name__)
27
+
28
+ _ENV_PREFIX = "DEEPAGENTS_CLI_"
29
+
30
+
31
+ def resolve_env_var(name: str) -> str | None:
32
+ """Look up an env var with `DEEPAGENTS_CLI_` prefix override.
33
+
34
+ Checks `DEEPAGENTS_CLI_{name}` first, then falls back to `{name}`.
35
+
36
+ If the prefixed variable is *present* in the environment (even as an empty
37
+ string), the canonical variable is never consulted. This lets users
38
+ set `DEEPAGENTS_CLI_X=""` to shadow a canonically-set key -- the function
39
+ will return `None` (since empty strings are normalized to `None`),
40
+ effectively suppressing the canonical value.
41
+
42
+ If `name` already carries the prefix, the double-prefixed lookup is skipped
43
+ to avoid nonsensical `DEEPAGENTS_CLI_DEEPAGENTS_CLI_*` reads
44
+ (e.g., when the name comes from a user's `config.toml`).
45
+
46
+ Args:
47
+ name: The canonical environment variable name (e.g.
48
+ `ANTHROPIC_API_KEY`).
49
+
50
+ Returns:
51
+ The resolved value, or `None` when absent or empty.
52
+ """
53
+ if not name.startswith(_ENV_PREFIX):
54
+ prefixed = f"{_ENV_PREFIX}{name}"
55
+ if prefixed in os.environ:
56
+ val = os.environ[prefixed]
57
+ if not val and os.environ.get(name):
58
+ logger.debug(
59
+ "%s is set but empty, blocking non-empty %s. "
60
+ "Unset %s to use the canonical variable.",
61
+ prefixed,
62
+ name,
63
+ prefixed,
64
+ )
65
+ return val or None
66
+ return os.environ.get(name) or None
67
+
68
+
69
+ class ModelConfigError(Exception):
70
+ """Raised when model configuration or creation fails."""
71
+
72
+
73
+ @dataclass(frozen=True)
74
+ class ModelSpec:
75
+ """A model specification in `provider:model` format.
76
+
77
+ Examples:
78
+ >>> spec = ModelSpec.parse("anthropic:claude-sonnet-4-5")
79
+ >>> spec.provider
80
+ 'anthropic'
81
+ >>> spec.model
82
+ 'claude-sonnet-4-5'
83
+ >>> str(spec)
84
+ 'anthropic:claude-sonnet-4-5'
85
+ """
86
+
87
+ provider: str
88
+ """The provider name (e.g., `'anthropic'`, `'openai'`)."""
89
+
90
+ model: str
91
+ """The model identifier (e.g., `'claude-sonnet-4-5'`, `'gpt-4o'`)."""
92
+
93
+ def __post_init__(self) -> None:
94
+ """Validate the model spec after initialization.
95
+
96
+ Raises:
97
+ ValueError: If provider or model is empty.
98
+ """
99
+ if not self.provider:
100
+ msg = "Provider cannot be empty"
101
+ raise ValueError(msg)
102
+ if not self.model:
103
+ msg = "Model cannot be empty"
104
+ raise ValueError(msg)
105
+
106
+ @classmethod
107
+ def parse(cls, spec: str) -> ModelSpec:
108
+ """Parse a model specification string.
109
+
110
+ Args:
111
+ spec: Model specification in `'provider:model'` format.
112
+
113
+ Returns:
114
+ Parsed ModelSpec instance.
115
+
116
+ Raises:
117
+ ValueError: If the spec is not in valid `'provider:model'` format.
118
+ """
119
+ if ":" not in spec:
120
+ msg = (
121
+ f"Invalid model spec '{spec}': must be in provider:model format "
122
+ "(e.g., 'anthropic:claude-sonnet-4-5')"
123
+ )
124
+ raise ValueError(msg)
125
+ provider, model = spec.split(":", 1)
126
+ return cls(provider=provider, model=model)
127
+
128
+ @classmethod
129
+ def try_parse(cls, spec: str) -> ModelSpec | None:
130
+ """Non-raising variant of `parse`.
131
+
132
+ Args:
133
+ spec: Model specification in `provider:model` format.
134
+
135
+ Returns:
136
+ Parsed `ModelSpec`, or `None` when *spec* is not valid.
137
+ """
138
+ try:
139
+ return cls.parse(spec)
140
+ except ValueError:
141
+ return None
142
+
143
+ def __str__(self) -> str:
144
+ """Return the model spec as a string in `provider:model` format."""
145
+ return f"{self.provider}:{self.model}"
146
+
147
+
148
+ class ModelProfileEntry(TypedDict):
149
+ """Profile data for a model with override tracking."""
150
+
151
+ profile: dict[str, Any]
152
+ """Merged profile dict (upstream defaults + config.toml overrides).
153
+
154
+ Keys vary by provider (e.g., `max_input_tokens`, `tool_calling`).
155
+ """
156
+
157
+ overridden_keys: frozenset[str]
158
+ """Keys in `profile` whose values came from config.toml rather than the
159
+ upstream provider package."""
160
+
161
+
162
+ class ProviderConfig(TypedDict, total=False):
163
+ """Configuration for a model provider.
164
+
165
+ The optional `class_path` field allows bypassing `init_chat_model` entirely
166
+ and instantiating an arbitrary `BaseChatModel` subclass via importlib.
167
+
168
+ !!! warning
169
+
170
+ Setting `class_path` executes arbitrary Python code from the user's
171
+ config file. This has the same trust model as `pyproject.toml` build
172
+ scripts — the user controls their own machine.
173
+ """
174
+
175
+ enabled: bool
176
+ """Whether this provider appears in the model switcher.
177
+
178
+ Defaults to `True`. Set to `False` to hide a package-discovered provider
179
+ and all its models from the `/model` selector. Useful when a LangChain
180
+ provider package is installed as a transitive dependency but should not
181
+ be user-visible.
182
+ """
183
+
184
+ models: list[str]
185
+ """List of model identifiers available from this provider."""
186
+
187
+ api_key_env: str
188
+ """Environment variable name containing the API key."""
189
+
190
+ base_url: str
191
+ """Custom base URL."""
192
+
193
+ # Level 2: arbitrary BaseChatModel classes
194
+
195
+ class_path: str
196
+ """Fully-qualified Python class in `module.path:ClassName` format.
197
+
198
+ When set, `create_model` imports this class and instantiates it directly
199
+ instead of calling `init_chat_model`.
200
+ """
201
+
202
+ params: dict[str, Any]
203
+ """Extra keyword arguments forwarded to the model constructor.
204
+
205
+ Flat keys (e.g., `temperature = 0`) are provider-wide defaults applied to
206
+ every model from this provider. Model-keyed sub-tables (e.g.,
207
+ `[params."qwen3:4b"]`) override individual values for that model only;
208
+ the merge is shallow (model wins on conflict).
209
+ """
210
+
211
+ profile: dict[str, Any]
212
+ """Overrides merged into the model's runtime profile dict.
213
+
214
+ Flat keys (e.g., `max_input_tokens = 4096`) are provider-wide defaults.
215
+ Model-keyed sub-tables (e.g., `[profile."claude-sonnet-4-5"]`) override
216
+ individual values for that model only; the merge is shallow.
217
+ """
218
+
219
+
220
+ DEFAULT_CONFIG_DIR = Path.home() / ".docagent"
221
+ """Directory for user-level Deep Agents configuration (`~/.docagent`)."""
222
+
223
+ DEFAULT_CONFIG_PATH = DEFAULT_CONFIG_DIR / "config.toml"
224
+ """Path to the user's model configuration file (`~/.docagent/config.toml`)."""
225
+
226
+ PROVIDER_API_KEY_ENV: dict[str, str] = {
227
+ "anthropic": "ANTHROPIC_API_KEY",
228
+ "azure_openai": "AZURE_OPENAI_API_KEY",
229
+ "baseten": "BASETEN_API_KEY",
230
+ "cohere": "COHERE_API_KEY",
231
+ "deepseek": "DEEPSEEK_API_KEY",
232
+ "fireworks": "FIREWORKS_API_KEY",
233
+ "google_genai": "GOOGLE_API_KEY",
234
+ "google_vertexai": "GOOGLE_CLOUD_PROJECT",
235
+ "groq": "GROQ_API_KEY",
236
+ "huggingface": "HUGGINGFACEHUB_API_TOKEN",
237
+ "ibm": "WATSONX_APIKEY",
238
+ "litellm": "LITELLM_API_KEY",
239
+ "mistralai": "MISTRAL_API_KEY",
240
+ "nvidia": "NVIDIA_API_KEY",
241
+ "openai": "OPENAI_API_KEY",
242
+ "openrouter": "OPENROUTER_API_KEY",
243
+ "perplexity": "PPLX_API_KEY",
244
+ "together": "TOGETHER_API_KEY",
245
+ "xai": "XAI_API_KEY",
246
+ }
247
+ """Well-known providers mapped to the env var that holds their API key.
248
+
249
+ Used by `has_provider_credentials` to verify credentials *before* model
250
+ creation, so the UI can show a warning icon and a specific error message
251
+ (e.g., "ANTHROPIC_API_KEY not set") instead of letting the provider fail at call
252
+ time.
253
+
254
+ Providers not listed here fall through to the config-file check or the langchain
255
+ registry fallback.
256
+ """
257
+
258
+
259
+ # Module-level caches — cleared by `clear_caches()`.
260
+ _available_models_cache: dict[str, list[str]] | None = None
261
+ _builtin_providers_cache: dict[str, Any] | None = None
262
+ _default_config_cache: ModelConfig | None = None
263
+ _provider_profiles_cache: dict[str, dict[str, Any]] = {}
264
+ _provider_profiles_lock = threading.Lock()
265
+ _profiles_cache: Mapping[str, ModelProfileEntry] | None = None
266
+ _profiles_override_cache: tuple[int, Mapping[str, ModelProfileEntry]] | None = None
267
+
268
+
269
+ def clear_caches() -> None:
270
+ """Reset module-level caches so the next call recomputes from scratch.
271
+
272
+ Intended for tests and for the `/reload` command.
273
+ """
274
+ global _available_models_cache, _builtin_providers_cache, _default_config_cache, _profiles_cache, _profiles_override_cache # noqa: PLW0603, E501 # Module-level caches require global statement
275
+ _available_models_cache = None
276
+ _builtin_providers_cache = None
277
+ _default_config_cache = None
278
+ _provider_profiles_cache.clear()
279
+ _profiles_cache = None
280
+ _profiles_override_cache = None
281
+ invalidate_thread_config_cache()
282
+
283
+
284
+ def _get_builtin_providers() -> dict[str, Any]:
285
+ """Return langchain's built-in provider registry.
286
+
287
+ Tries the newer `_BUILTIN_PROVIDERS` name first, then falls back to
288
+ the legacy `_SUPPORTED_PROVIDERS` for older langchain versions.
289
+
290
+ Results are cached after the first call; use `clear_caches()` to reset.
291
+
292
+ Returns:
293
+ The provider registry dict from `langchain.chat_models.base`.
294
+ """
295
+ global _builtin_providers_cache # noqa: PLW0603 # Module-level cache requires global statement
296
+ if _builtin_providers_cache is not None:
297
+ return _builtin_providers_cache
298
+
299
+ # Deferred: langchain.chat_models pulls in heavy provider registry,
300
+ # only needed when resolving provider names for model config.
301
+ from langchain.chat_models import base
302
+
303
+ registry: dict[str, Any] | None = getattr(base, "_BUILTIN_PROVIDERS", None)
304
+ if registry is None:
305
+ registry = getattr(base, "_SUPPORTED_PROVIDERS", None)
306
+ _builtin_providers_cache = registry if registry is not None else {}
307
+ return _builtin_providers_cache
308
+
309
+
310
+ def _get_provider_profile_modules() -> list[tuple[str, str]]:
311
+ """Build a `(provider, profile_module)` list from langchain's provider registry.
312
+
313
+ Reads the built-in provider registry from `langchain.chat_models.base`
314
+ to discover every provider that `init_chat_model` knows about, then derives
315
+ the `<package>.data._profiles` module path for each.
316
+
317
+ Returns:
318
+ List of `(provider_name, profile_module_path)` tuples.
319
+ """
320
+ providers = _get_builtin_providers()
321
+
322
+ result: list[tuple[str, str]] = []
323
+ seen: set[tuple[str, str]] = set()
324
+
325
+ for provider_name, (module_path, *_rest) in providers.items():
326
+ package_root = module_path.split(".", maxsplit=1)[0]
327
+ profile_module = f"{package_root}.data._profiles"
328
+ key = (provider_name, profile_module)
329
+ if key not in seen:
330
+ seen.add(key)
331
+ result.append((provider_name, profile_module))
332
+
333
+ return result
334
+
335
+
336
+ def _load_provider_profiles(module_path: str) -> dict[str, Any]:
337
+ """Load `_PROFILES` from a provider's data module.
338
+
339
+ Results are cached by `module_path` so repeated calls (e.g., from both
340
+ `get_available_models` and `get_model_profiles`) reuse the same dict.
341
+ Use `clear_caches()` to reset.
342
+
343
+ Locates the package on disk with `importlib.util.find_spec` and loads *only*
344
+ the `_profiles.py` file via `spec_from_file_location`.
345
+
346
+ Args:
347
+ module_path: Dotted module path (e.g., `"langchain_openai.data._profiles"`).
348
+
349
+ Returns:
350
+ The `_PROFILES` dictionary from the module, or an empty dict if
351
+ the module has no such attribute.
352
+
353
+ Raises:
354
+ ImportError: If the package is not installed or the profile module
355
+ cannot be found on disk.
356
+ """
357
+ with _provider_profiles_lock:
358
+ cached = _provider_profiles_cache.get(module_path)
359
+ if cached is not None: # `is not None` so empty profile dicts are cached
360
+ return cached
361
+
362
+ parts = module_path.split(".")
363
+ package_root = parts[0]
364
+
365
+ spec = importlib.util.find_spec(package_root)
366
+ if spec is None:
367
+ msg = f"Package {package_root} is not installed"
368
+ raise ImportError(msg)
369
+
370
+ # Determine the package directory from the spec.
371
+ if spec.origin:
372
+ package_dir = Path(spec.origin).parent
373
+ elif spec.submodule_search_locations:
374
+ package_dir = Path(next(iter(spec.submodule_search_locations)))
375
+ else:
376
+ msg = f"Cannot determine location for {package_root}"
377
+ raise ImportError(msg)
378
+
379
+ # Build the path to the target file (e.g., data/_profiles.py).
380
+ relative_parts = parts[1:] # ["data", "_profiles"]
381
+ profiles_path = package_dir.joinpath(
382
+ *relative_parts[:-1], f"{relative_parts[-1]}.py"
383
+ )
384
+
385
+ if not profiles_path.exists():
386
+ msg = f"Profile module not found: {profiles_path}"
387
+ raise ImportError(msg)
388
+
389
+ file_spec = importlib.util.spec_from_file_location(module_path, profiles_path)
390
+ if file_spec is None or file_spec.loader is None:
391
+ msg = f"Could not create module spec for {profiles_path}"
392
+ raise ImportError(msg)
393
+
394
+ module = importlib.util.module_from_spec(file_spec)
395
+ file_spec.loader.exec_module(module)
396
+ profiles = getattr(module, "_PROFILES", {})
397
+ _provider_profiles_cache[module_path] = profiles
398
+ return profiles
399
+
400
+
401
+ def _profile_module_from_class_path(class_path: str) -> str | None:
402
+ """Derive the profile module path from a `class_path` config value.
403
+
404
+ Args:
405
+ class_path: Fully-qualified class in `module.path:ClassName` format.
406
+
407
+ Returns:
408
+ Dotted module path like `langchain_baseten.data._profiles`, or None
409
+ if `class_path` is malformed.
410
+ """
411
+ if ":" not in class_path:
412
+ return None
413
+ module_part, _ = class_path.split(":", 1)
414
+ package_root = module_part.split(".", maxsplit=1)[0]
415
+ if not package_root:
416
+ return None
417
+ return f"{package_root}.data._profiles"
418
+
419
+
420
+ def get_available_models() -> dict[str, list[str]]:
421
+ """Get available models dynamically from installed LangChain provider packages.
422
+
423
+ Imports model profiles from each provider package and extracts model names.
424
+
425
+ Results are cached after the first call; use `clear_caches()` to reset.
426
+
427
+ Returns:
428
+ Dictionary mapping provider names to lists of model identifiers.
429
+ Includes providers from the langchain registry, config-file
430
+ providers with explicit model lists, and `class_path` providers
431
+ whose packages expose a `_profiles` module.
432
+ """
433
+ global _available_models_cache # noqa: PLW0603 # Module-level cache requires global statement
434
+ if _available_models_cache is not None:
435
+ return _available_models_cache
436
+
437
+ available: dict[str, list[str]] = {}
438
+ config = ModelConfig.load()
439
+
440
+ # Try to load from langchain provider profile data.
441
+ # Build the list dynamically from langchain's supported-provider registry
442
+ # so new providers are picked up automatically when langchain adds them.
443
+ provider_modules = _get_provider_profile_modules()
444
+ registry_providers: set[str] = set()
445
+
446
+ for provider, module_path in provider_modules:
447
+ registry_providers.add(provider)
448
+ # Skip providers explicitly disabled in config.
449
+ if not config.is_provider_enabled(provider):
450
+ logger.debug(
451
+ "Provider '%s' is disabled in config; skipping registry discovery",
452
+ provider,
453
+ )
454
+ continue
455
+ try:
456
+ profiles = _load_provider_profiles(module_path)
457
+ except ImportError:
458
+ logger.debug(
459
+ "Could not import profiles from %s (package may not be installed)",
460
+ module_path,
461
+ )
462
+ continue
463
+ except Exception:
464
+ logger.warning(
465
+ "Failed to load profiles from %s, skipping provider '%s'",
466
+ module_path,
467
+ provider,
468
+ exc_info=True,
469
+ )
470
+ continue
471
+
472
+ # Filter to models that support tool calling and text I/O.
473
+ models = [
474
+ name
475
+ for name, profile in profiles.items()
476
+ if profile.get("tool_calling", False)
477
+ and profile.get("text_inputs", True) is not False
478
+ and profile.get("text_outputs", True) is not False
479
+ ]
480
+
481
+ models.sort()
482
+ if models:
483
+ available[provider] = models
484
+
485
+ # Merge in models from config file (custom providers like ollama, fireworks)
486
+ for provider_name, provider_config in config.providers.items():
487
+ # Respect enabled = false (hide provider entirely).
488
+ if not config.is_provider_enabled(provider_name):
489
+ logger.debug(
490
+ "Provider '%s' is disabled in config; skipping",
491
+ provider_name,
492
+ )
493
+ continue
494
+
495
+ config_models = list(provider_config.get("models", []))
496
+
497
+ # For class_path providers not in the built-in registry, auto-discover
498
+ # models from the package's _profiles.py when no explicit models list.
499
+ if (
500
+ not config_models
501
+ and provider_name not in registry_providers
502
+ and provider_name not in available
503
+ ):
504
+ class_path = provider_config.get("class_path", "")
505
+ profile_module = _profile_module_from_class_path(class_path)
506
+ if profile_module:
507
+ try:
508
+ profiles = _load_provider_profiles(profile_module)
509
+ except ImportError:
510
+ logger.debug(
511
+ "Could not import profiles from %s for class_path "
512
+ "provider '%s' (package may not be installed)",
513
+ profile_module,
514
+ provider_name,
515
+ )
516
+ except Exception:
517
+ logger.warning(
518
+ "Failed to load profiles from %s for class_path provider '%s'",
519
+ profile_module,
520
+ provider_name,
521
+ exc_info=True,
522
+ )
523
+ else:
524
+ config_models = sorted(
525
+ name
526
+ for name, profile in profiles.items()
527
+ if profile.get("tool_calling", False)
528
+ and profile.get("text_inputs", True) is not False
529
+ and profile.get("text_outputs", True) is not False
530
+ )
531
+
532
+ if provider_name not in available:
533
+ if config_models:
534
+ available[provider_name] = config_models
535
+ else:
536
+ # Append any config models not already discovered
537
+ existing = set(available[provider_name])
538
+ for model in config_models:
539
+ if model not in existing:
540
+ available[provider_name].append(model)
541
+
542
+ _available_models_cache = available
543
+ return available
544
+
545
+
546
+ def _build_entry(
547
+ base: dict[str, Any],
548
+ overrides: dict[str, Any],
549
+ cli_override: dict[str, Any] | None,
550
+ ) -> ModelProfileEntry:
551
+ """Build a profile entry by merging base, overrides, and CLI override.
552
+
553
+ Args:
554
+ base: Upstream profile dict (empty for config-only models).
555
+ overrides: `config.toml` profile overrides.
556
+ cli_override: Extra fields from `--profile-override`.
557
+
558
+ Returns:
559
+ Profile entry with merged data and override tracking.
560
+ """
561
+ merged = {**base, **overrides}
562
+ overridden_keys = set(overrides)
563
+ if cli_override:
564
+ merged = {**merged, **cli_override}
565
+ overridden_keys |= set(cli_override)
566
+ return ModelProfileEntry(
567
+ profile=merged,
568
+ overridden_keys=frozenset(overridden_keys),
569
+ )
570
+
571
+
572
+ def get_model_profiles(
573
+ *,
574
+ cli_override: dict[str, Any] | None = None,
575
+ ) -> Mapping[str, ModelProfileEntry]:
576
+ """Load upstream profiles merged with config.toml overrides.
577
+
578
+ Keyed by `provider:model` spec string. Each entry contains the
579
+ merged profile dict and the set of keys overridden by config.toml.
580
+
581
+ Unlike `get_available_models()`, this includes all models from upstream
582
+ profiles regardless of capability filters (tool calling, text I/O).
583
+
584
+ Results are cached; use `clear_caches()` to reset. When `cli_override` is
585
+ provided the result is stored in a single-slot cache keyed by
586
+ `id(cli_override)`. This relies on the caller retaining the same dict
587
+ object for the session (the CLI stores it once on the app instance);
588
+ passing a different dict with the same contents will bypass the cache
589
+ and overwrite the previous entry.
590
+
591
+ Args:
592
+ cli_override: Extra profile fields from `--profile-override`.
593
+
594
+ When provided, these are merged on top of every profile entry
595
+ (after upstream + config.toml) and their keys are added to
596
+ `overridden_keys`.
597
+
598
+ Returns:
599
+ Read-only mapping of spec strings to profile entries.
600
+ """
601
+ global _profiles_cache, _profiles_override_cache # noqa: PLW0603 # Module-level caches require global statement
602
+ if cli_override is None and _profiles_cache is not None:
603
+ return _profiles_cache
604
+ if cli_override is not None and _profiles_override_cache is not None:
605
+ cached_id, cached_result = _profiles_override_cache
606
+ if cached_id == id(cli_override):
607
+ return cached_result
608
+
609
+ result: dict[str, ModelProfileEntry] = {}
610
+ config = ModelConfig.load()
611
+
612
+ # Collect upstream profiles from provider packages.
613
+ seen_specs: set[str] = set()
614
+ provider_modules = _get_provider_profile_modules()
615
+ registry_providers: set[str] = set()
616
+ for provider, module_path in provider_modules:
617
+ registry_providers.add(provider)
618
+ # Skip providers explicitly disabled in config.
619
+ if not config.is_provider_enabled(provider):
620
+ logger.debug(
621
+ "Provider '%s' is disabled in config; skipping profiles",
622
+ provider,
623
+ )
624
+ continue
625
+ try:
626
+ profiles = _load_provider_profiles(module_path)
627
+ except ImportError:
628
+ logger.debug(
629
+ "Could not import profiles from %s for provider '%s'",
630
+ module_path,
631
+ provider,
632
+ )
633
+ continue
634
+ except Exception:
635
+ logger.warning(
636
+ "Failed to load profiles from %s for provider '%s'",
637
+ module_path,
638
+ provider,
639
+ exc_info=True,
640
+ )
641
+ continue
642
+
643
+ for model_name, upstream_profile in profiles.items():
644
+ spec = f"{provider}:{model_name}"
645
+ seen_specs.add(spec)
646
+ overrides = config.get_profile_overrides(provider, model_name=model_name)
647
+ result[spec] = _build_entry(upstream_profile, overrides, cli_override)
648
+
649
+ # Add config-only models and class_path provider profiles.
650
+ for provider_name, provider_config in config.providers.items():
651
+ if not config.is_provider_enabled(provider_name):
652
+ logger.debug(
653
+ "Provider '%s' is disabled in config; skipping profiles",
654
+ provider_name,
655
+ )
656
+ continue
657
+ # For class_path providers not in the built-in registry, load
658
+ # upstream profiles from the package's _profiles.py.
659
+ if provider_name not in registry_providers:
660
+ class_path = provider_config.get("class_path", "")
661
+ profile_module = _profile_module_from_class_path(class_path)
662
+ if profile_module:
663
+ try:
664
+ pkg_profiles = _load_provider_profiles(profile_module)
665
+ except ImportError:
666
+ logger.debug(
667
+ "Could not import profiles from %s for class_path "
668
+ "provider '%s' (package may not be installed)",
669
+ profile_module,
670
+ provider_name,
671
+ )
672
+ except Exception:
673
+ logger.warning(
674
+ "Failed to load profiles from %s for class_path provider '%s'",
675
+ profile_module,
676
+ provider_name,
677
+ exc_info=True,
678
+ )
679
+ else:
680
+ for model_name, upstream_profile in pkg_profiles.items():
681
+ spec = f"{provider_name}:{model_name}"
682
+ seen_specs.add(spec)
683
+ overrides = config.get_profile_overrides(
684
+ provider_name, model_name=model_name
685
+ )
686
+ result[spec] = _build_entry(
687
+ upstream_profile, overrides, cli_override
688
+ )
689
+
690
+ config_models = provider_config.get("models", [])
691
+ for model_name in config_models:
692
+ spec = f"{provider_name}:{model_name}"
693
+ if spec not in seen_specs:
694
+ overrides = config.get_profile_overrides(
695
+ provider_name, model_name=model_name
696
+ )
697
+ result[spec] = _build_entry({}, overrides, cli_override)
698
+
699
+ frozen = MappingProxyType(result)
700
+ if cli_override is None:
701
+ _profiles_cache = frozen
702
+ else:
703
+ _profiles_override_cache = (id(cli_override), frozen)
704
+ return frozen
705
+
706
+
707
+ def has_provider_credentials(provider: str) -> bool | None:
708
+ """Check if credentials are available for a provider.
709
+
710
+ Resolution order:
711
+
712
+ 1. Config-file providers (`config.toml`) with `api_key_env` — takes
713
+ priority so user overrides are respected.
714
+ 2. Config-file providers with `class_path` but no `api_key_env` —
715
+ assumed to manage their own auth (e.g., custom headers, JWT, mTLS).
716
+ 3. Hardcoded `PROVIDER_API_KEY_ENV` mapping (anthropic, openai, etc.).
717
+ 4. For any other provider (e.g., third-party langchain provider
718
+ packages), credential status is unknown — the provider itself will
719
+ report auth failures at model-creation time.
720
+
721
+ Args:
722
+ provider: Provider name.
723
+
724
+ Returns:
725
+ True if credentials are confirmed available or the provider is
726
+ expected to manage its own auth (e.g., `class_path` providers),
727
+ False if confirmed missing, or None if credential status cannot
728
+ be determined.
729
+ """
730
+ # Config-file providers take priority when api_key_env is specified.
731
+ config = ModelConfig.load()
732
+ provider_config = config.providers.get(provider)
733
+ if provider_config:
734
+ result = config.has_credentials(provider)
735
+ if result is not None:
736
+ return result
737
+ # class_path providers that omit api_key_env manage their own auth
738
+ # (e.g., custom headers, JWT, mTLS) — treat as available.
739
+ if provider_config.get("class_path"):
740
+ return True
741
+ # No api_key_env in config — fall through to hardcoded map.
742
+
743
+ # Fall back to hardcoded well-known providers.
744
+ env_var = PROVIDER_API_KEY_ENV.get(provider)
745
+ if env_var:
746
+ return bool(resolve_env_var(env_var))
747
+
748
+ # Provider not found in config or hardcoded map — credential status is
749
+ # unknown. The provider itself will report auth failures at
750
+ # model-creation time.
751
+ logger.debug(
752
+ "No credential information for provider '%s'; deferring auth to provider",
753
+ provider,
754
+ )
755
+ return None
756
+
757
+
758
+ def get_credential_env_var(provider: str) -> str | None:
759
+ """Return the env var name that holds credentials for a provider.
760
+
761
+ Checks the config file first (user override), then falls back to the
762
+ hardcoded `PROVIDER_API_KEY_ENV` map.
763
+
764
+ Args:
765
+ provider: Provider name.
766
+
767
+ Returns:
768
+ Environment variable name, or None if unknown.
769
+ """
770
+ config = ModelConfig.load()
771
+ config_env = config.get_api_key_env(provider)
772
+ if config_env:
773
+ return config_env
774
+ return PROVIDER_API_KEY_ENV.get(provider)
775
+
776
+
777
+ @dataclass(frozen=True)
778
+ class ModelConfig:
779
+ """Parsed model configuration from `config.toml`.
780
+
781
+ Instances are immutable once constructed. The `providers` mapping is
782
+ wrapped in `MappingProxyType` to prevent accidental mutation of the
783
+ globally cached singleton returned by `load()`.
784
+ """
785
+
786
+ default_model: str | None = None
787
+ """The user's intentional default model (from config file `[models].default`)."""
788
+
789
+ recent_model: str | None = None
790
+ """The most recently switched-to model (from config file `[models].recent`)."""
791
+
792
+ providers: Mapping[str, ProviderConfig] = field(default_factory=dict)
793
+ """Read-only mapping of provider names to their configurations."""
794
+
795
+ def __post_init__(self) -> None:
796
+ """Freeze the providers dict into a read-only proxy."""
797
+ if not isinstance(self.providers, MappingProxyType):
798
+ object.__setattr__(self, "providers", MappingProxyType(self.providers))
799
+
800
+ @classmethod
801
+ def load(cls, config_path: Path | None = None) -> ModelConfig:
802
+ """Load config from file.
803
+
804
+ When called with the default path, results are cached for the
805
+ lifetime of the process. Use `clear_caches()` to reset.
806
+
807
+ Args:
808
+ config_path: Path to config file. Defaults to ~/.docagent/config.toml.
809
+
810
+ Returns:
811
+ Parsed `ModelConfig` instance.
812
+ Returns empty config if file is missing, unreadable, or contains
813
+ invalid TOML syntax.
814
+ """
815
+ global _default_config_cache # noqa: PLW0603 # Module-level cache requires global statement
816
+ is_default = config_path is None
817
+ if is_default and _default_config_cache is not None:
818
+ return _default_config_cache
819
+
820
+ if config_path is None:
821
+ config_path = DEFAULT_CONFIG_PATH
822
+
823
+ if not config_path.exists():
824
+ fallback = cls()
825
+ if is_default:
826
+ _default_config_cache = fallback
827
+ return fallback
828
+
829
+ try:
830
+ with config_path.open("rb") as f:
831
+ data = tomllib.load(f)
832
+ except tomllib.TOMLDecodeError as e:
833
+ logger.warning(
834
+ "Config file %s has invalid TOML syntax: %s. "
835
+ "Ignoring config file. Fix the file or delete it to reset.",
836
+ config_path,
837
+ e,
838
+ )
839
+ fallback = cls()
840
+ if is_default:
841
+ _default_config_cache = fallback
842
+ return fallback
843
+ except (PermissionError, OSError) as e:
844
+ logger.warning("Could not read config file %s: %s", config_path, e)
845
+ fallback = cls()
846
+ if is_default:
847
+ _default_config_cache = fallback
848
+ return fallback
849
+
850
+ models_section = data.get("models", {})
851
+ config = cls(
852
+ default_model=models_section.get("default"),
853
+ recent_model=models_section.get("recent"),
854
+ providers=models_section.get("providers", {}),
855
+ )
856
+
857
+ # Validate config consistency
858
+ config._validate()
859
+
860
+ if is_default:
861
+ _default_config_cache = config
862
+
863
+ return config
864
+
865
+ def _validate(self) -> None:
866
+ """Validate internal consistency of the config.
867
+
868
+ Issues warnings for invalid configurations but does not raise exceptions,
869
+ allowing the app to continue with potentially degraded functionality.
870
+ """
871
+ # Warn if default_model is set but doesn't use provider:model format
872
+ if self.default_model and ":" not in self.default_model:
873
+ logger.warning(
874
+ "default_model '%s' should use provider:model format "
875
+ "(e.g., 'anthropic:claude-sonnet-4-5')",
876
+ self.default_model,
877
+ )
878
+
879
+ # Warn if recent_model is set but doesn't use provider:model format
880
+ if self.recent_model and ":" not in self.recent_model:
881
+ logger.warning(
882
+ "recent_model '%s' should use provider:model format "
883
+ "(e.g., 'anthropic:claude-sonnet-4-5')",
884
+ self.recent_model,
885
+ )
886
+
887
+ # Validate enabled field type and class_path format / params references
888
+ for name, provider in self.providers.items():
889
+ enabled = provider.get("enabled")
890
+ if enabled is not None and not isinstance(enabled, bool):
891
+ logger.warning(
892
+ "Provider '%s' has non-boolean 'enabled' value %r "
893
+ "(expected true/false). Provider will remain visible.",
894
+ name,
895
+ enabled,
896
+ )
897
+
898
+ class_path = provider.get("class_path")
899
+ if class_path and ":" not in class_path:
900
+ logger.warning(
901
+ "Provider '%s' has invalid class_path '%s': "
902
+ "must be in module.path:ClassName format "
903
+ "(e.g., 'my_package.models:MyChatModel')",
904
+ name,
905
+ class_path,
906
+ )
907
+
908
+ models = set(provider.get("models", []))
909
+
910
+ params = provider.get("params", {})
911
+ for key, value in params.items():
912
+ if isinstance(value, dict) and key not in models:
913
+ logger.warning(
914
+ "Provider '%s' has params for '%s' "
915
+ "which is not in its models list",
916
+ name,
917
+ key,
918
+ )
919
+
920
+ def is_provider_enabled(self, provider_name: str) -> bool:
921
+ """Check whether a provider should appear in the model switcher.
922
+
923
+ A provider is disabled when its config explicitly sets
924
+ `enabled = false`. Providers not present in the config file are
925
+ always considered enabled.
926
+
927
+ Args:
928
+ provider_name: The provider to check.
929
+
930
+ Returns:
931
+ `False` if the provider is explicitly disabled, `True` otherwise.
932
+ """
933
+ provider = self.providers.get(provider_name)
934
+ if not provider:
935
+ return True
936
+ return provider.get("enabled") is not False
937
+
938
+ def get_all_models(self) -> list[tuple[str, str]]:
939
+ """Get all models as `(model_name, provider_name)` tuples.
940
+
941
+ Returns raw config data — does not filter by `is_provider_enabled`.
942
+ For the filtered set shown in the model switcher, use
943
+ `get_available_models()`.
944
+
945
+ Returns:
946
+ List of tuples containing `(model_name, provider_name)`.
947
+ """
948
+ return [
949
+ (model, provider_name)
950
+ for provider_name, provider_config in self.providers.items()
951
+ for model in provider_config.get("models", [])
952
+ ]
953
+
954
+ def get_provider_for_model(self, model_name: str) -> str | None:
955
+ """Find the provider that contains this model.
956
+
957
+ Returns raw config data — does not filter by `is_provider_enabled`.
958
+
959
+ Args:
960
+ model_name: The model identifier to look up.
961
+
962
+ Returns:
963
+ Provider name if found, None otherwise.
964
+ """
965
+ for provider_name, provider_config in self.providers.items():
966
+ if model_name in provider_config.get("models", []):
967
+ return provider_name
968
+ return None
969
+
970
+ def has_credentials(self, provider_name: str) -> bool | None:
971
+ """Check if credentials are available for a provider.
972
+
973
+ This is the config-file-driven credential check, supporting custom
974
+ providers (e.g., local Ollama with no key required). For the hardcoded
975
+ `PROVIDER_API_KEY_ENV`-based check used in the hot-swap path, see the
976
+ module-level `has_provider_credentials()`.
977
+
978
+ Args:
979
+ provider_name: The provider to check.
980
+
981
+ Returns:
982
+ True if credentials are confirmed available, False if confirmed
983
+ missing, or None if no `api_key_env` is configured and
984
+ credential status cannot be determined.
985
+ """
986
+ provider = self.providers.get(provider_name)
987
+ if not provider:
988
+ return False
989
+ env_var = provider.get("api_key_env")
990
+ if not env_var:
991
+ return None # No key configured — can't verify
992
+ return bool(resolve_env_var(env_var))
993
+
994
+ def get_base_url(self, provider_name: str) -> str | None:
995
+ """Get custom base URL.
996
+
997
+ Args:
998
+ provider_name: The provider to get base URL for.
999
+
1000
+ Returns:
1001
+ Base URL if configured, None otherwise.
1002
+ """
1003
+ provider = self.providers.get(provider_name)
1004
+ return provider.get("base_url") if provider else None
1005
+
1006
+ def get_api_key_env(self, provider_name: str) -> str | None:
1007
+ """Get the environment variable name for a provider's API key.
1008
+
1009
+ Args:
1010
+ provider_name: The provider to get API key env var for.
1011
+
1012
+ Returns:
1013
+ Environment variable name if configured, None otherwise.
1014
+ """
1015
+ provider = self.providers.get(provider_name)
1016
+ return provider.get("api_key_env") if provider else None
1017
+
1018
+ def get_class_path(self, provider_name: str) -> str | None:
1019
+ """Get the custom class path for a provider.
1020
+
1021
+ Args:
1022
+ provider_name: The provider to look up.
1023
+
1024
+ Returns:
1025
+ Class path in `module.path:ClassName` format, or None.
1026
+ """
1027
+ provider = self.providers.get(provider_name)
1028
+ return provider.get("class_path") if provider else None
1029
+
1030
+ def get_kwargs(
1031
+ self, provider_name: str, *, model_name: str | None = None
1032
+ ) -> dict[str, Any]:
1033
+ """Get extra constructor kwargs for a provider.
1034
+
1035
+ Reads the `params` table from the provider config. Flat keys are
1036
+ provider-wide defaults; model-keyed sub-tables are per-model
1037
+ overrides that shallow-merge on top (model wins on conflict).
1038
+
1039
+ Args:
1040
+ provider_name: The provider to look up.
1041
+ model_name: Optional model name for per-model overrides.
1042
+
1043
+ Returns:
1044
+ Dictionary of extra kwargs (empty if none configured).
1045
+ """
1046
+ provider = self.providers.get(provider_name)
1047
+ if not provider:
1048
+ return {}
1049
+ params = provider.get("params", {})
1050
+ result = {k: v for k, v in params.items() if not isinstance(v, dict)}
1051
+ if model_name:
1052
+ overrides = params.get(model_name)
1053
+ if isinstance(overrides, dict):
1054
+ result.update(overrides)
1055
+ return result
1056
+
1057
+ def get_profile_overrides(
1058
+ self, provider_name: str, *, model_name: str | None = None
1059
+ ) -> dict[str, Any]:
1060
+ """Get profile overrides for a provider.
1061
+
1062
+ Reads the `profile` table from the provider config. Flat keys are
1063
+ provider-wide defaults; model-keyed sub-tables are per-model overrides
1064
+ that shallow-merge on top (model wins on conflict).
1065
+
1066
+ Args:
1067
+ provider_name: The provider to look up.
1068
+ model_name: Optional model name for per-model overrides.
1069
+
1070
+ Returns:
1071
+ Dictionary of profile overrides (empty if none configured).
1072
+ """
1073
+ provider = self.providers.get(provider_name)
1074
+ if not provider:
1075
+ return {}
1076
+ profile = provider.get("profile", {})
1077
+ result = {k: v for k, v in profile.items() if not isinstance(v, dict)}
1078
+ if model_name:
1079
+ overrides = profile.get(model_name)
1080
+ if isinstance(overrides, dict):
1081
+ result.update(overrides)
1082
+ return result
1083
+
1084
+
1085
+ def _save_model_field(
1086
+ field: str, model_spec: str, config_path: Path | None = None
1087
+ ) -> bool:
1088
+ """Read-modify-write a `[models].<field>` key in the config file.
1089
+
1090
+ Args:
1091
+ field: Key name under the `[models]` table (e.g., `'default'` or `'recent'`).
1092
+ model_spec: The model to save in `provider:model` format.
1093
+ config_path: Path to config file.
1094
+
1095
+ Defaults to `~/.docagent/config.toml`.
1096
+
1097
+ Returns:
1098
+ True if save succeeded, False if it failed due to I/O errors.
1099
+ """
1100
+ if config_path is None:
1101
+ config_path = DEFAULT_CONFIG_PATH
1102
+
1103
+ try:
1104
+ config_path.parent.mkdir(parents=True, exist_ok=True)
1105
+
1106
+ # Read existing config or start fresh
1107
+ if config_path.exists():
1108
+ with config_path.open("rb") as f:
1109
+ data = tomllib.load(f)
1110
+ else:
1111
+ data = {}
1112
+
1113
+ if "models" not in data:
1114
+ data["models"] = {}
1115
+ data["models"][field] = model_spec
1116
+
1117
+ # Write to temp file then rename to prevent corruption if write is interrupted
1118
+ fd, tmp_path = tempfile.mkstemp(dir=config_path.parent, suffix=".tmp")
1119
+ try:
1120
+ with os.fdopen(fd, "wb") as f:
1121
+ tomli_w.dump(data, f)
1122
+ Path(tmp_path).replace(config_path)
1123
+ except BaseException:
1124
+ # Clean up temp file on any failure
1125
+ with contextlib.suppress(OSError):
1126
+ Path(tmp_path).unlink()
1127
+ raise
1128
+ except (OSError, tomllib.TOMLDecodeError):
1129
+ logger.exception("Could not save %s model preference", field)
1130
+ return False
1131
+ else:
1132
+ # Invalidate config cache so the next load() picks up the change.
1133
+ global _default_config_cache # noqa: PLW0603 # Module-level cache requires global statement
1134
+ _default_config_cache = None
1135
+ return True
1136
+
1137
+
1138
+ def save_default_model(model_spec: str, config_path: Path | None = None) -> bool:
1139
+ """Update the default model in config file.
1140
+
1141
+ Reads existing config (if any), updates `[models].default`, and writes
1142
+ back using proper TOML serialization.
1143
+
1144
+ Args:
1145
+ model_spec: The model to set as default in `provider:model` format.
1146
+ config_path: Path to config file.
1147
+
1148
+ Defaults to `~/.docagent/config.toml`.
1149
+
1150
+ Returns:
1151
+ True if save succeeded, False if it failed due to I/O errors.
1152
+
1153
+ Note:
1154
+ This function does not preserve comments in the config file.
1155
+ """
1156
+ return _save_model_field("default", model_spec, config_path)
1157
+
1158
+
1159
+ def clear_default_model(config_path: Path | None = None) -> bool:
1160
+ """Remove the default model from the config file.
1161
+
1162
+ Deletes the `[models].default` key so that future launches fall back to
1163
+ `[models].recent` or environment auto-detection.
1164
+
1165
+ Args:
1166
+ config_path: Path to config file.
1167
+
1168
+ Defaults to `~/.docagent/config.toml`.
1169
+
1170
+ Returns:
1171
+ True if the key was removed (or was already absent), False on I/O error.
1172
+ """
1173
+ if config_path is None:
1174
+ config_path = DEFAULT_CONFIG_PATH
1175
+
1176
+ if not config_path.exists():
1177
+ return True # Nothing to clear
1178
+
1179
+ try:
1180
+ with config_path.open("rb") as f:
1181
+ data = tomllib.load(f)
1182
+
1183
+ models_section = data.get("models")
1184
+ if not isinstance(models_section, dict) or "default" not in models_section:
1185
+ return True # Already absent
1186
+
1187
+ del models_section["default"]
1188
+
1189
+ fd, tmp_path = tempfile.mkstemp(dir=config_path.parent, suffix=".tmp")
1190
+ try:
1191
+ with os.fdopen(fd, "wb") as f:
1192
+ tomli_w.dump(data, f)
1193
+ Path(tmp_path).replace(config_path)
1194
+ except BaseException:
1195
+ with contextlib.suppress(OSError):
1196
+ Path(tmp_path).unlink()
1197
+ raise
1198
+ except (OSError, tomllib.TOMLDecodeError):
1199
+ logger.exception("Could not clear default model preference")
1200
+ return False
1201
+ else:
1202
+ global _default_config_cache # noqa: PLW0603 # Module-level cache requires global statement
1203
+ _default_config_cache = None
1204
+ return True
1205
+
1206
+
1207
+ def is_warning_suppressed(key: str, config_path: Path | None = None) -> bool:
1208
+ """Check if a warning key is suppressed in the config file.
1209
+
1210
+ Reads the `[warnings].suppress` list from `config.toml` and checks
1211
+ whether `key` is present.
1212
+
1213
+ Args:
1214
+ key: Warning identifier to check (e.g., `'ripgrep'`).
1215
+ config_path: Path to config file.
1216
+
1217
+ Defaults to `~/.docagent/config.toml`.
1218
+
1219
+ Returns:
1220
+ `True` if the warning is suppressed, `False` otherwise (including
1221
+ when the file is missing, unreadable, or has no
1222
+ `[warnings]` section).
1223
+ """
1224
+ if config_path is None:
1225
+ config_path = DEFAULT_CONFIG_PATH
1226
+
1227
+ try:
1228
+ if not config_path.exists():
1229
+ return False
1230
+ with config_path.open("rb") as f:
1231
+ data = tomllib.load(f)
1232
+ except (OSError, tomllib.TOMLDecodeError):
1233
+ logger.debug(
1234
+ "Could not read config file %s for warning suppression check",
1235
+ config_path,
1236
+ exc_info=True,
1237
+ )
1238
+ return False
1239
+
1240
+ suppress_list = data.get("warnings", {}).get("suppress", [])
1241
+ if not isinstance(suppress_list, list):
1242
+ logger.debug(
1243
+ "[warnings].suppress in %s should be a list, got %s",
1244
+ config_path,
1245
+ type(suppress_list).__name__,
1246
+ )
1247
+ return False
1248
+ return key in suppress_list
1249
+
1250
+
1251
+ def suppress_warning(key: str, config_path: Path | None = None) -> bool:
1252
+ """Add a warning key to the suppression list in the config file.
1253
+
1254
+ Reads existing config (if any), adds `key` to `[warnings].suppress`,
1255
+ and writes back using atomic temp-file rename. Deduplicates entries.
1256
+
1257
+ Args:
1258
+ key: Warning identifier to suppress (e.g., `'ripgrep'`).
1259
+ config_path: Path to config file.
1260
+
1261
+ Defaults to `~/.docagent/config.toml`.
1262
+
1263
+ Returns:
1264
+ `True` if save succeeded, `False` if it failed due to I/O errors.
1265
+ """
1266
+ if config_path is None:
1267
+ config_path = DEFAULT_CONFIG_PATH
1268
+
1269
+ try:
1270
+ config_path.parent.mkdir(parents=True, exist_ok=True)
1271
+
1272
+ if config_path.exists():
1273
+ with config_path.open("rb") as f:
1274
+ data = tomllib.load(f)
1275
+ else:
1276
+ data = {}
1277
+
1278
+ if "warnings" not in data:
1279
+ data["warnings"] = {}
1280
+ suppress_list: list[str] = data["warnings"].get("suppress", [])
1281
+ if key not in suppress_list:
1282
+ suppress_list.append(key)
1283
+ data["warnings"]["suppress"] = suppress_list
1284
+
1285
+ fd, tmp_path = tempfile.mkstemp(dir=config_path.parent, suffix=".tmp")
1286
+ try:
1287
+ with os.fdopen(fd, "wb") as f:
1288
+ tomli_w.dump(data, f)
1289
+ Path(tmp_path).replace(config_path)
1290
+ except BaseException:
1291
+ with contextlib.suppress(OSError):
1292
+ Path(tmp_path).unlink()
1293
+ raise
1294
+ except (OSError, tomllib.TOMLDecodeError):
1295
+ logger.exception("Could not save warning suppression for '%s'", key)
1296
+ return False
1297
+ return True
1298
+
1299
+
1300
+ THREAD_COLUMN_DEFAULTS: dict[str, bool] = {
1301
+ "thread_id": False,
1302
+ "messages": True,
1303
+ "created_at": True,
1304
+ "updated_at": True,
1305
+ "git_branch": False,
1306
+ "cwd": False,
1307
+ "initial_prompt": True,
1308
+ "agent_name": False,
1309
+ }
1310
+ """Default visibility for thread selector columns."""
1311
+
1312
+
1313
+ class ThreadConfig(NamedTuple):
1314
+ """Coalesced thread-selector configuration read from a single TOML parse."""
1315
+
1316
+ columns: dict[str, bool]
1317
+ """Column visibility settings."""
1318
+
1319
+ relative_time: bool
1320
+ """Whether to display timestamps as relative time."""
1321
+
1322
+ sort_order: str
1323
+ """`'updated_at'` or `'created_at'`."""
1324
+
1325
+
1326
+ _thread_config_cache: ThreadConfig | None = None
1327
+
1328
+
1329
+ def load_thread_config(config_path: Path | None = None) -> ThreadConfig:
1330
+ """Load all thread-selector settings from one config file read.
1331
+
1332
+ Returns a cached result when reading the default config path. The
1333
+ prewarm worker calls this at startup so subsequent opens of the
1334
+ `/threads` modal avoid disk I/O entirely.
1335
+
1336
+ Args:
1337
+ config_path: Path to config file.
1338
+
1339
+ Returns:
1340
+ Coalesced thread configuration.
1341
+ """
1342
+ global _thread_config_cache # noqa: PLW0603 # Module-level cache requires global statement
1343
+
1344
+ if config_path is None:
1345
+ if _thread_config_cache is not None:
1346
+ return _thread_config_cache
1347
+ config_path = DEFAULT_CONFIG_PATH
1348
+ use_default = config_path == DEFAULT_CONFIG_PATH
1349
+
1350
+ columns = dict(THREAD_COLUMN_DEFAULTS)
1351
+ relative_time = True
1352
+ sort_order = "updated_at"
1353
+
1354
+ try:
1355
+ if not config_path.exists():
1356
+ result = ThreadConfig(columns, relative_time, sort_order)
1357
+ if use_default:
1358
+ _thread_config_cache = result
1359
+ return result
1360
+ with config_path.open("rb") as f:
1361
+ data = tomllib.load(f)
1362
+ threads_section = data.get("threads", {})
1363
+
1364
+ # columns
1365
+ raw_columns = threads_section.get("columns", {})
1366
+ if isinstance(raw_columns, dict):
1367
+ for key in columns:
1368
+ if key in raw_columns and isinstance(raw_columns[key], bool):
1369
+ columns[key] = raw_columns[key]
1370
+
1371
+ # relative_time
1372
+ rt_value = threads_section.get("relative_time")
1373
+ if isinstance(rt_value, bool):
1374
+ relative_time = rt_value
1375
+
1376
+ # sort_order
1377
+ so_value = threads_section.get("sort_order")
1378
+ if so_value in {"updated_at", "created_at"}:
1379
+ sort_order = so_value
1380
+ except (OSError, tomllib.TOMLDecodeError):
1381
+ logger.warning("Could not read thread config; using defaults", exc_info=True)
1382
+ # Do not cache on error — allow retry on next call in case the
1383
+ # file is fixed or permissions are restored.
1384
+ return ThreadConfig(columns, relative_time, sort_order)
1385
+
1386
+ result = ThreadConfig(columns, relative_time, sort_order)
1387
+ if use_default:
1388
+ _thread_config_cache = result
1389
+ return result
1390
+
1391
+
1392
+ def invalidate_thread_config_cache() -> None:
1393
+ """Clear the cached `ThreadConfig` so the next load re-reads disk."""
1394
+ global _thread_config_cache # noqa: PLW0603 # Module-level cache requires global statement
1395
+ _thread_config_cache = None
1396
+
1397
+
1398
+ def load_thread_columns(config_path: Path | None = None) -> dict[str, bool]:
1399
+ """Load thread column visibility from config file.
1400
+
1401
+ Args:
1402
+ config_path: Path to config file.
1403
+
1404
+ Returns:
1405
+ Dict mapping column names to visibility booleans.
1406
+ """
1407
+ if config_path is None:
1408
+ config_path = DEFAULT_CONFIG_PATH
1409
+
1410
+ result = dict(THREAD_COLUMN_DEFAULTS)
1411
+ try:
1412
+ if not config_path.exists():
1413
+ return result
1414
+ with config_path.open("rb") as f:
1415
+ data = tomllib.load(f)
1416
+ columns = data.get("threads", {}).get("columns", {})
1417
+ if isinstance(columns, dict):
1418
+ for key in result:
1419
+ if key in columns and isinstance(columns[key], bool):
1420
+ result[key] = columns[key]
1421
+ except (OSError, tomllib.TOMLDecodeError):
1422
+ logger.debug("Could not read thread column config", exc_info=True)
1423
+ return result
1424
+
1425
+
1426
+ def save_thread_columns(
1427
+ columns: dict[str, bool], config_path: Path | None = None
1428
+ ) -> bool:
1429
+ """Save thread column visibility to config file.
1430
+
1431
+ Args:
1432
+ columns: Dict mapping column names to visibility booleans.
1433
+ config_path: Path to config file.
1434
+
1435
+ Returns:
1436
+ True if save succeeded, False on I/O error.
1437
+ """
1438
+ if config_path is None:
1439
+ config_path = DEFAULT_CONFIG_PATH
1440
+
1441
+ try:
1442
+ config_path.parent.mkdir(parents=True, exist_ok=True)
1443
+
1444
+ if config_path.exists():
1445
+ with config_path.open("rb") as f:
1446
+ data = tomllib.load(f)
1447
+ else:
1448
+ data = {}
1449
+
1450
+ if "threads" not in data:
1451
+ data["threads"] = {}
1452
+ data["threads"]["columns"] = columns
1453
+
1454
+ fd, tmp_path = tempfile.mkstemp(dir=config_path.parent, suffix=".tmp")
1455
+ try:
1456
+ with os.fdopen(fd, "wb") as f:
1457
+ tomli_w.dump(data, f)
1458
+ Path(tmp_path).replace(config_path)
1459
+ except BaseException:
1460
+ with contextlib.suppress(OSError):
1461
+ Path(tmp_path).unlink()
1462
+ raise
1463
+ except (OSError, tomllib.TOMLDecodeError):
1464
+ logger.exception("Could not save thread column preferences")
1465
+ return False
1466
+ invalidate_thread_config_cache()
1467
+ return True
1468
+
1469
+
1470
+ def load_thread_relative_time(config_path: Path | None = None) -> bool:
1471
+ """Load the relative-time display preference for thread timestamps.
1472
+
1473
+ Args:
1474
+ config_path: Path to config file.
1475
+
1476
+ Returns:
1477
+ True if timestamps should display as relative time.
1478
+ """
1479
+ if config_path is None:
1480
+ config_path = DEFAULT_CONFIG_PATH
1481
+ try:
1482
+ if not config_path.exists():
1483
+ return True
1484
+ with config_path.open("rb") as f:
1485
+ data = tomllib.load(f)
1486
+ value = data.get("threads", {}).get("relative_time")
1487
+ if isinstance(value, bool):
1488
+ return value
1489
+ except (OSError, tomllib.TOMLDecodeError):
1490
+ logger.debug("Could not read thread relative_time config", exc_info=True)
1491
+ return True
1492
+
1493
+
1494
+ def save_thread_relative_time(enabled: bool, config_path: Path | None = None) -> bool:
1495
+ """Save the relative-time display preference for thread timestamps.
1496
+
1497
+ Args:
1498
+ enabled: Whether to display relative timestamps.
1499
+ config_path: Path to config file.
1500
+
1501
+ Returns:
1502
+ True if save succeeded, False on I/O error.
1503
+ """
1504
+ if config_path is None:
1505
+ config_path = DEFAULT_CONFIG_PATH
1506
+ try:
1507
+ config_path.parent.mkdir(parents=True, exist_ok=True)
1508
+ if config_path.exists():
1509
+ with config_path.open("rb") as f:
1510
+ data = tomllib.load(f)
1511
+ else:
1512
+ data = {}
1513
+ if "threads" not in data:
1514
+ data["threads"] = {}
1515
+ data["threads"]["relative_time"] = enabled
1516
+ fd, tmp_path = tempfile.mkstemp(dir=config_path.parent, suffix=".tmp")
1517
+ try:
1518
+ with os.fdopen(fd, "wb") as f:
1519
+ tomli_w.dump(data, f)
1520
+ Path(tmp_path).replace(config_path)
1521
+ except BaseException:
1522
+ with contextlib.suppress(OSError):
1523
+ Path(tmp_path).unlink()
1524
+ raise
1525
+ except (OSError, tomllib.TOMLDecodeError):
1526
+ logger.exception("Could not save thread relative_time preference")
1527
+ return False
1528
+ invalidate_thread_config_cache()
1529
+ return True
1530
+
1531
+
1532
+ def load_thread_sort_order(config_path: Path | None = None) -> str:
1533
+ """Load the sort order preference for the thread selector.
1534
+
1535
+ Args:
1536
+ config_path: Path to config file.
1537
+
1538
+ Returns:
1539
+ `"updated_at"` or `"created_at"`.
1540
+ """
1541
+ if config_path is None:
1542
+ config_path = DEFAULT_CONFIG_PATH
1543
+ try:
1544
+ if not config_path.exists():
1545
+ return "updated_at"
1546
+ with config_path.open("rb") as f:
1547
+ data = tomllib.load(f)
1548
+ value = data.get("threads", {}).get("sort_order")
1549
+ if value in {"updated_at", "created_at"}:
1550
+ return value
1551
+ except (OSError, tomllib.TOMLDecodeError):
1552
+ logger.debug("Could not read thread sort_order config", exc_info=True)
1553
+ return "updated_at"
1554
+
1555
+
1556
+ def save_thread_sort_order(sort_order: str, config_path: Path | None = None) -> bool:
1557
+ """Save the sort order preference for the thread selector.
1558
+
1559
+ Args:
1560
+ sort_order: `"updated_at"` or `"created_at"`.
1561
+ config_path: Path to config file.
1562
+
1563
+ Returns:
1564
+ True if save succeeded, False on I/O error.
1565
+
1566
+ Raises:
1567
+ ValueError: If `sort_order` is not a recognised value.
1568
+ """
1569
+ if sort_order not in {"updated_at", "created_at"}:
1570
+ msg = (
1571
+ f"Invalid sort_order {sort_order!r}; expected 'updated_at' or 'created_at'"
1572
+ )
1573
+ raise ValueError(msg)
1574
+ if config_path is None:
1575
+ config_path = DEFAULT_CONFIG_PATH
1576
+ try:
1577
+ config_path.parent.mkdir(parents=True, exist_ok=True)
1578
+ if config_path.exists():
1579
+ with config_path.open("rb") as f:
1580
+ data = tomllib.load(f)
1581
+ else:
1582
+ data = {}
1583
+ if "threads" not in data:
1584
+ data["threads"] = {}
1585
+ data["threads"]["sort_order"] = sort_order
1586
+ fd, tmp_path = tempfile.mkstemp(dir=config_path.parent, suffix=".tmp")
1587
+ try:
1588
+ with os.fdopen(fd, "wb") as f:
1589
+ tomli_w.dump(data, f)
1590
+ Path(tmp_path).replace(config_path)
1591
+ except Exception:
1592
+ with contextlib.suppress(OSError):
1593
+ Path(tmp_path).unlink()
1594
+ raise
1595
+ except (OSError, tomllib.TOMLDecodeError):
1596
+ logger.exception("Could not save thread sort_order preference")
1597
+ return False
1598
+ invalidate_thread_config_cache()
1599
+ return True
1600
+
1601
+
1602
+ def save_recent_model(model_spec: str, config_path: Path | None = None) -> bool:
1603
+ """Update the recently used model in config file.
1604
+
1605
+ Writes to `[models].recent` instead of `[models].default`, so that `/model`
1606
+ switches do not overwrite the user's intentional default.
1607
+
1608
+ Args:
1609
+ model_spec: The model to save in `provider:model` format.
1610
+ config_path: Path to config file.
1611
+
1612
+ Defaults to `~/.docagent/config.toml`.
1613
+
1614
+ Returns:
1615
+ True if save succeeded, False if it failed due to I/O errors.
1616
+
1617
+ Note:
1618
+ This function does not preserve comments in the config file.
1619
+ """
1620
+ return _save_model_field("recent", model_spec, config_path)