tsugite-cli 0.25.2__tar.gz → 0.25.3__tar.gz

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 (183) hide show
  1. {tsugite_cli-0.25.2 → tsugite_cli-0.25.3}/PKG-INFO +1 -1
  2. {tsugite_cli-0.25.2 → tsugite_cli-0.25.3}/pyproject.toml +1 -1
  3. {tsugite_cli-0.25.2 → tsugite_cli-0.25.3}/tsugite/providers/anthropic.py +7 -5
  4. {tsugite_cli-0.25.2 → tsugite_cli-0.25.3}/tsugite/providers/base.py +48 -0
  5. {tsugite_cli-0.25.2 → tsugite_cli-0.25.3}/tsugite/providers/openai_compat.py +8 -10
  6. {tsugite_cli-0.25.2 → tsugite_cli-0.25.3}/tsugite/providers/openrouter.py +2 -3
  7. {tsugite_cli-0.25.2 → tsugite_cli-0.25.3}/tsugite/secrets/sqlite.py +89 -13
  8. {tsugite_cli-0.25.2 → tsugite_cli-0.25.3}/.gitignore +0 -0
  9. {tsugite_cli-0.25.2 → tsugite_cli-0.25.3}/AGENTS.md +0 -0
  10. {tsugite_cli-0.25.2 → tsugite_cli-0.25.3}/CONTRIBUTING.md +0 -0
  11. {tsugite_cli-0.25.2 → tsugite_cli-0.25.3}/LICENSE +0 -0
  12. {tsugite_cli-0.25.2 → tsugite_cli-0.25.3}/README.md +0 -0
  13. {tsugite_cli-0.25.2 → tsugite_cli-0.25.3}/tsugite/__init__.py +0 -0
  14. {tsugite_cli-0.25.2 → tsugite_cli-0.25.3}/tsugite/agent_inheritance.py +0 -0
  15. {tsugite_cli-0.25.2 → tsugite_cli-0.25.3}/tsugite/agent_preparation.py +0 -0
  16. {tsugite_cli-0.25.2 → tsugite_cli-0.25.3}/tsugite/agent_runner/__init__.py +0 -0
  17. {tsugite_cli-0.25.2 → tsugite_cli-0.25.3}/tsugite/agent_runner/exec_directives.py +0 -0
  18. {tsugite_cli-0.25.2 → tsugite_cli-0.25.3}/tsugite/agent_runner/exec_runner.py +0 -0
  19. {tsugite_cli-0.25.2 → tsugite_cli-0.25.3}/tsugite/agent_runner/helpers.py +0 -0
  20. {tsugite_cli-0.25.2 → tsugite_cli-0.25.3}/tsugite/agent_runner/history_integration.py +0 -0
  21. {tsugite_cli-0.25.2 → tsugite_cli-0.25.3}/tsugite/agent_runner/metrics.py +0 -0
  22. {tsugite_cli-0.25.2 → tsugite_cli-0.25.3}/tsugite/agent_runner/models.py +0 -0
  23. {tsugite_cli-0.25.2 → tsugite_cli-0.25.3}/tsugite/agent_runner/runner.py +0 -0
  24. {tsugite_cli-0.25.2 → tsugite_cli-0.25.3}/tsugite/agent_runner/steps.py +0 -0
  25. {tsugite_cli-0.25.2 → tsugite_cli-0.25.3}/tsugite/agent_runner/validation.py +0 -0
  26. {tsugite_cli-0.25.2 → tsugite_cli-0.25.3}/tsugite/agent_utils.py +0 -0
  27. {tsugite_cli-0.25.2 → tsugite_cli-0.25.3}/tsugite/approval.py +0 -0
  28. {tsugite_cli-0.25.2 → tsugite_cli-0.25.3}/tsugite/attachments/__init__.py +0 -0
  29. {tsugite_cli-0.25.2 → tsugite_cli-0.25.3}/tsugite/attachments/agent_config.py +0 -0
  30. {tsugite_cli-0.25.2 → tsugite_cli-0.25.3}/tsugite/attachments/auto_context.py +0 -0
  31. {tsugite_cli-0.25.2 → tsugite_cli-0.25.3}/tsugite/attachments/base.py +0 -0
  32. {tsugite_cli-0.25.2 → tsugite_cli-0.25.3}/tsugite/attachments/delegation.py +0 -0
  33. {tsugite_cli-0.25.2 → tsugite_cli-0.25.3}/tsugite/attachments/file.py +0 -0
  34. {tsugite_cli-0.25.2 → tsugite_cli-0.25.3}/tsugite/attachments/inline.py +0 -0
  35. {tsugite_cli-0.25.2 → tsugite_cli-0.25.3}/tsugite/attachments/storage.py +0 -0
  36. {tsugite_cli-0.25.2 → tsugite_cli-0.25.3}/tsugite/attachments/url.py +0 -0
  37. {tsugite_cli-0.25.2 → tsugite_cli-0.25.3}/tsugite/builtin_agents/.gitkeep +0 -0
  38. {tsugite_cli-0.25.2 → tsugite_cli-0.25.3}/tsugite/builtin_agents/code_searcher.md +0 -0
  39. {tsugite_cli-0.25.2 → tsugite_cli-0.25.3}/tsugite/builtin_agents/default.md +0 -0
  40. {tsugite_cli-0.25.2 → tsugite_cli-0.25.3}/tsugite/builtin_agents/file_searcher.md +0 -0
  41. {tsugite_cli-0.25.2 → tsugite_cli-0.25.3}/tsugite/builtin_agents/job_verifier.md +0 -0
  42. {tsugite_cli-0.25.2 → tsugite_cli-0.25.3}/tsugite/builtin_agents/job_worker.md +0 -0
  43. {tsugite_cli-0.25.2 → tsugite_cli-0.25.3}/tsugite/builtin_agents/onboard.md +0 -0
  44. {tsugite_cli-0.25.2 → tsugite_cli-0.25.3}/tsugite/builtin_context.py +0 -0
  45. {tsugite_cli-0.25.2 → tsugite_cli-0.25.3}/tsugite/builtin_skills/.gitkeep +0 -0
  46. {tsugite_cli-0.25.2 → tsugite_cli-0.25.3}/tsugite/builtin_skills/codebase-exploration/SKILL.md +0 -0
  47. {tsugite_cli-0.25.2 → tsugite_cli-0.25.3}/tsugite/builtin_skills/external-agent-bridge/SKILL.md +0 -0
  48. {tsugite_cli-0.25.2 → tsugite_cli-0.25.3}/tsugite/builtin_skills/python-math/SKILL.md +0 -0
  49. {tsugite_cli-0.25.2 → tsugite_cli-0.25.3}/tsugite/builtin_skills/response-patterns/SKILL.md +0 -0
  50. {tsugite_cli-0.25.2 → tsugite_cli-0.25.3}/tsugite/builtin_skills/scheduling/SKILL.md +0 -0
  51. {tsugite_cli-0.25.2 → tsugite_cli-0.25.3}/tsugite/builtin_skills/skill-authoring/SKILL.md +0 -0
  52. {tsugite_cli-0.25.2 → tsugite_cli-0.25.3}/tsugite/builtin_skills/tsugite-agent-basics/SKILL.md +0 -0
  53. {tsugite_cli-0.25.2 → tsugite_cli-0.25.3}/tsugite/builtin_skills/tsugite-jinja-reference/SKILL.md +0 -0
  54. {tsugite_cli-0.25.2 → tsugite_cli-0.25.3}/tsugite/builtin_skills/tsugite-skill-basics/SKILL.md +0 -0
  55. {tsugite_cli-0.25.2 → tsugite_cli-0.25.3}/tsugite/cache.py +0 -0
  56. {tsugite_cli-0.25.2 → tsugite_cli-0.25.3}/tsugite/cancellation.py +0 -0
  57. {tsugite_cli-0.25.2 → tsugite_cli-0.25.3}/tsugite/cli/__init__.py +0 -0
  58. {tsugite_cli-0.25.2 → tsugite_cli-0.25.3}/tsugite/cli/agents.py +0 -0
  59. {tsugite_cli-0.25.2 → tsugite_cli-0.25.3}/tsugite/cli/attachments.py +0 -0
  60. {tsugite_cli-0.25.2 → tsugite_cli-0.25.3}/tsugite/cli/cache.py +0 -0
  61. {tsugite_cli-0.25.2 → tsugite_cli-0.25.3}/tsugite/cli/chat.py +0 -0
  62. {tsugite_cli-0.25.2 → tsugite_cli-0.25.3}/tsugite/cli/config.py +0 -0
  63. {tsugite_cli-0.25.2 → tsugite_cli-0.25.3}/tsugite/cli/daemon.py +0 -0
  64. {tsugite_cli-0.25.2 → tsugite_cli-0.25.3}/tsugite/cli/exec.py +0 -0
  65. {tsugite_cli-0.25.2 → tsugite_cli-0.25.3}/tsugite/cli/helpers.py +0 -0
  66. {tsugite_cli-0.25.2 → tsugite_cli-0.25.3}/tsugite/cli/history.py +0 -0
  67. {tsugite_cli-0.25.2 → tsugite_cli-0.25.3}/tsugite/cli/init.py +0 -0
  68. {tsugite_cli-0.25.2 → tsugite_cli-0.25.3}/tsugite/cli/models.py +0 -0
  69. {tsugite_cli-0.25.2 → tsugite_cli-0.25.3}/tsugite/cli/plugins.py +0 -0
  70. {tsugite_cli-0.25.2 → tsugite_cli-0.25.3}/tsugite/cli/render.py +0 -0
  71. {tsugite_cli-0.25.2 → tsugite_cli-0.25.3}/tsugite/cli/run.py +0 -0
  72. {tsugite_cli-0.25.2 → tsugite_cli-0.25.3}/tsugite/cli/secrets.py +0 -0
  73. {tsugite_cli-0.25.2 → tsugite_cli-0.25.3}/tsugite/cli/skills.py +0 -0
  74. {tsugite_cli-0.25.2 → tsugite_cli-0.25.3}/tsugite/cli/tools.py +0 -0
  75. {tsugite_cli-0.25.2 → tsugite_cli-0.25.3}/tsugite/cli/usage.py +0 -0
  76. {tsugite_cli-0.25.2 → tsugite_cli-0.25.3}/tsugite/cli/validate.py +0 -0
  77. {tsugite_cli-0.25.2 → tsugite_cli-0.25.3}/tsugite/cli/workspace.py +0 -0
  78. {tsugite_cli-0.25.2 → tsugite_cli-0.25.3}/tsugite/config.py +0 -0
  79. {tsugite_cli-0.25.2 → tsugite_cli-0.25.3}/tsugite/console.py +0 -0
  80. {tsugite_cli-0.25.2 → tsugite_cli-0.25.3}/tsugite/constants.py +0 -0
  81. {tsugite_cli-0.25.2 → tsugite_cli-0.25.3}/tsugite/context.py +0 -0
  82. {tsugite_cli-0.25.2 → tsugite_cli-0.25.3}/tsugite/context_block.py +0 -0
  83. {tsugite_cli-0.25.2 → tsugite_cli-0.25.3}/tsugite/core/__init__.py +0 -0
  84. {tsugite_cli-0.25.2 → tsugite_cli-0.25.3}/tsugite/core/agent.py +0 -0
  85. {tsugite_cli-0.25.2 → tsugite_cli-0.25.3}/tsugite/core/content_blocks.py +0 -0
  86. {tsugite_cli-0.25.2 → tsugite_cli-0.25.3}/tsugite/core/executor.py +0 -0
  87. {tsugite_cli-0.25.2 → tsugite_cli-0.25.3}/tsugite/core/executor_registry.py +0 -0
  88. {tsugite_cli-0.25.2 → tsugite_cli-0.25.3}/tsugite/core/memory.py +0 -0
  89. {tsugite_cli-0.25.2 → tsugite_cli-0.25.3}/tsugite/core/proxy.py +0 -0
  90. {tsugite_cli-0.25.2 → tsugite_cli-0.25.3}/tsugite/core/record_store.py +0 -0
  91. {tsugite_cli-0.25.2 → tsugite_cli-0.25.3}/tsugite/core/sandbox.py +0 -0
  92. {tsugite_cli-0.25.2 → tsugite_cli-0.25.3}/tsugite/core/state.py +0 -0
  93. {tsugite_cli-0.25.2 → tsugite_cli-0.25.3}/tsugite/core/subprocess_executor.py +0 -0
  94. {tsugite_cli-0.25.2 → tsugite_cli-0.25.3}/tsugite/core/tools.py +0 -0
  95. {tsugite_cli-0.25.2 → tsugite_cli-0.25.3}/tsugite/daemon_client.py +0 -0
  96. {tsugite_cli-0.25.2 → tsugite_cli-0.25.3}/tsugite/events/__init__.py +0 -0
  97. {tsugite_cli-0.25.2 → tsugite_cli-0.25.3}/tsugite/events/base.py +0 -0
  98. {tsugite_cli-0.25.2 → tsugite_cli-0.25.3}/tsugite/events/bus.py +0 -0
  99. {tsugite_cli-0.25.2 → tsugite_cli-0.25.3}/tsugite/events/events.py +0 -0
  100. {tsugite_cli-0.25.2 → tsugite_cli-0.25.3}/tsugite/events/helpers.py +0 -0
  101. {tsugite_cli-0.25.2 → tsugite_cli-0.25.3}/tsugite/exceptions.py +0 -0
  102. {tsugite_cli-0.25.2 → tsugite_cli-0.25.3}/tsugite/history/__init__.py +0 -0
  103. {tsugite_cli-0.25.2 → tsugite_cli-0.25.3}/tsugite/history/base.py +0 -0
  104. {tsugite_cli-0.25.2 → tsugite_cli-0.25.3}/tsugite/history/models.py +0 -0
  105. {tsugite_cli-0.25.2 → tsugite_cli-0.25.3}/tsugite/history/reconstruction.py +0 -0
  106. {tsugite_cli-0.25.2 → tsugite_cli-0.25.3}/tsugite/history/registry.py +0 -0
  107. {tsugite_cli-0.25.2 → tsugite_cli-0.25.3}/tsugite/history/sqlite_backend.py +0 -0
  108. {tsugite_cli-0.25.2 → tsugite_cli-0.25.3}/tsugite/history/sqlite_conn.py +0 -0
  109. {tsugite_cli-0.25.2 → tsugite_cli-0.25.3}/tsugite/history/sqlite_schema.py +0 -0
  110. {tsugite_cli-0.25.2 → tsugite_cli-0.25.3}/tsugite/history/ui_events.py +0 -0
  111. {tsugite_cli-0.25.2 → tsugite_cli-0.25.3}/tsugite/hooks.py +0 -0
  112. {tsugite_cli-0.25.2 → tsugite_cli-0.25.3}/tsugite/interaction.py +0 -0
  113. {tsugite_cli-0.25.2 → tsugite_cli-0.25.3}/tsugite/md_agents.py +0 -0
  114. {tsugite_cli-0.25.2 → tsugite_cli-0.25.3}/tsugite/models.py +0 -0
  115. {tsugite_cli-0.25.2 → tsugite_cli-0.25.3}/tsugite/options.py +0 -0
  116. {tsugite_cli-0.25.2 → tsugite_cli-0.25.3}/tsugite/permissions.py +0 -0
  117. {tsugite_cli-0.25.2 → tsugite_cli-0.25.3}/tsugite/plugins.py +0 -0
  118. {tsugite_cli-0.25.2 → tsugite_cli-0.25.3}/tsugite/prompt_xml.py +0 -0
  119. {tsugite_cli-0.25.2 → tsugite_cli-0.25.3}/tsugite/providers/__init__.py +0 -0
  120. {tsugite_cli-0.25.2 → tsugite_cli-0.25.3}/tsugite/providers/model_cache.py +0 -0
  121. {tsugite_cli-0.25.2 → tsugite_cli-0.25.3}/tsugite/providers/model_registry.py +0 -0
  122. {tsugite_cli-0.25.2 → tsugite_cli-0.25.3}/tsugite/providers/ollama.py +0 -0
  123. {tsugite_cli-0.25.2 → tsugite_cli-0.25.3}/tsugite/py.typed +0 -0
  124. {tsugite_cli-0.25.2 → tsugite_cli-0.25.3}/tsugite/renderer.py +0 -0
  125. {tsugite_cli-0.25.2 → tsugite_cli-0.25.3}/tsugite/schemas/__init__.py +0 -0
  126. {tsugite_cli-0.25.2 → tsugite_cli-0.25.3}/tsugite/schemas/agent.schema.json +0 -0
  127. {tsugite_cli-0.25.2 → tsugite_cli-0.25.3}/tsugite/secrets/__init__.py +0 -0
  128. {tsugite_cli-0.25.2 → tsugite_cli-0.25.3}/tsugite/secrets/backend.py +0 -0
  129. {tsugite_cli-0.25.2 → tsugite_cli-0.25.3}/tsugite/secrets/env.py +0 -0
  130. {tsugite_cli-0.25.2 → tsugite_cli-0.25.3}/tsugite/secrets/exec.py +0 -0
  131. {tsugite_cli-0.25.2 → tsugite_cli-0.25.3}/tsugite/secrets/file.py +0 -0
  132. {tsugite_cli-0.25.2 → tsugite_cli-0.25.3}/tsugite/secrets/masking.py +0 -0
  133. {tsugite_cli-0.25.2 → tsugite_cli-0.25.3}/tsugite/secrets/redaction.py +0 -0
  134. {tsugite_cli-0.25.2 → tsugite_cli-0.25.3}/tsugite/secrets/registry.py +0 -0
  135. {tsugite_cli-0.25.2 → tsugite_cli-0.25.3}/tsugite/shell_tool_config.py +0 -0
  136. {tsugite_cli-0.25.2 → tsugite_cli-0.25.3}/tsugite/skill_discovery.py +0 -0
  137. {tsugite_cli-0.25.2 → tsugite_cli-0.25.3}/tsugite/templates/AGENTS.md +0 -0
  138. {tsugite_cli-0.25.2 → tsugite_cli-0.25.3}/tsugite/templates/IDENTITY.md +0 -0
  139. {tsugite_cli-0.25.2 → tsugite_cli-0.25.3}/tsugite/templates/MEMORY.md +0 -0
  140. {tsugite_cli-0.25.2 → tsugite_cli-0.25.3}/tsugite/templates/USER.md +0 -0
  141. {tsugite_cli-0.25.2 → tsugite_cli-0.25.3}/tsugite/templates/personas/casual-technical.md +0 -0
  142. {tsugite_cli-0.25.2 → tsugite_cli-0.25.3}/tsugite/templates/personas/marvin.md +0 -0
  143. {tsugite_cli-0.25.2 → tsugite_cli-0.25.3}/tsugite/templates/personas/minimal.md +0 -0
  144. {tsugite_cli-0.25.2 → tsugite_cli-0.25.3}/tsugite/tools/__init__.py +0 -0
  145. {tsugite_cli-0.25.2 → tsugite_cli-0.25.3}/tsugite/tools/agents.py +0 -0
  146. {tsugite_cli-0.25.2 → tsugite_cli-0.25.3}/tsugite/tools/artifacts.py +0 -0
  147. {tsugite_cli-0.25.2 → tsugite_cli-0.25.3}/tsugite/tools/daemon_control.py +0 -0
  148. {tsugite_cli-0.25.2 → tsugite_cli-0.25.3}/tsugite/tools/fs.py +0 -0
  149. {tsugite_cli-0.25.2 → tsugite_cli-0.25.3}/tsugite/tools/history.py +0 -0
  150. {tsugite_cli-0.25.2 → tsugite_cli-0.25.3}/tsugite/tools/http.py +0 -0
  151. {tsugite_cli-0.25.2 → tsugite_cli-0.25.3}/tsugite/tools/interactive.py +0 -0
  152. {tsugite_cli-0.25.2 → tsugite_cli-0.25.3}/tsugite/tools/jobs.py +0 -0
  153. {tsugite_cli-0.25.2 → tsugite_cli-0.25.3}/tsugite/tools/notify.py +0 -0
  154. {tsugite_cli-0.25.2 → tsugite_cli-0.25.3}/tsugite/tools/schedule.py +0 -0
  155. {tsugite_cli-0.25.2 → tsugite_cli-0.25.3}/tsugite/tools/secrets.py +0 -0
  156. {tsugite_cli-0.25.2 → tsugite_cli-0.25.3}/tsugite/tools/sessions.py +0 -0
  157. {tsugite_cli-0.25.2 → tsugite_cli-0.25.3}/tsugite/tools/shell.py +0 -0
  158. {tsugite_cli-0.25.2 → tsugite_cli-0.25.3}/tsugite/tools/shell_tools.py +0 -0
  159. {tsugite_cli-0.25.2 → tsugite_cli-0.25.3}/tsugite/tools/skills.py +0 -0
  160. {tsugite_cli-0.25.2 → tsugite_cli-0.25.3}/tsugite/tools/time.py +0 -0
  161. {tsugite_cli-0.25.2 → tsugite_cli-0.25.3}/tsugite/tsugite.py +0 -0
  162. {tsugite_cli-0.25.2 → tsugite_cli-0.25.3}/tsugite/ui/__init__.py +0 -0
  163. {tsugite_cli-0.25.2 → tsugite_cli-0.25.3}/tsugite/ui/base.py +0 -0
  164. {tsugite_cli-0.25.2 → tsugite_cli-0.25.3}/tsugite/ui/chat.py +0 -0
  165. {tsugite_cli-0.25.2 → tsugite_cli-0.25.3}/tsugite/ui/dispatch.py +0 -0
  166. {tsugite_cli-0.25.2 → tsugite_cli-0.25.3}/tsugite/ui/helpers.py +0 -0
  167. {tsugite_cli-0.25.2 → tsugite_cli-0.25.3}/tsugite/ui/jsonl.py +0 -0
  168. {tsugite_cli-0.25.2 → tsugite_cli-0.25.3}/tsugite/ui/plain.py +0 -0
  169. {tsugite_cli-0.25.2 → tsugite_cli-0.25.3}/tsugite/ui/repl_chat.py +0 -0
  170. {tsugite_cli-0.25.2 → tsugite_cli-0.25.3}/tsugite/ui/repl_commands.py +0 -0
  171. {tsugite_cli-0.25.2 → tsugite_cli-0.25.3}/tsugite/ui/repl_completer.py +0 -0
  172. {tsugite_cli-0.25.2 → tsugite_cli-0.25.3}/tsugite/ui/repl_handler.py +0 -0
  173. {tsugite_cli-0.25.2 → tsugite_cli-0.25.3}/tsugite/ui_context.py +0 -0
  174. {tsugite_cli-0.25.2 → tsugite_cli-0.25.3}/tsugite/ui_surfaces.py +0 -0
  175. {tsugite_cli-0.25.2 → tsugite_cli-0.25.3}/tsugite/usage/__init__.py +0 -0
  176. {tsugite_cli-0.25.2 → tsugite_cli-0.25.3}/tsugite/usage/quota.py +0 -0
  177. {tsugite_cli-0.25.2 → tsugite_cli-0.25.3}/tsugite/usage/store.py +0 -0
  178. {tsugite_cli-0.25.2 → tsugite_cli-0.25.3}/tsugite/user_agent.py +0 -0
  179. {tsugite_cli-0.25.2 → tsugite_cli-0.25.3}/tsugite/utils.py +0 -0
  180. {tsugite_cli-0.25.2 → tsugite_cli-0.25.3}/tsugite/workspace/__init__.py +0 -0
  181. {tsugite_cli-0.25.2 → tsugite_cli-0.25.3}/tsugite/workspace/models.py +0 -0
  182. {tsugite_cli-0.25.2 → tsugite_cli-0.25.3}/tsugite/workspace/session.py +0 -0
  183. {tsugite_cli-0.25.2 → tsugite_cli-0.25.3}/tsugite/workspace/templates.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: tsugite-cli
