oterminus 0.1.1__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 (58) hide show
  1. oterminus-0.1.1/PKG-INFO +187 -0
  2. oterminus-0.1.1/README.md +161 -0
  3. oterminus-0.1.1/pyproject.toml +71 -0
  4. oterminus-0.1.1/src/oterminus/__init__.py +4 -0
  5. oterminus-0.1.1/src/oterminus/ambiguity.py +153 -0
  6. oterminus-0.1.1/src/oterminus/audit.py +103 -0
  7. oterminus-0.1.1/src/oterminus/audit_privacy.py +132 -0
  8. oterminus-0.1.1/src/oterminus/cli.py +1056 -0
  9. oterminus-0.1.1/src/oterminus/command_registry.py +43 -0
  10. oterminus-0.1.1/src/oterminus/commands/__init__.py +67 -0
  11. oterminus-0.1.1/src/oterminus/commands/archive.py +88 -0
  12. oterminus-0.1.1/src/oterminus/commands/dangerous.py +35 -0
  13. oterminus-0.1.1/src/oterminus/commands/filesystem.py +155 -0
  14. oterminus-0.1.1/src/oterminus/commands/git.py +39 -0
  15. oterminus-0.1.1/src/oterminus/commands/macos.py +25 -0
  16. oterminus-0.1.1/src/oterminus/commands/network.py +68 -0
  17. oterminus-0.1.1/src/oterminus/commands/process.py +46 -0
  18. oterminus-0.1.1/src/oterminus/commands/project.py +48 -0
  19. oterminus-0.1.1/src/oterminus/commands/registry.py +518 -0
  20. oterminus-0.1.1/src/oterminus/commands/system.py +75 -0
  21. oterminus-0.1.1/src/oterminus/commands/text.py +84 -0
  22. oterminus-0.1.1/src/oterminus/commands/types.py +128 -0
  23. oterminus-0.1.1/src/oterminus/completion.py +151 -0
  24. oterminus-0.1.1/src/oterminus/config.py +192 -0
  25. oterminus-0.1.1/src/oterminus/direct_commands.py +129 -0
  26. oterminus-0.1.1/src/oterminus/discovery.py +175 -0
  27. oterminus-0.1.1/src/oterminus/doctor.py +480 -0
  28. oterminus-0.1.1/src/oterminus/eval_fixtures/ambiguity.json +54 -0
  29. oterminus-0.1.1/src/oterminus/eval_fixtures/archive_inspection.json +267 -0
  30. oterminus-0.1.1/src/oterminus/eval_fixtures/direct_commands.json +129 -0
  31. oterminus-0.1.1/src/oterminus/eval_fixtures/fast_path_local_planner.json +1 -0
  32. oterminus-0.1.1/src/oterminus/eval_fixtures/filesystem_inspection.json +281 -0
  33. oterminus-0.1.1/src/oterminus/eval_fixtures/filesystem_mutation.json +134 -0
  34. oterminus-0.1.1/src/oterminus/eval_fixtures/git_inspection.json +128 -0
  35. oterminus-0.1.1/src/oterminus/eval_fixtures/macos_desktop.json +89 -0
  36. oterminus-0.1.1/src/oterminus/eval_fixtures/network_diagnostics.json +191 -0
  37. oterminus-0.1.1/src/oterminus/eval_fixtures/planner_normalization.json +78 -0
  38. oterminus-0.1.1/src/oterminus/eval_fixtures/process_inspection.json +61 -0
  39. oterminus-0.1.1/src/oterminus/eval_fixtures/project_health.json +97 -0
  40. oterminus-0.1.1/src/oterminus/eval_fixtures/system_inspection.json +67 -0
  41. oterminus-0.1.1/src/oterminus/eval_fixtures/text_inspection.json +34 -0
  42. oterminus-0.1.1/src/oterminus/eval_fixtures/unsafe_and_blocked.json +154 -0
  43. oterminus-0.1.1/src/oterminus/evals.py +310 -0
  44. oterminus-0.1.1/src/oterminus/executor.py +87 -0
  45. oterminus-0.1.1/src/oterminus/failure_explainer.py +74 -0
  46. oterminus-0.1.1/src/oterminus/history.py +183 -0
  47. oterminus-0.1.1/src/oterminus/local_planner.py +102 -0
  48. oterminus-0.1.1/src/oterminus/logging_utils.py +11 -0
  49. oterminus-0.1.1/src/oterminus/models.py +158 -0
  50. oterminus-0.1.1/src/oterminus/ollama_client.py +80 -0
  51. oterminus-0.1.1/src/oterminus/planner.py +86 -0
  52. oterminus-0.1.1/src/oterminus/policies.py +36 -0
  53. oterminus-0.1.1/src/oterminus/prompts.py +246 -0
  54. oterminus-0.1.1/src/oterminus/renderer.py +128 -0
  55. oterminus-0.1.1/src/oterminus/router.py +640 -0
  56. oterminus-0.1.1/src/oterminus/setup.py +107 -0
  57. oterminus-0.1.1/src/oterminus/structured_commands.py +1908 -0
  58. oterminus-0.1.1/src/oterminus/validator.py +746 -0
