cade-cli 0.15.4__tar.gz → 0.15.6__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 (166) hide show
  1. {cade_cli-0.15.4 → cade_cli-0.15.6}/PKG-INFO +3 -3
  2. {cade_cli-0.15.4 → cade_cli-0.15.6}/README.md +1 -1
  3. {cade_cli-0.15.4 → cade_cli-0.15.6}/pyproject.toml +2 -2
  4. cade_cli-0.15.6/src/cade_mcp_local/web/extract.py +115 -0
  5. {cade_cli-0.15.4 → cade_cli-0.15.6}/src/cadecoder/cli/commands/mem.py +4 -0
  6. {cade_cli-0.15.4 → cade_cli-0.15.6}/src/cadecoder/ui/display.py +12 -1
  7. {cade_cli-0.15.4 → cade_cli-0.15.6}/src/cadecoder/voice/audio.py +49 -0
  8. {cade_cli-0.15.4 → cade_cli-0.15.6}/src/cadecoder/voice/stt.py +35 -23
  9. cade_cli-0.15.4/src/cade_mcp_local/web/extract.py +0 -54
  10. {cade_cli-0.15.4 → cade_cli-0.15.6}/.gitignore +0 -0
  11. {cade_cli-0.15.4 → cade_cli-0.15.6}/LICENSE +0 -0
  12. {cade_cli-0.15.4 → cade_cli-0.15.6}/src/cade_mcp_local/__init__.py +0 -0
  13. {cade_cli-0.15.4 → cade_cli-0.15.6}/src/cade_mcp_local/__main__.py +0 -0
  14. {cade_cli-0.15.4 → cade_cli-0.15.6}/src/cade_mcp_local/_metadata.py +0 -0
  15. {cade_cli-0.15.4 → cade_cli-0.15.6}/src/cade_mcp_local/config.py +0 -0
  16. {cade_cli-0.15.4 → cade_cli-0.15.6}/src/cade_mcp_local/errors.py +0 -0
  17. {cade_cli-0.15.4 → cade_cli-0.15.6}/src/cade_mcp_local/resources.py +0 -0
  18. {cade_cli-0.15.4 → cade_cli-0.15.6}/src/cade_mcp_local/server.py +0 -0
  19. {cade_cli-0.15.4 → cade_cli-0.15.6}/src/cade_mcp_local/tools/__init__.py +0 -0
  20. {cade_cli-0.15.4 → cade_cli-0.15.6}/src/cade_mcp_local/tools/_read_cache.py +0 -0
  21. {cade_cli-0.15.4 → cade_cli-0.15.6}/src/cade_mcp_local/tools/agents.py +0 -0
  22. {cade_cli-0.15.4 → cade_cli-0.15.6}/src/cade_mcp_local/tools/context.py +0 -0
  23. {cade_cli-0.15.4 → cade_cli-0.15.6}/src/cade_mcp_local/tools/filesystem.py +0 -0
  24. {cade_cli-0.15.4 → cade_cli-0.15.6}/src/cade_mcp_local/tools/git.py +0 -0
  25. {cade_cli-0.15.4 → cade_cli-0.15.6}/src/cade_mcp_local/tools/notifications.py +0 -0
  26. {cade_cli-0.15.4 → cade_cli-0.15.6}/src/cade_mcp_local/tools/questions.py +0 -0
  27. {cade_cli-0.15.4 → cade_cli-0.15.6}/src/cade_mcp_local/tools/results.py +0 -0
  28. {cade_cli-0.15.4 → cade_cli-0.15.6}/src/cade_mcp_local/tools/schemas.py +0 -0
  29. {cade_cli-0.15.4 → cade_cli-0.15.6}/src/cade_mcp_local/tools/search.py +0 -0
  30. {cade_cli-0.15.4 → cade_cli-0.15.6}/src/cade_mcp_local/tools/shell.py +0 -0
  31. {cade_cli-0.15.4 → cade_cli-0.15.6}/src/cade_mcp_local/tools/snippets.py +0 -0
  32. {cade_cli-0.15.4 → cade_cli-0.15.6}/src/cade_mcp_local/tools/tasks.py +0 -0
  33. {cade_cli-0.15.4 → cade_cli-0.15.6}/src/cade_mcp_local/tools/web_fetch.py +0 -0
  34. {cade_cli-0.15.4 → cade_cli-0.15.6}/src/cade_mcp_local/tools/web_search.py +0 -0
  35. {cade_cli-0.15.4 → cade_cli-0.15.6}/src/cade_mcp_local/utils.py +0 -0
  36. {cade_cli-0.15.4 → cade_cli-0.15.6}/src/cade_mcp_local/web/__init__.py +0 -0
  37. {cade_cli-0.15.4 → cade_cli-0.15.6}/src/cade_mcp_local/web/arcade_client.py +0 -0
  38. {cade_cli-0.15.4 → cade_cli-0.15.6}/src/cade_mcp_local/web/cache.py +0 -0
  39. {cade_cli-0.15.4 → cade_cli-0.15.6}/src/cade_mcp_local/web/markdown.py +0 -0
  40. {cade_cli-0.15.4 → cade_cli-0.15.6}/src/cade_mcp_local/web/preapproved.py +0 -0
  41. {cade_cli-0.15.4 → cade_cli-0.15.6}/src/cade_mcp_local/web/prompts.py +0 -0
  42. {cade_cli-0.15.4 → cade_cli-0.15.6}/src/cade_mcp_local/web/scrape.py +0 -0
  43. {cade_cli-0.15.4 → cade_cli-0.15.6}/src/cade_mcp_local/web/search/__init__.py +0 -0
  44. {cade_cli-0.15.4 → cade_cli-0.15.6}/src/cade_mcp_local/web/search/arcade.py +0 -0
  45. {cade_cli-0.15.4 → cade_cli-0.15.6}/src/cade_mcp_local/web/search/base.py +0 -0
  46. {cade_cli-0.15.4 → cade_cli-0.15.6}/src/cade_mcp_local/web/search/http.py +0 -0
  47. {cade_cli-0.15.4 → cade_cli-0.15.6}/src/cadecoder/__init__.py +0 -0
  48. {cade_cli-0.15.4 → cade_cli-0.15.6}/src/cadecoder/ai/__init__.py +0 -0
  49. {cade_cli-0.15.4 → cade_cli-0.15.6}/src/cadecoder/ai/prompts.py +0 -0
  50. {cade_cli-0.15.4 → cade_cli-0.15.6}/src/cadecoder/cli/__init__.py +0 -0
  51. {cade_cli-0.15.4 → cade_cli-0.15.6}/src/cadecoder/cli/app.py +0 -0
  52. {cade_cli-0.15.4 → cade_cli-0.15.6}/src/cadecoder/cli/auth.py +0 -0
  53. {cade_cli-0.15.4 → cade_cli-0.15.6}/src/cadecoder/cli/commands/__init__.py +0 -0
  54. {cade_cli-0.15.4 → cade_cli-0.15.6}/src/cadecoder/cli/commands/account.py +0 -0
  55. {cade_cli-0.15.4 → cade_cli-0.15.6}/src/cadecoder/cli/commands/agents.py +0 -0
  56. {cade_cli-0.15.4 → cade_cli-0.15.6}/src/cadecoder/cli/commands/auth.py +0 -0
  57. {cade_cli-0.15.4 → cade_cli-0.15.6}/src/cadecoder/cli/commands/chat.py +0 -0
  58. {cade_cli-0.15.4 → cade_cli-0.15.6}/src/cadecoder/cli/commands/cron.py +0 -0
  59. {cade_cli-0.15.4 → cade_cli-0.15.6}/src/cadecoder/cli/commands/hooks.py +0 -0
  60. {cade_cli-0.15.4 → cade_cli-0.15.6}/src/cadecoder/cli/commands/mcp.py +0 -0
  61. {cade_cli-0.15.4 → cade_cli-0.15.6}/src/cadecoder/cli/commands/serve.py +0 -0
  62. {cade_cli-0.15.4 → cade_cli-0.15.6}/src/cadecoder/cli/commands/tasks.py +0 -0
  63. {cade_cli-0.15.4 → cade_cli-0.15.6}/src/cadecoder/cli/commands/thread.py +0 -0
  64. {cade_cli-0.15.4 → cade_cli-0.15.6}/src/cadecoder/cli/commands/tools.py +0 -0
  65. {cade_cli-0.15.4 → cade_cli-0.15.6}/src/cadecoder/cli/onboarding.py +0 -0
  66. {cade_cli-0.15.4 → cade_cli-0.15.6}/src/cadecoder/core/__init__.py +0 -0
  67. {cade_cli-0.15.4 → cade_cli-0.15.6}/src/cadecoder/core/agent.py +0 -0
  68. {cade_cli-0.15.4 → cade_cli-0.15.6}/src/cadecoder/core/comms.py +0 -0
  69. {cade_cli-0.15.4 → cade_cli-0.15.6}/src/cadecoder/core/config.py +0 -0
  70. {cade_cli-0.15.4 → cade_cli-0.15.6}/src/cadecoder/core/constants.py +0 -0
  71. {cade_cli-0.15.4 → cade_cli-0.15.6}/src/cadecoder/core/errors.py +0 -0
  72. {cade_cli-0.15.4 → cade_cli-0.15.6}/src/cadecoder/core/frozen.py +0 -0
  73. {cade_cli-0.15.4 → cade_cli-0.15.6}/src/cadecoder/core/git.py +0 -0
  74. {cade_cli-0.15.4 → cade_cli-0.15.6}/src/cadecoder/core/inbox.py +0 -0
  75. {cade_cli-0.15.4 → cade_cli-0.15.6}/src/cadecoder/core/logging.py +0 -0
  76. {cade_cli-0.15.4 → cade_cli-0.15.6}/src/cadecoder/core/memory.py +0 -0
  77. {cade_cli-0.15.4 → cade_cli-0.15.6}/src/cadecoder/core/model_specs.py +0 -0
  78. {cade_cli-0.15.4 → cade_cli-0.15.6}/src/cadecoder/core/names.py +0 -0
  79. {cade_cli-0.15.4 → cade_cli-0.15.6}/src/cadecoder/core/paths.py +0 -0
  80. {cade_cli-0.15.4 → cade_cli-0.15.6}/src/cadecoder/core/runtime.py +0 -0
  81. {cade_cli-0.15.4 → cade_cli-0.15.6}/src/cadecoder/core/secrets.py +0 -0
  82. {cade_cli-0.15.4 → cade_cli-0.15.6}/src/cadecoder/core/spawn.py +0 -0
  83. {cade_cli-0.15.4 → cade_cli-0.15.6}/src/cadecoder/core/types.py +0 -0
  84. {cade_cli-0.15.4 → cade_cli-0.15.6}/src/cadecoder/execution/__init__.py +0 -0
  85. {cade_cli-0.15.4 → cade_cli-0.15.6}/src/cadecoder/execution/context_window.py +0 -0
  86. {cade_cli-0.15.4 → cade_cli-0.15.6}/src/cadecoder/execution/mcp_schema_index.py +0 -0
  87. {cade_cli-0.15.4 → cade_cli-0.15.6}/src/cadecoder/execution/orchestrator.py +0 -0
  88. {cade_cli-0.15.4 → cade_cli-0.15.6}/src/cadecoder/execution/parallel.py +0 -0
  89. {cade_cli-0.15.4 → cade_cli-0.15.6}/src/cadecoder/execution/tool_result_store.py +0 -0
  90. {cade_cli-0.15.4 → cade_cli-0.15.6}/src/cadecoder/execution/tool_schema_cache.py +0 -0
  91. {cade_cli-0.15.4 → cade_cli-0.15.6}/src/cadecoder/hooks/__init__.py +0 -0
  92. {cade_cli-0.15.4 → cade_cli-0.15.6}/src/cadecoder/hooks/config.py +0 -0
  93. {cade_cli-0.15.4 → cade_cli-0.15.6}/src/cadecoder/hooks/engine.py +0 -0
  94. {cade_cli-0.15.4 → cade_cli-0.15.6}/src/cadecoder/hooks/executors/__init__.py +0 -0
  95. {cade_cli-0.15.4 → cade_cli-0.15.6}/src/cadecoder/hooks/executors/callback.py +0 -0
  96. {cade_cli-0.15.4 → cade_cli-0.15.6}/src/cadecoder/hooks/executors/command.py +0 -0
  97. {cade_cli-0.15.4 → cade_cli-0.15.6}/src/cadecoder/hooks/executors/http.py +0 -0
  98. {cade_cli-0.15.4 → cade_cli-0.15.6}/src/cadecoder/hooks/executors/prompt.py +0 -0
  99. {cade_cli-0.15.4 → cade_cli-0.15.6}/src/cadecoder/hooks/executors/ssrf_guard.py +0 -0
  100. {cade_cli-0.15.4 → cade_cli-0.15.6}/src/cadecoder/hooks/matchers.py +0 -0
  101. {cade_cli-0.15.4 → cade_cli-0.15.6}/src/cadecoder/hooks/registry.py +0 -0
  102. {cade_cli-0.15.4 → cade_cli-0.15.6}/src/cadecoder/hooks/types.py +0 -0
  103. {cade_cli-0.15.4 → cade_cli-0.15.6}/src/cadecoder/providers/__init__.py +0 -0
  104. {cade_cli-0.15.4 → cade_cli-0.15.6}/src/cadecoder/providers/anthropic.py +0 -0
  105. {cade_cli-0.15.4 → cade_cli-0.15.6}/src/cadecoder/providers/base.py +0 -0
  106. {cade_cli-0.15.4 → cade_cli-0.15.6}/src/cadecoder/providers/ollama.py +0 -0
  107. {cade_cli-0.15.4 → cade_cli-0.15.6}/src/cadecoder/providers/openai.py +0 -0
  108. {cade_cli-0.15.4 → cade_cli-0.15.6}/src/cadecoder/serve/__init__.py +0 -0
  109. {cade_cli-0.15.4 → cade_cli-0.15.6}/src/cadecoder/serve/adapters/__init__.py +0 -0
  110. {cade_cli-0.15.4 → cade_cli-0.15.6}/src/cadecoder/serve/adapters/base.py +0 -0
  111. {cade_cli-0.15.4 → cade_cli-0.15.6}/src/cadecoder/serve/adapters/desktop.py +0 -0
  112. {cade_cli-0.15.4 → cade_cli-0.15.6}/src/cadecoder/serve/adapters/registry.py +0 -0
  113. {cade_cli-0.15.4 → cade_cli-0.15.6}/src/cadecoder/serve/adapters/stub.py +0 -0
  114. {cade_cli-0.15.4 → cade_cli-0.15.6}/src/cadecoder/serve/adapters/telegram.py +0 -0
  115. {cade_cli-0.15.4 → cade_cli-0.15.6}/src/cadecoder/serve/allowlist.py +0 -0
  116. {cade_cli-0.15.4 → cade_cli-0.15.6}/src/cadecoder/serve/chunker.py +0 -0
  117. {cade_cli-0.15.4 → cade_cli-0.15.6}/src/cadecoder/serve/commands.py +0 -0
  118. {cade_cli-0.15.4 → cade_cli-0.15.6}/src/cadecoder/serve/config.py +0 -0
  119. {cade_cli-0.15.4 → cade_cli-0.15.6}/src/cadecoder/serve/cron.py +0 -0
  120. {cade_cli-0.15.4 → cade_cli-0.15.6}/src/cadecoder/serve/daemon.py +0 -0
  121. {cade_cli-0.15.4 → cade_cli-0.15.6}/src/cadecoder/serve/elicitation.py +0 -0
  122. {cade_cli-0.15.4 → cade_cli-0.15.6}/src/cadecoder/serve/format.py +0 -0
  123. {cade_cli-0.15.4 → cade_cli-0.15.6}/src/cadecoder/serve/install.py +0 -0
  124. {cade_cli-0.15.4 → cade_cli-0.15.6}/src/cadecoder/serve/lock.py +0 -0
  125. {cade_cli-0.15.4 → cade_cli-0.15.6}/src/cadecoder/serve/observability.py +0 -0
  126. {cade_cli-0.15.4 → cade_cli-0.15.6}/src/cadecoder/serve/owner.py +0 -0
  127. {cade_cli-0.15.4 → cade_cli-0.15.6}/src/cadecoder/serve/progress_sink.py +0 -0
  128. {cade_cli-0.15.4 → cade_cli-0.15.6}/src/cadecoder/serve/router.py +0 -0
  129. {cade_cli-0.15.4 → cade_cli-0.15.6}/src/cadecoder/serve/secrets.py +0 -0
  130. {cade_cli-0.15.4 → cade_cli-0.15.6}/src/cadecoder/serve/services.py +0 -0
  131. {cade_cli-0.15.4 → cade_cli-0.15.6}/src/cadecoder/serve/sinks.py +0 -0
  132. {cade_cli-0.15.4 → cade_cli-0.15.6}/src/cadecoder/serve/worker.py +0 -0
  133. {cade_cli-0.15.4 → cade_cli-0.15.6}/src/cadecoder/storage/__init__.py +0 -0
  134. {cade_cli-0.15.4 → cade_cli-0.15.6}/src/cadecoder/storage/threads.py +0 -0
  135. {cade_cli-0.15.4 → cade_cli-0.15.6}/src/cadecoder/tasks/__init__.py +0 -0
  136. {cade_cli-0.15.4 → cade_cli-0.15.6}/src/cadecoder/tasks/lock.py +0 -0
  137. {cade_cli-0.15.4 → cade_cli-0.15.6}/src/cadecoder/tasks/scheduler.py +0 -0
  138. {cade_cli-0.15.4 → cade_cli-0.15.6}/src/cadecoder/tasks/store.py +0 -0
  139. {cade_cli-0.15.4 → cade_cli-0.15.6}/src/cadecoder/tasks/types.py +0 -0
  140. {cade_cli-0.15.4 → cade_cli-0.15.6}/src/cadecoder/templates/login_failed.html +0 -0
  141. {cade_cli-0.15.4 → cade_cli-0.15.6}/src/cadecoder/templates/login_success.html +0 -0
  142. {cade_cli-0.15.4 → cade_cli-0.15.6}/src/cadecoder/templates/serve/launchd.plist.tmpl +0 -0
  143. {cade_cli-0.15.4 → cade_cli-0.15.6}/src/cadecoder/templates/serve/serve.toml.example +0 -0
  144. {cade_cli-0.15.4 → cade_cli-0.15.6}/src/cadecoder/templates/serve/systemd.service.tmpl +0 -0
  145. {cade_cli-0.15.4 → cade_cli-0.15.6}/src/cadecoder/templates/styles.css +0 -0
  146. {cade_cli-0.15.4 → cade_cli-0.15.6}/src/cadecoder/tools/__init__.py +0 -0
  147. {cade_cli-0.15.4 → cade_cli-0.15.6}/src/cadecoder/tools/manager/__init__.py +0 -0
  148. {cade_cli-0.15.4 → cade_cli-0.15.6}/src/cadecoder/tools/manager/_request_ctx.py +0 -0
  149. {cade_cli-0.15.4 → cade_cli-0.15.6}/src/cadecoder/tools/manager/base.py +0 -0
  150. {cade_cli-0.15.4 → cade_cli-0.15.6}/src/cadecoder/tools/manager/composite.py +0 -0
  151. {cade_cli-0.15.4 → cade_cli-0.15.6}/src/cadecoder/tools/manager/config.py +0 -0
  152. {cade_cli-0.15.4 → cade_cli-0.15.6}/src/cadecoder/tools/manager/mcp.py +0 -0
  153. {cade_cli-0.15.4 → cade_cli-0.15.6}/src/cadecoder/tools/search/__init__.py +0 -0
  154. {cade_cli-0.15.4 → cade_cli-0.15.6}/src/cadecoder/tools/search/discovered.py +0 -0
  155. {cade_cli-0.15.4 → cade_cli-0.15.6}/src/cadecoder/tools/search/scoring.py +0 -0
  156. {cade_cli-0.15.4 → cade_cli-0.15.6}/src/cadecoder/tools/search/service.py +0 -0
  157. {cade_cli-0.15.4 → cade_cli-0.15.6}/src/cadecoder/ui/__init__.py +0 -0
  158. {cade_cli-0.15.4 → cade_cli-0.15.6}/src/cadecoder/ui/completion.py +0 -0
  159. {cade_cli-0.15.4 → cade_cli-0.15.6}/src/cadecoder/ui/elicitation.py +0 -0
  160. {cade_cli-0.15.4 → cade_cli-0.15.6}/src/cadecoder/ui/input.py +0 -0
  161. {cade_cli-0.15.4 → cade_cli-0.15.6}/src/cadecoder/ui/session.py +0 -0
  162. {cade_cli-0.15.4 → cade_cli-0.15.6}/src/cadecoder/ui/slash.py +0 -0
  163. {cade_cli-0.15.4 → cade_cli-0.15.6}/src/cadecoder/voice/__init__.py +0 -0
  164. {cade_cli-0.15.4 → cade_cli-0.15.6}/src/cadecoder/voice/cleanup.py +0 -0
  165. {cade_cli-0.15.4 → cade_cli-0.15.6}/src/cadecoder/voice/session.py +0 -0
  166. {cade_cli-0.15.4 → cade_cli-0.15.6}/src/cadecoder/voice/tts.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cade-cli
