cade-cli 0.13.2__tar.gz → 0.13.4__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 (161) hide show
  1. {cade_cli-0.13.2 → cade_cli-0.13.4}/PKG-INFO +1 -1
  2. {cade_cli-0.13.2 → cade_cli-0.13.4}/pyproject.toml +1 -1
  3. cade_cli-0.13.4/src/cade_mcp_local/config.py +116 -0
  4. {cade_cli-0.13.2 → cade_cli-0.13.4}/src/cade_mcp_local/tools/filesystem.py +18 -14
  5. {cade_cli-0.13.2 → cade_cli-0.13.4}/src/cade_mcp_local/utils.py +6 -2
  6. {cade_cli-0.13.2 → cade_cli-0.13.4}/src/cadecoder/cli/app.py +28 -1
  7. {cade_cli-0.13.2 → cade_cli-0.13.4}/src/cadecoder/cli/commands/chat.py +26 -0
  8. {cade_cli-0.13.2 → cade_cli-0.13.4}/src/cadecoder/cli/commands/context.py +16 -2
  9. {cade_cli-0.13.2 → cade_cli-0.13.4}/src/cadecoder/cli/commands/cron.py +39 -19
  10. cade_cli-0.13.4/src/cadecoder/cli/commands/mem.py +176 -0
  11. {cade_cli-0.13.2 → cade_cli-0.13.4}/src/cadecoder/cli/commands/model.py +112 -12
  12. {cade_cli-0.13.2 → cade_cli-0.13.4}/src/cadecoder/cli/commands/persona.py +190 -24
  13. {cade_cli-0.13.2 → cade_cli-0.13.4}/src/cadecoder/cli/commands/tools.py +110 -8
  14. {cade_cli-0.13.2 → cade_cli-0.13.4}/src/cadecoder/core/config.py +35 -10
  15. {cade_cli-0.13.2 → cade_cli-0.13.4}/src/cadecoder/core/constants.py +1 -8
  16. cade_cli-0.13.4/src/cadecoder/core/model_specs.py +194 -0
  17. {cade_cli-0.13.2 → cade_cli-0.13.4}/src/cadecoder/core/runtime.py +1 -0
  18. {cade_cli-0.13.2 → cade_cli-0.13.4}/src/cadecoder/execution/context_window.py +4 -83
  19. {cade_cli-0.13.2 → cade_cli-0.13.4}/src/cadecoder/execution/orchestrator.py +4 -2
  20. {cade_cli-0.13.2 → cade_cli-0.13.4}/src/cadecoder/providers/__init__.py +7 -2
  21. {cade_cli-0.13.2 → cade_cli-0.13.4}/src/cadecoder/providers/anthropic.py +53 -9
  22. {cade_cli-0.13.2 → cade_cli-0.13.4}/src/cadecoder/providers/base.py +8 -1
  23. {cade_cli-0.13.2 → cade_cli-0.13.4}/src/cadecoder/providers/ollama.py +30 -18
  24. {cade_cli-0.13.2 → cade_cli-0.13.4}/src/cadecoder/providers/openai.py +72 -39
  25. {cade_cli-0.13.2 → cade_cli-0.13.4}/src/cadecoder/serve/allowlist.py +34 -6
  26. {cade_cli-0.13.2 → cade_cli-0.13.4}/src/cadecoder/serve/config.py +28 -2
  27. {cade_cli-0.13.2 → cade_cli-0.13.4}/src/cadecoder/storage/personas.py +27 -0
  28. {cade_cli-0.13.2 → cade_cli-0.13.4}/src/cadecoder/tasks/scheduler.py +90 -4
  29. {cade_cli-0.13.2 → cade_cli-0.13.4}/src/cadecoder/templates/serve/serve.toml.example +1 -1
  30. {cade_cli-0.13.2 → cade_cli-0.13.4}/src/cadecoder/ui/session.py +27 -9
  31. cade_cli-0.13.2/src/cade_mcp_local/config.py +0 -68
  32. cade_cli-0.13.2/src/cadecoder/cli/commands/mem.py +0 -149
  33. {cade_cli-0.13.2 → cade_cli-0.13.4}/.gitignore +0 -0
  34. {cade_cli-0.13.2 → cade_cli-0.13.4}/LICENSE +0 -0
  35. {cade_cli-0.13.2 → cade_cli-0.13.4}/README.md +0 -0
  36. {cade_cli-0.13.2 → cade_cli-0.13.4}/src/cade_mcp_local/__init__.py +0 -0
  37. {cade_cli-0.13.2 → cade_cli-0.13.4}/src/cade_mcp_local/__main__.py +0 -0
  38. {cade_cli-0.13.2 → cade_cli-0.13.4}/src/cade_mcp_local/_metadata.py +0 -0
  39. {cade_cli-0.13.2 → cade_cli-0.13.4}/src/cade_mcp_local/errors.py +0 -0
  40. {cade_cli-0.13.2 → cade_cli-0.13.4}/src/cade_mcp_local/resources.py +0 -0
  41. {cade_cli-0.13.2 → cade_cli-0.13.4}/src/cade_mcp_local/server.py +0 -0
  42. {cade_cli-0.13.2 → cade_cli-0.13.4}/src/cade_mcp_local/tools/__init__.py +0 -0
  43. {cade_cli-0.13.2 → cade_cli-0.13.4}/src/cade_mcp_local/tools/_read_cache.py +0 -0
  44. {cade_cli-0.13.2 → cade_cli-0.13.4}/src/cade_mcp_local/tools/context.py +0 -0
  45. {cade_cli-0.13.2 → cade_cli-0.13.4}/src/cade_mcp_local/tools/git.py +0 -0
  46. {cade_cli-0.13.2 → cade_cli-0.13.4}/src/cade_mcp_local/tools/notifications.py +0 -0
  47. {cade_cli-0.13.2 → cade_cli-0.13.4}/src/cade_mcp_local/tools/questions.py +0 -0
  48. {cade_cli-0.13.2 → cade_cli-0.13.4}/src/cade_mcp_local/tools/results.py +0 -0
  49. {cade_cli-0.13.2 → cade_cli-0.13.4}/src/cade_mcp_local/tools/schemas.py +0 -0
  50. {cade_cli-0.13.2 → cade_cli-0.13.4}/src/cade_mcp_local/tools/search.py +0 -0
  51. {cade_cli-0.13.2 → cade_cli-0.13.4}/src/cade_mcp_local/tools/shell.py +0 -0
  52. {cade_cli-0.13.2 → cade_cli-0.13.4}/src/cade_mcp_local/tools/snippets.py +0 -0
  53. {cade_cli-0.13.2 → cade_cli-0.13.4}/src/cade_mcp_local/tools/tasks.py +0 -0
  54. {cade_cli-0.13.2 → cade_cli-0.13.4}/src/cade_mcp_local/tools/web_fetch.py +0 -0
  55. {cade_cli-0.13.2 → cade_cli-0.13.4}/src/cade_mcp_local/tools/web_search.py +0 -0
  56. {cade_cli-0.13.2 → cade_cli-0.13.4}/src/cade_mcp_local/web/__init__.py +0 -0
  57. {cade_cli-0.13.2 → cade_cli-0.13.4}/src/cade_mcp_local/web/arcade_client.py +0 -0
  58. {cade_cli-0.13.2 → cade_cli-0.13.4}/src/cade_mcp_local/web/cache.py +0 -0
  59. {cade_cli-0.13.2 → cade_cli-0.13.4}/src/cade_mcp_local/web/extract.py +0 -0
  60. {cade_cli-0.13.2 → cade_cli-0.13.4}/src/cade_mcp_local/web/markdown.py +0 -0
  61. {cade_cli-0.13.2 → cade_cli-0.13.4}/src/cade_mcp_local/web/preapproved.py +0 -0
  62. {cade_cli-0.13.2 → cade_cli-0.13.4}/src/cade_mcp_local/web/prompts.py +0 -0
  63. {cade_cli-0.13.2 → cade_cli-0.13.4}/src/cade_mcp_local/web/scrape.py +0 -0
  64. {cade_cli-0.13.2 → cade_cli-0.13.4}/src/cade_mcp_local/web/search/__init__.py +0 -0
  65. {cade_cli-0.13.2 → cade_cli-0.13.4}/src/cade_mcp_local/web/search/arcade.py +0 -0
  66. {cade_cli-0.13.2 → cade_cli-0.13.4}/src/cade_mcp_local/web/search/base.py +0 -0
  67. {cade_cli-0.13.2 → cade_cli-0.13.4}/src/cade_mcp_local/web/search/http.py +0 -0
  68. {cade_cli-0.13.2 → cade_cli-0.13.4}/src/cadecoder/__init__.py +0 -0
  69. {cade_cli-0.13.2 → cade_cli-0.13.4}/src/cadecoder/ai/__init__.py +0 -0
  70. {cade_cli-0.13.2 → cade_cli-0.13.4}/src/cadecoder/ai/prompts.py +0 -0
  71. {cade_cli-0.13.2 → cade_cli-0.13.4}/src/cadecoder/cli/__init__.py +0 -0
  72. {cade_cli-0.13.2 → cade_cli-0.13.4}/src/cadecoder/cli/auth.py +0 -0
  73. {cade_cli-0.13.2 → cade_cli-0.13.4}/src/cadecoder/cli/commands/__init__.py +0 -0
  74. {cade_cli-0.13.2 → cade_cli-0.13.4}/src/cadecoder/cli/commands/account.py +0 -0
  75. {cade_cli-0.13.2 → cade_cli-0.13.4}/src/cadecoder/cli/commands/auth.py +0 -0
  76. {cade_cli-0.13.2 → cade_cli-0.13.4}/src/cadecoder/cli/commands/channels.py +0 -0
  77. {cade_cli-0.13.2 → cade_cli-0.13.4}/src/cadecoder/cli/commands/hooks.py +0 -0
  78. {cade_cli-0.13.2 → cade_cli-0.13.4}/src/cadecoder/cli/commands/mcp.py +0 -0
  79. {cade_cli-0.13.2 → cade_cli-0.13.4}/src/cadecoder/cli/commands/serve.py +0 -0
  80. {cade_cli-0.13.2 → cade_cli-0.13.4}/src/cadecoder/cli/commands/tasks.py +0 -0
  81. {cade_cli-0.13.2 → cade_cli-0.13.4}/src/cadecoder/cli/commands/thread.py +0 -0
  82. {cade_cli-0.13.2 → cade_cli-0.13.4}/src/cadecoder/core/__init__.py +0 -0
  83. {cade_cli-0.13.2 → cade_cli-0.13.4}/src/cadecoder/core/errors.py +0 -0
  84. {cade_cli-0.13.2 → cade_cli-0.13.4}/src/cadecoder/core/frozen.py +0 -0
  85. {cade_cli-0.13.2 → cade_cli-0.13.4}/src/cadecoder/core/git.py +0 -0
  86. {cade_cli-0.13.2 → cade_cli-0.13.4}/src/cadecoder/core/logging.py +0 -0
  87. {cade_cli-0.13.2 → cade_cli-0.13.4}/src/cadecoder/core/names.py +0 -0
  88. {cade_cli-0.13.2 → cade_cli-0.13.4}/src/cadecoder/core/paths.py +0 -0
  89. {cade_cli-0.13.2 → cade_cli-0.13.4}/src/cadecoder/core/types.py +0 -0
  90. {cade_cli-0.13.2 → cade_cli-0.13.4}/src/cadecoder/execution/__init__.py +0 -0
  91. {cade_cli-0.13.2 → cade_cli-0.13.4}/src/cadecoder/execution/mcp_schema_index.py +0 -0
  92. {cade_cli-0.13.2 → cade_cli-0.13.4}/src/cadecoder/execution/parallel.py +0 -0
  93. {cade_cli-0.13.2 → cade_cli-0.13.4}/src/cadecoder/execution/tool_result_store.py +0 -0
  94. {cade_cli-0.13.2 → cade_cli-0.13.4}/src/cadecoder/execution/tool_schema_cache.py +0 -0
  95. {cade_cli-0.13.2 → cade_cli-0.13.4}/src/cadecoder/hooks/__init__.py +0 -0
  96. {cade_cli-0.13.2 → cade_cli-0.13.4}/src/cadecoder/hooks/config.py +0 -0
  97. {cade_cli-0.13.2 → cade_cli-0.13.4}/src/cadecoder/hooks/engine.py +0 -0
  98. {cade_cli-0.13.2 → cade_cli-0.13.4}/src/cadecoder/hooks/executors/__init__.py +0 -0
  99. {cade_cli-0.13.2 → cade_cli-0.13.4}/src/cadecoder/hooks/executors/callback.py +0 -0
  100. {cade_cli-0.13.2 → cade_cli-0.13.4}/src/cadecoder/hooks/executors/command.py +0 -0
  101. {cade_cli-0.13.2 → cade_cli-0.13.4}/src/cadecoder/hooks/executors/http.py +0 -0
  102. {cade_cli-0.13.2 → cade_cli-0.13.4}/src/cadecoder/hooks/executors/prompt.py +0 -0
  103. {cade_cli-0.13.2 → cade_cli-0.13.4}/src/cadecoder/hooks/executors/ssrf_guard.py +0 -0
  104. {cade_cli-0.13.2 → cade_cli-0.13.4}/src/cadecoder/hooks/matchers.py +0 -0
  105. {cade_cli-0.13.2 → cade_cli-0.13.4}/src/cadecoder/hooks/registry.py +0 -0
  106. {cade_cli-0.13.2 → cade_cli-0.13.4}/src/cadecoder/hooks/types.py +0 -0
  107. {cade_cli-0.13.2 → cade_cli-0.13.4}/src/cadecoder/serve/__init__.py +0 -0
  108. {cade_cli-0.13.2 → cade_cli-0.13.4}/src/cadecoder/serve/adapters/__init__.py +0 -0
  109. {cade_cli-0.13.2 → cade_cli-0.13.4}/src/cadecoder/serve/adapters/base.py +0 -0
  110. {cade_cli-0.13.2 → cade_cli-0.13.4}/src/cadecoder/serve/adapters/desktop.py +0 -0
  111. {cade_cli-0.13.2 → cade_cli-0.13.4}/src/cadecoder/serve/adapters/registry.py +0 -0
  112. {cade_cli-0.13.2 → cade_cli-0.13.4}/src/cadecoder/serve/adapters/stub.py +0 -0
  113. {cade_cli-0.13.2 → cade_cli-0.13.4}/src/cadecoder/serve/adapters/telegram.py +0 -0
  114. {cade_cli-0.13.2 → cade_cli-0.13.4}/src/cadecoder/serve/chunker.py +0 -0
  115. {cade_cli-0.13.2 → cade_cli-0.13.4}/src/cadecoder/serve/commands.py +0 -0
  116. {cade_cli-0.13.2 → cade_cli-0.13.4}/src/cadecoder/serve/daemon.py +0 -0
  117. {cade_cli-0.13.2 → cade_cli-0.13.4}/src/cadecoder/serve/elicitation.py +0 -0
  118. {cade_cli-0.13.2 → cade_cli-0.13.4}/src/cadecoder/serve/format.py +0 -0
  119. {cade_cli-0.13.2 → cade_cli-0.13.4}/src/cadecoder/serve/install.py +0 -0
  120. {cade_cli-0.13.2 → cade_cli-0.13.4}/src/cadecoder/serve/lock.py +0 -0
  121. {cade_cli-0.13.2 → cade_cli-0.13.4}/src/cadecoder/serve/observability.py +0 -0
  122. {cade_cli-0.13.2 → cade_cli-0.13.4}/src/cadecoder/serve/progress_sink.py +0 -0
  123. {cade_cli-0.13.2 → cade_cli-0.13.4}/src/cadecoder/serve/router.py +0 -0
  124. {cade_cli-0.13.2 → cade_cli-0.13.4}/src/cadecoder/serve/secrets.py +0 -0
  125. {cade_cli-0.13.2 → cade_cli-0.13.4}/src/cadecoder/serve/sinks.py +0 -0
  126. {cade_cli-0.13.2 → cade_cli-0.13.4}/src/cadecoder/serve/worker.py +0 -0
  127. {cade_cli-0.13.2 → cade_cli-0.13.4}/src/cadecoder/storage/__init__.py +0 -0
  128. {cade_cli-0.13.2 → cade_cli-0.13.4}/src/cadecoder/storage/threads.py +0 -0
  129. {cade_cli-0.13.2 → cade_cli-0.13.4}/src/cadecoder/tasks/__init__.py +0 -0
  130. {cade_cli-0.13.2 → cade_cli-0.13.4}/src/cadecoder/tasks/channels.py +0 -0
  131. {cade_cli-0.13.2 → cade_cli-0.13.4}/src/cadecoder/tasks/lock.py +0 -0
  132. {cade_cli-0.13.2 → cade_cli-0.13.4}/src/cadecoder/tasks/notifications.py +0 -0
  133. {cade_cli-0.13.2 → cade_cli-0.13.4}/src/cadecoder/tasks/store.py +0 -0
  134. {cade_cli-0.13.2 → cade_cli-0.13.4}/src/cadecoder/tasks/types.py +0 -0
  135. {cade_cli-0.13.2 → cade_cli-0.13.4}/src/cadecoder/templates/login_failed.html +0 -0
  136. {cade_cli-0.13.2 → cade_cli-0.13.4}/src/cadecoder/templates/login_success.html +0 -0
  137. {cade_cli-0.13.2 → cade_cli-0.13.4}/src/cadecoder/templates/serve/launchd.plist.tmpl +0 -0
  138. {cade_cli-0.13.2 → cade_cli-0.13.4}/src/cadecoder/templates/serve/systemd.service.tmpl +0 -0
  139. {cade_cli-0.13.2 → cade_cli-0.13.4}/src/cadecoder/templates/styles.css +0 -0
  140. {cade_cli-0.13.2 → cade_cli-0.13.4}/src/cadecoder/tools/__init__.py +0 -0
  141. {cade_cli-0.13.2 → cade_cli-0.13.4}/src/cadecoder/tools/manager/__init__.py +0 -0
  142. {cade_cli-0.13.2 → cade_cli-0.13.4}/src/cadecoder/tools/manager/_request_ctx.py +0 -0
  143. {cade_cli-0.13.2 → cade_cli-0.13.4}/src/cadecoder/tools/manager/base.py +0 -0
  144. {cade_cli-0.13.2 → cade_cli-0.13.4}/src/cadecoder/tools/manager/composite.py +0 -0
  145. {cade_cli-0.13.2 → cade_cli-0.13.4}/src/cadecoder/tools/manager/config.py +0 -0
  146. {cade_cli-0.13.2 → cade_cli-0.13.4}/src/cadecoder/tools/manager/mcp.py +0 -0
  147. {cade_cli-0.13.2 → cade_cli-0.13.4}/src/cadecoder/tools/search/__init__.py +0 -0
  148. {cade_cli-0.13.2 → cade_cli-0.13.4}/src/cadecoder/tools/search/discovered.py +0 -0
  149. {cade_cli-0.13.2 → cade_cli-0.13.4}/src/cadecoder/tools/search/scoring.py +0 -0
  150. {cade_cli-0.13.2 → cade_cli-0.13.4}/src/cadecoder/tools/search/service.py +0 -0
  151. {cade_cli-0.13.2 → cade_cli-0.13.4}/src/cadecoder/ui/__init__.py +0 -0
  152. {cade_cli-0.13.2 → cade_cli-0.13.4}/src/cadecoder/ui/display.py +0 -0
  153. {cade_cli-0.13.2 → cade_cli-0.13.4}/src/cadecoder/ui/elicitation.py +0 -0
  154. {cade_cli-0.13.2 → cade_cli-0.13.4}/src/cadecoder/ui/input.py +0 -0
  155. {cade_cli-0.13.2 → cade_cli-0.13.4}/src/cadecoder/ui/slash.py +0 -0
  156. {cade_cli-0.13.2 → cade_cli-0.13.4}/src/cadecoder/voice/__init__.py +0 -0
  157. {cade_cli-0.13.2 → cade_cli-0.13.4}/src/cadecoder/voice/audio.py +0 -0
  158. {cade_cli-0.13.2 → cade_cli-0.13.4}/src/cadecoder/voice/cleanup.py +0 -0
  159. {cade_cli-0.13.2 → cade_cli-0.13.4}/src/cadecoder/voice/session.py +0 -0
  160. {cade_cli-0.13.2 → cade_cli-0.13.4}/src/cadecoder/voice/stt.py +0 -0
  161. {cade_cli-0.13.2 → cade_cli-0.13.4}/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.13.2