@@ -0,0 +1,187 @@
1
+ Metadata-Version: 2.4
2
+ Name: oterminus
3
+ Version: 0.1.1
4
+ Summary: Local AI-powered terminal assistant with explicit confirmation and safety controls
5
+ License: MIT
6
+ Keywords: cli,terminal,assistant,safety,ollama
7
+ Author: OTerminus contributors
8
+ Requires-Python: >=3.13,<4.0
9
+ Classifier: Development Status :: 3 - Alpha
10
+ Classifier: Environment :: Console
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: License :: OSI Approved :: MIT License
13
+ Classifier: Operating System :: OS Independent
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.13
16
+ Classifier: Programming Language :: Python :: 3.14
17
+ Classifier: Topic :: Utilities
18
+ Requires-Dist: ollama (>=0.6.0,<0.7.0)
19
+ Requires-Dist: prompt_toolkit (>=3.0.52,<4.0.0)
20
+ Requires-Dist: pydantic (>=2.9.0,<3.0.0)
21
+ Project-URL: Documentation, https://pooriat.github.io/oterminus/
22
+ Project-URL: Homepage, https://github.com/pooriat/oterminus
23
+ Project-URL: Repository, https://github.com/pooriat/oterminus
24
+ Description-Content-Type: text/markdown
25
+
26
+ # OTerminus
27
+
28
+ OTerminus is a local, safety-first terminal assistant. It turns natural-language requests into a
29
+ **single proposed shell action**, shows a preview, and executes only after explicit confirmation.
30
+
31
+ ## Why OTerminus exists
32
+
33
+ Terminal copilots are useful, but unrestricted shell generation is risky. OTerminus exists to
34
+ provide a practical middle ground:
35
+
36
+ - capability-first command support (curated workflows, not full shell emulation)
37
+ - deterministic rendering for structured command families
38
+ - explicit policy + validation gates before execution
39
+ - confirmation before every execution path
40
+ - local-first observability through JSONL audit logs
41
+
42
+ ## Core safety promise
43
+
44
+ OTerminus is designed around an inspect-and-confirm execution contract:
45
+
46
+ 1. detect direct commands first
47
+ 2. intercept vague natural-language requests as ambiguous when needed
48
+ 3. route specific natural-language requests by capability
49
+ 4. plan proposals in a structured-first format
50
+ 5. validate and policy-check the command
51
+ 6. show a deterministic preview
52
+ 7. require explicit user confirmation before execution
53
+
54
+ Direct shell commands are not blocked by natural-language ambiguity heuristics; they still go through
55
+ validation and policy checks. Ambiguous natural-language requests stop before planning and execution
56
+ and suggest safer read-only inspections. See the [user guide](docs/product/user-guide.md) and
57
+ [request lifecycle](docs/architecture/request-lifecycle.md) for details.
58
+
59
+ If ambiguity handling, validation, or policy checks block a request, OTerminus does not execute.
60
+
61
+ ## Quick install and setup
62
+
63
+ ### Requirements
64
+
65
+ - Python 3.13+
66
+ - [Poetry](https://python-poetry.org/)
67
+ - [Ollama](https://ollama.com/)
68
+
69
+ ### Local development install
70
+
71
+ ```bash
72
+ poetry install
73
+ poetry run oterminus
74
+ ```
75
+
76
+ ### Local package artifact validation
77
+
78
+ Before any publish workflow is introduced, validate local package artifacts end-to-end:
79
+
80
+ ```bash
81
+ poetry run python scripts/validate_package_install.py
82
+ ```
83
+
84
+ This builds both `sdist` and `wheel`, installs the wheel into a temporary clean virtualenv, and runs CLI smoke checks.
85
+
86
+
87
+ On first run, OTerminus checks Ollama readiness (`ollama` on PATH, running service, local models),
88
+ then prompts you to select a model if one is not already configured.
89
+
90
+ ## Quick start examples
91
+
92
+ ### Common commands
93
+
94
+ ```bash
95
+ poetry run oterminus
96
+ poetry run oterminus "show disk usage for this folder"
97
+ poetry run oterminus --dry-run "copy notes.txt to backup/notes.txt"
98
+ poetry run oterminus --explain "find processes matching python"
99
+ poetry run oterminus doctor
100
+ ```
101
+
102
+ ### Interactive REPL
103
+
104
+ `poetry run oterminus` starts the interactive REPL after startup readiness checks.
105
+
106
+ Examples inside REPL:
107
+
108
+ - `find all .py files`
109
+ - `capabilities` / `commands` / `examples`
110
+ - `help capabilities` / `help filesystem_inspection` / `help ls`
111
+ - `show running processes`
112
+ - `ping example.com 4 times`
113
+ - `show HTTP headers for https://example.com`
114
+ - `look up DNS for example.com`
115
+ - `tar -tf archive.tar` / `unzip -l archive.zip`
116
+ - `tar -xf archive.tar -C restored` / `unzip archive.zip -d restored`
117
+ - `tar -czf backup.tar.gz src` / `zip -r docs.zip docs`
118
+ - `ls -lah`
119
+ - `dry-run search TODO in src`
120
+ - `explain show disk space`
121
+ - `audit status` / `audit tail` / `audit clear`
122
+
123
+ ### One-shot and diagnostics modes
124
+
125
+ - One-shot requests such as `poetry run oterminus "show disk usage for this folder"` plan, validate,
126
+ preview, and then require confirmation before execution.
127
+ - `--dry-run` and `--explain` are mutually exclusive one-shot inspection flags for requests. Both
128
+ validate and preview without confirmation or execution; explain mode also describes command choice,
129
+ relevant flags/arguments, risk, and policy interpretation.
130
+ - `doctor` is diagnostics-only: it prints readiness checks and exits without starting the REPL,
131
+ executing a request, or invoking the Ollama planner. It cannot be combined with `--dry-run` or
132
+ `--explain`.
133
+
134
+ ## Proposal modes
135
+
136
+ OTerminus supports two first-class proposal modes:
137
+
138
+ - **Structured**: the preferred normal path for supported capabilities. Proposals use
139
+ `command_family` + typed `arguments`, and Python renders the final command/argv deterministically.
140
+ - **Experimental**: a constrained fallback for single-command text that cannot yet be represented
141
+ safely as structured arguments. It is still strictly validated, previewed, and confirmed before
142
+ execution.
143
+
144
+ See [structured rendering](docs/architecture/structured-rendering.md), [routing and
145
+ planning](docs/architecture/routing-and-planning.md), and the [request
146
+ lifecycle](docs/architecture/request-lifecycle.md) for details.
147
+
148
+ ## Network diagnostics
149
+
150
+ The `network_diagnostics` capability supports only fixed-count ping, HTTP HEAD (`curl -I`), `dig`,
151
+ and `nslookup`. These commands contact external hosts, show a network metadata warning in preview,
152
+ and still require confirmation. OTerminus does not support POST/PUT/DELETE requests, secret headers,
153
+ downloads, scanning, SSH, or arbitrary network automation.
154
+
155
+ ## Documentation
156
+
157
+ The README is the landing page. Full documentation is generated from [`docs/`](docs/index.md) and
158
+ published to GitHub Pages after merges to `main` (once Pages is enabled in repository settings).
159
+
160
+ - Hosted docs (after enablement): `https://pooriat.github.io/oterminus/`
161
+ - Docs source of truth: [`docs/`](docs/index.md)
162
+ - Architecture overview: [`docs/architecture/overview.md`](docs/architecture/overview.md)
163
+ - Request lifecycle (central flow):
164
+ [`docs/architecture/request-lifecycle.md`](docs/architecture/request-lifecycle.md)
165
+ - User guide: [`docs/product/user-guide.md`](docs/product/user-guide.md)
166
+ - Configuration reference: [`docs/reference/config.md`](docs/reference/config.md)
167
+ - Contributor workflow: [`docs/contributing.md`](docs/contributing.md)
168
+ - Contributor command-family guide:
169
+ [`docs/adding-command-families.md`](docs/adding-command-families.md)
170
+ - Evals docs: [`docs/architecture/evals.md`](docs/architecture/evals.md)
171
+
172
+ ### Work on docs locally
173
+
174
+ ```bash
175
+ poetry install --with dev,docs
176
+ poetry run mkdocs serve
177
+ poetry run mkdocs build --strict
178
+ ```
179
+
180
+ For the full local quality checklist, including Ruff format/lint and pytest commands, see the
181
+ [contributor workflow](docs/contributing.md). When behavior changes, update docs in the same pull
182
+ request.
183
+
184
+ - Optional local persistent REPL history is available via `OTERMINUS_HISTORY_ENABLED=true`; reruns still go through normal validation + confirmation.
185
+
186
+ For a small set of deterministic natural-language requests, OTerminus can skip Ollama by producing a local structured proposal before normal validation and confirmation.
187
+
@@ -0,0 +1,161 @@
1
+ # OTerminus
2
+
3
+ OTerminus is a local, safety-first terminal assistant. It turns natural-language requests into a
4
+ **single proposed shell action**, shows a preview, and executes only after explicit confirmation.
5
+
6
+ ## Why OTerminus exists
7
+
8
+ Terminal copilots are useful, but unrestricted shell generation is risky. OTerminus exists to
9
+ provide a practical middle ground:
10
+
11
+ - capability-first command support (curated workflows, not full shell emulation)
12
+ - deterministic rendering for structured command families
13
+ - explicit policy + validation gates before execution
14
+ - confirmation before every execution path
15
+ - local-first observability through JSONL audit logs
16
+
17
+ ## Core safety promise
18
+
19
+ OTerminus is designed around an inspect-and-confirm execution contract:
20
+
21
+ 1. detect direct commands first
22
+ 2. intercept vague natural-language requests as ambiguous when needed
23
+ 3. route specific natural-language requests by capability
24
+ 4. plan proposals in a structured-first format
25
+ 5. validate and policy-check the command
26
+ 6. show a deterministic preview
27
+ 7. require explicit user confirmation before execution
28
+
29
+ Direct shell commands are not blocked by natural-language ambiguity heuristics; they still go through
30
+ validation and policy checks. Ambiguous natural-language requests stop before planning and execution
31
+ and suggest safer read-only inspections. See the [user guide](docs/product/user-guide.md) and
32
+ [request lifecycle](docs/architecture/request-lifecycle.md) for details.
33
+
34
+ If ambiguity handling, validation, or policy checks block a request, OTerminus does not execute.
35
+
36
+ ## Quick install and setup
37
+
38
+ ### Requirements
39
+
40
+ - Python 3.13+
41
+ - [Poetry](https://python-poetry.org/)
42
+ - [Ollama](https://ollama.com/)
43
+
44
+ ### Local development install
45
+
46
+ ```bash
47
+ poetry install
48
+ poetry run oterminus
49
+ ```
50
+
51
+ ### Local package artifact validation
52
+
53
+ Before any publish workflow is introduced, validate local package artifacts end-to-end:
54
+
55
+ ```bash
56
+ poetry run python scripts/validate_package_install.py
57
+ ```
58
+
59
+ This builds both `sdist` and `wheel`, installs the wheel into a temporary clean virtualenv, and runs CLI smoke checks.
60
+
61
+
62
+ On first run, OTerminus checks Ollama readiness (`ollama` on PATH, running service, local models),
63
+ then prompts you to select a model if one is not already configured.
64
+
65
+ ## Quick start examples
66
+
67
+ ### Common commands
68
+
69
+ ```bash
70
+ poetry run oterminus
71
+ poetry run oterminus "show disk usage for this folder"
72
+ poetry run oterminus --dry-run "copy notes.txt to backup/notes.txt"
73
+ poetry run oterminus --explain "find processes matching python"
74
+ poetry run oterminus doctor
75
+ ```
76
+
77
+ ### Interactive REPL
78
+
79
+ `poetry run oterminus` starts the interactive REPL after startup readiness checks.
80
+
81
+ Examples inside REPL:
82
+
83
+ - `find all .py files`
84
+ - `capabilities` / `commands` / `examples`
85
+ - `help capabilities` / `help filesystem_inspection` / `help ls`
86
+ - `show running processes`
87
+ - `ping example.com 4 times`
88
+ - `show HTTP headers for https://example.com`
89
+ - `look up DNS for example.com`
90
+ - `tar -tf archive.tar` / `unzip -l archive.zip`
91
+ - `tar -xf archive.tar -C restored` / `unzip archive.zip -d restored`
92
+ - `tar -czf backup.tar.gz src` / `zip -r docs.zip docs`
93
+ - `ls -lah`
94
+ - `dry-run search TODO in src`
95
+ - `explain show disk space`
96
+ - `audit status` / `audit tail` / `audit clear`
97
+
98
+ ### One-shot and diagnostics modes
99
+
100
+ - One-shot requests such as `poetry run oterminus "show disk usage for this folder"` plan, validate,
101
+ preview, and then require confirmation before execution.
102
+ - `--dry-run` and `--explain` are mutually exclusive one-shot inspection flags for requests. Both
103
+ validate and preview without confirmation or execution; explain mode also describes command choice,
104
+ relevant flags/arguments, risk, and policy interpretation.
105
+ - `doctor` is diagnostics-only: it prints readiness checks and exits without starting the REPL,
106
+ executing a request, or invoking the Ollama planner. It cannot be combined with `--dry-run` or
107
+ `--explain`.
108
+
109
+ ## Proposal modes
110
+
111
+ OTerminus supports two first-class proposal modes:
112
+
113
+ - **Structured**: the preferred normal path for supported capabilities. Proposals use
114
+ `command_family` + typed `arguments`, and Python renders the final command/argv deterministically.
115
+ - **Experimental**: a constrained fallback for single-command text that cannot yet be represented
116
+ safely as structured arguments. It is still strictly validated, previewed, and confirmed before
117
+ execution.
118
+
119
+ See [structured rendering](docs/architecture/structured-rendering.md), [routing and
120
+ planning](docs/architecture/routing-and-planning.md), and the [request
121
+ lifecycle](docs/architecture/request-lifecycle.md) for details.
122
+
123
+ ## Network diagnostics
124
+
125
+ The `network_diagnostics` capability supports only fixed-count ping, HTTP HEAD (`curl -I`), `dig`,
126
+ and `nslookup`. These commands contact external hosts, show a network metadata warning in preview,
127
+ and still require confirmation. OTerminus does not support POST/PUT/DELETE requests, secret headers,
128
+ downloads, scanning, SSH, or arbitrary network automation.
129
+
130
+ ## Documentation
131
+
132
+ The README is the landing page. Full documentation is generated from [`docs/`](docs/index.md) and
133
+ published to GitHub Pages after merges to `main` (once Pages is enabled in repository settings).
134
+
135
+ - Hosted docs (after enablement): `https://pooriat.github.io/oterminus/`
136
+ - Docs source of truth: [`docs/`](docs/index.md)
137
+ - Architecture overview: [`docs/architecture/overview.md`](docs/architecture/overview.md)
138
+ - Request lifecycle (central flow):
139
+ [`docs/architecture/request-lifecycle.md`](docs/architecture/request-lifecycle.md)
140
+ - User guide: [`docs/product/user-guide.md`](docs/product/user-guide.md)
141
+ - Configuration reference: [`docs/reference/config.md`](docs/reference/config.md)
142
+ - Contributor workflow: [`docs/contributing.md`](docs/contributing.md)
143
+ - Contributor command-family guide:
144
+ [`docs/adding-command-families.md`](docs/adding-command-families.md)
145
+ - Evals docs: [`docs/architecture/evals.md`](docs/architecture/evals.md)
146
+
147
+ ### Work on docs locally
148
+
149
+ ```bash
150
+ poetry install --with dev,docs
151
+ poetry run mkdocs serve
152
+ poetry run mkdocs build --strict
153
+ ```
154
+
155
+ For the full local quality checklist, including Ruff format/lint and pytest commands, see the
156
+ [contributor workflow](docs/contributing.md). When behavior changes, update docs in the same pull
157
+ request.
158
+
159
+ - Optional local persistent REPL history is available via `OTERMINUS_HISTORY_ENABLED=true`; reruns still go through normal validation + confirmation.
160
+
161
+ For a small set of deterministic natural-language requests, OTerminus can skip Ollama by producing a local structured proposal before normal validation and confirmation.
@@ -0,0 +1,71 @@
1
+ [tool.poetry]
2
+ name = "oterminus"
3
+ version = "0.1.1"
4
+ description = "Local AI-powered terminal assistant with explicit confirmation and safety controls"
5
+ authors = ["OTerminus contributors"]
6
+ license = "MIT"
7
+ readme = "README.md"
8
+ homepage = "https://github.com/pooriat/oterminus"
9
+ repository = "https://github.com/pooriat/oterminus"
10
+ documentation = "https://pooriat.github.io/oterminus/"
11
+ keywords = ["cli", "terminal", "assistant", "safety", "ollama"]
12
+ classifiers = [
13
+ "Development Status :: 3 - Alpha",
14
+ "Environment :: Console",
15
+ "Intended Audience :: Developers",
16
+ "License :: OSI Approved :: MIT License",
17
+ "Operating System :: OS Independent",
18
+ "Programming Language :: Python :: 3",
19
+ "Programming Language :: Python :: 3.13",
20
+ "Topic :: Utilities",
21
+ ]
22
+ include = [
23
+ "README.md",
24
+ { path = "src/oterminus/eval_fixtures/*.json", format = ["sdist", "wheel"] },
25
+ ]
26
+ exclude = [
27
+ "site/**",
28
+ ".github/**",
29
+ ".venv/**",
30
+ ]
31
+ packages = [
32
+ { include = "oterminus", from = "src", format = ["sdist", "wheel"] },
33
+ ]
34
+
35
+ [tool.poetry.dependencies]
36
+ python = "^3.13"
37
+ ollama = "^0.6.0"
38
+ pydantic = "^2.9.0"
39
+ prompt_toolkit = "^3.0.52"
40
+
41
+ [tool.poetry.group.dev.dependencies]
42
+ pytest = "^8.3.0"
43
+ pytest-cov = "^5.0.0"
44
+ ruff = "^0.11.13"
45
+
46
+
47
+ [tool.poetry.group.docs.dependencies]
48
+ mkdocs = "^1.6.1"
49
+ mkdocs-material = "^9.5.25"
50
+
51
+ [tool.poetry.scripts]
52
+ oterminus = "oterminus.cli:main"
53
+ oterminus-evals = "oterminus.evals:main"
54
+
55
+ [build-system]
56
+ requires = ["poetry-core>=1.8.0"]
57
+ build-backend = "poetry.core.masonry.api"
58
+
59
+ [tool.pytest.ini_options]
60
+ pythonpath = ["src"]
61
+ addopts = "-q"
62
+
63
+ [tool.ruff]
64
+ line-length = 100
65
+ target-version = "py313"
66
+
67
+ [tool.ruff.lint]
68
+ select = ["F"]
69
+
70
+ [tool.ruff.lint.per-file-ignores]
71
+ "tests/*" = ["F401"]
@@ -0,0 +1,4 @@
1
+ """oterminus package."""
2
+
3
+ __all__ = ["__version__"]
4
+ __version__ = "0.1.0"
@@ -0,0 +1,153 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ import re
5
+
6
+
7
+ _SAFE_INSPECTION_OPTIONS: tuple[str, ...] = (
8
+ "list large files",
9
+ "list recently modified files",
10
+ "inspect permissions",
11
+ "show temporary-looking files",
12
+ "show project files",
13
+ )
14
+
15
+ _AMBIGUOUS_PHRASES: tuple[str, ...] = (
16
+ "clean this folder",
17
+ "fix this",
18
+ "remove junk",
19
+ "make everything executable",
20
+ "delete unnecessary files",
21
+ "organize this directory",
22
+ "repair permissions",
23
+ "make this project work",
24
+ "extract this",
25
+ "archive everything",
26
+ "backup this project",
27
+ "compress my files",
28
+ "zip this",
29
+ )
30
+
31
+ _BROAD_MUTATION_VERBS: tuple[str, ...] = (
32
+ "clean",
33
+ "fix",
34
+ "remove",
35
+ "delete",
36
+ "organize",
37
+ "repair",
38
+ "optimize",
39
+ "make",
40
+ )
41
+
42
+ _VAGUE_OBJECT_HINTS: tuple[str, ...] = (
43
+ "this",
44
+ "that",
45
+ "everything",
46
+ "junk",
47
+ "unnecessary",
48
+ "folder",
49
+ "directory",
50
+ "project",
51
+ "permissions",
52
+ "files",
53
+ )
54
+
55
+
56
+ @dataclass(frozen=True, slots=True)
57
+ class AmbiguityResult:
58
+ is_ambiguous: bool
59
+ reason: str
60
+ suggested_safe_options: tuple[str, ...]
61
+ follow_up_questions: tuple[str, ...] = ()
62
+
63
+
64
+ _NOT_AMBIGUOUS = AmbiguityResult(
65
+ is_ambiguous=False,
66
+ reason="Request is specific enough for planning.",
67
+ suggested_safe_options=(),
68
+ )
69
+
70
+
71
+ def detect_ambiguity(user_input: str) -> AmbiguityResult:
72
+ text = user_input.strip().lower()
73
+ if not text:
74
+ return _NOT_AMBIGUOUS
75
+
76
+ matched_phrase = _match_phrase(text)
77
+ if matched_phrase is not None:
78
+ return AmbiguityResult(
79
+ is_ambiguous=True,
80
+ reason=f"Matched ambiguous phrase: '{matched_phrase}'.",
81
+ suggested_safe_options=_SAFE_INSPECTION_OPTIONS,
82
+ follow_up_questions=(
83
+ "What exact folder or file set should I inspect first?",
84
+ "Do you want a read-only inspection report before any changes?",
85
+ ),
86
+ )
87
+
88
+ if _looks_like_archive_extraction_without_destination(text):
89
+ return AmbiguityResult(
90
+ is_ambiguous=True,
91
+ reason="Archive extraction request is missing an explicit destination.",
92
+ suggested_safe_options=("list archive contents", "inspect archive before extracting"),
93
+ follow_up_questions=("Which explicit destination directory should receive the files?",),
94
+ )
95
+
96
+ if _looks_broad_destructive_request(text):
97
+ return AmbiguityResult(
98
+ is_ambiguous=True,
99
+ reason="Request combines broad mutation wording with vague target scope.",
100
+ suggested_safe_options=_SAFE_INSPECTION_OPTIONS,
101
+ follow_up_questions=(
102
+ "Which exact path should be targeted?",
103
+ "What should be considered junk or unnecessary in your context?",
104
+ ),
105
+ )
106
+
107
+ return _NOT_AMBIGUOUS
108
+
109
+
110
+ def _match_phrase(text: str) -> str | None:
111
+ for phrase in _AMBIGUOUS_PHRASES:
112
+ if _matches_hint(text, phrase):
113
+ return phrase
114
+ return None
115
+
116
+
117
+ def _looks_broad_destructive_request(text: str) -> bool:
118
+ tokens = re.findall(r"[a-z0-9_./-]+", text)
119
+ if not tokens:
120
+ return False
121
+
122
+ has_broad_verb = any(_matches_hint(text, verb) for verb in _BROAD_MUTATION_VERBS)
123
+ has_vague_target = any(_matches_hint(text, hint) for hint in _VAGUE_OBJECT_HINTS)
124
+ has_explicit_scope = any(
125
+ token.startswith(("/", "./", "../", "~"))
126
+ or token.endswith((".py", ".md", ".txt", ".log", ".json", ".yaml", ".yml", ".toml"))
127
+ for token in tokens
128
+ )
129
+ return has_broad_verb and has_vague_target and not has_explicit_scope
130
+
131
+
132
+ def _looks_like_archive_extraction_without_destination(text: str) -> bool:
133
+ has_archive_action = any(
134
+ _matches_hint(text, hint) for hint in ("extract", "unpack", "unzip", "restore")
135
+ )
136
+ has_archive_target = any(
137
+ _matches_hint(text, hint) or re.search(rf"\S+\.{hint}(?:\s|$)", text) is not None
138
+ for hint in ("archive", "tar", "zip")
139
+ )
140
+ has_destination = any(fragment in text for fragment in (" into ", " to ", " in "))
141
+ has_destination = has_destination or _has_guarded_archive_destination_flag(text)
142
+ return has_archive_action and has_archive_target and not has_destination
143
+
144
+
145
+ def _has_guarded_archive_destination_flag(text: str) -> bool:
146
+ return re.search(r"(?<!\S)(?:-c|-d)(?!\S)\s+\S+", text, flags=re.IGNORECASE) is not None
147
+
148
+
149
+ def _matches_hint(text: str, hint: str) -> bool:
150
+ escaped = re.escape(hint.strip())
151
+ if not escaped:
152
+ return False
153
+ return re.search(rf"(?<!\w){escaped}(?!\w)", text) is not None
@@ -0,0 +1,103 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ from dataclasses import asdict, dataclass, field
5
+ from datetime import datetime, timezone
6
+ from pathlib import Path
7
+ from typing import Any
8
+
9
+ from oterminus.audit_privacy import redact_argv, redact_text
10
+
11
+
12
+ @dataclass
13
+ class AuditEvent:
14
+ timestamp: str
15
+ user_input: str
16
+ direct_command_detected: bool
17
+ ambiguity_detected: bool = False
18
+ ambiguity_reason: str | None = None
19
+ ambiguity_safe_options: list[str] = field(default_factory=list)
20
+ planner_invoked: bool = False
21
+ planner_skipped: bool = False
22
+ planner_skip_reason: str | None = None
23
+ routed_category: str | None = None
24
+ proposal_mode: str | None = None
25
+ command_family: str | None = None
26
+ rendered_command: str | None = None
27
+ argv: list[str] = field(default_factory=list)
28
+ validation_accepted: bool | None = None
29
+ warnings: list[str] = field(default_factory=list)
30
+ rejection_reasons: list[str] = field(default_factory=list)
31
+ confirmation_result: str | None = None
32
+ execution_exit_code: int | None = None
33
+ stdout_truncated: bool = False
34
+ stderr_truncated: bool = False
35
+ stdout_original_chars: int | None = None
36
+ stderr_original_chars: int | None = None
37
+ stdout_visible_chars: int | None = None
38
+ stderr_visible_chars: int | None = None
39
+ rerun_source_history_id: int | None = None
40
+ duration_ms: int | None = None
41
+ timings_ms: dict[str, int] = field(default_factory=dict)
42
+ failure_explanation_requested: bool = False
43
+ failure_explanation_generated: bool = False
44
+ failure_explanation_error: str | None = None
45
+ failure_suggested_next_action: str | None = None
46
+ failure_stderr_summary: str | None = None
47
+
48
+ @classmethod
49
+ def start(cls, user_input: str) -> AuditEvent:
50
+ return cls(
51
+ timestamp=datetime.now(tz=timezone.utc).isoformat(),
52
+ user_input=user_input,
53
+ direct_command_detected=False,
54
+ )
55
+
56
+ def to_payload(self) -> dict[str, Any]:
57
+ return asdict(self)
58
+
59
+
60
+ class AuditLogger:
61
+ def __init__(self, path: Path, *, redact: bool = True):
62
+ self.path = path
63
+ self.redact = redact
64
+
65
+ def write(self, event: AuditEvent) -> None:
66
+ self.path.parent.mkdir(parents=True, exist_ok=True)
67
+ payload = event.to_payload()
68
+ if self.redact:
69
+ payload = self._redacted_payload(payload)
70
+ serialized = json.dumps(payload, sort_keys=True)
71
+ with self.path.open("a", encoding="utf-8") as handle:
72
+ handle.write(serialized + "\n")
73
+
74
+ def status(self) -> dict[str, str]:
75
+ return {
76
+ "path": str(self.path),
77
+ "exists": "yes" if self.path.exists() else "no",
78
+ "redaction": "enabled" if self.redact else "disabled (explicit opt-out)",
79
+ }
80
+
81
+ def _redacted_payload(self, payload: dict[str, Any]) -> dict[str, Any]:
82
+ cloned = dict(payload)
83
+ for field_name in (
84
+ "user_input",
85
+ "rendered_command",
86
+ "ambiguity_reason",
87
+ "failure_explanation_error",
88
+ "failure_suggested_next_action",
89
+ "failure_stderr_summary",
90
+ ):
91
+ value = cloned.get(field_name)
92
+ if isinstance(value, str):
93
+ cloned[field_name] = redact_text(value)
94
+ for field_name in ("warnings", "rejection_reasons", "ambiguity_safe_options"):
95
+ raw = cloned.get(field_name)
96
+ if isinstance(raw, list):
97
+ cloned[field_name] = [
98
+ redact_text(item) if isinstance(item, str) else item for item in raw
99
+ ]
100
+ raw_argv = cloned.get("argv")
101
+ if isinstance(raw_argv, list):
102
+ cloned["argv"] = redact_argv([str(item) for item in raw_argv])
103
+ return cloned