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.
- oterminus-0.1.1/PKG-INFO +187 -0
- oterminus-0.1.1/README.md +161 -0
- oterminus-0.1.1/pyproject.toml +71 -0
- oterminus-0.1.1/src/oterminus/__init__.py +4 -0
- oterminus-0.1.1/src/oterminus/ambiguity.py +153 -0
- oterminus-0.1.1/src/oterminus/audit.py +103 -0
- oterminus-0.1.1/src/oterminus/audit_privacy.py +132 -0
- oterminus-0.1.1/src/oterminus/cli.py +1056 -0
- oterminus-0.1.1/src/oterminus/command_registry.py +43 -0
- oterminus-0.1.1/src/oterminus/commands/__init__.py +67 -0
- oterminus-0.1.1/src/oterminus/commands/archive.py +88 -0
- oterminus-0.1.1/src/oterminus/commands/dangerous.py +35 -0
- oterminus-0.1.1/src/oterminus/commands/filesystem.py +155 -0
- oterminus-0.1.1/src/oterminus/commands/git.py +39 -0
- oterminus-0.1.1/src/oterminus/commands/macos.py +25 -0
- oterminus-0.1.1/src/oterminus/commands/network.py +68 -0
- oterminus-0.1.1/src/oterminus/commands/process.py +46 -0
- oterminus-0.1.1/src/oterminus/commands/project.py +48 -0
- oterminus-0.1.1/src/oterminus/commands/registry.py +518 -0
- oterminus-0.1.1/src/oterminus/commands/system.py +75 -0
- oterminus-0.1.1/src/oterminus/commands/text.py +84 -0
- oterminus-0.1.1/src/oterminus/commands/types.py +128 -0
- oterminus-0.1.1/src/oterminus/completion.py +151 -0
- oterminus-0.1.1/src/oterminus/config.py +192 -0
- oterminus-0.1.1/src/oterminus/direct_commands.py +129 -0
- oterminus-0.1.1/src/oterminus/discovery.py +175 -0
- oterminus-0.1.1/src/oterminus/doctor.py +480 -0
- oterminus-0.1.1/src/oterminus/eval_fixtures/ambiguity.json +54 -0
- oterminus-0.1.1/src/oterminus/eval_fixtures/archive_inspection.json +267 -0
- oterminus-0.1.1/src/oterminus/eval_fixtures/direct_commands.json +129 -0
- oterminus-0.1.1/src/oterminus/eval_fixtures/fast_path_local_planner.json +1 -0
- oterminus-0.1.1/src/oterminus/eval_fixtures/filesystem_inspection.json +281 -0
- oterminus-0.1.1/src/oterminus/eval_fixtures/filesystem_mutation.json +134 -0
- oterminus-0.1.1/src/oterminus/eval_fixtures/git_inspection.json +128 -0
- oterminus-0.1.1/src/oterminus/eval_fixtures/macos_desktop.json +89 -0
- oterminus-0.1.1/src/oterminus/eval_fixtures/network_diagnostics.json +191 -0
- oterminus-0.1.1/src/oterminus/eval_fixtures/planner_normalization.json +78 -0
- oterminus-0.1.1/src/oterminus/eval_fixtures/process_inspection.json +61 -0
- oterminus-0.1.1/src/oterminus/eval_fixtures/project_health.json +97 -0
- oterminus-0.1.1/src/oterminus/eval_fixtures/system_inspection.json +67 -0
- oterminus-0.1.1/src/oterminus/eval_fixtures/text_inspection.json +34 -0
- oterminus-0.1.1/src/oterminus/eval_fixtures/unsafe_and_blocked.json +154 -0
- oterminus-0.1.1/src/oterminus/evals.py +310 -0
- oterminus-0.1.1/src/oterminus/executor.py +87 -0
- oterminus-0.1.1/src/oterminus/failure_explainer.py +74 -0
- oterminus-0.1.1/src/oterminus/history.py +183 -0
- oterminus-0.1.1/src/oterminus/local_planner.py +102 -0
- oterminus-0.1.1/src/oterminus/logging_utils.py +11 -0
- oterminus-0.1.1/src/oterminus/models.py +158 -0
- oterminus-0.1.1/src/oterminus/ollama_client.py +80 -0
- oterminus-0.1.1/src/oterminus/planner.py +86 -0
- oterminus-0.1.1/src/oterminus/policies.py +36 -0
- oterminus-0.1.1/src/oterminus/prompts.py +246 -0
- oterminus-0.1.1/src/oterminus/renderer.py +128 -0
- oterminus-0.1.1/src/oterminus/router.py +640 -0
- oterminus-0.1.1/src/oterminus/setup.py +107 -0
- oterminus-0.1.1/src/oterminus/structured_commands.py +1908 -0
- oterminus-0.1.1/src/oterminus/validator.py +746 -0
oterminus-0.1.1/PKG-INFO
ADDED
|
@@ -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,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
|