specsmith 0.3.0.dev128__tar.gz → 0.3.0.dev129__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 (143) hide show
  1. {specsmith-0.3.0.dev128/src/specsmith.egg-info → specsmith-0.3.0.dev129}/PKG-INFO +5 -3
  2. {specsmith-0.3.0.dev128 → specsmith-0.3.0.dev129}/pyproject.toml +3 -2
  3. {specsmith-0.3.0.dev128 → specsmith-0.3.0.dev129}/src/specsmith/agent/providers/__init__.py +12 -2
  4. specsmith-0.3.0.dev129/src/specsmith/agent/providers/mistral.py +175 -0
  5. {specsmith-0.3.0.dev128 → specsmith-0.3.0.dev129}/src/specsmith/agent/runner.py +11 -14
  6. {specsmith-0.3.0.dev128 → specsmith-0.3.0.dev129}/src/specsmith/agent/tools.py +300 -0
  7. {specsmith-0.3.0.dev128 → specsmith-0.3.0.dev129/src/specsmith.egg-info}/PKG-INFO +5 -3
  8. {specsmith-0.3.0.dev128 → specsmith-0.3.0.dev129}/src/specsmith.egg-info/SOURCES.txt +1 -0
  9. {specsmith-0.3.0.dev128 → specsmith-0.3.0.dev129}/src/specsmith.egg-info/requires.txt +4 -1
  10. {specsmith-0.3.0.dev128 → specsmith-0.3.0.dev129}/LICENSE +0 -0
  11. {specsmith-0.3.0.dev128 → specsmith-0.3.0.dev129}/README.md +0 -0
  12. {specsmith-0.3.0.dev128 → specsmith-0.3.0.dev129}/setup.cfg +0 -0
  13. {specsmith-0.3.0.dev128 → specsmith-0.3.0.dev129}/src/epistemic/__init__.py +0 -0
  14. {specsmith-0.3.0.dev128 → specsmith-0.3.0.dev129}/src/epistemic/belief.py +0 -0
  15. {specsmith-0.3.0.dev128 → specsmith-0.3.0.dev129}/src/epistemic/certainty.py +0 -0
  16. {specsmith-0.3.0.dev128 → specsmith-0.3.0.dev129}/src/epistemic/failure_graph.py +0 -0
  17. {specsmith-0.3.0.dev128 → specsmith-0.3.0.dev129}/src/epistemic/py.typed +0 -0
  18. {specsmith-0.3.0.dev128 → specsmith-0.3.0.dev129}/src/epistemic/recovery.py +0 -0
  19. {specsmith-0.3.0.dev128 → specsmith-0.3.0.dev129}/src/epistemic/session.py +0 -0
  20. {specsmith-0.3.0.dev128 → specsmith-0.3.0.dev129}/src/epistemic/stress_tester.py +0 -0
  21. {specsmith-0.3.0.dev128 → specsmith-0.3.0.dev129}/src/epistemic/trace.py +0 -0
  22. {specsmith-0.3.0.dev128 → specsmith-0.3.0.dev129}/src/specsmith/__init__.py +0 -0
  23. {specsmith-0.3.0.dev128 → specsmith-0.3.0.dev129}/src/specsmith/__main__.py +0 -0
  24. {specsmith-0.3.0.dev128 → specsmith-0.3.0.dev129}/src/specsmith/agent/__init__.py +0 -0
  25. {specsmith-0.3.0.dev128 → specsmith-0.3.0.dev129}/src/specsmith/agent/core.py +0 -0
  26. {specsmith-0.3.0.dev128 → specsmith-0.3.0.dev129}/src/specsmith/agent/hooks.py +0 -0
  27. {specsmith-0.3.0.dev128 → specsmith-0.3.0.dev129}/src/specsmith/agent/profiles/epistemic-auditor.md +0 -0
  28. {specsmith-0.3.0.dev128 → specsmith-0.3.0.dev129}/src/specsmith/agent/profiles/planner.md +0 -0
  29. {specsmith-0.3.0.dev128 → specsmith-0.3.0.dev129}/src/specsmith/agent/profiles/verifier.md +0 -0
  30. {specsmith-0.3.0.dev128 → specsmith-0.3.0.dev129}/src/specsmith/agent/providers/anthropic.py +0 -0
  31. {specsmith-0.3.0.dev128 → specsmith-0.3.0.dev129}/src/specsmith/agent/providers/gemini.py +0 -0
  32. {specsmith-0.3.0.dev128 → specsmith-0.3.0.dev129}/src/specsmith/agent/providers/ollama.py +0 -0
  33. {specsmith-0.3.0.dev128 → specsmith-0.3.0.dev129}/src/specsmith/agent/providers/openai.py +0 -0
  34. {specsmith-0.3.0.dev128 → specsmith-0.3.0.dev129}/src/specsmith/agent/skills.py +0 -0
  35. {specsmith-0.3.0.dev128 → specsmith-0.3.0.dev129}/src/specsmith/architect.py +0 -0
  36. {specsmith-0.3.0.dev128 → specsmith-0.3.0.dev129}/src/specsmith/auditor.py +0 -0
  37. {specsmith-0.3.0.dev128 → specsmith-0.3.0.dev129}/src/specsmith/auth.py +0 -0
  38. {specsmith-0.3.0.dev128 → specsmith-0.3.0.dev129}/src/specsmith/cli.py +0 -0
  39. {specsmith-0.3.0.dev128 → specsmith-0.3.0.dev129}/src/specsmith/commands/__init__.py +0 -0
  40. {specsmith-0.3.0.dev128 → specsmith-0.3.0.dev129}/src/specsmith/compressor.py +0 -0
  41. {specsmith-0.3.0.dev128 → specsmith-0.3.0.dev129}/src/specsmith/config.py +0 -0
  42. {specsmith-0.3.0.dev128 → specsmith-0.3.0.dev129}/src/specsmith/credit_analyzer.py +0 -0
  43. {specsmith-0.3.0.dev128 → specsmith-0.3.0.dev129}/src/specsmith/credits.py +0 -0
  44. {specsmith-0.3.0.dev128 → specsmith-0.3.0.dev129}/src/specsmith/differ.py +0 -0
  45. {specsmith-0.3.0.dev128 → specsmith-0.3.0.dev129}/src/specsmith/doctor.py +0 -0
  46. {specsmith-0.3.0.dev128 → specsmith-0.3.0.dev129}/src/specsmith/epistemic/__init__.py +0 -0
  47. {specsmith-0.3.0.dev128 → specsmith-0.3.0.dev129}/src/specsmith/epistemic/belief.py +0 -0
  48. {specsmith-0.3.0.dev128 → specsmith-0.3.0.dev129}/src/specsmith/epistemic/certainty.py +0 -0
  49. {specsmith-0.3.0.dev128 → specsmith-0.3.0.dev129}/src/specsmith/epistemic/failure_graph.py +0 -0
  50. {specsmith-0.3.0.dev128 → specsmith-0.3.0.dev129}/src/specsmith/epistemic/recovery.py +0 -0
  51. {specsmith-0.3.0.dev128 → specsmith-0.3.0.dev129}/src/specsmith/epistemic/stress_tester.py +0 -0
  52. {specsmith-0.3.0.dev128 → specsmith-0.3.0.dev129}/src/specsmith/executor.py +0 -0
  53. {specsmith-0.3.0.dev128 → specsmith-0.3.0.dev129}/src/specsmith/exporter.py +0 -0
  54. {specsmith-0.3.0.dev128 → specsmith-0.3.0.dev129}/src/specsmith/importer.py +0 -0
  55. {specsmith-0.3.0.dev128 → specsmith-0.3.0.dev129}/src/specsmith/integrations/__init__.py +0 -0
  56. {specsmith-0.3.0.dev128 → specsmith-0.3.0.dev129}/src/specsmith/integrations/aider.py +0 -0
  57. {specsmith-0.3.0.dev128 → specsmith-0.3.0.dev129}/src/specsmith/integrations/base.py +0 -0
  58. {specsmith-0.3.0.dev128 → specsmith-0.3.0.dev129}/src/specsmith/integrations/claude_code.py +0 -0
  59. {specsmith-0.3.0.dev128 → specsmith-0.3.0.dev129}/src/specsmith/integrations/copilot.py +0 -0
  60. {specsmith-0.3.0.dev128 → specsmith-0.3.0.dev129}/src/specsmith/integrations/cursor.py +0 -0
  61. {specsmith-0.3.0.dev128 → specsmith-0.3.0.dev129}/src/specsmith/integrations/gemini.py +0 -0
  62. {specsmith-0.3.0.dev128 → specsmith-0.3.0.dev129}/src/specsmith/integrations/warp.py +0 -0
  63. {specsmith-0.3.0.dev128 → specsmith-0.3.0.dev129}/src/specsmith/integrations/windsurf.py +0 -0
  64. {specsmith-0.3.0.dev128 → specsmith-0.3.0.dev129}/src/specsmith/ledger.py +0 -0
  65. {specsmith-0.3.0.dev128 → specsmith-0.3.0.dev129}/src/specsmith/patent.py +0 -0
  66. {specsmith-0.3.0.dev128 → specsmith-0.3.0.dev129}/src/specsmith/plugins.py +0 -0
  67. {specsmith-0.3.0.dev128 → specsmith-0.3.0.dev129}/src/specsmith/rate_limits.py +0 -0
  68. {specsmith-0.3.0.dev128 → specsmith-0.3.0.dev129}/src/specsmith/releaser.py +0 -0
  69. {specsmith-0.3.0.dev128 → specsmith-0.3.0.dev129}/src/specsmith/requirements.py +0 -0
  70. {specsmith-0.3.0.dev128 → specsmith-0.3.0.dev129}/src/specsmith/scaffolder.py +0 -0
  71. {specsmith-0.3.0.dev128 → specsmith-0.3.0.dev129}/src/specsmith/session.py +0 -0
  72. {specsmith-0.3.0.dev128 → specsmith-0.3.0.dev129}/src/specsmith/templates/agents.md.j2 +0 -0
  73. {specsmith-0.3.0.dev128 → specsmith-0.3.0.dev129}/src/specsmith/templates/community/bug_report.md.j2 +0 -0
  74. {specsmith-0.3.0.dev128 → specsmith-0.3.0.dev129}/src/specsmith/templates/community/code_of_conduct.md.j2 +0 -0
  75. {specsmith-0.3.0.dev128 → specsmith-0.3.0.dev129}/src/specsmith/templates/community/contributing.md.j2 +0 -0
  76. {specsmith-0.3.0.dev128 → specsmith-0.3.0.dev129}/src/specsmith/templates/community/feature_request.md.j2 +0 -0
  77. {specsmith-0.3.0.dev128 → specsmith-0.3.0.dev129}/src/specsmith/templates/community/license-Apache-2.0.j2 +0 -0
  78. {specsmith-0.3.0.dev128 → specsmith-0.3.0.dev129}/src/specsmith/templates/community/license-MIT.j2 +0 -0
  79. {specsmith-0.3.0.dev128 → specsmith-0.3.0.dev129}/src/specsmith/templates/community/pull_request_template.md.j2 +0 -0
  80. {specsmith-0.3.0.dev128 → specsmith-0.3.0.dev129}/src/specsmith/templates/community/security.md.j2 +0 -0
  81. {specsmith-0.3.0.dev128 → specsmith-0.3.0.dev129}/src/specsmith/templates/docs/architecture.md.j2 +0 -0
  82. {specsmith-0.3.0.dev128 → specsmith-0.3.0.dev129}/src/specsmith/templates/docs/mkdocs.yml.j2 +0 -0
  83. {specsmith-0.3.0.dev128 → specsmith-0.3.0.dev129}/src/specsmith/templates/docs/readthedocs.yaml.j2 +0 -0
  84. {specsmith-0.3.0.dev128 → specsmith-0.3.0.dev129}/src/specsmith/templates/docs/requirements.md.j2 +0 -0
  85. {specsmith-0.3.0.dev128 → specsmith-0.3.0.dev129}/src/specsmith/templates/docs/test-spec.md.j2 +0 -0
  86. {specsmith-0.3.0.dev128 → specsmith-0.3.0.dev129}/src/specsmith/templates/docs/workflow.md.j2 +0 -0
  87. {specsmith-0.3.0.dev128 → specsmith-0.3.0.dev129}/src/specsmith/templates/editorconfig.j2 +0 -0
  88. {specsmith-0.3.0.dev128 → specsmith-0.3.0.dev129}/src/specsmith/templates/gitattributes.j2 +0 -0
  89. {specsmith-0.3.0.dev128 → specsmith-0.3.0.dev129}/src/specsmith/templates/gitignore.j2 +0 -0
  90. {specsmith-0.3.0.dev128 → specsmith-0.3.0.dev129}/src/specsmith/templates/go/go.mod.j2 +0 -0
  91. {specsmith-0.3.0.dev128 → specsmith-0.3.0.dev129}/src/specsmith/templates/go/main.go.j2 +0 -0
  92. {specsmith-0.3.0.dev128 → specsmith-0.3.0.dev129}/src/specsmith/templates/governance/belief-registry.md.j2 +0 -0
  93. {specsmith-0.3.0.dev128 → specsmith-0.3.0.dev129}/src/specsmith/templates/governance/context-budget.md.j2 +0 -0
  94. {specsmith-0.3.0.dev128 → specsmith-0.3.0.dev129}/src/specsmith/templates/governance/drift-metrics.md.j2 +0 -0
  95. {specsmith-0.3.0.dev128 → specsmith-0.3.0.dev129}/src/specsmith/templates/governance/epistemic-axioms.md.j2 +0 -0
  96. {specsmith-0.3.0.dev128 → specsmith-0.3.0.dev129}/src/specsmith/templates/governance/failure-modes.md.j2 +0 -0
  97. {specsmith-0.3.0.dev128 → specsmith-0.3.0.dev129}/src/specsmith/templates/governance/roles.md.j2 +0 -0
  98. {specsmith-0.3.0.dev128 → specsmith-0.3.0.dev129}/src/specsmith/templates/governance/rules.md.j2 +0 -0
  99. {specsmith-0.3.0.dev128 → specsmith-0.3.0.dev129}/src/specsmith/templates/governance/uncertainty-map.md.j2 +0 -0
  100. {specsmith-0.3.0.dev128 → specsmith-0.3.0.dev129}/src/specsmith/templates/governance/verification.md.j2 +0 -0
  101. {specsmith-0.3.0.dev128 → specsmith-0.3.0.dev129}/src/specsmith/templates/governance/workflow.md.j2 +0 -0
  102. {specsmith-0.3.0.dev128 → specsmith-0.3.0.dev129}/src/specsmith/templates/js/package.json.j2 +0 -0
  103. {specsmith-0.3.0.dev128 → specsmith-0.3.0.dev129}/src/specsmith/templates/ledger.md.j2 +0 -0
  104. {specsmith-0.3.0.dev128 → specsmith-0.3.0.dev129}/src/specsmith/templates/python/cli.py.j2 +0 -0
  105. {specsmith-0.3.0.dev128 → specsmith-0.3.0.dev129}/src/specsmith/templates/python/init.py.j2 +0 -0
  106. {specsmith-0.3.0.dev128 → specsmith-0.3.0.dev129}/src/specsmith/templates/python/pyproject.toml.j2 +0 -0
  107. {specsmith-0.3.0.dev128 → specsmith-0.3.0.dev129}/src/specsmith/templates/readme.md.j2 +0 -0
  108. {specsmith-0.3.0.dev128 → specsmith-0.3.0.dev129}/src/specsmith/templates/rust/Cargo.toml.j2 +0 -0
  109. {specsmith-0.3.0.dev128 → specsmith-0.3.0.dev129}/src/specsmith/templates/rust/main.rs.j2 +0 -0
  110. {specsmith-0.3.0.dev128 → specsmith-0.3.0.dev129}/src/specsmith/templates/scripts/exec.cmd.j2 +0 -0
  111. {specsmith-0.3.0.dev128 → specsmith-0.3.0.dev129}/src/specsmith/templates/scripts/exec.sh.j2 +0 -0
  112. {specsmith-0.3.0.dev128 → specsmith-0.3.0.dev129}/src/specsmith/templates/scripts/run.cmd.j2 +0 -0
  113. {specsmith-0.3.0.dev128 → specsmith-0.3.0.dev129}/src/specsmith/templates/scripts/run.sh.j2 +0 -0
  114. {specsmith-0.3.0.dev128 → specsmith-0.3.0.dev129}/src/specsmith/templates/scripts/setup.cmd.j2 +0 -0
  115. {specsmith-0.3.0.dev128 → specsmith-0.3.0.dev129}/src/specsmith/templates/scripts/setup.sh.j2 +0 -0
  116. {specsmith-0.3.0.dev128 → specsmith-0.3.0.dev129}/src/specsmith/templates/workflows/release.yml.j2 +0 -0
  117. {specsmith-0.3.0.dev128 → specsmith-0.3.0.dev129}/src/specsmith/tools.py +0 -0
  118. {specsmith-0.3.0.dev128 → specsmith-0.3.0.dev129}/src/specsmith/trace.py +0 -0
  119. {specsmith-0.3.0.dev128 → specsmith-0.3.0.dev129}/src/specsmith/updater.py +0 -0
  120. {specsmith-0.3.0.dev128 → specsmith-0.3.0.dev129}/src/specsmith/upgrader.py +0 -0
  121. {specsmith-0.3.0.dev128 → specsmith-0.3.0.dev129}/src/specsmith/validator.py +0 -0
  122. {specsmith-0.3.0.dev128 → specsmith-0.3.0.dev129}/src/specsmith/vcs/__init__.py +0 -0
  123. {specsmith-0.3.0.dev128 → specsmith-0.3.0.dev129}/src/specsmith/vcs/base.py +0 -0
  124. {specsmith-0.3.0.dev128 → specsmith-0.3.0.dev129}/src/specsmith/vcs/bitbucket.py +0 -0
  125. {specsmith-0.3.0.dev128 → specsmith-0.3.0.dev129}/src/specsmith/vcs/github.py +0 -0
  126. {specsmith-0.3.0.dev128 → specsmith-0.3.0.dev129}/src/specsmith/vcs/gitlab.py +0 -0
  127. {specsmith-0.3.0.dev128 → specsmith-0.3.0.dev129}/src/specsmith/vcs_commands.py +0 -0
  128. {specsmith-0.3.0.dev128 → specsmith-0.3.0.dev129}/src/specsmith/workspace.py +0 -0
  129. {specsmith-0.3.0.dev128 → specsmith-0.3.0.dev129}/src/specsmith.egg-info/dependency_links.txt +0 -0
  130. {specsmith-0.3.0.dev128 → specsmith-0.3.0.dev129}/src/specsmith.egg-info/entry_points.txt +0 -0
  131. {specsmith-0.3.0.dev128 → specsmith-0.3.0.dev129}/src/specsmith.egg-info/top_level.txt +0 -0
  132. {specsmith-0.3.0.dev128 → specsmith-0.3.0.dev129}/tests/test_auditor.py +0 -0
  133. {specsmith-0.3.0.dev128 → specsmith-0.3.0.dev129}/tests/test_cli.py +0 -0
  134. {specsmith-0.3.0.dev128 → specsmith-0.3.0.dev129}/tests/test_compressor.py +0 -0
  135. {specsmith-0.3.0.dev128 → specsmith-0.3.0.dev129}/tests/test_epistemic.py +0 -0
  136. {specsmith-0.3.0.dev128 → specsmith-0.3.0.dev129}/tests/test_importer.py +0 -0
  137. {specsmith-0.3.0.dev128 → specsmith-0.3.0.dev129}/tests/test_integrations.py +0 -0
  138. {specsmith-0.3.0.dev128 → specsmith-0.3.0.dev129}/tests/test_rate_limits.py +0 -0
  139. {specsmith-0.3.0.dev128 → specsmith-0.3.0.dev129}/tests/test_scaffolder.py +0 -0
  140. {specsmith-0.3.0.dev128 → specsmith-0.3.0.dev129}/tests/test_smoke.py +0 -0
  141. {specsmith-0.3.0.dev128 → specsmith-0.3.0.dev129}/tests/test_tools.py +0 -0
  142. {specsmith-0.3.0.dev128 → specsmith-0.3.0.dev129}/tests/test_validator.py +0 -0
  143. {specsmith-0.3.0.dev128 → specsmith-0.3.0.dev129}/tests/test_vcs.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: specsmith
