cli-to-tools 0.2.1__tar.gz → 0.2.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 (30) hide show
  1. {cli_to_tools-0.2.1 → cli_to_tools-0.2.2}/PKG-INFO +9 -2
  2. {cli_to_tools-0.2.1 → cli_to_tools-0.2.2}/README.md +8 -1
  3. {cli_to_tools-0.2.1 → cli_to_tools-0.2.2}/pyproject.toml +1 -1
  4. {cli_to_tools-0.2.1 → cli_to_tools-0.2.2}/src/cli_to_tools/_effects.py +33 -5
  5. {cli_to_tools-0.2.1 → cli_to_tools-0.2.2}/src/cli_to_tools.egg-info/PKG-INFO +9 -2
  6. {cli_to_tools-0.2.1 → cli_to_tools-0.2.2}/tests/test_effects.py +24 -2
  7. {cli_to_tools-0.2.1 → cli_to_tools-0.2.2}/setup.cfg +0 -0
  8. {cli_to_tools-0.2.1 → cli_to_tools-0.2.2}/src/cli_to_tools/__init__.py +0 -0
  9. {cli_to_tools-0.2.1 → cli_to_tools-0.2.2}/src/cli_to_tools/_model.py +0 -0
  10. {cli_to_tools-0.2.1 → cli_to_tools-0.2.2}/src/cli_to_tools/_shell.py +0 -0
  11. {cli_to_tools-0.2.1 → cli_to_tools-0.2.2}/src/cli_to_tools/_spec.py +0 -0
  12. {cli_to_tools-0.2.1 → cli_to_tools-0.2.2}/src/cli_to_tools/_translate.py +0 -0
  13. {cli_to_tools-0.2.1 → cli_to_tools-0.2.2}/src/cli_to_tools/agentltl.py +0 -0
  14. {cli_to_tools-0.2.1 → cli_to_tools-0.2.2}/src/cli_to_tools/packs/cloud.yaml +0 -0
  15. {cli_to_tools-0.2.1 → cli_to_tools-0.2.2}/src/cli_to_tools/packs/docker.yaml +0 -0
  16. {cli_to_tools-0.2.1 → cli_to_tools-0.2.2}/src/cli_to_tools/packs/files.yaml +0 -0
  17. {cli_to_tools-0.2.1 → cli_to_tools-0.2.2}/src/cli_to_tools/packs/git.yaml +0 -0
  18. {cli_to_tools-0.2.1 → cli_to_tools-0.2.2}/src/cli_to_tools/packs/network.yaml +0 -0
  19. {cli_to_tools-0.2.1 → cli_to_tools-0.2.2}/src/cli_to_tools/packs/python.yaml +0 -0
  20. {cli_to_tools-0.2.1 → cli_to_tools-0.2.2}/src/cli_to_tools/packs/secrets.yaml +0 -0
  21. {cli_to_tools-0.2.1 → cli_to_tools-0.2.2}/src/cli_to_tools/packs/shell.yaml +0 -0
  22. {cli_to_tools-0.2.1 → cli_to_tools-0.2.2}/src/cli_to_tools.egg-info/SOURCES.txt +0 -0
  23. {cli_to_tools-0.2.1 → cli_to_tools-0.2.2}/src/cli_to_tools.egg-info/dependency_links.txt +0 -0
  24. {cli_to_tools-0.2.1 → cli_to_tools-0.2.2}/src/cli_to_tools.egg-info/requires.txt +0 -0
  25. {cli_to_tools-0.2.1 → cli_to_tools-0.2.2}/src/cli_to_tools.egg-info/top_level.txt +0 -0
  26. {cli_to_tools-0.2.1 → cli_to_tools-0.2.2}/tests/test_agentltl.py +0 -0
  27. {cli_to_tools-0.2.1 → cli_to_tools-0.2.2}/tests/test_secrets.py +0 -0
  28. {cli_to_tools-0.2.1 → cli_to_tools-0.2.2}/tests/test_shell.py +0 -0
  29. {cli_to_tools-0.2.1 → cli_to_tools-0.2.2}/tests/test_translate.py +0 -0
  30. {cli_to_tools-0.2.1 → cli_to_tools-0.2.2}/tests/test_writes.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cli-to-tools
