opendot 0.0.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.
- opendot-0.0.1/.github/workflows/ci.yml +26 -0
- opendot-0.0.1/.github/workflows/release.yml +46 -0
- opendot-0.0.1/.gitignore +39 -0
- opendot-0.0.1/CONTRIBUTING.md +55 -0
- opendot-0.0.1/LICENSE +21 -0
- opendot-0.0.1/PKG-INFO +169 -0
- opendot-0.0.1/README.md +137 -0
- opendot-0.0.1/SECURITY.md +47 -0
- opendot-0.0.1/pyproject.toml +46 -0
- opendot-0.0.1/src/opendot/__init__.py +18 -0
- opendot-0.0.1/src/opendot/agent/__init__.py +1 -0
- opendot-0.0.1/src/opendot/agent/config.py +27 -0
- opendot-0.0.1/src/opendot/agent/events.py +34 -0
- opendot-0.0.1/src/opendot/agent/explorers.py +93 -0
- opendot-0.0.1/src/opendot/agent/loop.py +263 -0
- opendot-0.0.1/src/opendot/agent/prompt.py +30 -0
- opendot-0.0.1/src/opendot/agent/usage.py +36 -0
- opendot-0.0.1/src/opendot/cli.py +364 -0
- opendot-0.0.1/src/opendot/mcp/__init__.py +14 -0
- opendot-0.0.1/src/opendot/mcp/manager.py +202 -0
- opendot-0.0.1/src/opendot/reversibility/__init__.py +2 -0
- opendot-0.0.1/src/opendot/reversibility/classifier.py +106 -0
- opendot-0.0.1/src/opendot/reversibility/engine.py +82 -0
- opendot-0.0.1/src/opendot/reversibility/ledger.py +62 -0
- opendot-0.0.1/src/opendot/reversibility/rules.py +64 -0
- opendot-0.0.1/src/opendot/reversibility/snapshots.py +220 -0
- opendot-0.0.1/src/opendot/tools/__init__.py +1 -0
- opendot-0.0.1/src/opendot/tools/local.py +332 -0
- opendot-0.0.1/src/opendot/tools/office.py +132 -0
- opendot-0.0.1/src/opendot/tui.py +409 -0
- opendot-0.0.1/tests/test_classifier.py +78 -0
- opendot-0.0.1/tests/test_explorers.py +83 -0
- opendot-0.0.1/tests/test_mcp.py +112 -0
- opendot-0.0.1/tests/test_office.py +102 -0
- opendot-0.0.1/tests/test_rules.py +51 -0
- opendot-0.0.1/tests/test_snapshots.py +174 -0
- opendot-0.0.1/tests/test_tools.py +76 -0
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
|
|
8
|
+
jobs:
|
|
9
|
+
test:
|
|
10
|
+
runs-on: ubuntu-latest
|
|
11
|
+
strategy:
|
|
12
|
+
fail-fast: false
|
|
13
|
+
matrix:
|
|
14
|
+
python-version: ["3.10", "3.11", "3.12"]
|
|
15
|
+
steps:
|
|
16
|
+
- uses: actions/checkout@v4
|
|
17
|
+
- name: Set up Python ${{ matrix.python-version }}
|
|
18
|
+
uses: actions/setup-python@v5
|
|
19
|
+
with:
|
|
20
|
+
python-version: ${{ matrix.python-version }}
|
|
21
|
+
- name: Install
|
|
22
|
+
run: |
|
|
23
|
+
python -m pip install --upgrade pip
|
|
24
|
+
pip install -e ".[office,dev]"
|
|
25
|
+
- name: Run tests
|
|
26
|
+
run: pytest -q
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
name: Release
|
|
2
|
+
|
|
3
|
+
# Publish to PyPI when you push a version tag, e.g.:
|
|
4
|
+
# git tag v0.1.0 && git push origin v0.1.0
|
|
5
|
+
# Uses PyPI Trusted Publishing (OIDC) — no API token stored in GitHub.
|
|
6
|
+
# One-time setup on PyPI: project → Publishing → add a trusted publisher for
|
|
7
|
+
# this repo + workflow file `release.yml` + environment `pypi`.
|
|
8
|
+
|
|
9
|
+
on:
|
|
10
|
+
push:
|
|
11
|
+
tags: ["v*"]
|
|
12
|
+
|
|
13
|
+
jobs:
|
|
14
|
+
build:
|
|
15
|
+
runs-on: ubuntu-latest
|
|
16
|
+
steps:
|
|
17
|
+
- uses: actions/checkout@v4
|
|
18
|
+
- uses: actions/setup-python@v5
|
|
19
|
+
with:
|
|
20
|
+
python-version: "3.12"
|
|
21
|
+
- name: Build sdist + wheel
|
|
22
|
+
run: |
|
|
23
|
+
python -m pip install --upgrade pip build
|
|
24
|
+
python -m build
|
|
25
|
+
- name: Check artifacts
|
|
26
|
+
run: |
|
|
27
|
+
pip install twine
|
|
28
|
+
twine check dist/*
|
|
29
|
+
- uses: actions/upload-artifact@v4
|
|
30
|
+
with:
|
|
31
|
+
name: dist
|
|
32
|
+
path: dist/
|
|
33
|
+
|
|
34
|
+
publish:
|
|
35
|
+
needs: build
|
|
36
|
+
runs-on: ubuntu-latest
|
|
37
|
+
environment: pypi # PyPI trusted-publisher environment
|
|
38
|
+
permissions:
|
|
39
|
+
id-token: write # required for OIDC (Trusted Publishing)
|
|
40
|
+
steps:
|
|
41
|
+
- uses: actions/download-artifact@v4
|
|
42
|
+
with:
|
|
43
|
+
name: dist
|
|
44
|
+
path: dist/
|
|
45
|
+
- name: Publish to PyPI
|
|
46
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
opendot-0.0.1/.gitignore
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# Python
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*.egg
|
|
5
|
+
*.egg-info/
|
|
6
|
+
.eggs/
|
|
7
|
+
build/
|
|
8
|
+
dist/
|
|
9
|
+
*.so
|
|
10
|
+
|
|
11
|
+
# Virtual environments
|
|
12
|
+
.venv/
|
|
13
|
+
venv/
|
|
14
|
+
env/
|
|
15
|
+
ENV/
|
|
16
|
+
|
|
17
|
+
# Packaging / build
|
|
18
|
+
*.whl
|
|
19
|
+
pip-wheel-metadata/
|
|
20
|
+
|
|
21
|
+
# Test / coverage / type caches
|
|
22
|
+
.pytest_cache/
|
|
23
|
+
.mypy_cache/
|
|
24
|
+
.ruff_cache/
|
|
25
|
+
.coverage
|
|
26
|
+
htmlcov/
|
|
27
|
+
.tox/
|
|
28
|
+
|
|
29
|
+
# Editor / OS
|
|
30
|
+
.vscode/
|
|
31
|
+
.idea/
|
|
32
|
+
*.swp
|
|
33
|
+
.DS_Store
|
|
34
|
+
|
|
35
|
+
# opendot local state (snapshot store lives in ~/.opendot; never commit any local copy)
|
|
36
|
+
.opendot/
|
|
37
|
+
|
|
38
|
+
# Environment / secrets
|
|
39
|
+
.env
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# Contributing to opendot
|
|
2
|
+
|
|
3
|
+
Thanks for your interest! opendot is an interactive terminal AI agent whose
|
|
4
|
+
defining feature is **trustworthy reversibility** — you can see and cleanly undo
|
|
5
|
+
everything it does. Contributions are welcome; please keep that guarantee intact.
|
|
6
|
+
|
|
7
|
+
## Setup
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
git clone https://github.com/vedaant00/opendot
|
|
11
|
+
cd opendot
|
|
12
|
+
uv venv && uv pip install -e ".[office,dev]" # or: python -m venv .venv && pip install -e ".[office,dev]"
|
|
13
|
+
pytest # should be all green
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## Project layout
|
|
17
|
+
|
|
18
|
+
```
|
|
19
|
+
src/opendot/
|
|
20
|
+
cli.py entry point (subcommands, one-shot, REPL)
|
|
21
|
+
tui.py the full-screen Textual TUI
|
|
22
|
+
agent/ the model-agnostic ReAct loop (LiteLLM), events, prompt, usage
|
|
23
|
+
tools/ local file/shell tools + office (.xlsx/.pptx)
|
|
24
|
+
reversibility/ THE MOAT: content-addressed snapshots, ledger, undo, classifier
|
|
25
|
+
mcp/ MCP client (connect external MCP servers)
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## The one rule: don't break reversibility
|
|
29
|
+
|
|
30
|
+
`reversibility/` is the reason opendot exists. If you touch it, or add a tool
|
|
31
|
+
that mutates the filesystem:
|
|
32
|
+
|
|
33
|
+
- Every mutating action **must** snapshot before it runs (so `undo` restores
|
|
34
|
+
exact prior bytes).
|
|
35
|
+
- Anything whose effect can escape the workspace (network, `sudo`, deletes
|
|
36
|
+
outside the working dir, external/MCP calls) **must** be routed through the
|
|
37
|
+
confirmation gate and recorded as irreversible in the ledger.
|
|
38
|
+
- The snapshot round-trip must be exact — see `tests/test_snapshots.py`. Add
|
|
39
|
+
tests for any change here; a lying `undo` defeats the whole project.
|
|
40
|
+
|
|
41
|
+
## Tests
|
|
42
|
+
|
|
43
|
+
Please add tests for changes. Run `pytest` before opening a PR — CI runs it on
|
|
44
|
+
Python 3.10–3.12.
|
|
45
|
+
|
|
46
|
+
## Style
|
|
47
|
+
|
|
48
|
+
Match the surrounding code: small, clear functions; comments explain *why*, not
|
|
49
|
+
*what*. Keep the tool/UI honest — don't add UI affordances or hints for features
|
|
50
|
+
that don't actually work.
|
|
51
|
+
|
|
52
|
+
## Opening a PR
|
|
53
|
+
|
|
54
|
+
Describe the change and why. If it's non-trivial, an issue first to align on the
|
|
55
|
+
approach is appreciated.
|
opendot-0.0.1/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Vedaant Singh
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
opendot-0.0.1/PKG-INFO
ADDED
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: opendot
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: An interactive terminal AI agent you can fully undo — any model, acts on your real files, every action reversible.
|
|
5
|
+
Project-URL: Repository, https://github.com/vedaant00/opendot
|
|
6
|
+
Author: Vedaant Singh
|
|
7
|
+
License-Expression: MIT
|
|
8
|
+
License-File: LICENSE
|
|
9
|
+
Keywords: agent,ai,cli,coding-agent,litellm,llm,terminal
|
|
10
|
+
Classifier: Development Status :: 3 - Alpha
|
|
11
|
+
Classifier: Environment :: Console
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Topic :: Software Development
|
|
16
|
+
Classifier: Topic :: Utilities
|
|
17
|
+
Requires-Python: >=3.10
|
|
18
|
+
Requires-Dist: litellm>=1.50
|
|
19
|
+
Requires-Dist: mcp>=1.2
|
|
20
|
+
Requires-Dist: prompt-toolkit>=3.0
|
|
21
|
+
Requires-Dist: rich>=13.0
|
|
22
|
+
Requires-Dist: textual>=0.60
|
|
23
|
+
Provides-Extra: dev
|
|
24
|
+
Requires-Dist: openpyxl>=3.1; extra == 'dev'
|
|
25
|
+
Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
|
|
26
|
+
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
27
|
+
Requires-Dist: python-pptx>=0.6; extra == 'dev'
|
|
28
|
+
Provides-Extra: office
|
|
29
|
+
Requires-Dist: openpyxl>=3.1; extra == 'office'
|
|
30
|
+
Requires-Dist: python-pptx>=0.6; extra == 'office'
|
|
31
|
+
Description-Content-Type: text/markdown
|
|
32
|
+
|
|
33
|
+
<div align="center">
|
|
34
|
+
|
|
35
|
+
```
|
|
36
|
+
_ _
|
|
37
|
+
___ _ __ ___ _ __ __| | ___ | |_
|
|
38
|
+
/ _ \| '_ \ / _ \ '_ \ / _` |/ _ \| __|
|
|
39
|
+
| (_) | |_) | __/ | | | (_| | (_) | |_
|
|
40
|
+
\___/| .__/ \___|_| |_|\__,_|\___/ \__|
|
|
41
|
+
|_|
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
**An interactive terminal AI agent you can fully undo.**
|
|
45
|
+
|
|
46
|
+
[](https://github.com/vedaant00/opendot/actions/workflows/ci.yml)
|
|
47
|
+
[](https://pypi.org/project/opendot/)
|
|
48
|
+
[](https://pepy.tech/project/opendot)
|
|
49
|
+
[](LICENSE)
|
|
50
|
+
|
|
51
|
+
</div>
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
opendot works directly on your real files and shell — but unlike other terminal
|
|
55
|
+
agents, **every action it takes is snapshotted first**, so you can see exactly
|
|
56
|
+
what it did and cleanly walk it back. Files *and* shell commands, not just
|
|
57
|
+
in-repo edits. Commands whose effects escape your workspace (network, sudo,
|
|
58
|
+
`git push`, deleting outside the working dir) are flagged and confirmed before
|
|
59
|
+
they run, with an honest note about what can't be undone.
|
|
60
|
+
|
|
61
|
+
That's the point of opendot: an agent you can let loose because nothing it does
|
|
62
|
+
is a surprise, and (almost) nothing is irreversible.
|
|
63
|
+
|
|
64
|
+
## Install
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
# try it instantly, no install
|
|
68
|
+
uvx opendot
|
|
69
|
+
|
|
70
|
+
# recommended (isolated global CLI)
|
|
71
|
+
uv tool install opendot # or: pipx install opendot
|
|
72
|
+
|
|
73
|
+
# also works
|
|
74
|
+
pip install opendot
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
## Use
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
opendot # open an interactive chat
|
|
81
|
+
opendot -p "summarize this project" # one-shot, for scripts / CI
|
|
82
|
+
opendot --model claude-sonnet-4-5 # any model (see below)
|
|
83
|
+
|
|
84
|
+
opendot log # audit: what has the agent done here?
|
|
85
|
+
opendot undo # revert the last action
|
|
86
|
+
opendot undo 000004 # restore the workspace to before action #4
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
Inside the chat, slash-commands: `/log`, `/undo`, `/clear`, `/compact`,
|
|
90
|
+
`/model`, `/help`.
|
|
91
|
+
|
|
92
|
+
## Any model
|
|
93
|
+
|
|
94
|
+
opendot uses [LiteLLM](https://docs.litellm.ai), so any model works — cloud,
|
|
95
|
+
local, or Hugging Face. Set the provider's API key in your environment and pass
|
|
96
|
+
`--model`:
|
|
97
|
+
|
|
98
|
+
| Provider | Env var | Example `--model` |
|
|
99
|
+
|----------|---------|-------------------|
|
|
100
|
+
| OpenAI | `OPENAI_API_KEY` | `gpt-4o` |
|
|
101
|
+
| Anthropic | `ANTHROPIC_API_KEY` | `claude-sonnet-4-5` |
|
|
102
|
+
| Google | `GEMINI_API_KEY` | `gemini/gemini-2.0-flash` |
|
|
103
|
+
| Ollama (local) | — | `ollama/qwen2.5` |
|
|
104
|
+
|
|
105
|
+
Reasoning models stream their thinking live.
|
|
106
|
+
|
|
107
|
+
## Connect MCP servers
|
|
108
|
+
|
|
109
|
+
opendot is an [MCP](https://modelcontextprotocol.io) client: connect any MCP
|
|
110
|
+
server and its tools become available to the agent alongside the built-in ones.
|
|
111
|
+
|
|
112
|
+
```bash
|
|
113
|
+
# a stdio server — put its launch command after `--`
|
|
114
|
+
opendot mcp add <name> --env KEY=VALUE -- <command> [args...]
|
|
115
|
+
|
|
116
|
+
# a remote server (http/sse)
|
|
117
|
+
opendot mcp add <name> --url <https url>
|
|
118
|
+
|
|
119
|
+
opendot mcp list # show configured servers
|
|
120
|
+
opendot mcp remove <name> # remove one
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
Servers are stored in `~/.opendot/mcp.json` and connect automatically on the
|
|
124
|
+
next launch; connected servers appear in the sidebar.
|
|
125
|
+
|
|
126
|
+
Because opendot can't know what an external tool does, **every MCP tool call is
|
|
127
|
+
treated as irreversible** — it's confirmed before running and marked ✗ in the
|
|
128
|
+
ledger. Your built-in file/shell actions stay snapshotted and undoable as usual.
|
|
129
|
+
|
|
130
|
+
## Project rules — `OPENDOT.md`
|
|
131
|
+
|
|
132
|
+
Drop an `OPENDOT.md` in your project. Its prose is given to the agent as
|
|
133
|
+
context. You can also control what gets snapshotted with an `opendot` block:
|
|
134
|
+
|
|
135
|
+
````markdown
|
|
136
|
+
```opendot
|
|
137
|
+
# snapshot these even though they'd normally be skipped:
|
|
138
|
+
snapshot: dist
|
|
139
|
+
# never snapshot these:
|
|
140
|
+
skip: data, *.log
|
|
141
|
+
```
|
|
142
|
+
````
|
|
143
|
+
|
|
144
|
+
By default opendot skips `.git`, `node_modules`, virtualenvs, and build caches
|
|
145
|
+
when snapshotting — your rules override those in either direction.
|
|
146
|
+
|
|
147
|
+
## How the reversibility works
|
|
148
|
+
|
|
149
|
+
- Before every file write or shell command, opendot snapshots the working
|
|
150
|
+
directory into a **content-addressed store** in `~/.opendot` (each unique file
|
|
151
|
+
stored once, so snapshots are cheap).
|
|
152
|
+
- Every action is recorded in an **append-only ledger** you can inspect with
|
|
153
|
+
`opendot log`.
|
|
154
|
+
- `opendot undo` restores the workspace to a chosen point, exactly.
|
|
155
|
+
- A conservative **classifier** decides which shell commands are workspace-
|
|
156
|
+
contained (auto-run, undoable) vs. escaping (confirmed first, marked
|
|
157
|
+
irreversible). When unsure, it asks.
|
|
158
|
+
|
|
159
|
+
Honest boundary: opendot cannot undo effects that leave your machine (a sent
|
|
160
|
+
email, a dropped remote database, a `git push`). It tells you *before* running
|
|
161
|
+
those, rather than pretending otherwise.
|
|
162
|
+
|
|
163
|
+
## Status
|
|
164
|
+
|
|
165
|
+
Early (alpha). The interactive agent, local tools, and the full reversibility
|
|
166
|
+
engine work and are tested. Streaming, slash-commands, and `OPENDOT.md` rules
|
|
167
|
+
are in. A richer TUI and more tools are coming.
|
|
168
|
+
|
|
169
|
+
MIT licensed.
|
opendot-0.0.1/README.md
ADDED
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
<div align="center">
|
|
2
|
+
|
|
3
|
+
```
|
|
4
|
+
_ _
|
|
5
|
+
___ _ __ ___ _ __ __| | ___ | |_
|
|
6
|
+
/ _ \| '_ \ / _ \ '_ \ / _` |/ _ \| __|
|
|
7
|
+
| (_) | |_) | __/ | | | (_| | (_) | |_
|
|
8
|
+
\___/| .__/ \___|_| |_|\__,_|\___/ \__|
|
|
9
|
+
|_|
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
**An interactive terminal AI agent you can fully undo.**
|
|
13
|
+
|
|
14
|
+
[](https://github.com/vedaant00/opendot/actions/workflows/ci.yml)
|
|
15
|
+
[](https://pypi.org/project/opendot/)
|
|
16
|
+
[](https://pepy.tech/project/opendot)
|
|
17
|
+
[](LICENSE)
|
|
18
|
+
|
|
19
|
+
</div>
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
opendot works directly on your real files and shell — but unlike other terminal
|
|
23
|
+
agents, **every action it takes is snapshotted first**, so you can see exactly
|
|
24
|
+
what it did and cleanly walk it back. Files *and* shell commands, not just
|
|
25
|
+
in-repo edits. Commands whose effects escape your workspace (network, sudo,
|
|
26
|
+
`git push`, deleting outside the working dir) are flagged and confirmed before
|
|
27
|
+
they run, with an honest note about what can't be undone.
|
|
28
|
+
|
|
29
|
+
That's the point of opendot: an agent you can let loose because nothing it does
|
|
30
|
+
is a surprise, and (almost) nothing is irreversible.
|
|
31
|
+
|
|
32
|
+
## Install
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
# try it instantly, no install
|
|
36
|
+
uvx opendot
|
|
37
|
+
|
|
38
|
+
# recommended (isolated global CLI)
|
|
39
|
+
uv tool install opendot # or: pipx install opendot
|
|
40
|
+
|
|
41
|
+
# also works
|
|
42
|
+
pip install opendot
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Use
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
opendot # open an interactive chat
|
|
49
|
+
opendot -p "summarize this project" # one-shot, for scripts / CI
|
|
50
|
+
opendot --model claude-sonnet-4-5 # any model (see below)
|
|
51
|
+
|
|
52
|
+
opendot log # audit: what has the agent done here?
|
|
53
|
+
opendot undo # revert the last action
|
|
54
|
+
opendot undo 000004 # restore the workspace to before action #4
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Inside the chat, slash-commands: `/log`, `/undo`, `/clear`, `/compact`,
|
|
58
|
+
`/model`, `/help`.
|
|
59
|
+
|
|
60
|
+
## Any model
|
|
61
|
+
|
|
62
|
+
opendot uses [LiteLLM](https://docs.litellm.ai), so any model works — cloud,
|
|
63
|
+
local, or Hugging Face. Set the provider's API key in your environment and pass
|
|
64
|
+
`--model`:
|
|
65
|
+
|
|
66
|
+
| Provider | Env var | Example `--model` |
|
|
67
|
+
|----------|---------|-------------------|
|
|
68
|
+
| OpenAI | `OPENAI_API_KEY` | `gpt-4o` |
|
|
69
|
+
| Anthropic | `ANTHROPIC_API_KEY` | `claude-sonnet-4-5` |
|
|
70
|
+
| Google | `GEMINI_API_KEY` | `gemini/gemini-2.0-flash` |
|
|
71
|
+
| Ollama (local) | — | `ollama/qwen2.5` |
|
|
72
|
+
|
|
73
|
+
Reasoning models stream their thinking live.
|
|
74
|
+
|
|
75
|
+
## Connect MCP servers
|
|
76
|
+
|
|
77
|
+
opendot is an [MCP](https://modelcontextprotocol.io) client: connect any MCP
|
|
78
|
+
server and its tools become available to the agent alongside the built-in ones.
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
# a stdio server — put its launch command after `--`
|
|
82
|
+
opendot mcp add <name> --env KEY=VALUE -- <command> [args...]
|
|
83
|
+
|
|
84
|
+
# a remote server (http/sse)
|
|
85
|
+
opendot mcp add <name> --url <https url>
|
|
86
|
+
|
|
87
|
+
opendot mcp list # show configured servers
|
|
88
|
+
opendot mcp remove <name> # remove one
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
Servers are stored in `~/.opendot/mcp.json` and connect automatically on the
|
|
92
|
+
next launch; connected servers appear in the sidebar.
|
|
93
|
+
|
|
94
|
+
Because opendot can't know what an external tool does, **every MCP tool call is
|
|
95
|
+
treated as irreversible** — it's confirmed before running and marked ✗ in the
|
|
96
|
+
ledger. Your built-in file/shell actions stay snapshotted and undoable as usual.
|
|
97
|
+
|
|
98
|
+
## Project rules — `OPENDOT.md`
|
|
99
|
+
|
|
100
|
+
Drop an `OPENDOT.md` in your project. Its prose is given to the agent as
|
|
101
|
+
context. You can also control what gets snapshotted with an `opendot` block:
|
|
102
|
+
|
|
103
|
+
````markdown
|
|
104
|
+
```opendot
|
|
105
|
+
# snapshot these even though they'd normally be skipped:
|
|
106
|
+
snapshot: dist
|
|
107
|
+
# never snapshot these:
|
|
108
|
+
skip: data, *.log
|
|
109
|
+
```
|
|
110
|
+
````
|
|
111
|
+
|
|
112
|
+
By default opendot skips `.git`, `node_modules`, virtualenvs, and build caches
|
|
113
|
+
when snapshotting — your rules override those in either direction.
|
|
114
|
+
|
|
115
|
+
## How the reversibility works
|
|
116
|
+
|
|
117
|
+
- Before every file write or shell command, opendot snapshots the working
|
|
118
|
+
directory into a **content-addressed store** in `~/.opendot` (each unique file
|
|
119
|
+
stored once, so snapshots are cheap).
|
|
120
|
+
- Every action is recorded in an **append-only ledger** you can inspect with
|
|
121
|
+
`opendot log`.
|
|
122
|
+
- `opendot undo` restores the workspace to a chosen point, exactly.
|
|
123
|
+
- A conservative **classifier** decides which shell commands are workspace-
|
|
124
|
+
contained (auto-run, undoable) vs. escaping (confirmed first, marked
|
|
125
|
+
irreversible). When unsure, it asks.
|
|
126
|
+
|
|
127
|
+
Honest boundary: opendot cannot undo effects that leave your machine (a sent
|
|
128
|
+
email, a dropped remote database, a `git push`). It tells you *before* running
|
|
129
|
+
those, rather than pretending otherwise.
|
|
130
|
+
|
|
131
|
+
## Status
|
|
132
|
+
|
|
133
|
+
Early (alpha). The interactive agent, local tools, and the full reversibility
|
|
134
|
+
engine work and are tested. Streaming, slash-commands, and `OPENDOT.md` rules
|
|
135
|
+
are in. A richer TUI and more tools are coming.
|
|
136
|
+
|
|
137
|
+
MIT licensed.
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# Security
|
|
2
|
+
|
|
3
|
+
## Threat model
|
|
4
|
+
|
|
5
|
+
### Overview
|
|
6
|
+
|
|
7
|
+
opendot is an AI-powered agent that runs **locally on your machine**. It gives a
|
|
8
|
+
model access to real tools: shell execution, file read/write/edit, and — if you
|
|
9
|
+
configure them — external MCP servers.
|
|
10
|
+
|
|
11
|
+
### No sandbox
|
|
12
|
+
|
|
13
|
+
opendot does **not** sandbox the agent. Two features exist to keep you in
|
|
14
|
+
control, but neither is a security boundary:
|
|
15
|
+
|
|
16
|
+
- **Confirmation prompts** ask before running commands opendot judges
|
|
17
|
+
irreversible or workspace-escaping (network, `sudo`, deletes outside the
|
|
18
|
+
working dir, external MCP calls). This is a UX safety net, not isolation.
|
|
19
|
+
- **Reversibility** snapshots the workspace before each action so you can `undo`
|
|
20
|
+
file/shell changes. This lets you *walk back* mistakes made inside the
|
|
21
|
+
workspace — it does **not** prevent an action, and it cannot undo effects that
|
|
22
|
+
leave your machine (a sent request, a dropped remote database, `git push`).
|
|
23
|
+
|
|
24
|
+
If you need true isolation, run opendot inside a Docker container or VM.
|
|
25
|
+
|
|
26
|
+
### Out of scope
|
|
27
|
+
|
|
28
|
+
| Category | Rationale |
|
|
29
|
+
|----------|-----------|
|
|
30
|
+
| Sandbox escapes | opendot is not a sandbox — the confirm/undo system is a UX layer (see above). |
|
|
31
|
+
| Effects outside the workspace | Actions that leave your machine are flagged as irreversible before running, but cannot be undone; running them is your decision. |
|
|
32
|
+
| LLM provider data handling | Data sent to your configured model provider is governed by their policies. |
|
|
33
|
+
| MCP server behavior | External MCP servers you configure are outside opendot's trust boundary. |
|
|
34
|
+
| Malicious config files | You control your own `~/.opendot` config; modifying it is not an attack vector. |
|
|
35
|
+
|
|
36
|
+
## Reporting a vulnerability
|
|
37
|
+
|
|
38
|
+
> [!IMPORTANT]
|
|
39
|
+
> We do not accept AI-generated security reports. Please only submit findings you
|
|
40
|
+
> have personally verified.
|
|
41
|
+
|
|
42
|
+
To report a security issue, use the repository's **GitHub Security Advisory
|
|
43
|
+
"Report a Vulnerability"** tab. We'll acknowledge your report and keep you
|
|
44
|
+
informed of progress toward a fix, and may ask for additional detail.
|
|
45
|
+
|
|
46
|
+
We appreciate responsible disclosure and will make every effort to acknowledge
|
|
47
|
+
your contribution.
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "opendot"
|
|
7
|
+
version = "0.0.1"
|
|
8
|
+
description = "An interactive terminal AI agent you can fully undo — any model, acts on your real files, every action reversible."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = "MIT"
|
|
11
|
+
requires-python = ">=3.10"
|
|
12
|
+
authors = [{ name = "Vedaant Singh" }]
|
|
13
|
+
keywords = ["ai", "agent", "cli", "llm", "litellm", "coding-agent", "terminal"]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Development Status :: 3 - Alpha",
|
|
16
|
+
"Environment :: Console",
|
|
17
|
+
"Intended Audience :: Developers",
|
|
18
|
+
"License :: OSI Approved :: MIT License",
|
|
19
|
+
"Programming Language :: Python :: 3",
|
|
20
|
+
"Topic :: Software Development",
|
|
21
|
+
"Topic :: Utilities",
|
|
22
|
+
]
|
|
23
|
+
dependencies = [
|
|
24
|
+
"litellm>=1.50", # any model: cloud, local, HF, through one interface
|
|
25
|
+
"rich>=13.0", # pretty terminal output (plain REPL fallback)
|
|
26
|
+
"textual>=0.60", # full-screen TUI (main pane + live ledger sidebar)
|
|
27
|
+
"prompt_toolkit>=3.0", # plain REPL input (history, editing)
|
|
28
|
+
"mcp>=1.2", # MCP client: connect to external MCP servers' tools
|
|
29
|
+
]
|
|
30
|
+
|
|
31
|
+
[project.optional-dependencies]
|
|
32
|
+
office = ["openpyxl>=3.1", "python-pptx>=0.6"] # xlsx / pptx read+edit tools
|
|
33
|
+
dev = ["pytest>=8.0", "pytest-asyncio>=0.23", "openpyxl>=3.1", "python-pptx>=0.6"]
|
|
34
|
+
|
|
35
|
+
[project.scripts]
|
|
36
|
+
opendot = "opendot.cli:main"
|
|
37
|
+
|
|
38
|
+
[project.urls]
|
|
39
|
+
Repository = "https://github.com/vedaant00/opendot"
|
|
40
|
+
|
|
41
|
+
[tool.hatch.build.targets.wheel]
|
|
42
|
+
packages = ["src/opendot"]
|
|
43
|
+
|
|
44
|
+
[tool.pytest.ini_options]
|
|
45
|
+
testpaths = ["tests"]
|
|
46
|
+
asyncio_mode = "auto" # pytest-asyncio runs `async def test_*` without per-test markers
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"""opendot — an interactive terminal AI agent you can fully undo.
|
|
2
|
+
|
|
3
|
+
Public SDK surface (so CLI, and future clients, are thin layers over this):
|
|
4
|
+
|
|
5
|
+
from opendot import Agent, AgentConfig
|
|
6
|
+
|
|
7
|
+
agent = Agent(AgentConfig(model="gpt-4o"))
|
|
8
|
+
async for event in agent.run("list the python files and summarize them"):
|
|
9
|
+
...
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from opendot.agent.loop import Agent
|
|
13
|
+
from opendot.agent.config import AgentConfig
|
|
14
|
+
from opendot.agent.events import Event
|
|
15
|
+
|
|
16
|
+
__version__ = "0.0.1"
|
|
17
|
+
|
|
18
|
+
__all__ = ["Agent", "AgentConfig", "Event", "__version__"]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""The opendot agent: the model-agnostic ReAct loop, config, events, prompt."""
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"""Agent configuration.
|
|
2
|
+
|
|
3
|
+
Model IDs are whatever LiteLLM accepts (``gpt-4o``, ``claude-sonnet-4-5``,
|
|
4
|
+
``ollama/qwen2.5``, ``huggingface/...``, etc.) — opendot is model-agnostic and
|
|
5
|
+
does not maintain its own provider list. API keys are read from the environment
|
|
6
|
+
by LiteLLM in the usual way (OPENAI_API_KEY, ANTHROPIC_API_KEY, ...).
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import os
|
|
12
|
+
from dataclasses import dataclass, field
|
|
13
|
+
|
|
14
|
+
# A sensible default that works if the user has an OpenAI key; over/ridden via
|
|
15
|
+
# --model, the OPENDOT_MODEL env var, or config.
|
|
16
|
+
DEFAULT_MODEL = os.environ.get("OPENDOT_MODEL", "gpt-4o")
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclass
|
|
20
|
+
class AgentConfig:
|
|
21
|
+
"""Everything the agent loop needs. Kept minimal for v1."""
|
|
22
|
+
|
|
23
|
+
model: str = DEFAULT_MODEL
|
|
24
|
+
workdir: str = field(default_factory=os.getcwd)
|
|
25
|
+
max_steps: int = 40 # hard bound on tool-calling turns per user message
|
|
26
|
+
temperature: float | None = None
|
|
27
|
+
system_prompt: str | None = None # None => use the built-in default
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"""Events streamed by the agent loop.
|
|
2
|
+
|
|
3
|
+
The loop is an async generator of these. The CLI renders them; a future SDK
|
|
4
|
+
consumer or remote client can consume them just as easily. Keeping this a small,
|
|
5
|
+
stable vocabulary is what lets other surfaces (TUI, web, Slack) be thin adapters.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from dataclasses import dataclass, field
|
|
11
|
+
from typing import Any, Literal
|
|
12
|
+
|
|
13
|
+
EventType = Literal[
|
|
14
|
+
"thinking", # a streamed chunk of the model's reasoning (reasoning models)
|
|
15
|
+
"text", # a streamed chunk of the assistant's answer
|
|
16
|
+
"tool_start", # the agent is about to run a tool
|
|
17
|
+
"tool_end", # a tool finished (result attached)
|
|
18
|
+
"final", # the assistant's turn is complete
|
|
19
|
+
"error", # something went wrong
|
|
20
|
+
# parallel read-only explorers
|
|
21
|
+
"explorer_start", # a subagent lane started (text=task, lane=index)
|
|
22
|
+
"explorer_step", # a subagent did something (text=summary, lane=index)
|
|
23
|
+
"explorer_done", # a subagent finished (text=finding summary, lane=index)
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@dataclass
|
|
28
|
+
class Event:
|
|
29
|
+
type: EventType
|
|
30
|
+
text: str = "" # for "text" / "error" / explorer_*
|
|
31
|
+
tool: str = "" # for tool_start / tool_end
|
|
32
|
+
args: dict[str, Any] = field(default_factory=dict) # tool_start
|
|
33
|
+
result: str = "" # tool_end
|
|
34
|
+
lane: int = -1 # explorer lane index (-1 = main agent)
|