3
+ Version: 0.13.4
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
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "cade-cli"
3
- version = "0.13.2"
3
+ version = "0.13.4"
4
4
  description = "Cade - The CLI Agent from Arcade.dev"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.11"
@@ -0,0 +1,116 @@
1
+ """Runtime configuration for the local MCP server.
2
+
3
+ Configuration is resolved lazily at runtime, not at import time.
4
+ This allows proper testing and correct behavior when installed as a package.
5
+ """
6
+
7
+ import os
8
+ import pathlib
9
+ from functools import lru_cache
10
+
11
+ # Environment variable that disables filesystem path confinement. When set to a
12
+ # truthy value, filesystem tools may read/write paths outside the project root.
13
+ #
14
+ # This is intentionally a single on/off switch for now. A future iteration may
15
+ # replace it with a richer policy (e.g. an explicit list of additional allowed
16
+ # roots); keep `path_restrictions_enabled()` as the single read point so callers
17
+ # don't need to change when that happens.
18
+ PATH_RESTRICTIONS_ENV: str = "CADE_DISABLE_PATH_RESTRICTIONS"
19
+
20
+ _TRUTHY_VALUES: frozenset[str] = frozenset({"1", "true", "yes", "on"})
21
+
22
+
23
+ def path_restrictions_enabled() -> bool:
24
+ """Return whether filesystem tools must stay within the project root.
25
+
26
+ Restrictions are ON by default. They are lifted only when the
27
+ ``CADE_DISABLE_PATH_RESTRICTIONS`` environment variable is set to a truthy
28
+ value (``1``, ``true``, ``yes``, or ``on``, case-insensitive).
29
+
30
+ Returns:
31
+ True when path confinement should be enforced, False when tools may
32
+ access any path on the filesystem.
33
+
34
+ Examples:
35
+ >>> import os
36
+ >>> os.environ.pop("CADE_DISABLE_PATH_RESTRICTIONS", None)
37
+ >>> path_restrictions_enabled()
38
+ True
39
+ >>> os.environ["CADE_DISABLE_PATH_RESTRICTIONS"] = "1"
40
+ >>> path_restrictions_enabled()
41
+ False
42
+ """
43
+ raw = os.environ.get(PATH_RESTRICTIONS_ENV, "").strip().lower()
44
+ return raw not in _TRUTHY_VALUES
45
+
46
+
47
+ def disable_path_restrictions() -> None:
48
+ """Lift filesystem path confinement for this process and its children.
49
+
50
+ Sets ``CADE_DISABLE_PATH_RESTRICTIONS`` in the environment so that child
51
+ processes (notably the local-tools MCP subprocess, which inherits
52
+ ``os.environ``) and the serve sandbox hook all observe the override.
53
+
54
+ This is the single supported way to flip the toggle on from code, keeping
55
+ the environment-variable name encapsulated in this module.
56
+ """
57
+ os.environ[PATH_RESTRICTIONS_ENV] = "1"
58
+
59
+
60
+ @lru_cache(maxsize=1)
61
+ def get_project_root() -> pathlib.Path:
62
+ """Get the project root directory.
63
+
64
+ Resolution order:
65
+ 1. CADE_PROJECT_ROOT environment variable (set by parent process)
66
+ 2. Current working directory
67
+
68
+ This is cached for performance but can be reset with reset_config().
69
+
70
+ Returns:
71
+ Resolved path to the project root directory.
72
+ """
73
+ if root := os.environ.get("CADE_PROJECT_ROOT"):
74
+ return pathlib.Path(root).resolve()
75
+ return pathlib.Path.cwd().resolve()
76
+
77
+
78
+ def reset_config() -> None:
79
+ """Clear cached configuration.
80
+
81
+ Call this when:
82
+ - The working directory has changed
83
+ - CADE_PROJECT_ROOT environment variable has changed
84
+ - During testing to ensure isolation
85
+ """
86
+ get_project_root.cache_clear()
87
+
88
+
89
+ # Default ignore patterns for file listings and searches
90
+ DEFAULT_IGNORE_PATTERNS: list[str] = [
91
+ ".git",
92
+ "__pycache__",
93
+ "node_modules",
94
+ ".venv",
95
+ "venv",
96
+ ".mypy_cache",
97
+ ".pytest_cache",
98
+ "*.pyc",
99
+ ".DS_Store",
100
+ "*.egg-info",
101
+ "dist",
102
+ "build",
103
+ ".tox",
104
+ ".coverage",
105
+ "htmlcov",
106
+ ".idea",
107
+ ".vscode",
108
+ ".cursor",
109
+ ".claude",
110
+ ]
111
+
112
+ # File operation limits
113
+ MAX_LIST_DEPTH: int = 10
114
+ MAX_LIST_RESULTS: int = 1000
115
+ MAX_PREVIEW_BYTES: int = 100_000
116
+ DEFAULT_TIMEOUT: int = 60
@@ -19,6 +19,7 @@ from cade_mcp_local.config import (
19
19
  MAX_LIST_RESULTS,
20
20
  MAX_PREVIEW_BYTES,
21
21
  get_project_root,
22
+ path_restrictions_enabled,
22
23
  )