3
- Version: 0.25.2
3
+ Version: 0.25.3
4
4
  Summary: Micro-agent runner for task automation using markdown definitions
5
5
  Author: Justyn Shull
6
6
  License: GNU AFFERO GENERAL PUBLIC LICENSE
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "tsugite-cli"
3
- version = "0.25.2"
3
+ version = "0.25.3"
4
4
  description = "Micro-agent runner for task automation using markdown definitions"
5
5
  authors = [{ name = "Justyn Shull" }]
6
6
  requires-python = ">=3.11"
@@ -5,7 +5,6 @@ from __future__ import annotations
5
5
  import asyncio
6
6
  import json
7
7
  import logging
8
- import os
9
8
  import re
10
9
  from typing import Any, AsyncIterator
11
10
 
@@ -13,7 +12,7 @@ import httpx
13
12
 
14
13
  from tsugite.user_agent import set_user_agent_header
15
14
 
16
- from .base import CompletionResponse, ModelInfo, StreamChunk, Usage, default_count_tokens
15
+ from .base import CompletionResponse, ModelInfo, StreamChunk, Usage, default_count_tokens, resolve_api_key
17
16
  from .model_registry import calculate_cost, register_models
18
17
  from .model_registry import get_model_info as _get_model_info
19
18
 
@@ -63,9 +62,11 @@ _ANTHROPIC_MODELS: dict[str, ModelInfo] = {
63
62
  class AnthropicProvider:
64
63
  """Provider for the Anthropic Messages API."""
65
64
 
65
+ api_key_env = "ANTHROPIC_API_KEY"
66
+
66
67
  def __init__(self, name: str = "anthropic", api_key: str | None = None, api_base: str | None = None):
67
68
  self.name = name
68
- self.api_key = api_key or os.getenv("ANTHROPIC_API_KEY")
69
+ self.api_key = api_key
69
70
  self.api_base = (api_base or API_BASE).rstrip("/")
70
71
  self._client: httpx.AsyncClient | None = None
71
72
  self._client_loop = None
@@ -92,8 +93,9 @@ class AnthropicProvider:
92
93
  "anthropic-version": API_VERSION,
93
94
  }