3
- Version: 0.15.4
3
+ Version: 0.15.6
4
4
  Summary: Cade - The CLI Agent from Arcade.dev
5
5
  Project-URL: Homepage, https://arcade.dev
6
6
  Project-URL: Documentation, https://docs.arcade.dev
@@ -43,7 +43,7 @@ Classifier: Topic :: Software Development
43
43
  Classifier: Topic :: Software Development :: Code Generators
44
44
  Classifier: Typing :: Typed
45
45
  Requires-Python: >=3.11
46
- Requires-Dist: agent-library<1.0.0,>=0.14.0
46
+ Requires-Dist: agent-library<1.0.0,>=0.14.1
47
47
  Requires-Dist: anthropic<1.0.0,>=0.34.0
48
48
  Requires-Dist: arcade-core<5.0.0,>=4.7.0
49
49
  Requires-Dist: arcade-mcp-server<2.0.0,>=1.22.0
@@ -123,7 +123,7 @@ The install script detects your OS/arch, downloads the matching prebuilt binary
123
123
  `~/.local/bin` — no GitHub token required. Pin a version or install location:
124
124
 
125
125
  ```bash
126
- CADE_VERSION=0.15.4 CADE_INSTALL_DIR=/usr/local/bin \
126
+ CADE_VERSION=0.15.5 CADE_INSTALL_DIR=/usr/local/bin \
127
127
  sh -c "$(curl -fsSL https://arcadeagent.dev/install)"
128
128
  ```