23
24
  from cade_mcp_local.errors import (
24
25
  FileOperationError,
@@ -47,22 +48,25 @@ def _read_text_file(file_path: pathlib.Path) -> str:
47
48
 
48
49
 
49
50
  def _write_text_file(file_path: pathlib.Path, content: str) -> None:
50
- """Write text content to a file, ensuring it's within project root."""
51
- project_root = get_project_root()
51
+ """Write text content to a file, ensuring it's within project root.
52
+
53
+ Confinement to the project root is skipped when path restrictions are
54
+ disabled (see `path_restrictions_enabled`).
55
+ """
52
56
  resolved = file_path.resolve()
53
- resolved_root = project_root.resolve()
54
57
 
55
- # Security check: must be within project root
56
- # Use string prefix check as fallback for symlink/worktree edge cases
57
- try:
58
- resolved.relative_to(resolved_root)
59
- except ValueError:
60
- # Fallback: compare resolved string paths
61
- if not str(resolved).startswith(str(resolved_root) + "/"):
62
- raise PathSecurityError(
63
- str(file_path),
64
- f"outside project root (resolved={resolved}, root={resolved_root})",
65
- )
58
+ if path_restrictions_enabled():
59
+ resolved_root = get_project_root().resolve()
60
+ # Security check: must be within project root.
61
+ # Use string prefix check as fallback for symlink/worktree edge cases.
62
+ try:
63
+ resolved.relative_to(resolved_root)
64
+ except ValueError:
65
+ if not str(resolved).startswith(str(resolved_root) + "/"):
66
+ raise PathSecurityError(
67
+ str(file_path),
68
+ f"outside project root (resolved={resolved}, root={resolved_root})",
69
+ )
66
70
 
67
71
  # Create parent directories if needed
68
72
  resolved.parent.mkdir(parents=True, exist_ok=True)
@@ -3,7 +3,11 @@
3
3
  import fnmatch
4
4
  import pathlib
5
5
 
6
- from cade_mcp_local.config import DEFAULT_IGNORE_PATTERNS, get_project_root
6
+ from cade_mcp_local.config import (
7
+ DEFAULT_IGNORE_PATTERNS,
8
+ get_project_root,
9
+ path_restrictions_enabled,
10
+ )
7
11
 
8
12
 
9
13
  def resolve_path(
@@ -46,7 +50,7 @@ def resolve_path(
46
50
  else:
47
51
  resolved = (base_dir / path).resolve()
48
52
 
49
- if confine_to_root:
53
+ if confine_to_root and path_restrictions_enabled():
50
54
  root = get_project_root().resolve()
51
55
  try:
52
56
  resolved.relative_to(root)
@@ -90,7 +90,12 @@ app.add_typer(
90
90
  help="Activation contexts (persona + model + MCP bundle)",
91
91
  rich_help_panel=PANEL_CONFIGURE,
92
92
  )
93
- app.add_typer(persona_app, name="persona", help="Persona library", rich_help_panel=PANEL_CONFIGURE)
93
+ app.add_typer(
94
+ persona_app,
95
+ name="persona",
96
+ help="Personas (custom prompt + model)",
97
+ rich_help_panel=PANEL_CONFIGURE,
98
+ )
94
99
  app.add_typer(model_app, name="model", help="AI models", rich_help_panel=PANEL_CONFIGURE)
95
100
  app.add_typer(mcp_app, name="mcp", help="MCP servers", rich_help_panel=PANEL_CONFIGURE)
96
101
 
@@ -261,6 +266,15 @@ def main_callback(
261
266
  help="Run under a named Cade activation context.",
262
267
  ),
263
268
  ] = None,
269
+ no_path_restrictions: Annotated[
270
+ bool,
271
+ typer.Option(
272
+ "--no-path-restrictions",
273
+ help="Let filesystem tools access any path (disables project-root confinement). "
274
+ "Use with caution.",
275
+ is_eager=True,
276
+ ),
277
+ ] = False,
264
278
  message: Annotated[
265
279
  str | None,
266
280
  typer.Option(
@@ -281,8 +295,21 @@ def main_callback(
281
295
  ] = None,
282
296
  ) -> None:
283
297
  """Resolve runtime, set up logging, and dispatch to chat/resume/-m when no subcommand is given."""
298
+ if no_path_restrictions:
299
+ from cade_mcp_local.config import disable_path_restrictions
300
+
301
+ disable_path_restrictions()
302
+
284
303
  runtime = build_runtime(context)
285
304
  if persona:
305
+ from cadecoder.storage.personas import persona_exists
306
+
307
+ if not persona_exists(persona):
308
+ console.print(
309
+ f"[red]Persona '{persona}' not found.[/red] "
310
+ "Run 'cade persona list' to see available personas."
311
+ )
312
+ raise typer.Exit(1)
286
313
  runtime = replace(runtime, persona_name=persona)
287
314
  runtime.ensure_dirs()
288
315
  set_runtime(runtime)
@@ -114,6 +114,14 @@ def chat(
114
114
  help="Use a saved persona (custom system prompt). See 'cade persona list'.",
115
115
  ),
116
116
  ] = None,
117
+ no_path_restrictions: Annotated[
118
+ bool,
119
+ typer.Option(
120
+ "--no-path-restrictions",
121
+ help="Let filesystem tools access any path (disables project-root confinement). "
122
+ "Use with caution.",
123
+ ),
124
+ ] = False,
117
125
  ) -> None:
118
126
  """
119
127
  Start an interactive chat session with AI.
@@ -123,6 +131,11 @@ def chat(
123
131
  """
124
132
  command_name = "chat"
125
133
  try:
134
+ if no_path_restrictions:
135
+ from cade_mcp_local.config import disable_path_restrictions
136
+
137
+ disable_path_restrictions()
138
+
126
139
  if endpoint:
127
140
  _configure_custom_endpoint(endpoint)
128
141
 
@@ -329,6 +342,14 @@ def resume(
329
342
  help="Use a saved persona (custom system prompt). See 'cade persona list'.",
330
343
  ),
331
344
  ] = None,
