dotagents-cli 0.6.0__tar.gz → 0.6.2__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 (108) hide show
  1. {dotagents_cli-0.6.0 → dotagents_cli-0.6.2}/CHANGELOG.md +31 -1
  2. {dotagents_cli-0.6.0 → dotagents_cli-0.6.2}/PKG-INFO +2 -2
  3. {dotagents_cli-0.6.0 → dotagents_cli-0.6.2}/pyproject.toml +1 -1
  4. {dotagents_cli-0.6.0 → dotagents_cli-0.6.2}/src/dotagents/__init__.py +1 -1
  5. {dotagents_cli-0.6.0 → dotagents_cli-0.6.2}/src/dotagents/_hooks.py +13 -2
  6. {dotagents_cli-0.6.0 → dotagents_cli-0.6.2}/src/dotagents/cli/__init__.py +9 -3
  7. {dotagents_cli-0.6.0 → dotagents_cli-0.6.2}/src/dotagents/cli/build_pyz.py +1 -1
  8. {dotagents_cli-0.6.0 → dotagents_cli-0.6.2}/src/dotagents/cli/env.py +9 -0
  9. {dotagents_cli-0.6.0 → dotagents_cli-0.6.2}/tests/test_fix_cli.py +7 -4
  10. {dotagents_cli-0.6.0 → dotagents_cli-0.6.2}/tests/test_hooks.py +35 -0
  11. dotagents_cli-0.6.2/tests/test_mcp.py +60 -0
  12. {dotagents_cli-0.6.0 → dotagents_cli-0.6.2}/tools/pyz_smoke.sh +1 -1
  13. {dotagents_cli-0.6.0 → dotagents_cli-0.6.2}/.gitignore +0 -0
  14. {dotagents_cli-0.6.0 → dotagents_cli-0.6.2}/LICENSE +0 -0
  15. {dotagents_cli-0.6.0 → dotagents_cli-0.6.2}/README.md +0 -0
  16. {dotagents_cli-0.6.0 → dotagents_cli-0.6.2}/docs/api/agents.md +0 -0
  17. {dotagents_cli-0.6.0 → dotagents_cli-0.6.2}/docs/api/cli.md +0 -0
  18. {dotagents_cli-0.6.0 → dotagents_cli-0.6.2}/docs/api/commands.md +0 -0
  19. {dotagents_cli-0.6.0 → dotagents_cli-0.6.2}/docs/api/context.md +0 -0
  20. {dotagents_cli-0.6.0 → dotagents_cli-0.6.2}/docs/api/env.md +0 -0
  21. {dotagents_cli-0.6.0 → dotagents_cli-0.6.2}/docs/api/fs.md +0 -0
  22. {dotagents_cli-0.6.0 → dotagents_cli-0.6.2}/docs/api/hooks.md +0 -0
  23. {dotagents_cli-0.6.0 → dotagents_cli-0.6.2}/docs/api/merge.md +0 -0
  24. {dotagents_cli-0.6.0 → dotagents_cli-0.6.2}/docs/api/overlays.md +0 -0
  25. {dotagents_cli-0.6.0 → dotagents_cli-0.6.2}/docs/api/reference.md +0 -0
  26. {dotagents_cli-0.6.0 → dotagents_cli-0.6.2}/docs/api/resources.md +0 -0
  27. {dotagents_cli-0.6.0 → dotagents_cli-0.6.2}/docs/api/scope.md +0 -0
  28. {dotagents_cli-0.6.0 → dotagents_cli-0.6.2}/docs/api/skills.md +0 -0
  29. {dotagents_cli-0.6.0 → dotagents_cli-0.6.2}/docs/api/sources.md +0 -0
  30. {dotagents_cli-0.6.0 → dotagents_cli-0.6.2}/docs/api/wrappers.md +0 -0
  31. {dotagents_cli-0.6.0 → dotagents_cli-0.6.2}/docs/changelog.md +0 -0
  32. {dotagents_cli-0.6.0 → dotagents_cli-0.6.2}/docs/guide/authoring.md +0 -0
  33. {dotagents_cli-0.6.0 → dotagents_cli-0.6.2}/docs/guide/commands.md +0 -0
  34. {dotagents_cli-0.6.0 → dotagents_cli-0.6.2}/docs/guide/install.md +0 -0
  35. {dotagents_cli-0.6.0 → dotagents_cli-0.6.2}/docs/guide/overlays.md +0 -0
  36. {dotagents_cli-0.6.0 → dotagents_cli-0.6.2}/docs/guide/private-sync.md +0 -0
  37. {dotagents_cli-0.6.0 → dotagents_cli-0.6.2}/docs/index.md +0 -0
  38. {dotagents_cli-0.6.0 → dotagents_cli-0.6.2}/install.py +0 -0
  39. {dotagents_cli-0.6.0 → dotagents_cli-0.6.2}/mkdocs.yml +0 -0
  40. {dotagents_cli-0.6.0 → dotagents_cli-0.6.2}/src/dotagents/AGENTS.md +0 -0
  41. {dotagents_cli-0.6.0 → dotagents_cli-0.6.2}/src/dotagents/__main__.py +0 -0
  42. {dotagents_cli-0.6.0 → dotagents_cli-0.6.2}/src/dotagents/_agents.py +0 -0
  43. {dotagents_cli-0.6.0 → dotagents_cli-0.6.2}/src/dotagents/_context.py +0 -0
  44. {dotagents_cli-0.6.0 → dotagents_cli-0.6.2}/src/dotagents/_env.py +0 -0
  45. {dotagents_cli-0.6.0 → dotagents_cli-0.6.2}/src/dotagents/_fs.py +0 -0
  46. {dotagents_cli-0.6.0 → dotagents_cli-0.6.2}/src/dotagents/_merge.py +0 -0
  47. {dotagents_cli-0.6.0 → dotagents_cli-0.6.2}/src/dotagents/_overlay/dotagents/cmds/findings.py +0 -0
  48. {dotagents_cli-0.6.0 → dotagents_cli-0.6.2}/src/dotagents/_overlay/dotagents/cmds/launch.py +0 -0
  49. {dotagents_cli-0.6.0 → dotagents_cli-0.6.2}/src/dotagents/_overlay/dotagents/hooks/preinvocation_antigravity_context.py +0 -0
  50. {dotagents_cli-0.6.0 → dotagents_cli-0.6.2}/src/dotagents/_overlay/dotagents/hooks/pretooluse_codex_env.py +0 -0
  51. {dotagents_cli-0.6.0 → dotagents_cli-0.6.2}/src/dotagents/_overlay/dotagents/hooks/sessionstart_codex_context.py +0 -0
  52. {dotagents_cli-0.6.0 → dotagents_cli-0.6.2}/src/dotagents/_overlay/dotagents/templates/AGENTS.md +0 -0
  53. {dotagents_cli-0.6.0 → dotagents_cli-0.6.2}/src/dotagents/_overlay/dotagents/templates/PROJECT.md +0 -0
  54. {dotagents_cli-0.6.0 → dotagents_cli-0.6.2}/src/dotagents/_overlays.py +0 -0
  55. {dotagents_cli-0.6.0 → dotagents_cli-0.6.2}/src/dotagents/_resources.py +0 -0
  56. {dotagents_cli-0.6.0 → dotagents_cli-0.6.2}/src/dotagents/_scope.py +0 -0
  57. {dotagents_cli-0.6.0 → dotagents_cli-0.6.2}/src/dotagents/_skills.py +0 -0
  58. {dotagents_cli-0.6.0 → dotagents_cli-0.6.2}/src/dotagents/_sources.py +0 -0
  59. {dotagents_cli-0.6.0 → dotagents_cli-0.6.2}/src/dotagents/_wrappers.py +0 -0
  60. {dotagents_cli-0.6.0 → dotagents_cli-0.6.2}/src/dotagents/cli/_common.py +0 -0
  61. {dotagents_cli-0.6.0 → dotagents_cli-0.6.2}/src/dotagents/cli/about.py +0 -0
  62. {dotagents_cli-0.6.0 → dotagents_cli-0.6.2}/src/dotagents/cli/context.py +0 -0
  63. {dotagents_cli-0.6.0 → dotagents_cli-0.6.2}/src/dotagents/cli/init.py +0 -0
  64. {dotagents_cli-0.6.0 → dotagents_cli-0.6.2}/src/dotagents/cli/overlays.py +0 -0
  65. {dotagents_cli-0.6.0 → dotagents_cli-0.6.2}/src/dotagents/cli/path.py +0 -0
  66. {dotagents_cli-0.6.0 → dotagents_cli-0.6.2}/src/dotagents/py.typed +0 -0
  67. {dotagents_cli-0.6.0 → dotagents_cli-0.6.2}/tests/_helpers.py +0 -0
  68. {dotagents_cli-0.6.0 → dotagents_cli-0.6.2}/tests/_shell.py +0 -0
  69. {dotagents_cli-0.6.0 → dotagents_cli-0.6.2}/tests/conftest.py +0 -0
  70. {dotagents_cli-0.6.0 → dotagents_cli-0.6.2}/tests/test_about.py +0 -0
  71. {dotagents_cli-0.6.0 → dotagents_cli-0.6.2}/tests/test_agents.py +0 -0
  72. {dotagents_cli-0.6.0 → dotagents_cli-0.6.2}/tests/test_api_header.py +0 -0
  73. {dotagents_cli-0.6.0 → dotagents_cli-0.6.2}/tests/test_audit_leak.py +0 -0
  74. {dotagents_cli-0.6.0 → dotagents_cli-0.6.2}/tests/test_build_pyz.py +0 -0
  75. {dotagents_cli-0.6.0 → dotagents_cli-0.6.2}/tests/test_cli_main.py +0 -0
  76. {dotagents_cli-0.6.0 → dotagents_cli-0.6.2}/tests/test_cloud_setup.py +0 -0
  77. {dotagents_cli-0.6.0 → dotagents_cli-0.6.2}/tests/test_cmds_discovery.py +0 -0
  78. {dotagents_cli-0.6.0 → dotagents_cli-0.6.2}/tests/test_context.py +0 -0
  79. {dotagents_cli-0.6.0 → dotagents_cli-0.6.2}/tests/test_env.py +0 -0
  80. {dotagents_cli-0.6.0 → dotagents_cli-0.6.2}/tests/test_env_cli_scope.py +0 -0
  81. {dotagents_cli-0.6.0 → dotagents_cli-0.6.2}/tests/test_env_format.py +0 -0
  82. {dotagents_cli-0.6.0 → dotagents_cli-0.6.2}/tests/test_env_regressions.py +0 -0
  83. {dotagents_cli-0.6.0 → dotagents_cli-0.6.2}/tests/test_env_safety.py +0 -0
  84. {dotagents_cli-0.6.0 → dotagents_cli-0.6.2}/tests/test_findings_cmd.py +0 -0
  85. {dotagents_cli-0.6.0 → dotagents_cli-0.6.2}/tests/test_fix_adapters.py +0 -0
  86. {dotagents_cli-0.6.0 → dotagents_cli-0.6.2}/tests/test_fix_env.py +0 -0
  87. {dotagents_cli-0.6.0 → dotagents_cli-0.6.2}/tests/test_fix_hooks.py +0 -0
  88. {dotagents_cli-0.6.0 → dotagents_cli-0.6.2}/tests/test_fix_init.py +0 -0
  89. {dotagents_cli-0.6.0 → dotagents_cli-0.6.2}/tests/test_fix_overlays.py +0 -0
  90. {dotagents_cli-0.6.0 → dotagents_cli-0.6.2}/tests/test_fix_packaging.py +0 -0
  91. {dotagents_cli-0.6.0 → dotagents_cli-0.6.2}/tests/test_fix_scope_context.py +0 -0
  92. {dotagents_cli-0.6.0 → dotagents_cli-0.6.2}/tests/test_fix_skills_adapters.py +0 -0
  93. {dotagents_cli-0.6.0 → dotagents_cli-0.6.2}/tests/test_init_base.py +0 -0
  94. {dotagents_cli-0.6.0 → dotagents_cli-0.6.2}/tests/test_last_mile.py +0 -0
  95. {dotagents_cli-0.6.0 → dotagents_cli-0.6.2}/tests/test_launch_cmd.py +0 -0
  96. {dotagents_cli-0.6.0 → dotagents_cli-0.6.2}/tests/test_overlay_libs.py +0 -0
  97. {dotagents_cli-0.6.0 → dotagents_cli-0.6.2}/tests/test_overlays_cli.py +0 -0
  98. {dotagents_cli-0.6.0 → dotagents_cli-0.6.2}/tests/test_overlays_command.py +0 -0
  99. {dotagents_cli-0.6.0 → dotagents_cli-0.6.2}/tests/test_path_timeout_frontmatter.py +0 -0
  100. {dotagents_cli-0.6.0 → dotagents_cli-0.6.2}/tests/test_powershell_env_hook.py +0 -0
  101. {dotagents_cli-0.6.0 → dotagents_cli-0.6.2}/tests/test_review_security.py +0 -0
  102. {dotagents_cli-0.6.0 → dotagents_cli-0.6.2}/tests/test_scope.py +0 -0
  103. {dotagents_cli-0.6.0 → dotagents_cli-0.6.2}/tests/test_sources.py +0 -0
  104. {dotagents_cli-0.6.0 → dotagents_cli-0.6.2}/tests/test_sources_uri.py +0 -0
  105. {dotagents_cli-0.6.0 → dotagents_cli-0.6.2}/tests/test_wire_hooks.py +0 -0
  106. {dotagents_cli-0.6.0 → dotagents_cli-0.6.2}/tests/test_wrappers.py +0 -0
  107. {dotagents_cli-0.6.0 → dotagents_cli-0.6.2}/tools/audit.py +0 -0
  108. {dotagents_cli-0.6.0 → dotagents_cli-0.6.2}/tools/cloud-setup.sh +0 -0