3
- Version: 0.2.1
3
+ Version: 0.2.2
4
4
  Summary: Translate shell command lines into ordered, structured tool calls.
5
5
  License: MIT
6
6
  Project-URL: Homepage, https://agentltl.github.io/shell/
@@ -46,11 +46,18 @@ The library only translates. Executing the command line stays with the caller.
46
46
  Paste this into your coding agent (Claude Code, Codex, Copilot CLI, Mistral Vibe...):
47
47
 
48
48
  ```
49
- Install cli-to-tools from https://github.com/AgentLTL/cli-to-tools in a virtual environment (pip install "git+https://github.com/AgentLTL/cli-to-tools.git"), then use cli_to_tools.Translator to translate a shell command line of mine into tool calls and show me the result.
49
+ Install cli-to-tools (pip install cli-to-tools, in a virtual environment), then use cli_to_tools.Translator to translate a shell command line of mine into tool calls and show me the result.
50
50
  ```
51
51
 
52
52
  Or install manually:
53
53
 
54
+ ```bash
55
+ pip install cli-to-tools
56
+ pip install "cli-to-tools[agentltl]" # with AgentLTL, for cli_to_tools.agentltl
57
+ ```
58
+
59
+ To work on the library itself, from a clone:
60
+
54
61
  ```bash
55
62
  pip install -e ".[dev]"
56
63
  pip install -e /path/to/AgentLTL # only for cli_to_tools.agentltl
@@ -27,11 +27,18 @@ The library only translates. Executing the command line stays with the caller.
27
27
  Paste this into your coding agent (Claude Code, Codex, Copilot CLI, Mistral Vibe...):
28
28
 
29
29
  ```
30
- Install cli-to-tools from https://github.com/AgentLTL/cli-to-tools in a virtual environment (pip install "git+https://github.com/AgentLTL/cli-to-tools.git"), then use cli_to_tools.Translator to translate a shell command line of mine into tool calls and show me the result.
30
+ Install cli-to-tools (pip install cli-to-tools, in a virtual environment), then use cli_to_tools.Translator to translate a shell command line of mine into tool calls and show me the result.
31
31
  ```
32
32
 
33
33
  Or install manually:
34
34
 
35
+ ```bash
36
+ pip install cli-to-tools
37
+ pip install "cli-to-tools[agentltl]" # with AgentLTL, for cli_to_tools.agentltl
38
+ ```
39
+
40
+ To work on the library itself, from a clone:
41
+
35
42
  ```bash
36
43
  pip install -e ".[dev]"
37
44
  pip install -e /path/to/AgentLTL # only for cli_to_tools.agentltl
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "cli-to-tools"
7
- version = "0.2.1"
7
+ version = "0.2.2"
8
8
  description = "Translate shell command lines into ordered, structured tool calls."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -35,6 +35,16 @@ UNKNOWN_PATHS = "unknown_paths"
35
35
  _DEVICES = ("/dev/null", "/dev/stdout", "/dev/stderr", "/dev/tty")
36
36
 
37
37
 
38
+ def posix(path: str) -> str:
39
+ """*path* with ``/`` separators on every platform (a no-op off Windows).
40
+
41
+ Rules write path globs with ``/`` (``**/.env``, ``src/*.py``), and two calls name the
42
+ same file the same way, so every path this package reports uses ``/``; Python opens
43
+ ``C:/proj/a.txt`` on Windows just as well as ``C:\\proj\\a.txt``.
44
+ """
45
+ return path.replace(os.sep, "/") if os.sep != "/" else path
46
+
47
+
38
48
  @dataclass(frozen=True)
39
49
  class Paths:
40
50
  """Where relative paths are resolved from (``cwd``), and the project root, if any."""
@@ -42,10 +52,28 @@ class Paths:
42
52
  cwd: str = ""
43
53
  root: str = ""
44
54
 
