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.
- {cli_to_tools-0.2.1 → cli_to_tools-0.2.2}/PKG-INFO +9 -2
- {cli_to_tools-0.2.1 → cli_to_tools-0.2.2}/README.md +8 -1
- {cli_to_tools-0.2.1 → cli_to_tools-0.2.2}/pyproject.toml +1 -1
- {cli_to_tools-0.2.1 → cli_to_tools-0.2.2}/src/cli_to_tools/_effects.py +33 -5
- {cli_to_tools-0.2.1 → cli_to_tools-0.2.2}/src/cli_to_tools.egg-info/PKG-INFO +9 -2
- {cli_to_tools-0.2.1 → cli_to_tools-0.2.2}/tests/test_effects.py +24 -2
- {cli_to_tools-0.2.1 → cli_to_tools-0.2.2}/setup.cfg +0 -0
- {cli_to_tools-0.2.1 → cli_to_tools-0.2.2}/src/cli_to_tools/__init__.py +0 -0
- {cli_to_tools-0.2.1 → cli_to_tools-0.2.2}/src/cli_to_tools/_model.py +0 -0
- {cli_to_tools-0.2.1 → cli_to_tools-0.2.2}/src/cli_to_tools/_shell.py +0 -0
- {cli_to_tools-0.2.1 → cli_to_tools-0.2.2}/src/cli_to_tools/_spec.py +0 -0
- {cli_to_tools-0.2.1 → cli_to_tools-0.2.2}/src/cli_to_tools/_translate.py +0 -0
- {cli_to_tools-0.2.1 → cli_to_tools-0.2.2}/src/cli_to_tools/agentltl.py +0 -0
- {cli_to_tools-0.2.1 → cli_to_tools-0.2.2}/src/cli_to_tools/packs/cloud.yaml +0 -0
- {cli_to_tools-0.2.1 → cli_to_tools-0.2.2}/src/cli_to_tools/packs/docker.yaml +0 -0
- {cli_to_tools-0.2.1 → cli_to_tools-0.2.2}/src/cli_to_tools/packs/files.yaml +0 -0
- {cli_to_tools-0.2.1 → cli_to_tools-0.2.2}/src/cli_to_tools/packs/git.yaml +0 -0
- {cli_to_tools-0.2.1 → cli_to_tools-0.2.2}/src/cli_to_tools/packs/network.yaml +0 -0
- {cli_to_tools-0.2.1 → cli_to_tools-0.2.2}/src/cli_to_tools/packs/python.yaml +0 -0
- {cli_to_tools-0.2.1 → cli_to_tools-0.2.2}/src/cli_to_tools/packs/secrets.yaml +0 -0
- {cli_to_tools-0.2.1 → cli_to_tools-0.2.2}/src/cli_to_tools/packs/shell.yaml +0 -0
- {cli_to_tools-0.2.1 → cli_to_tools-0.2.2}/src/cli_to_tools.egg-info/SOURCES.txt +0 -0
- {cli_to_tools-0.2.1 → cli_to_tools-0.2.2}/src/cli_to_tools.egg-info/dependency_links.txt +0 -0
- {cli_to_tools-0.2.1 → cli_to_tools-0.2.2}/src/cli_to_tools.egg-info/requires.txt +0 -0
- {cli_to_tools-0.2.1 → cli_to_tools-0.2.2}/src/cli_to_tools.egg-info/top_level.txt +0 -0
- {cli_to_tools-0.2.1 → cli_to_tools-0.2.2}/tests/test_agentltl.py +0 -0
- {cli_to_tools-0.2.1 → cli_to_tools-0.2.2}/tests/test_secrets.py +0 -0
- {cli_to_tools-0.2.1 → cli_to_tools-0.2.2}/tests/test_shell.py +0 -0
- {cli_to_tools-0.2.1 → cli_to_tools-0.2.2}/tests/test_translate.py +0 -0
- {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.
|
|
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
|
|
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
|
|
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
|
|
@@ -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(("
|
|
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.
|
|
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
|
|
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"] == [
|
|
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
|
-
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|