@@ -7,6 +7,34 @@ and this project adheres to [Semantic Versioning](https://semver.org/).
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.6.2] - 2026-09-28
11
+
12
+ ### Added
13
+
14
+ - With the `duho` floor bump, every `dotagents` invocation now checks
15
+ `DOTAGENTS_MCP=stdio` and serves its full command tree as an MCP server
16
+ over stdio when set. `env` opts itself out of that tool surface
17
+ (`_mcp_ = False`): its whole point is printing resolved, possibly secret
18
+ values, which should never be a callable MCP tool. Every other command is
19
+ unaffected, and `env` still works normally on the CLI.
20
+
21
+ ### Changed
22
+
23
+ - The `duho` floor moves to `>=0.6.0,<0.7` (from `>=0.5.0,<0.6`); the vendored
24
+ `.pyz` pin (`build-pyz --duho-version`) moves with it, per the repo's own
25
+ rule that the two must always agree. No other source change was needed.
26
+
27
+ ## [0.6.1] - 2026-09-27
28
+
29
+ ### Fixed
30
+
31
+ - Re-running `init` after an upgrade from 0.5.x replaces the `CwdChanged` hooks
32
+ and Codex's `PreToolUse` hook that release wrote, instead of adding the new
33
+ ones beside them (every `cd` ran both, and the old one still wrote the
34
+ unquoted project path). A hook an earlier release wrote is recognised by its
35
+ label and a command that runs dotagents, reads `AGENTS.md` or
36
+ `AGENTS_PROJECT_ROOT`, or runs one of dotagents' hook scripts.
37
+
10
38
  ## [0.6.0] - 2026-09-25