129
129
 
@@ -35,7 +35,7 @@ The install script detects your OS/arch, downloads the matching prebuilt binary
35
35
  `~/.local/bin` — no GitHub token required. Pin a version or install location:
36
36
 
37
37
  ```bash
38
- CADE_VERSION=0.15.4 CADE_INSTALL_DIR=/usr/local/bin \
38
+ CADE_VERSION=0.15.5 CADE_INSTALL_DIR=/usr/local/bin \
39
39
  sh -c "$(curl -fsSL https://arcadeagent.dev/install)"
40
40
  ```
41
41
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "cade-cli"
3
- version = "0.15.4"
3
+ version = "0.15.6"
4
4
  description = "Cade - The CLI Agent from Arcade.dev"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.11"
@@ -38,7 +38,7 @@ dependencies = [
38
38
  "prompt-toolkit>=3.0.52,<4.0.0",
39
39
  "httpx>=0.27.0,<1.0.0",
40
40
  "filelock>=3.0.0,<4.0.0",
41
- "agent-library>=0.14.0,<1.0.0",
41
+ "agent-library>=0.14.1,<1.0.0",
42
42
  "croniter>=2.0.0,<4.0.0",
43
43
  "python-telegram-bot>=21.0,<22.0",
44
44
  "keyring>=24.0,<26.0",
@@ -0,0 +1,115 @@
1
+ """Secondary-model extraction for WebFetch.
2
+
3
+ Single non-streaming ``Provider.complete()`` call so the same code path
4
+ works for Anthropic, OpenAI, and Ollama users alike. The extraction model is
5
+ resolved from (in priority order) an explicit override, ``CADE_WEB_FETCH_MODEL``,
6
+ the user's configured default model, then ``DEFAULT_EXTRACT_MODEL``. When the
7
+ preferred model's provider isn't configured (e.g. a Claude default on an
8
+ OpenAI-only setup), extraction falls back to whatever provider the user *did*
9
+ configure rather than failing outright.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import os
15
+
16
+ from cade_mcp_local.errors import WebFetchError
17
+ from cade_mcp_local.web.prompts import make_secondary_model_prompt
18
+ from cadecoder.providers import get_provider_for_model, provider_registry
19
+ from cadecoder.providers.base import Provider, ProviderRequest, ProviderType
20
+
21
+ DEFAULT_EXTRACT_MODEL = "claude-haiku-4-5-20251001"
22
+
23
+ # Cheap/fast per-provider extraction models, used only as a fallback when the
24
+ # preferred model's provider is unavailable but another provider is registered.
25
+ # Extraction is a one-shot summarization pass, so a small model is sufficient.
26
+ _FALLBACK_MODEL_BY_PROVIDER: dict[ProviderType, str] = {
27
+ ProviderType.ANTHROPIC: DEFAULT_EXTRACT_MODEL,
28
+ ProviderType.OPENAI: "gpt-5-mini",
29
+ ProviderType.OLLAMA: "llama3.1",
30
+ }
31
+
32
+
33
+ def _configured_default_model() -> str | None:
34
+ """Return the user's configured default model, or None if unavailable.
35
+
36
+ Reads the global settings directly so the extraction step honors whatever
37
+ provider/model the user set up during onboarding.
38
+ """
39
+ try:
40
+ from cadecoder.core.config import _load_global_settings
41
+
42
+ return _load_global_settings().model or None
43
+ except Exception: # noqa: BLE001 - config is best-effort here; fall back to default
44
+ return None
45
+
46
+
47
+ def _resolve_extraction_target(model: str | None) -> tuple[str, Provider]:
48
+ """Resolve the extraction model and a usable provider.
49
+
50
+ Args:
51
+ model: An explicit model override, if any.
52
+
53
+ Returns:
54
+ A ``(model, provider)`` tuple ready to run an extraction.
55
+
56
+ Raises:
57
+ LookupError: If no LLM provider is registered at all.
58
+ """
59
+ chosen = (
60
+ model
61
+ or os.environ.get("CADE_WEB_FETCH_MODEL")
62
+ or _configured_default_model()
63
+ or DEFAULT_EXTRACT_MODEL
64
+ )
65
+
66
+ provider = get_provider_for_model(chosen)
67
+ if provider is not None:
68
+ return chosen, provider
69
+
70
+ # The preferred model's provider isn't configured. Fall back to whatever
71
+ # provider the user did configure (the registry's default) so non-preapproved
72
+ # fetches still work on OpenAI/Ollama-only setups.
73
+ fallback = provider_registry.get_default()
74
+ if fallback is None:
75
+ raise LookupError("no LLM provider is registered")
76
+
77
+ fallback_model = _FALLBACK_MODEL_BY_PROVIDER.get(fallback.provider_type, chosen)
78
+ return fallback_model, fallback
79
+
80
+
81
+ async def extract_with_model(
82
+ prompt: str,
83
+ markdown: str,
84
+ *,
85
+ model: str | None = None,
86
+ url: str = "",
87
+ ) -> tuple[str, str]:
88
+ """Run a one-shot extraction. Returns (answer, model_used).
89
+
90
+ ``url`` is only used to make the error message useful when no provider is
91
+ available or the provider rejects the request.
92
+ """
93
+ try:
94
+ chosen, provider = _resolve_extraction_target(model)
95
+ except LookupError:
96
+ raise WebFetchError(
97
+ url or "(no url)",
98
+ "no provider available for extraction; set ANTHROPIC_API_KEY/OPENAI_API_KEY, "
99
+ "configure a model via cade onboarding, or override CADE_WEB_FETCH_MODEL",
100
+ ) from None
101
+
102
+ full_prompt = make_secondary_model_prompt(prompt, markdown)
103
+ request = ProviderRequest(
104
+ messages=[{"role": "user", "content": full_prompt}],
105
+ model=chosen,
106
+ max_tokens=2048,
107
+ temperature=0.2,
108
+ stream=False,
109
+ )
110
+ try:
111
+ response = await provider.complete(request)
112
+ except Exception as e:
113
+ raise WebFetchError(url or "(no url)", f"extraction model failed: {e}") from e
114
+
115
+ return (response.content or "").strip(), chosen
@@ -43,6 +43,10 @@ def _apply_memory_env() -> Path:
43
43
  os.environ["DOCUMENTS_PATH"] = str(librarian_dir / "documents")
44
44
  os.environ["SOURCES_CONFIG_PATH"] = str(librarian_dir / "sources.json")
45
45
  os.environ["HOME"] = str(persona_dir)
46
+ # Rebrand librarian's usage hints (e.g. "Add a source with: libr add ...")
47
+ # to "cade mem". Must be set before librarian.cli is imported, which only
48
+ # happens later via _load_librarian_app().
49
+ os.environ["LIBRARIAN_PROG_NAME"] = "cade mem"
46
50
  return persona_dir
47
51
 
48
52
 
@@ -465,7 +465,6 @@ _PRIMARY_ARG_KEYS: tuple[str, ...] = (
465
465
  # Generic last-resort labels.
466
466
  "name",
467
467
  "title",
468
- "tool_name",
469
468
  )
470
469
 
471
470
 
@@ -811,6 +810,18 @@ def get_display_tool_name(tool_name: str, args: dict[str, Any]) -> str:
811
810
 
812
811
  def get_tool_context(tool_name: str, args: dict[str, Any]) -> str:
813
812
  """Return parenthesized context for the tool (used by session for 'Calling tool' line)."""
813
+ # Unwrap the Arcade meta-tools so the context describes the *inner* tool
814
+ # (e.g. the doc title) instead of echoing the wrapper's tool_name arg —
815
+ # which produced redundant headlines like
816
+ # ``GoogleDocs_CreateDocumentFromText("GoogleDocs_CreateDocumentFromText")``.
817
+ if tool_name in ("Arcade_UseTool", "Arcade_ExecuteTool"):
818
+ inner_name = get_display_tool_name(tool_name, args)
819
+ if inner_name and inner_name != tool_name:
820
+ inner_args = args.get("inputs")
821
+ if not isinstance(inner_args, dict):
822
+ inner_args = {}
823
+ return get_tool_context(inner_name, inner_args)
824
+
814
825
  server, tool = _parse_tool_name(tool_name)
815
826
  handler = _find_registry_handler(server, tool)
816
827
  if handler:
@@ -52,6 +52,34 @@ def _rms(audio: np.ndarray) -> float:
52
52
  return float(np.sqrt(np.mean(audio.astype(np.float64) ** 2)))
53
53
 
54
54
 
55
+ # Substrings that identify a Bluetooth headset reported as an input device.
56
+ # Such devices frequently return all-zero samples on macOS because the input
57
+ # route fails to switch to the headset mic — a routing issue, not a permission
58
+ # one — so the silence error gives different advice in that case.
59
+ _BLUETOOTH_MIC_HINTS: Final[tuple[str, ...]] = (
60
+ "airpods",
61
+ "beats",
62
+ "bluetooth",
63
+ "headphones",
64
+ "headset",
65
+ "buds",
66
+ "powerbeats",
67
+ )
68
+
69
+
70
+ def _looks_like_bluetooth_mic(device_name: str) -> bool:
71
+ """Return True when *device_name* looks like a Bluetooth headset.
72
+
73
+ Args:
74
+ device_name: The human-readable input device name from PortAudio.
75
+
76
+ Returns:
77
+ True if the name matches a known Bluetooth-headset substring.
78
+ """
79
+ name = device_name.lower()
80
+ return any(hint in name for hint in _BLUETOOTH_MIC_HINTS)
81
+
82
+
55
83
  def check_mic_permission(
56
84
  device: str | int | None = None,
57
85
  sample_rate: int = DEFAULT_SAMPLE_RATE,
@@ -103,6 +131,27 @@ def check_mic_permission(
103
131
  import platform
104
132
 
105
133
  if platform.system() == "Darwin":
134
+ if _looks_like_bluetooth_mic(device_name):
135
+ # Bluetooth headsets (AirPods, Beats, etc.) commonly report
136
+ # silence on macOS because the input route fails to switch to
137
+ # the headset's mic — a routing problem, not a permission one.
138
+ return False, (
139
+ f"Microphone '{device_name}' returns silence (all zeros).\n"
140
+ "Bluetooth headsets (AirPods, Beats, etc.) often can't be "
141
+ "used as a mic on macOS.\n"
142
+ "\n"
143
+ "Try one of these:\n"
144
+ " • Switch input to the built-in mic: System Settings > "
145
+ "Sound > Input > MacBook/Built-in Microphone\n"
146
+ " • Or pin a specific device in ~/.cade/config/cade.toml "
147
+ 'under [voice]: audio_input_device = "MacBook Pro Microphone"\n'
148
+ " (type /devices in the chat to list available devices)\n"
149
+ " • Or disconnect/reconnect the headset and try again\n"
150
+ "\n"
151
+ "If you intended to use the built-in mic, also confirm your "
152
+ "terminal has mic access in System Settings > Privacy & "
153
+ "Security > Microphone."
154
+ )
106
155
  return False, (
107
156
  f"Microphone '{device_name}' returns silence (all zeros).\n"
108
157
  "This usually means macOS is blocking microphone access.\n"
@@ -7,8 +7,6 @@ Models are downloaded from HuggingFace on first use and cached automatically.
7
7
  import asyncio
8
8
  import logging
9
9
  import os
10
- import tempfile
11
- import wave
12
10
 
13
11
  import numpy as np
14
12
 
@@ -17,6 +15,10 @@ from cadecoder.core.errors import VoiceError
17
15
  from cadecoder.core.logging import log
18
16
  from cadecoder.voice.audio import DEFAULT_SAMPLE_RATE
19
17
 
18
+ # Whisper always operates at 16 kHz. When given a numpy array (rather than a
19
+ # file path) mlx-whisper does NOT resample, so the samples must already be 16 kHz.
20
+ _WHISPER_SAMPLE_RATE = 16_000
21
+
20
22
 
21
23
  def _suppress_hf_logging() -> None:
22
24
  """Suppress noisy HuggingFace Hub download/progress logging.
@@ -109,17 +111,21 @@ class WhisperSTT:
109
111
  raise VoiceError("STT model not loaded — call _ensure_loaded() first")
110
112
 
111
113
  try:
112
- # mlx_whisper.transcribe accepts a file path or numpy array.
113
- # Write to a temp WAV file for maximum compatibility across
114
- # mlx-whisper versions.
115
- with tempfile.NamedTemporaryFile(suffix=".wav", delete=True) as tmp:
116
- self._write_wav(tmp.name, audio, sample_rate)
117
- result = self._mlx_whisper.transcribe( # type: ignore[union-attr]
118
- tmp.name,
119
- path_or_hf_repo=self.model,
120
- language=self.language,
121
- fp16=False,
122
- )
114
+ # Pass the raw float32 samples directly. Writing a temp WAV and
115
+ # passing the *path* makes mlx-whisper shell out to `ffmpeg` to
116
+ # decode it (not bundled in the frozen binary, and often absent on
117
+ # user machines). Whisper accepts a 16 kHz mono float32 array with
118
+ # no external tools — the recorder already produces exactly that.
119
+ samples = np.asarray(audio, dtype=np.float32).flatten()
120
+ if sample_rate != _WHISPER_SAMPLE_RATE:
121
+ samples = _resample(samples, sample_rate, _WHISPER_SAMPLE_RATE)
122
+
123
+ result = self._mlx_whisper.transcribe( # type: ignore[union-attr]
124
+ samples,
125
+ path_or_hf_repo=self.model,
126
+ language=self.language,
127
+ fp16=False,
128
+ )
123
129
 
124
130
  text = result.get("text", "").strip()
125
131
  log.debug(f"Transcribed {len(audio) / sample_rate:.1f}s -> '{text[:80]}'")
@@ -128,13 +134,19 @@ class WhisperSTT:
128
134
  except Exception as exc:
129
135
  raise VoiceError(f"Transcription failed: {exc}") from exc
130
136
 
131
- @staticmethod
132
- def _write_wav(path: str, audio: np.ndarray, sample_rate: int) -> None:
133
- """Write a float32 numpy array to a 16-bit PCM WAV file."""
134
- # Convert float32 [-1.0, 1.0] to int16
135
- pcm = (audio * 32767).clip(-32768, 32767).astype(np.int16)
136
- with wave.open(path, "wb") as wf:
137
- wf.setnchannels(1)
138
- wf.setsampwidth(2) # 16-bit
139
- wf.setframerate(sample_rate)
140
- wf.writeframes(pcm.tobytes())
137
+
138
+ def _resample(samples: np.ndarray, src_rate: int, dst_rate: int) -> np.ndarray:
139
+ """Linearly resample mono float32 audio to ``dst_rate`` (no external deps).
140
+
141
+ Adequate for speech fed to Whisper. Only used on the rare path where the
142
+ recorder's rate differs from Whisper's 16 kHz.
143
+ """
144
+ if src_rate == dst_rate or samples.size == 0:
145
+ return samples
146
+ duration = samples.shape[0] / float(src_rate)
147
+ n_out = int(round(duration * dst_rate))
148
+ if n_out <= 0:
149
+ return np.array([], dtype=np.float32)
150
+ x_old = np.linspace(0.0, duration, num=samples.shape[0], endpoint=False)
151
+ x_new = np.linspace(0.0, duration, num=n_out, endpoint=False)
152
+ return np.interp(x_new, x_old, samples).astype(np.float32)
@@ -1,54 +0,0 @@
1
- """Secondary-model extraction for WebFetch.
2
-
3
- Single non-streaming ``Provider.complete()`` call so the same code path
4
- works for Anthropic, OpenAI, and Ollama users alike. Default model is
5
- ``claude-haiku-4-5-20251001``; override via ``CADE_WEB_FETCH_MODEL``.
6
- """
7
-
8
- from __future__ import annotations
9
-
10
- import os
11
-
12
- from cade_mcp_local.errors import WebFetchError
13
- from cade_mcp_local.web.prompts import make_secondary_model_prompt
14
- from cadecoder.providers import get_provider_for_model
15
- from cadecoder.providers.base import ProviderRequest
16
-
17
- DEFAULT_EXTRACT_MODEL = "claude-haiku-4-5-20251001"
18
-
19
-
20
- async def extract_with_model(
21
- prompt: str,
22
- markdown: str,
23
- *,
24
- model: str | None = None,
25
- url: str = "",
26
- ) -> tuple[str, str]:
27
- """Run a one-shot extraction. Returns (answer, model_used).
28
-
29
- ``url`` is only used to make the error message useful when the
30
- provider rejects the request.
31
- """
32
- chosen = model or os.environ.get("CADE_WEB_FETCH_MODEL") or DEFAULT_EXTRACT_MODEL
33
- provider = get_provider_for_model(chosen)
34
- if provider is None:
35
- raise WebFetchError(
36
- url or "(no url)",
37
- f"no provider available for extraction model {chosen!r}; "
38
- "set ANTHROPIC_API_KEY/OPENAI_API_KEY or CADE_WEB_FETCH_MODEL",
39
- )
40
-
41
- full_prompt = make_secondary_model_prompt(prompt, markdown)
42
- request = ProviderRequest(
43
- messages=[{"role": "user", "content": full_prompt}],
44
- model=chosen,
45
- max_tokens=2048,
46
- temperature=0.2,
47
- stream=False,
48
- )
49
- try:
50
- response = await provider.complete(request)
51
- except Exception as e:
52
- raise WebFetchError(url or "(no url)", f"extraction model failed: {e}") from e
53
-
54
- return (response.content or "").strip(), chosen
File without changes
File without changes