55
+ def __post_init__(self) -> None:
56
+ object.__setattr__(self, "cwd", posix(self.cwd))
57
+ object.__setattr__(self, "root", posix(self.root))
58
+
59
+
60
+ WINDOWS = os.name == "nt"
61
+
62
+
63
+ def is_absolute(path: str) -> bool:
64
+ """A rooted path: ``/x``, ``~``, or on Windows ``C:\\x``, ``C:/x`` and ``\\x``.
65
+
66
+ Off Windows ``\\x`` and ``C:\\x`` are ordinary relative file names."""
67
+ if path.startswith(("/", "~")):
68
+ return True
69
+ return WINDOWS and (path.startswith("\\") or (len(path) > 2 and path[1] == ":"
70
+ and path[2] in "/\\" and path[0].isalpha()))
71
+
45
72
 
46
73
  def is_device(target: str) -> bool:
47
74
  """Writing to these discards or shows the output; it overwrites no file."""
48
- return target in _DEVICES or target.startswith("/dev/fd/")
75
+ return (target in _DEVICES or target.startswith("/dev/fd/")
76
+ or (WINDOWS and target.upper() == "NUL"))
49
77
 
50
78
 
51
79
  def redirect_files(redirects: Sequence[Dict[str, str]]) -> Tuple[List[str], List[str], List[str]]:
@@ -69,7 +97,7 @@ def redirect_files(redirects: Sequence[Dict[str, str]]) -> Tuple[List[str], List
69
97
 
70
98
 
71
99
  def absolute(value: str, paths: Paths) -> str:
72
- return os.path.normpath(os.path.join(paths.cwd or os.getcwd(), os.path.expanduser(value)))
100
+ return posix(os.path.normpath(os.path.join(paths.cwd or os.getcwd(), os.path.expanduser(value))))
73
101
 
74
102
 
75
103
  def canonical(value: str, paths: Paths) -> str:
@@ -114,7 +142,7 @@ def relative_path_keys(args: Dict[str, Any], keys: Iterable[str]) -> List[str]:
114
142
  """The path arguments among *keys* holding a relative path."""
115
143
  out = []
116
144
  for key in keys:
117
- if any(v and not v.startswith(("/", "~", "$", "-")) and "://" not in v
145
+ if any(v and not is_absolute(v) and not v.startswith(("$", "-")) and "://" not in v
118
146
  for v in strings(args.get(key))):
119
147
  out.append(key)
120
148
  return out
@@ -138,8 +166,8 @@ def cd_target(name: str, args: Dict[str, Any], here: Optional[str]) -> Optional[
138
166
  return None
139
167
  target = os.path.expanduser(target)
140
168
  if os.path.isabs(target):
141
- return os.path.normpath(target)
142
- return None if here is None else os.path.normpath(os.path.join(here, target))
169
+ return posix(os.path.normpath(target))
170
+ return None if here is None else posix(os.path.normpath(os.path.join(here, target)))
143
171
 
144
172
 
145
173
  def patch_targets(name: str, args: Dict[str, Any], paths: Paths) -> None:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cli-to-tools
3
- Version: 0.2.1
3
+ Version: 0.2.2
4
4
  Summary: Translate shell command lines into ordered, structured tool calls.
5
5
  License: MIT
6
6
  Project-URL: Homepage, https://agentltl.github.io/shell/
@@ -46,11 +46,18 @@ The library only translates. Executing the command line stays with the caller.
46
46
  Paste this into your coding agent (Claude Code, Codex, Copilot CLI, Mistral Vibe...):
47
47
 
48
48
  ```
49
- Install cli-to-tools from https://github.com/AgentLTL/cli-to-tools in a virtual environment (pip install "git+https://github.com/AgentLTL/cli-to-tools.git"), then use cli_to_tools.Translator to translate a shell command line of mine into tool calls and show me the result.
49
+ Install cli-to-tools (pip install cli-to-tools, in a virtual environment), then use cli_to_tools.Translator to translate a shell command line of mine into tool calls and show me the result.
50
50
  ```
51
51
 
52
52
  Or install manually:
53
53
 
54
+ ```bash
55
+ pip install cli-to-tools
56
+ pip install "cli-to-tools[agentltl]" # with AgentLTL, for cli_to_tools.agentltl
57
+ ```
58
+
59
+ To work on the library itself, from a clone:
60
+
54
61
  ```bash
55
62
  pip install -e ".[dev]"
56
63
  pip install -e /path/to/AgentLTL # only for cli_to_tools.agentltl
@@ -1,5 +1,7 @@
1
1
  """Translator(shell_effects=True): redirections, paths after cd, run-time-only files."""
2
2
 
3
+ import os
4
+
3
5
  import pytest
4
6
 
5
7
  from cli_to_tools import Paths, SpecRegistry, Translator, UNKNOWN_PATHS
@@ -34,6 +36,25 @@ def test_redirections(registry):
34
36
  assert a["redirect_to"] == ["/dev/null"] and "overwrite_to" not in a
35
37
 
36
38
 
39
+ def test_windows_path_forms_are_rooted_and_nul_is_a_device_only_on_windows(monkeypatch):
40
+ from cli_to_tools import _effects
41
+ monkeypatch.setattr(_effects, "WINDOWS", True)
42
+ assert all(_effects.is_absolute(p) for p in ("C:\\foo", "c:/foo", "\\foo", "/foo", "~"))
43
+ assert not _effects.is_absolute("foo") and not _effects.is_absolute("C:foo")
44
+ assert _effects.is_device("NUL") and _effects.is_device("nul")
45
+
46
+ monkeypatch.setattr(_effects, "WINDOWS", False) # POSIX: these are ordinary file names
47
+ assert not _effects.is_absolute("C:\\foo") and not _effects.is_absolute("\\foo")
48
+ assert _effects.is_absolute("/foo") and _effects.is_absolute("~")
49
+ assert not _effects.is_device("NUL") and _effects.is_device("/dev/null")
50
+
51
+
52
+ @pytest.mark.skipif(os.name == "nt", reason="paths are resolved the Windows way there")
53
+ def test_on_posix_a_backslash_name_is_relative_and_NUL_is_a_file(registry):
54
+ assert args("cd $D; rm '\\foo'", registry)[UNKNOWN_PATHS] == ["paths"]
55
+ assert args("echo x > NUL", registry)["overwrite_to"] == ["/proj/NUL"]
56
+
57
+
37
58
  def test_cd_moves_where_relative_paths_point(registry):
38
59
  assert args("cd secrets && rm key", registry)["paths"] == ["/proj/secrets/key"]
39
60
  assert args("cd secrets; cd ..; rm key", registry)["paths"] == ["/proj/key"]
@@ -62,8 +83,9 @@ def test_files_only_known_at_run_time(registry):
62
83
 
63
84
  def test_resolved_variables_and_home(registry, tmp_path, monkeypatch):
64
85
  monkeypatch.setenv("HOME", str(tmp_path))
86
+ monkeypatch.setenv("USERPROFILE", str(tmp_path)) # what `~` expands to on Windows
65
87
  assert args("D=secrets; rm $D/key", registry)["paths"] == ["/proj/secrets/key"]
66
- assert args("rm ~/notes", registry)["paths"] == [str(tmp_path / "notes")]
88
+ assert args("rm ~/notes", registry)["paths"] == [(tmp_path / "notes").as_posix()]
67
89
 
68
90
 
69
91
  def test_patch_targets_come_from_the_diff(registry, tmp_path):
@@ -71,7 +93,7 @@ def test_patch_targets_come_from_the_diff(registry, tmp_path):
71
93
  "diff --git a/migrations/001.sql b/migrations/001.sql\n"
72
94
  "--- a/migrations/001.sql\n+++ b/migrations/001.sql\n@@ -1 +1 @@\n-x\n+y\n")
73
95
  assert args("git apply fix.diff", registry, str(tmp_path))["paths"] == [
74
- str(tmp_path / "migrations/001.sql")]
96
+ (tmp_path / "migrations/001.sql").as_posix()]
75
97
  assert args("git apply missing.diff", registry, str(tmp_path))[UNKNOWN_PATHS] is True
76
98
 
77
99
 
File without changes