94
95
  set_user_agent_header(headers)
95
- if self.api_key:
96
- headers["x-api-key"] = self.api_key
96
+ api_key = self.api_key or resolve_api_key(self.api_key_env)
97
+ if api_key:
98
+ headers["x-api-key"] = api_key
97
99
  return headers
98
100
 
99
101
  async def acompletion(
@@ -2,11 +2,20 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
+ import logging
6
+ import os
7
+ import shutil
8
+ import time
5
9
  from dataclasses import dataclass
6
10
  from typing import Any, AsyncIterator, Protocol, runtime_checkable
7
11
 
8
12
  _O200K_PREFIXES = ("gpt-4o", "o1", "o3", "o4", "chatgpt-4o")
9
13
  _tiktoken_cache: dict[str, Any] = {}
14
+ API_KEY_CACHE_TTL_SECONDS = 60
15
+ _secret_api_keys: dict[str, tuple[float, str | None]] = {}
16
+ _backend_errors_logged: set[str] = set()
17
+
18
+ logger = logging.getLogger(__name__)
10
19
 
11
20
 
12
21
  @dataclass
@@ -100,3 +109,42 @@ def default_count_tokens(text: str, model: str) -> int:
100
109
  return len(_tiktoken_cache[encoding_name].encode(text))
101
110
  except Exception:
102
111
  return len(text) // 4
112
+
113
+
114
+ def api_key_secret_name(env_var: str) -> str:
115
+ """Secrets-backend name for the key read from ``env_var``."""
116
+ return env_var.lower().replace("_", "-")
117
+
118
+
119
+ def resolve_api_key(env_var: str) -> str | None:
120
+ """Key from the secrets backend, else ``env_var``. Backend lookups are cached for ``API_KEY_CACHE_TTL_SECONDS``."""
121
+ now = time.monotonic()
122
+ cached = _secret_api_keys.get(env_var)
123
+ if cached is None or now - cached[0] >= API_KEY_CACHE_TTL_SECONDS:
124
+ from tsugite.secrets import get_backend
125
+
126
+ try:
127
+ _secret_api_keys[env_var] = (now, get_backend().get(api_key_secret_name(env_var)))
128
+ except Exception as e:
129
+ if env_var not in _backend_errors_logged:
130
+ _backend_errors_logged.add(env_var)
131
+ logger.warning("Secrets backend failed reading the key for %s, using the env var: %s", env_var, e)
132
+ return os.getenv(env_var)
133
+ return _secret_api_keys[env_var][1] or os.getenv(env_var)
134
+
135
+
136
+ def invalidate_api_key_cache() -> None:
137
+ _secret_api_keys.clear()
138
+ _backend_errors_logged.clear()
139
+
140
+
141
+ def configured_binary(provider: str) -> str | None:
142
+ from tsugite.config import load_config
143
+
144
+ return load_config().plugins.get(provider, {}).get("binary") or None
145
+
146
+
147
+ def resolve_binary(provider: str, default: str) -> str | None:
148
+ """Path of a CLI provider's executable from config ``plugins.<provider>.binary``, else ``default`` on PATH."""
149
+ configured = configured_binary(provider)
150
+ return shutil.which(os.path.expanduser(configured) if configured else default)
@@ -5,14 +5,13 @@ from __future__ import annotations
5
5
  import asyncio
6
6
  import json
7
7
  import logging
8
- import os
9
8
  from typing import Any, AsyncIterator
10
9
 
11
10
  import httpx
12
11
 
13
12
  from tsugite.user_agent import set_user_agent_header
14
13
 
15
- from .base import CompletionResponse, ModelInfo, StreamChunk, Usage, default_count_tokens
14
+ from .base import CompletionResponse, ModelInfo, StreamChunk, Usage, default_count_tokens, resolve_api_key
16
15
  from .model_registry import calculate_cost, register_models
17
16
  from .model_registry import get_model_info as _get_model_info
18
17
 
@@ -122,10 +121,12 @@ class OpenAICompatProvider:
122
121
  api_base: str,
123
122
  api_key: str | None = None,
124
123
  extra_headers: dict[str, str] | None = None,
124
+ api_key_env: str | None = None,
125
125
  ):