11
39
 
12
40
  ### Added
@@ -1280,7 +1308,9 @@ Patch release: the PATH/POSIX-conversion fix above (the only change since 0.3.0)
1280
1308
  affected module sources (`dotagents.cli`, `duho.presets`) to extracted temp files
1281
1309
  before dispatch; a no-op for a plain install.
1282
1310
 
1283
- [Unreleased]: https://github.com/jose-pr/dotagents/compare/v0.6.0...HEAD
1311
+ [Unreleased]: https://github.com/jose-pr/dotagents/compare/v0.6.2...HEAD
1312
+ [0.6.2]: https://github.com/jose-pr/dotagents/compare/v0.6.1...v0.6.2
1313
+ [0.6.1]: https://github.com/jose-pr/dotagents/compare/v0.6.0...v0.6.1
1284
1314
  [0.6.0]: https://github.com/jose-pr/dotagents/compare/v0.5.1...v0.6.0
1285
1315
  [0.5.1]: https://github.com/jose-pr/dotagents/compare/v0.5.0...v0.5.1
1286
1316
  [0.5.0]: https://github.com/jose-pr/dotagents/compare/v0.4.0...v0.5.0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: dotagents-cli
3
- Version: 0.6.0
3
+ Version: 0.6.2
4
4
  Summary: The dotagents CLI: install and manage a portable ~/.agents config for AI coding agents