3
- Version: 0.3.0.dev128
3
+ Version: 0.3.0.dev129
4
4
  Summary: Applied Epistemic Engineering toolkit — forge epistemically-governed scaffolds, stress-test belief systems, and run AEE pipelines.
5
5
  Author: BitConcepts
6
6
  License-Expression: MIT
@@ -41,12 +41,14 @@ Requires-Dist: types-pyyaml>=6.0; extra == "dev"
41
41
  Provides-Extra: docs
42
42
  Requires-Dist: mkdocs>=1.6; extra == "docs"
43
43
  Requires-Dist: mkdocs-material>=9.5; extra == "docs"
44
- Provides-Extra: anthopic
45
- Requires-Dist: anthropic>=0.56; extra == "anthopic"
44
+ Provides-Extra: anthropic
45
+ Requires-Dist: anthropic>=0.56; extra == "anthropic"
46
46
  Provides-Extra: openai
47
47
  Requires-Dist: openai>=1.0; extra == "openai"
48
48
  Provides-Extra: gemini
49
49
  Requires-Dist: google-generativeai>=0.8; extra == "gemini"
50
+ Provides-Extra: mistral
51
+ Requires-Dist: openai>=1.0; extra == "mistral"
50
52
  Provides-Extra: agent
51
53
  Requires-Dist: anthropic>=0.56; extra == "agent"