126
126
  self.name = name
127
127
  self.api_base = api_base.rstrip("/")
128
128
  self.api_key = api_key
129
+ self.api_key_env = api_key_env
129
130
  self.extra_headers = extra_headers or {}
130
131
  self._client: httpx.AsyncClient | None = None
131
132
  self._client_loop = None
@@ -148,8 +149,9 @@ class OpenAICompatProvider:
148
149
  headers: dict[str, str] = {"Content-Type": "application/json"}
149
150
  set_user_agent_header(headers)
150
151
  headers.update(self.extra_headers)
151
- if self.api_key:
152
- headers["Authorization"] = f"Bearer {self.api_key}"
152
+ api_key = self.api_key or (resolve_api_key(self.api_key_env) if self.api_key_env else None)
153
+ if api_key:
154
+ headers["Authorization"] = f"Bearer {api_key}"
153
155
  return headers
154
156
 
155
157
  async def acompletion(
@@ -282,16 +284,12 @@ def create_provider(name: str = "openai", **kwargs: Any) -> OpenAICompatProvider
282
284
 
283
285
  api_base = kwargs.get("api_base") or config.get("api_base") or "https://api.openai.com/v1"
284
286
 
285
- api_key = kwargs.get("api_key")
286
- if not api_key:
287
- env_key = config.get("api_key_env")
288
- api_key = os.getenv(env_key) if env_key else None
289
-
290
287
  extra_headers = {**config.get("extra_headers", {}), **kwargs.get("extra_headers", {})}
291
288
 
292
289
  return OpenAICompatProvider(
293
290
  name=name,
294
291
  api_base=api_base,
295
- api_key=api_key,
292
+ api_key=kwargs.get("api_key"),
296
293
  extra_headers=extra_headers,
294
+ api_key_env=config.get("api_key_env"),
297
295
  )
@@ -59,11 +59,10 @@ class OpenRouterProvider(OpenAICompatProvider):
59
59
 
60
60
 
61
61
  def create_provider(name: str = "openrouter", **kwargs: Any) -> OpenRouterProvider:
62
- import os
63
-
64
62
  return OpenRouterProvider(
65
63
  name=name,
66
64
  api_base=kwargs.get("api_base") or "https://openrouter.ai/api/v1",
67
- api_key=kwargs.get("api_key") or os.getenv("OPENROUTER_API_KEY"),
65
+ api_key=kwargs.get("api_key"),
68
66
  extra_headers=kwargs.get("extra_headers", {}),
67
+ api_key_env="OPENROUTER_API_KEY",
69
68
  )
@@ -3,10 +3,18 @@
3
3
  import base64
4
4
  import getpass
5
5
  import os
6
+ import secrets
7
+ import shutil
6
8
  import sqlite3
9
+ import subprocess
7
10
  import sys
11
+ import threading
8
12
  from pathlib import Path
9
13
 
14
+ KEYCHAIN_SERVICE = "tsugite"
15
+ # `security find-generic-password` exit status for errSecItemNotFound.
16
+ MACOS_ITEM_NOT_FOUND = 44
17
+
10
18
 
11
19
  class SqliteSecretBackend:
12
20
  """Stores secrets in SQLite, encrypted with Fernet. A passphrase is always required."""
@@ -18,14 +26,15 @@ class SqliteSecretBackend:
18
26
  self._db_path = Path(path)
19
27
  self._conn: sqlite3.Connection | None = None
20
28
  self._key_cache: dict[bytes, bytes] = {}
21
- self._passphrase = self._resolve_passphrase(
22
- config_passphrase=config.get("passphrase"),
23
- key_file=config.get("key_file"),
24
- )
25
- if not self._passphrase:
29
+ self._keychain_account = (config.get("keychain_account") or "secrets") if config.get("keychain") else None
30
+ if self._keychain_account and any(c in self._keychain_account for c in '"\\\r\n'):
31
+ raise ValueError("keychain_account must not contain quotes, backslashes or line breaks")
32
+ self._keychain_lock = threading.Lock()
33
+ self._passphrase_value = self._resolve_passphrase(config)
34
+ if not self._passphrase_value and not self._keychain_account:
26
35
  raise ValueError(
27
36
  "sqlite secrets backend requires a passphrase. Set TSUGITE_SECRETS_KEY env var, "
28
- "configure key_file in secrets config, or use an interactive terminal."
37
+ "configure key_file or keychain in secrets config, or use an interactive terminal."
29
38
  )
30
39
 
31
40
  def _get_conn(self) -> sqlite3.Connection:
@@ -40,25 +49,50 @@ class SqliteSecretBackend:
40
49
  self._conn.commit()
41
50
  return self._conn
42
51
 
43
- @staticmethod
44
- def _resolve_passphrase(
45
- config_passphrase: str | None = None,
46
- key_file: str | None = None,
47
- ) -> str | None:
52
+ def _resolve_passphrase(self, config: dict) -> str | None:
48
53
  env_key = os.environ.get("TSUGITE_SECRETS_KEY")
49
54
  if env_key:
50
55
  return env_key
56
+ key_file = config.get("key_file")
51
57
  if key_file:
52
58
  kf = Path(key_file).expanduser()
53
59
  if kf.is_file():
54
60
  return kf.read_text().strip()
55
- if config_passphrase:
56
- return config_passphrase
61
+ if config.get("keychain"):
62
+ return None
63
+ if config.get("passphrase"):
64
+ return config["passphrase"]
57
65
  if sys.stdin.isatty():
58
66
  passphrase = getpass.getpass("Secrets passphrase: ")
59
67
  return passphrase or None
60
68
  return None
61
69
 
70
+ @property
71
+ def _passphrase(self) -> str:
72
+ with self._keychain_lock:
73
+ if self._passphrase_value is None:
74
+ self._passphrase_value = self._keychain_passphrase()
75
+ return self._passphrase_value
76
+
77
+ def _keychain_passphrase(self) -> str:
78
+ account = self._keychain_account
79
+ key = _keychain_lookup(account)
80
+ if key:
81
+ return key
82
+ if self._get_conn().execute("SELECT 1 FROM secrets LIMIT 1").fetchone():
83
+ raise ValueError(
84
+ f"The OS keychain has no secrets key (service '{KEYCHAIN_SERVICE}', account '{account}'), "
85
+ f"but {self._db_path} already holds encrypted secrets. Restore the key to the keychain "
86
+ "or set TSUGITE_SECRETS_KEY."
87
+ )
88
+ key = secrets.token_urlsafe(32)
89
+ _keychain_store(account, key)
90
+ if _keychain_lookup(account) != key:
91
+ raise ValueError(
92
+ f"Stored the secrets key in the OS keychain (account '{account}') but could not read it back"
93
+ )
94
+ return key
95
+
62
96
  def _derive_key(self, salt: bytes) -> bytes:
63
97
  cached = self._key_cache.get(salt)
64
98
  if cached:
@@ -119,3 +153,45 @@ class SqliteSecretBackend:
119
153
  def list_names(self) -> list[str]:
120
154
  rows = self._get_conn().execute("SELECT name FROM secrets ORDER BY name").fetchall()
121
155
  return [r[0] for r in rows]
156
+
157
+
158
+ def _keychain_tool() -> str:
159
+ if sys.platform == "darwin":
160
+ return "/usr/bin/security"
161
+ tool = shutil.which("secret-tool")
162
+ if not tool:
163
+ raise ValueError(
164
+ "secrets keychain is enabled but secret-tool was not found. Install libsecret-tools "
165
+ "and run a Secret Service (gnome-keyring, KeePassXC or KWallet)."
166
+ )
167
+ return tool
168
+
169
+
170
+ def _keychain_lookup(account: str) -> str | None:
171
+ tool = _keychain_tool()
172
+ if sys.platform == "darwin":
173
+ cmd = [tool, "find-generic-password", "-s", KEYCHAIN_SERVICE, "-a", account, "-w"]
174
+ else:
175
+ cmd = [tool, "lookup", "service", KEYCHAIN_SERVICE, "account", account]
176
+ result = subprocess.run(cmd, capture_output=True, text=True)
177
+ if result.returncode == 0:
178
+ return result.stdout.strip()
179
+ # secret-tool exits 1 with empty stderr for a missing item.
180
+ missing = result.returncode == MACOS_ITEM_NOT_FOUND if sys.platform == "darwin" else not result.stderr.strip()
181
+ if missing:
182
+ return None
183
+ raise ValueError(f"Could not read the secrets key from the OS keychain: {result.stderr.strip()}")
184
+
185
+
186
+ def _keychain_store(account: str, key: str) -> None:
187
+ tool = _keychain_tool()
188
+ if sys.platform == "darwin":
189
+ # argv is visible to other local users through `ps`.
190
+ cmd = [tool, "-i"]
191
+ stdin = f'add-generic-password -s "{KEYCHAIN_SERVICE}" -a "{account}" -w "{key}"\n'
192
+ else:
193
+ cmd = [tool, "store", "--label=Tsugite secrets key", "service", KEYCHAIN_SERVICE, "account", account]
194
+ stdin = key
195
+ result = subprocess.run(cmd, input=stdin, capture_output=True, text=True)
196
+ if result.returncode != 0:
197
+ raise ValueError(f"Could not store the secrets key in the OS keychain: {result.stderr.strip()}")
File without changes
File without changes
File without changes
File without changes