5
5
  Project-URL: Homepage, https://github.com/jose-pr/dotagents/
6
6
  Project-URL: Repository, https://github.com/jose-pr/dotagents
@@ -23,7 +23,7 @@ Classifier: Programming Language :: Python :: 3.14
23
23
  Classifier: Topic :: Software Development :: Libraries :: Python Modules
24
24
  Classifier: Typing :: Typed
25
25
  Requires-Python: >=3.9
26
- Requires-Dist: duho<0.6,>=0.5.0
26
+ Requires-Dist: duho<0.7,>=0.6.0
27
27
  Requires-Dist: pathlib-next<0.10,>=0.9.0
28
28
  Requires-Dist: tomli>=1.1.0; python_version < '3.11'
29
29
  Provides-Extra: dev
@@ -35,7 +35,7 @@ classifiers = [
35
35
  # `build-pyz` vendors the floor of each of these (src/dotagents/cli/build_pyz.py);
36
36
  # raise a floor and move its pin there in the same change.
37
37
  dependencies = [
38
- "duho>=0.5.0,<0.6",
38
+ "duho>=0.6.0,<0.7",
39
39
  "pathlib_next>=0.9.0,<0.10",
40
40
  # overlay.toml manifests and TOML registries; tomllib is stdlib from 3.11.
41
41
  'tomli>=1.1.0; python_version < "3.11"',
@@ -1,3 +1,3 @@
1
1
  """dotagents: installable CLI for the dotagents agent-config payload."""
2
2
 
3
- __version__ = "0.6.0"
3
+ __version__ = "0.6.2"
@@ -25,6 +25,7 @@ the ``.pyz``.
25
25
  from __future__ import annotations
26
26
 
27
27
  import json
28
+ import re
28
29
  from pathlib import Path
29
30
  from typing import Any, Optional
30
31
 
@@ -32,9 +33,19 @@ from typing import Any, Optional
32
33
  #: Our hooks' statusMessage is `STATUS_PREFIX + label`, so a foreign hook that
33
34
  #: happens to use the same generic label ("Loading agent context") is never
34
35
  #: taken for ours. The bare label an earlier release wrote is still ours when
35
- #: the hook's command runs dotagents, so those hooks converge to the new one.
36
+ #: the hook's command is recognisably dotagents' (:data:`_LEGACY_COMMAND`), so
37
+ #: those hooks converge to the new one instead of running beside it.
36
38
  STATUS_PREFIX = "dotagents: "
37
39
 
40
+ #: What every command an earlier release wrote contains: `dotagents` itself
41
+ #: (SessionStart, the PowerShell loader), the project pin or `AGENTS.md`
42
+ #: (CwdChanged, which never names dotagents), or one of the deployed hook
43
+ #: scripts (Codex, Antigravity: `python3 ".../pretooluse_codex_env.py"`).
44
+ _LEGACY_COMMAND = re.compile(
45
+ r"dotagents|AGENTS_PROJECT_ROOT|AGENTS\.md"
46
+ r"|(?:sessionstart_codex_context|pretooluse_codex_env|preinvocation_antigravity_context)\.py"
47
+ )
48
+
38
49
  #: The hook keys dotagents manages; any other key a user added (a `timeout`,
39
50
  #: say) survives a refresh.
40
51
  _MANAGED_KEYS = frozenset({"type", "command", "shell", "commandWindows", "statusMessage"})
@@ -56,7 +67,7 @@ def _labelled_ours(hook: Any, status_message: "Optional[str]") -> bool:
56
67
  status = hook.get("statusMessage")
57
68
  if status == labels[0]:
58
69
  return True
59
- return status == labels[1] and "dotagents" in str(hook.get("command", ""))
70
+ return status == labels[1] and bool(_LEGACY_COMMAND.search(str(hook.get("command", ""))))
60
71
 
61
72
 
62
73
  def build_hook_entry(
@@ -217,9 +217,15 @@ def _discover_modules(directory: Path) -> "list":
217
217
  not an Exception. A command that imports but cannot build its parser is
218
218
  skipped the same way (`_buildable`). Falls back to duho's own
219
219
  per-directory unit, with a warning, if a duho release moves these
220
- helpers."""
220
+ helpers.
221
+
222
+ Uses duho's PUBLIC `import_from_path` (duho >= 0.6.0) for the file
223
+ import; `_commands_in_module` has no public counterpart yet, so it stays
224
+ a private import (see `duho.discovery.import_from_path`'s own docstring:
225
+ it is the public counterpart of the `_unique_module_name` +
226
+ `_import_from_path` pair this module used to call directly)."""
221
227
  try:
222
- from duho.discovery import _commands_in_module, _import_from_path, _unique_module_name
228
+ from duho.discovery import _commands_in_module, import_from_path
223
229
  except ImportError: # pragma: no cover -- a later duho without these internals
224
230
  from duho.discovery import discover_commands
225
231
 
@@ -234,7 +240,7 @@ def _discover_modules(directory: Path) -> "list":
234
240
  if file.name.startswith("_"):
235
241
  continue
236
242
  try:
237
- module = _import_from_path(_unique_module_name("duho._discovered." + file.stem), file)
243
+ module = import_from_path("duho._discovered." + file.stem, file)
238
244
  found = _commands_in_module(module, stem=file.stem)
239
245
  except (Exception, SystemExit) as exc: # noqa: BLE001 -- see docstring
240
246
  _LOGGER.warning("skipping command module %s: %s", file, _describe(exc))
@@ -73,7 +73,7 @@ class BuildPyz(LoggingArgs, Cmd):
73
73
  # stale pin here ships an artifact `pip install dotagents-cli` would refuse.
74
74
  # Pin the FLOOR of each declared range, not the latest patch -- the .pyz then
75
75
  # exercises the minimum the metadata promises.
76
- duho_version: str = "0.5.0"
76
+ duho_version: str = "0.6.0"
77
77
  "Pinned duho version to vendor."
78
78
  ("--duho-version",)
79
79
 
@@ -539,6 +539,15 @@ class Env(DotAgentsArgs):
539
539
 
540
540
  _parsername_ = "env"
541
541
 
542
+ # Opt out of the MCP tool surface (duho >= 0.6.0): this command's whole
543
+ # point is to print resolved, possibly secret values (see the class
544
+ # docstring's "Output is sensitive" note) -- exactly what an MCP client
545
+ # should never be handed as a callable tool. `_mcp_ = False` on a
546
+ # non-root command class hides it (and any subtree) from `tools/list`
547
+ # and refuses `tools/call` the same way an unknown name would; the CLI
548
+ # itself is unaffected.
549
+ _mcp_ = False
550
+
542
551
  format: str = "auto"
543
552
  (
544
553
  "Output format. Default 'auto' detects the calling shell. Shell forms: "
@@ -220,10 +220,13 @@ def test_moved_duho_internals_are_reported_not_silent(monkeypatch, tmp_path, cap
220
220
  real_import = builtins.__import__
221
221
 
222
222
  def renamed(name, globals=None, locals=None, fromlist=(), level=0):
223
- # What a duho release that renamed its private helpers looks like to
224
- # `from duho.discovery import _import_from_path, ...` -- and only to it.
225
- if name == "duho.discovery" and "_import_from_path" in (fromlist or ()):
226
- raise ImportError("cannot import name '_import_from_path'")
223
+ # What a duho release that renamed its remaining private helper looks
224
+ # like to `from duho.discovery import _commands_in_module, ...` --
225
+ # and only to it. `import_from_path` is duho's public counterpart
226
+ # (duho >= 0.6.0) and stays importable; `_commands_in_module` has no
227
+ # public counterpart yet, so it is the one still exposed to this risk.
228
+ if name == "duho.discovery" and "_commands_in_module" in (fromlist or ()):
229
+ raise ImportError("cannot import name '_commands_in_module'")
227
230
  return real_import(name, globals, locals, fromlist, level)
228
231
 
229
232
  monkeypatch.setattr(builtins, "__import__", renamed)
@@ -191,3 +191,38 @@ def test_status_match_does_not_touch_a_foreign_hook():
191
191
  commands = [h["command"] for e in merged for h in e["hooks"]]
192
192
  assert "mine" in commands
193
193
  assert CMD in commands
194
+
195
+
196
+ # The exact commands 0.5.x wrote. CwdChanged never named dotagents, and the
197
+ # Codex loader names only its script, so recognising a legacy hook by
198
+ # "dotagents" in its command left them in place beside the new ones.
199
+ _CWD_051 = (
200
+ 'if [ -n "$CLAUDE_ENV_FILE" ] && [ -d .agents ]; then printf "export '
201
+ "AGENTS_PROJECT_ROOT='%s'\\n\" \"$(pwd -W 2>/dev/null || pwd)\" >> \"$CLAUDE_ENV_FILE\"; "
202
+ 'fi; [ -f AGENTS.md ] && cat AGENTS.md || true'
203
+ )
204
+ _CWD_051_PS = (
205
+ "if (-not (Get-Command bash -ErrorAction SilentlyContinue)) { if (Test-Path AGENTS.md) "
206
+ "{ Get-Content AGENTS.md -Raw } }"
207
+ )
208
+ _CODEX_051 = 'python3 "/home/u/.codex/hooks/pretooluse_codex_env.py"'
209
+
210
+
211
+ @pytest.mark.parametrize("old, label", [
212
+ (_CWD_051, "Checking for AGENTS.md"),
213
+ (_CWD_051_PS, "Checking for AGENTS.md (PowerShell)"),
214
+ (_CODEX_051, "Loading agent env"),
215
+ ])
216
+ def test_every_hook_an_earlier_release_wrote_converges(old, label):
217
+ legacy = {"hooks": [{"type": "command", "command": old, "statusMessage": label}]}
218
+ merged, changed = _hooks.merge_hook([legacy], "NEW", status_message=label)
219
+ assert changed and len(merged) == 1, merged
220
+ assert merged[0]["hooks"][0]["command"] == "NEW"
221
+ assert merged[0]["hooks"][0]["statusMessage"] == "dotagents: " + label
222
+
223
+
224
+ def test_a_foreign_hook_with_our_bare_label_and_unrelated_command_stays():
225
+ foreign = {"hooks": [{"type": "command", "command": "notify-send hi",
226
+ "statusMessage": "Checking for AGENTS.md"}]}
227
+ merged, _ = _hooks.merge_hook([foreign], "NEW", status_message="Checking for AGENTS.md")
228
+ assert merged[0] == foreign and len(merged) == 2
@@ -0,0 +1,60 @@
1
+ """`env`'s MCP opt-out (duho >= 0.6.0).
2
+
3
+ `env` prints resolved, possibly secret values (its own docstring's "Output is
4
+ sensitive" note) -- exactly what an MCP client should never be handed as a
5
+ callable tool. It is a compiled, module-level command CLASS (`Env`, one of
6
+ `dotagents.cli._BUILTIN_COMMANDS`, dispatched via `duho.app`'s `commands=`),
7
+ not a discovered `.py` module command, so the opt-out is the class-attribute
8
+ form: `_mcp_ = False` set directly on `Env` (`dotagents/cli/env.py`) -- NOT a
9
+ module-level `_mcp_ = False` in a `cmds/` file, which is the form for a
10
+ discovered module command instead.
11
+
12
+ `test_env_command_is_hidden_from_mcp_tools` drives a REAL
13
+ `DOTAGENTS_MCP=stdio` session (the env-var trigger duho.app checks before
14
+ parsing argv) in a subprocess, under the suite's autouse isolated
15
+ HOME/AGENTS_* (tests/conftest.py) -- never the real `~/.agents`. It asserts
16
+ `dotagents.env` is absent from `tools/list` while `dotagents.about` (an
17
+ ordinary built-in) is still present, then that `env` still works as a normal
18
+ CLI command -- the opt-out only removes it from the MCP surface.
19
+ """
20
+
21
+ import json
22
+ import os
23
+ import subprocess
24
+ import sys
25
+
26
+ from dotagents.cli.env import Env
27
+
28
+
29
+ def test_env_command_is_hidden_from_mcp_tools():
30
+ request = json.dumps({"jsonrpc": "2.0", "id": 1, "method": "tools/list"})
31
+ env = dict(os.environ)
32
+ env["DOTAGENTS_MCP"] = "stdio"
33
+
34
+ proc = subprocess.run(
35
+ [sys.executable, "-m", "dotagents"],
36
+ input=request + "\n",
37
+ capture_output=True,
38
+ text=True,
39
+ env=env,
40
+ timeout=30,
41
+ )
42
+ assert proc.returncode == 0, proc.stderr
43
+
44
+ lines = [line for line in proc.stdout.splitlines() if line.strip()]
45
+ assert len(lines) == 1, proc.stdout
46
+ response = json.loads(lines[0])
47
+ names = {tool["name"] for tool in response["result"]["tools"]}
48
+
49
+ assert "dotagents.about" in names, names
50
+ assert "dotagents.env" not in names, names
51
+ assert not any(name.startswith("dotagents.env.") for name in names), names
52
+
53
+
54
+ def test_env_still_works_on_the_cli(capsys):
55
+ """The MCP opt-out is MCP-only: `env` dispatches normally on the CLI."""
56
+ command = Env()
57
+ command.format = "json"
58
+
59
+ assert command() == 0
60
+ assert isinstance(json.loads(capsys.readouterr().out), dict)
@@ -76,7 +76,7 @@ has_re "$h" "$(flag --agents)"
76
76
  # modules, findings and launch. Matched on the command-list line, since
77
77
  # `overlays`' own description mentions its `sync` subcommand.
78
78
  h=$(pyz --help)
79
- has "$h" "{init,build-pyz,context,env,path,overlays,about,findings,launch}"
79
+ has "$h" "{about,build-pyz,context,env,findings,init,launch,overlays,path}"
80
80
  lacks "$h" "link-project"
81
81
  lacks "$h" "sync-project"
82
82
  lacks "$h" "audit"
File without changes
File without changes
File without changes
File without changes
File without changes