52
54
  Requires-Dist: openai>=1.0; extra == "agent"
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "specsmith"
7
- version = "0.3.0.dev128"
7
+ version = "0.3.0.dev129"
8
8
  description = "Applied Epistemic Engineering toolkit — forge epistemically-governed scaffolds, stress-test belief systems, and run AEE pipelines."
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -55,9 +55,10 @@ docs = [
55
55
  "mkdocs-material>=9.5",
56
56
  ]
57
57
  # LLM provider extras for specsmith run (agentic client)
58
- anthopic = ["anthropic>=0.56"]
58
+ anthropic = ["anthropic>=0.56"]
59
59
  openai = ["openai>=1.0"]
60
60
  gemini = ["google-generativeai>=0.8"]
61
+ mistral = ["openai>=1.0"] # Mistral uses the openai SDK pointed at api.mistral.ai
61
62
  # Install all optional LLM providers
62
63
  agent = ["anthropic>=0.56", "openai>=1.0"]
63
64
  # Convenience bundle: everything
@@ -37,7 +37,7 @@ def get_provider(
37
37
  """Get a configured LLM provider.
38
38
 
39
39
  Args:
40
- provider_name: "anthropic", "openai", "gemini", "ollama", or None (auto-detect)
40
+ provider_name: "anthropic", "openai", "gemini", "mistral", "ollama", or None (auto-detect)
41
41
  model: specific model name, or None (use tier default)
42
42
  tier: ModelTier.FAST / BALANCED / POWERFUL
43
43
  base_url: override API base URL (for OpenAI-compatible proxies)
@@ -76,6 +76,13 @@ def get_provider(
76
76
  model=resolved_model or "gemini-2.5-pro",
77
77
  api_key=api_key or os.environ.get("GOOGLE_API_KEY", ""),
78
78
  )
79
+ elif provider_name == "mistral":
80
+ from specsmith.agent.providers.mistral import MistralProvider
81
+
82
+ return MistralProvider(
83
+ model=resolved_model or "mistral-large-latest",
84
+ api_key=api_key or os.environ.get("MISTRAL_API_KEY", ""),
85
+ )
79
86
  elif provider_name == "ollama":
80
87
  from specsmith.agent.providers.ollama import OllamaProvider
81
88
 
@@ -85,7 +92,7 @@ def get_provider(
85
92
  )
86
93
  else:
87
94
  raise ValueError(
88
- f"Unknown provider '{provider_name}'. Valid: anthropic, openai, gemini, ollama"
95
+ f"Unknown provider '{provider_name}'. Valid: anthropic, openai, gemini, mistral, ollama"
89
96
  )
90
97
 
91
98
 
@@ -101,6 +108,8 @@ def _auto_detect_provider() -> str:
101
108
  return "openai"
102
109
  if os.environ.get("GOOGLE_API_KEY"):
103
110
  return "gemini"
111
+ if os.environ.get("MISTRAL_API_KEY"):
112
+ return "mistral"
104
113
 
105
114
  # Try Ollama (no API key needed)
106
115
  import urllib.request
@@ -126,6 +135,7 @@ def list_providers() -> list[dict[str, str]]:
126
135
  ("anthropic", "ANTHROPIC_API_KEY"),
127
136
  ("openai", "OPENAI_API_KEY"),
128
137
  ("gemini", "GOOGLE_API_KEY"),
138
+ ("mistral", "MISTRAL_API_KEY"),
129
139
  ("ollama", ""),
130
140
  ]:
131
141
  if name == "ollama":
@@ -0,0 +1,175 @@
1
+ # SPDX-License-Identifier: MIT
2
+ # Copyright (c) 2026 BitConcepts, LLC. All rights reserved.
3
+ """Mistral AI provider for the specsmith agentic client.
4
+
5
+ Mistral's API is OpenAI-compatible, so we use the openai SDK pointed at
6
+ Mistral's endpoint. Pixtral models support vision/OCR.
7
+
8
+ Requires: pip install specsmith[mistral]
9
+
10
+ Environment:
11
+ MISTRAL_API_KEY — your Mistral API key (https://console.mistral.ai/)
12
+
13
+ Models:
14
+ mistral-large-latest — most capable text model
15
+ mistral-small-latest — fast, cheap
16
+ codestral-latest — code-optimised (FIM support)
17
+ pixtral-large-latest — vision + OCR (multimodal)
18
+ pixtral-12b-2409 — smaller vision model
19
+ """
20
+
21
+ from __future__ import annotations
22
+
23
+ from collections.abc import Iterator
24
+ from typing import Any
25
+
26
+ from specsmith.agent.core import (
27
+ CompletionResponse,
28
+ Message,
29
+ StreamToken,
30
+ Tool,
31
+ )
32
+
33
+ _MISTRAL_BASE_URL = "https://api.mistral.ai/v1"
34
+
35
+ # Pixtral models that support image/document input (OCR use-cases)
36
+ _VISION_MODELS = {"pixtral-large-latest", "pixtral-12b-2409", "pixtral-large-2411"}
37
+
38
+
39
+ class MistralProvider:
40
+ """Mistral AI provider. Uses the OpenAI-compatible chat completions API."""
41
+
42
+ provider_name = "mistral"
43
+
44
+ def __init__(self, model: str = "mistral-large-latest", api_key: str = "") -> None:
45
+ self.model = model
46
+ self._api_key = api_key
47
+ self._client: Any = None
48
+ self._ensure_client()
49
+
50
+ def _ensure_client(self) -> None:
51
+ try:
52
+ import openai
53
+
54
+ self._client = openai.OpenAI(
55
+ api_key=self._api_key or "placeholder",
56
+ base_url=_MISTRAL_BASE_URL,
57
+ )
58
+ except ImportError as e:
59
+ from specsmith.agent.core import ProviderNotAvailable
60
+
61
+ raise ProviderNotAvailable("mistral", "openai") from e
62
+
63
+ def is_available(self) -> bool:
64
+ try:
65
+ import openai # noqa: F401
66
+
67
+ return bool(self._api_key)
68
+ except ImportError:
69
+ return False
70
+
71
+ @property
72
+ def supports_vision(self) -> bool:
73
+ return self.model in _VISION_MODELS
74
+
75
+ def complete(
76
+ self,
77
+ messages: list[Message],
78
+ tools: list[Tool] | None = None,
79
+ max_tokens: int = 4096,
80
+ ) -> CompletionResponse:
81
+ msgs = [m.to_dict() for m in messages]
82
+ kwargs: dict[str, Any] = {
83
+ "model": self.model,
84
+ "messages": msgs,
85
+ "max_tokens": max_tokens,
86
+ }
87
+ if tools:
88
+ kwargs["tools"] = [t.to_openai_schema() for t in tools]
89
+ kwargs["tool_choice"] = "auto"
90
+
91
+ response = self._client.chat.completions.create(**kwargs)
92
+ choice = response.choices[0]
93
+ content = choice.message.content or ""
94
+
95
+ tool_calls: list[dict[str, Any]] = []
96
+ if choice.message.tool_calls:
97
+ for tc in choice.message.tool_calls:
98
+ import json
99
+
100
+ tool_calls.append(
101
+ {
102
+ "id": tc.id,
103
+ "name": tc.function.name,
104
+ "input": json.loads(tc.function.arguments or "{}"),
105
+ }
106
+ )
107
+
108
+ usage = response.usage
109
+ return CompletionResponse(
110
+ content=content,
111
+ model=response.model,
112
+ input_tokens=usage.prompt_tokens if usage else 0,
113
+ output_tokens=usage.completion_tokens if usage else 0,
114
+ tool_calls=tool_calls,
115
+ stop_reason=choice.finish_reason or "stop",
116
+ )
117
+
118
+ def stream(
119
+ self,
120
+ messages: list[Message],
121
+ tools: list[Tool] | None = None,
122
+ max_tokens: int = 4096,
123
+ ) -> Iterator[StreamToken]:
124
+ msgs = [m.to_dict() for m in messages]
125
+ kwargs: dict[str, Any] = {
126
+ "model": self.model,
127
+ "messages": msgs,
128
+ "max_tokens": max_tokens,
129
+ "stream": True,
130
+ }
131
+ stream = self._client.chat.completions.create(**kwargs)
132
+ for chunk in stream:
133
+ delta = chunk.choices[0].delta if chunk.choices else None
134
+ if delta and delta.content:
135
+ yield StreamToken(text=delta.content)
136
+ yield StreamToken(text="", is_final=True)
137
+
138
+ def ocr_image(self, image_path: str, prompt: str = "Extract all text from this image.") -> str:
139
+ """Extract text from an image using a Pixtral vision model.
140
+
141
+ Automatically switches to pixtral-large-latest if current model is text-only.
142
+ """
143
+ import base64
144
+ from pathlib import Path
145
+
146
+ model = self.model if self.supports_vision else "pixtral-large-latest"
147
+ img_bytes = Path(image_path).read_bytes()
148
+ b64 = base64.b64encode(img_bytes).decode()
149
+
150
+ # Detect MIME type from extension
151
+ ext = Path(image_path).suffix.lower()
152
+ mime_map = {
153
+ ".jpg": "image/jpeg",
154
+ ".jpeg": "image/jpeg",
155
+ ".png": "image/png",
156
+ ".gif": "image/gif",
157
+ ".webp": "image/webp",
158
+ ".pdf": "application/pdf",
159
+ }
160
+ mime = mime_map.get(ext, "image/png")
161
+
162
+ response = self._client.chat.completions.create(
163
+ model=model,
164
+ messages=[
165
+ {
166
+ "role": "user",
167
+ "content": [
168
+ {"type": "image_url", "image_url": {"url": f"data:{mime};base64,{b64}"}},
169
+ {"type": "text", "text": prompt},
170
+ ],
171
+ }
172
+ ],
173
+ max_tokens=4096,
174
+ )
175
+ return response.choices[0].message.content or ""
@@ -320,26 +320,23 @@ class AgentRunner:
320
320
  return final_response
321
321
 
322
322
  def _call_provider(self, messages: list[Message], silent: bool = False) -> CompletionResponse:
323
- """Call the LLM provider, streaming if enabled."""
324
- provider: Any = self._provider # provider is typed as Any at runtime for flexibility
325
- if self._stream and not silent:
326
- # Stream the response
323
+ """Call the LLM provider, streaming if enabled.
324
+
325
+ Streaming is disabled when tools are registered because the streaming
326
+ path cannot reliably capture tool_call blocks from the response.
327
+ Non-streaming is always used for tool-bearing turns.
328
+ """
329
+ provider: Any = self._provider
330
+ use_stream = self._stream and not silent and not self._tools
331
+ if use_stream:
327
332
  accumulated = ""
328
- for token in provider.stream(messages, tools=self._tools):
333
+ for token in provider.stream(messages, tools=None):
329
334
  if token.text:
330
335
  self._print(token.text, end="", flush=True)
331
336
  accumulated += token.text
332
337
  if token.is_final:
333
338
  self._print()
334
- # Re-call non-streaming for tool detection (some providers don't support tool streaming)
335
- # For now, do a second call if we didn't get tool calls
336
- if not accumulated.strip():
337
- return cast(CompletionResponse, provider.complete(messages, tools=self._tools))
338
- # Return a synthetic response with the streamed content
339
- return CompletionResponse(
340
- content=accumulated,
341
- model=str(provider.model),
342
- )
339
+ return CompletionResponse(content=accumulated, model=str(provider.model))
343
340
  else:
344
341
  response = cast(CompletionResponse, provider.complete(messages, tools=self._tools))
345
342
  if not silent and response.content:
@@ -16,6 +16,10 @@ Tool categories:
16
16
 
17
17
  from __future__ import annotations
18
18
 
19
+ import fnmatch
20
+ import os
21
+ import platform
22
+ import re
19
23
  import subprocess
20
24
  import sys
21
25
  from pathlib import Path
@@ -316,6 +320,100 @@ def build_tool_registry(project_dir: str = ".") -> list[Tool]:
316
320
  ],
317
321
  handler=lambda path, lines="": _read_file_handler(pd, path, lines),
318
322
  ),
323
+ # ----------------------------------------------------------------
324
+ # File system write tools
325
+ # ----------------------------------------------------------------
326
+ Tool(
327
+ name="write_file",
328
+ description=(
329
+ "Write content to a file (creates or overwrites). "
330
+ "Use for editing source code, docs, config files, etc. "
331
+ "Path is relative to project root."
332
+ ),
333
+ params=[
334
+ ToolParam("path", "File path relative to project root"),
335
+ ToolParam("content", "Full content to write to the file"),
336
+ ],
337
+ handler=lambda path, content: _write_file_handler(pd, path, content),
338
+ ),
339
+ Tool(
340
+ name="list_dir",
341
+ description=(
342
+ "List files and directories. Shows names, sizes, and types. "
343
+ "Use to explore project structure before reading files."
344
+ ),
345
+ params=[
346
+ ToolParam(
347
+ "path",
348
+ "Directory path relative to project root (default: root)",
349
+ required=False,
350
+ ),
351
+ ToolParam(
352
+ "pattern",
353
+ "Glob pattern to filter (e.g. '*.py', '*.md')",
354
+ required=False,
355
+ ),
356
+ ],
357
+ handler=lambda path=".", pattern="": _list_dir_handler(pd, path, pattern),
358
+ ),
359
+ Tool(
360
+ name="grep_files",
361
+ description=(
362
+ "Search for a regex pattern across files in the project. "
363
+ "Returns matching lines with file:line references. "
364
+ "Essential for finding where things are defined or used."
365
+ ),
366
+ params=[
367
+ ToolParam("pattern", "Regex pattern to search for"),
368
+ ToolParam(
369
+ "path",
370
+ "Directory or file to search (relative to root, default: root)",
371
+ required=False,
372
+ ),
373
+ ToolParam(
374
+ "glob",
375
+ "File glob filter e.g. '*.py' (default: all text files)",
376
+ required=False,
377
+ ),
378
+ ToolParam(
379
+ "ignore_case",
380
+ "'true' for case-insensitive search",
381
+ required=False,
382
+ ),
383
+ ],
384
+ handler=lambda pattern, path=".", glob="", ignore_case="false": _grep_handler(
385
+ pd, pattern, path, glob, ignore_case
386
+ ),
387
+ ),
388
+ # ----------------------------------------------------------------
389
+ # Shell execution — the most powerful tool
390
+ # ----------------------------------------------------------------
391
+ Tool(
392
+ name="run_command",
393
+ description=(
394
+ "Execute a shell command in the project directory and return stdout+stderr. "
395
+ "Use for: running tests (pytest), linting (ruff), building, git operations, "
396
+ "installing packages, checking file contents with CLI tools, anything. "
397
+ "Cross-platform: automatically uses PowerShell on Windows, bash on Linux/macOS. "
398
+ "Commands run with a 120-second timeout."
399
+ ),
400
+ params=[
401
+ ToolParam("command", "The shell command to execute"),
402
+ ToolParam(
403
+ "working_dir",
404
+ "Working directory relative to project root (default: root)",
405
+ required=False,
406
+ ),
407
+ ToolParam(
408
+ "timeout",
409
+ "Timeout in seconds (default 120, max 300)",
410
+ required=False,
411
+ ),
412
+ ],
413
+ handler=lambda command, working_dir=".", timeout="120": _run_command_handler(
414
+ pd, command, working_dir, timeout
415
+ ),
416
+ ),
319
417
  ]
320
418
 
321
419
  return tools
@@ -350,5 +448,207 @@ def _read_file_handler(project_dir: str, path: str, lines: str = "") -> str:
350
448
  return f"[ERROR] {e}"
351
449
 
352
450
 
451
+ def _write_file_handler(project_dir: str, path: str, content: str) -> str:
452
+ """Write content to a file within the project directory."""
453
+ root = Path(project_dir).resolve()
454
+ target = (root / path).resolve()
455
+ try:
456
+ target.relative_to(root)
457
+ except ValueError:
458
+ return f"[ERROR] Path '{path}' is outside the project directory"
459
+ try:
460
+ target.parent.mkdir(parents=True, exist_ok=True)
461
+ target.write_text(content, encoding="utf-8")
462
+ size = len(content.encode("utf-8"))
463
+ lines = content.count("\n") + 1
464
+ return f"Written: {path} ({lines} lines, {size} bytes)"
465
+ except Exception as e: # noqa: BLE001
466
+ return f"[ERROR] {e}"
467
+
468
+
469
+ def _list_dir_handler(project_dir: str, path: str = ".", pattern: str = "") -> str:
470
+ """List directory contents within the project."""
471
+ root = Path(project_dir).resolve()
472
+ target = (root / path).resolve()
473
+ try:
474
+ target.relative_to(root)
475
+ except ValueError:
476
+ return f"[ERROR] Path '{path}' is outside the project directory"
477
+ if not target.exists():
478
+ return f"[NOT FOUND] {path}"
479
+ if not target.is_dir():
480
+ return f"[NOT A DIR] {path}"
481
+ try:
482
+ entries = sorted(target.iterdir(), key=lambda p: (p.is_file(), p.name.lower()))
483
+ lines = []
484
+ for entry in entries:
485
+ if pattern and not fnmatch.fnmatch(entry.name, pattern):
486
+ continue
487
+ if entry.is_dir():
488
+ lines.append(f" {'DIR':>6} {entry.name}/")
489
+ else:
490
+ size = entry.stat().st_size
491
+ size_str = f"{size:,}" if size < 1_000_000 else f"{size // 1024:,}K"
492
+ lines.append(f" {size_str:>6} {entry.name}")
493
+ header = f"{path}/" if not path.endswith("/") else path
494
+ return f"{header}\n" + "\n".join(lines) if lines else f"{header} (empty)"
495
+ except Exception as e: # noqa: BLE001
496
+ return f"[ERROR] {e}"
497
+
498
+
499
+ def _grep_handler(
500
+ project_dir: str,
501
+ pattern: str,
502
+ path: str = ".",
503
+ glob: str = "",
504
+ ignore_case: str = "false",
505
+ ) -> str:
506
+ """Search for a regex pattern in files within the project."""
507
+ root = Path(project_dir).resolve()
508
+ target = (root / path).resolve()
509
+ try:
510
+ target.relative_to(root)
511
+ except ValueError:
512
+ return f"[ERROR] Path '{path}' is outside the project directory"
513
+
514
+ flags = re.IGNORECASE if ignore_case.lower() == "true" else 0
515
+ try:
516
+ compiled = re.compile(pattern, flags)
517
+ except re.error as e:
518
+ return f"[ERROR] Invalid regex: {e}"
519
+
520
+ _TEXT_EXTENSIONS = {
521
+ ".py",
522
+ ".md",
523
+ ".txt",
524
+ ".yml",
525
+ ".yaml",
526
+ ".toml",
527
+ ".json",
528
+ ".js",
529
+ ".ts",
530
+ ".html",
531
+ ".css",
532
+ ".sh",
533
+ ".ps1",
534
+ ".cmd",
535
+ ".bat",
536
+ ".rs",
537
+ ".go",
538
+ ".c",
539
+ ".cpp",
540
+ ".h",
541
+ ".java",
542
+ ".rb",
543
+ ".php",
544
+ ".tf",
545
+ ".ini",
546
+ ".cfg",
547
+ ".conf",
548
+ }
549
+ _SKIP_DIRS = {".git", "__pycache__", ".venv", "node_modules", ".mypy_cache", "dist", "build"}
550
+
551
+ results: list[str] = []
552
+ files_searched = 0
553
+
554
+ def search_file(fp: Path) -> None:
555
+ nonlocal files_searched
556
+ if glob and not fnmatch.fnmatch(fp.name, glob):
557
+ return
558
+ if not glob and fp.suffix.lower() not in _TEXT_EXTENSIONS:
559
+ return
560
+ try:
561
+ text = fp.read_text(encoding="utf-8", errors="ignore")
562
+ files_searched += 1
563
+ rel = fp.relative_to(root)
564
+ for i, line in enumerate(text.splitlines(), 1):
565
+ if compiled.search(line):
566
+ results.append(f"{rel}:{i}: {line.rstrip()}")
567
+ if len(results) >= 200:
568
+ return
569
+ except Exception: # noqa: BLE001
570
+ pass
571
+
572
+ if target.is_file():
573
+ search_file(target)
574
+ else:
575
+ for dirpath, dirnames, filenames in os.walk(target):
576
+ dirnames[:] = [d for d in dirnames if d not in _SKIP_DIRS]
577
+ for fname in sorted(filenames):
578
+ search_file(Path(dirpath) / fname)
579
+ if len(results) >= 200:
580
+ break
581
+ if len(results) >= 200:
582
+ break
583
+
584
+ if not results:
585
+ return f"No matches for '{pattern}' in {files_searched} file(s) searched."
586
+ summary = f"{len(results)} match(es) across {files_searched} file(s):"
587
+ if len(results) >= 200:
588
+ summary += " (truncated at 200)"
589
+ return summary + "\n" + "\n".join(results)
590
+
591
+
592
+ def _run_command_handler(
593
+ project_dir: str,
594
+ command: str,
595
+ working_dir: str = ".",
596
+ timeout: str = "120",
597
+ ) -> str:
598
+ """Execute a shell command and return combined stdout+stderr."""
599
+ root = Path(project_dir).resolve()
600
+ cwd = (root / working_dir).resolve()
601
+ try:
602
+ cwd.relative_to(root)
603
+ except ValueError:
604
+ return f"[ERROR] working_dir '{working_dir}' is outside the project directory"
605
+
606
+ try:
607
+ timeout_secs = min(int(timeout), 300)
608
+ except (ValueError, TypeError):
609
+ timeout_secs = 120
610
+
611
+ # Choose shell based on platform
612
+ is_windows = platform.system() == "Windows"
613
+ if is_windows:
614
+ shell_cmd = ["powershell", "-NoProfile", "-NonInteractive", "-Command", command]
615
+ else:
616
+ shell_cmd = ["bash", "-c", command]
617
+
618
+ try:
619
+ result = subprocess.run(
620
+ shell_cmd,
621
+ capture_output=True,
622
+ text=True,
623
+ cwd=str(cwd),
624
+ timeout=timeout_secs,
625
+ )
626
+ output = (result.stdout + result.stderr).strip()
627
+ exit_info = f"[exit {result.returncode}]" if result.returncode != 0 else "[exit 0]"
628
+ if len(output) > 12000:
629
+ output = output[:12000] + f"\n...(truncated, {len(output)} total chars)"
630
+ return f"{exit_info}\n{output}" if output else exit_info
631
+ except subprocess.TimeoutExpired:
632
+ return f"[TIMEOUT] Command exceeded {timeout_secs}s"
633
+ except FileNotFoundError:
634
+ # Shell not found (e.g. bash on Windows) — fall back
635
+ try:
636
+ result = subprocess.run(
637
+ command,
638
+ capture_output=True,
639
+ text=True,
640
+ cwd=str(cwd),
641
+ timeout=timeout_secs,
642
+ shell=True, # noqa: S602
643
+ )
644
+ output = (result.stdout + result.stderr).strip()
645
+ rc = result.returncode
646
+ return f"[exit {rc}]\n{output}" if output else f"[exit {rc}]"
647
+ except Exception as e2: # noqa: BLE001
648
+ return f"[ERROR] {e2}"
649
+ except Exception as e: # noqa: BLE001
650
+ return f"[ERROR] {e}"
651
+
652
+
353
653
  def get_tool_by_name(tools: list[Tool], name: str) -> Tool | None:
354
654
  return next((t for t in tools if t.name == name), None)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: specsmith
3
- Version: 0.3.0.dev128
3
+ Version: 0.3.0.dev129
4
4
  Summary: Applied Epistemic Engineering toolkit — forge epistemically-governed scaffolds, stress-test belief systems, and run AEE pipelines.
5
5
  Author: BitConcepts
6
6
  License-Expression: MIT
@@ -41,12 +41,14 @@ Requires-Dist: types-pyyaml>=6.0; extra == "dev"
41
41
  Provides-Extra: docs
42
42
  Requires-Dist: mkdocs>=1.6; extra == "docs"
43
43
  Requires-Dist: mkdocs-material>=9.5; extra == "docs"
44
- Provides-Extra: anthopic
45
- Requires-Dist: anthropic>=0.56; extra == "anthopic"
44
+ Provides-Extra: anthropic
45
+ Requires-Dist: anthropic>=0.56; extra == "anthropic"
46
46
  Provides-Extra: openai
47
47
  Requires-Dist: openai>=1.0; extra == "openai"
48
48
  Provides-Extra: gemini
49
49
  Requires-Dist: google-generativeai>=0.8; extra == "gemini"
50
+ Provides-Extra: mistral
51
+ Requires-Dist: openai>=1.0; extra == "mistral"
50
52
  Provides-Extra: agent
51
53
  Requires-Dist: anthropic>=0.56; extra == "agent"
52
54
  Requires-Dist: openai>=1.0; extra == "agent"
@@ -58,6 +58,7 @@ src/specsmith/agent/profiles/verifier.md
58
58
  src/specsmith/agent/providers/__init__.py
59
59
  src/specsmith/agent/providers/anthropic.py
60
60
  src/specsmith/agent/providers/gemini.py
61
+ src/specsmith/agent/providers/mistral.py
61
62
  src/specsmith/agent/providers/ollama.py
62
63
  src/specsmith/agent/providers/openai.py
63
64
  src/specsmith/commands/__init__.py
@@ -16,7 +16,7 @@ pytest-cov>=4.0
16
16
  ruff>=0.4
17
17
  mypy>=1.10
18
18
 
19
- [anthopic]
19
+ [anthropic]
20
20
  anthropic>=0.56
21
21
 
22
22
  [dev]
@@ -34,5 +34,8 @@ mkdocs-material>=9.5
34
34
  [gemini]
35
35
  google-generativeai>=0.8
36
36
 
37
+ [mistral]
38
+ openai>=1.0
39
+
37
40
  [openai]
38
41
  openai>=1.0