345
+ no_path_restrictions: Annotated[
346
+ bool,
347
+ typer.Option(
348
+ "--no-path-restrictions",
349
+ help="Let filesystem tools access any path (disables project-root confinement). "
350
+ "Use with caution.",
351
+ ),
352
+ ] = False,
332
353
  ) -> None:
333
354
  """Resume a saved chat thread.
334
355
 
@@ -343,6 +364,11 @@ def resume(
343
364
  """
344
365
  command_name = "resume"
345
366
  try:
367
+ if no_path_restrictions:
368
+ from cade_mcp_local.config import disable_path_restrictions
369
+
370
+ disable_path_restrictions()
371
+
346
372
  if endpoint:
347
373
  _configure_custom_endpoint(endpoint)
348
374
 
@@ -85,7 +85,14 @@ def create_context(
85
85
  else:
86
86
  ctx = ContextConfig(name=context_name)
87
87
  if persona:
88
- ctx.persona = validate_name(persona, kind="persona")
88
+ from cadecoder.storage.personas import persona_exists
89
+
90
+ persona_name = validate_name(persona, kind="persona")
91
+ if not persona_exists(persona_name):
92
+ console.print(f"[red]Persona '{persona_name}' not found.[/red]")
93
+ console.print("[dim]Use 'cade persona list' to see available personas.[/dim]")
94
+ raise typer.Exit(1)
95
+ ctx.persona = persona_name
89
96
  if model:
90
97
  ctx.model = model
91
98
  save_context_config(ctx)
@@ -163,8 +170,15 @@ def set_context_persona(
163
170
  persona: Annotated[str, typer.Argument(help="Persona name")],
164
171
  context: Annotated[str | None, typer.Option("--context", "-c")] = None,
165
172
  ) -> None:
173
+ from cadecoder.storage.personas import persona_exists
174
+
175
+ persona_name = validate_name(persona, kind="persona")
176
+ if not persona_exists(persona_name):
177
+ console.print(f"[red]Persona '{persona_name}' not found.[/red]")
178
+ console.print("[dim]Use 'cade persona list' to see available personas.[/dim]")
179
+ raise typer.Exit(1)
166
180
  ctx = load_context_config(context or _active_or_default())
167
- ctx.persona = validate_name(persona, kind="persona")
181
+ ctx.persona = persona_name
168
182
  save_context_config(ctx)
169
183
  reset_runtime()
170
184
  console.print(f"[green]Context '{ctx.name}' persona set to '{ctx.persona}'.[/green]")
@@ -10,12 +10,16 @@ from rich.console import Console
10
10
  from rich.table import Table
11
11
 
12
12
  from cadecoder.tasks.scheduler import (
13
+ AmbiguousTaskError,
14
+ TaskNotFoundError,
13
15
  add_scheduled_task,
14
16
  load_scheduled_tasks,
15
17
  remove_scheduled_task,
18
+ resolve_scheduled_task,
16
19
  save_scheduled_tasks,
17
20
  set_scheduled_task_enabled,
18
21
  )
22
+ from cadecoder.tasks.types import ScheduledTask
19
23
 
20
24
  cron_app = typer.Typer(
21
25
  name="cron",
@@ -33,6 +37,29 @@ def _cb(ctx: typer.Context) -> None:
33
37
  raise typer.Exit(0)
34
38
 
35
39
 
40
+ def _resolve_or_exit(token: str) -> ScheduledTask:
41
+ """Resolve a token to one task, printing a helpful error and exiting otherwise.
42
+
43
+ Args:
44
+ token: The id, name, or id prefix supplied on the command line.
45
+
46
+ Returns:
47
+ The single matching `ScheduledTask`.
48
+ """
49
+ try:
50
+ return resolve_scheduled_task(token)
51
+ except TaskNotFoundError:
52
+ console.print(f"[yellow]No scheduled task matching {token!r}[/yellow]")
53
+ raise typer.Exit(1) from None
54
+ except AmbiguousTaskError as e:
55
+ console.print(
56
+ f"[yellow]{token!r} matches {len(e.matches)} tasks; be more specific:[/yellow]"
57
+ )
58
+ for t in e.matches:
59
+ console.print(f" [cyan]{t.id}[/cyan] {t.name}")
60
+ raise typer.Exit(1) from None
61
+
62
+
36
63
  @cron_app.command("list")
37
64
  def list_cron(
38
65
  json_out: Annotated[bool, typer.Option("--json", help="Emit JSON")] = False,
@@ -86,39 +113,32 @@ def add_cron(
86
113
 
87
114
  @cron_app.command("remove")
88
115
  def remove_cron(task_id: Annotated[str, typer.Argument()]) -> None:
89
- if not remove_scheduled_task(task_id):
90
- console.print(f"[yellow]No scheduled task with id {task_id}[/yellow]")
91
- raise typer.Exit(1)
92
- console.print(f"[green]Removed {task_id}[/green]")
116
+ target = _resolve_or_exit(task_id)
117
+ remove_scheduled_task(target.id)
118
+ console.print(f"[green]Removed {target.id} ({target.name})[/green]")
93
119
 
94
120
 
95
121
  @cron_app.command("enable")
96
122
  def enable_cron(task_id: Annotated[str, typer.Argument()]) -> None:
97
- if not set_scheduled_task_enabled(task_id, True):
98
- console.print(f"[yellow]No scheduled task with id {task_id}[/yellow]")
99
- raise typer.Exit(1)
100
- console.print(f"[green]Enabled {task_id}[/green]")
123
+ target = _resolve_or_exit(task_id)
124
+ set_scheduled_task_enabled(target.id, True)
125
+ console.print(f"[green]Enabled {target.id} ({target.name})[/green]")
101
126
 
102
127
 
103
128
  @cron_app.command("disable")
104
129
  def disable_cron(task_id: Annotated[str, typer.Argument()]) -> None:
105
- if not set_scheduled_task_enabled(task_id, False):
106
- console.print(f"[yellow]No scheduled task with id {task_id}[/yellow]")
107
- raise typer.Exit(1)
108
- console.print(f"[green]Disabled {task_id}[/green]")
130
+ target = _resolve_or_exit(task_id)
131
+ set_scheduled_task_enabled(target.id, False)
132
+ console.print(f"[green]Disabled {target.id} ({target.name})[/green]")
109
133
 
110
134
 
111
135
  @cron_app.command("run-now")
112
136
  def run_now(task_id: Annotated[str, typer.Argument()]) -> None:
113
137
  """Mark a scheduled task to fire at the next tick by clearing last_fired."""
138
+ target = _resolve_or_exit(task_id)
114
139
  tasks = load_scheduled_tasks()
115
- changed = False
116
140
  for t in tasks:
117
- if t.id == task_id:
141
+ if t.id == target.id:
118
142
  t.last_fired = None
119
- changed = True
120
- if not changed:
121
- console.print(f"[yellow]No scheduled task with id {task_id}[/yellow]")
122
- raise typer.Exit(1)
123
143
  save_scheduled_tasks(tasks)
124
- console.print(f"[green]Queued {task_id} for next tick[/green]")
144
+ console.print(f"[green]Queued {target.id} ({target.name}) for next tick[/green]")
@@ -0,0 +1,176 @@
1
+ """``cade mem`` — persona-scoped memory management backed by agent-library.
2
+
3
+ The agent-library (``librarian``) ships a Typer CLI with commands for sources
4
+ (``list``/``add``/``rm``), search, the MCP server, index management, and
5
+ settings (``config``). Rather than re-implement that surface, ``cade mem``
6
+ drives librarian's Typer app **in-process** with ``prog_name="cade mem"`` so the
7
+ entire help tree renders as ``cade mem``/``cade mem search``/… — no
8
+ ``librarian`` or ``libr`` branding leaks through.
9
+
10
+ Storage is scoped to the active context/persona by setting librarian's
11
+ environment (``DATABASE_PATH`` / ``DOCUMENTS_PATH`` / ``SOURCES_CONFIG_PATH``)
12
+ and ``HOME`` *before* librarian is imported. Librarian reads ``HOME`` for its
13
+ ``~/.librarian`` config dir and the env vars for its storage at import time, so
14
+ the order matters: each ``cade mem`` invocation runs in its own process and
15
+ binds to exactly one persona.
16
+ """
17
+
18
+ from __future__ import annotations
19
+
20
+ from pathlib import Path
21
+ from typing import Any
22
+
23
+ import typer
24
+ from rich.console import Console
25
+
26
+ from cadecoder.cli.commands.persona import _ensure_memory_dir
27
+ from cadecoder.core.runtime import get_runtime
28
+
29
+ console = Console(stderr=True)
30
+
31
+ MEM_CONTEXT_SETTINGS = {
32
+ "allow_extra_args": True,
33
+ "ignore_unknown_options": True,
34
+ # Empty list so --help / -h flow through to librarian instead of being
35
+ # intercepted by Typer; `cade mem --help` renders the cade-branded help.
36
+ "help_option_names": [],
37
+ }
38
+
39
+ _MEM_HELP = "Manage Cade persona memory — sources, search, and indexing."
40
+
41
+
42
+ def _resolve_active_persona(ctx: typer.Context) -> str:
43
+ """Return the active persona name.
44
+
45
+ Precedence: ``--persona`` flag on the root ``cade`` invocation (stored in
46
+ ``ctx.obj``) → the active context's persona → ``"default"``.
47
+
48
+ Args:
49
+ ctx: The Typer/Click context for the ``mem`` invocation.
50
+
51
+ Returns:
52
+ The resolved persona name.
53
+ """
54
+ cursor: typer.Context | None = ctx
55
+ while cursor is not None:
56
+ obj = cursor.obj
57
+ if isinstance(obj, dict):
58
+ name = obj.get("persona")
59
+ if isinstance(name, str) and name.strip():
60
+ return name
61
+ cursor = cursor.parent
62
+
63
+ try:
64
+ name = get_runtime().persona_name
65
+ except Exception:
66
+ name = None
67
+ return name or "default"
68
+
69
+
70
+ def _apply_persona_env(persona_name: str) -> Path:
71
+ """Pin librarian's storage to a persona by setting process environment.
72
+
73
+ Librarian has two code paths that locate its storage and they disagree:
74
+ ``librarian.config`` reads ``DATABASE_PATH`` / ``DOCUMENTS_PATH`` /
75
+ ``SOURCES_CONFIG_PATH``, but the CLI derives ``~/.librarian`` from ``HOME``.
76
+ Setting both — with ``HOME`` pointed at the persona directory — makes them
77
+ converge on ``<persona>/.librarian/*``. This MUST run before librarian is
78
+ imported.
79
+
80
+ Args:
81
+ persona_name: The persona whose memory directory to bind to.
82
+
83
+ Returns:
84
+ The ``.librarian`` directory for the persona.
85
+ """
86
+ import os
87
+
88
+ persona_dir = _ensure_memory_dir(persona_name)
89
+ librarian_dir = persona_dir / ".librarian"
90
+ os.environ["DATABASE_PATH"] = str(librarian_dir / "index.db")
91
+ os.environ["DOCUMENTS_PATH"] = str(librarian_dir / "documents")
92
+ os.environ["SOURCES_CONFIG_PATH"] = str(librarian_dir / "sources.json")
93
+ os.environ["HOME"] = str(persona_dir)
94
+ return librarian_dir
95
+
96
+
97
+ def _load_librarian_app() -> Any:
98
+ """Import and return librarian's Typer app.
99
+
100
+ Imported lazily so the persona environment (see :func:`_apply_persona_env`)
101
+ is in place first, and so a missing agent-library install fails loudly only
102
+ when ``cade mem`` is actually used.
103
+
104
+ Returns:
105
+ The librarian Typer application instance.
106
+ """
107
+ from librarian.cli import app
108
+
109
+ return app
110
+
111
+
112
+ def _run_mem(args: list[str]) -> int:
113
+ """Run librarian's CLI in-process, branded as ``cade mem``.
114
+
115
+ Args:
116
+ args: Arguments to forward to the librarian CLI (e.g. ``["search", "x"]``).
117
+
118
+ Returns:
119
+ The process exit code.
120
+ """
121
+ try:
122
+ librarian_app = _load_librarian_app()
123
+ except ImportError:
124
+ console.print(
125
+ "[red]agent-library is not installed.[/red] Install it into the "
126
+ "active environment: [bold]uv pip install agent-library[/bold]."
127
+ )
128
+ return 1
129
+
130
+ # Rebrand the app so every help screen reads "cade mem" rather than "libr".
131
+ librarian_app.info.name = "cade mem"
132
+ librarian_app.info.help = _MEM_HELP
133
+
134
+ try:
135
+ librarian_app(args=args, prog_name="cade mem")
136
+ except SystemExit as exc:
137
+ code = exc.code
138
+ if code is None:
139
+ return 0
140
+ return code if isinstance(code, int) else 1
141
+ return 0
142
+
143
+
144
+ def mem_command(ctx: typer.Context) -> None:
145
+ """Run a ``cade mem`` subcommand scoped to the active persona.
146
+
147
+ Examples:
148
+ cade mem # show the cade mem help
149
+ cade mem list # list sources for the active persona
150
+ cade mem add ./docs # add a source under the active persona
151
+ cade mem search "topic" # search the active persona's memory
152
+ cade mem config show # show memory configuration
153
+ cade --persona anna mem list # scope to a different persona
154
+ """
155
+ persona = _resolve_active_persona(ctx)
156
+ memory_home = _apply_persona_env(persona).parent
157
+ forwarded = list(ctx.args)
158
+ # Surface the active scope on help / no-args runs — opaque env vars are
159
+ # easy to get wrong and this keeps the bridge visible.
160
+ if not forwarded or forwarded[0] in {"--help", "-h"}:
161
+ console.print(
162
+ f"[dim]cade mem: scoped to persona [bold]{persona}[/bold] "
163
+ f"(storage={memory_home})[/dim]\n"
164
+ )
165
+ rc = _run_mem(forwarded)
166
+ raise typer.Exit(rc)
167
+
168
+
169
+ __all__ = (
170
+ "MEM_CONTEXT_SETTINGS",
171
+ "_apply_persona_env",
172
+ "_load_librarian_app",
173
+ "_resolve_active_persona",
174
+ "_run_mem",
175
+ "mem_command",
176
+ )