oaken-cli 0.1.0__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 (37) hide show
  1. oaken_cli-0.1.0/.gitignore +241 -0
  2. oaken_cli-0.1.0/PKG-INFO +28 -0
  3. oaken_cli-0.1.0/README.md +9 -0
  4. oaken_cli-0.1.0/pyproject.toml +43 -0
  5. oaken_cli-0.1.0/src/oaken_cli/__init__.py +14 -0
  6. oaken_cli-0.1.0/src/oaken_cli/__main__.py +10 -0
  7. oaken_cli-0.1.0/src/oaken_cli/client.py +144 -0
  8. oaken_cli-0.1.0/src/oaken_cli/commands/__init__.py +3 -0
  9. oaken_cli-0.1.0/src/oaken_cli/commands/account_cmd.py +349 -0
  10. oaken_cli-0.1.0/src/oaken_cli/commands/agent_cmd.py +207 -0
  11. oaken_cli-0.1.0/src/oaken_cli/commands/completion.py +64 -0
  12. oaken_cli-0.1.0/src/oaken_cli/commands/content_cmd.py +266 -0
  13. oaken_cli-0.1.0/src/oaken_cli/commands/daemon_cmd.py +99 -0
  14. oaken_cli-0.1.0/src/oaken_cli/commands/mail_cmd.py +349 -0
  15. oaken_cli-0.1.0/src/oaken_cli/commands/mcp_cmd.py +263 -0
  16. oaken_cli-0.1.0/src/oaken_cli/commands/operation_cmd.py +162 -0
  17. oaken_cli-0.1.0/src/oaken_cli/commands/runtime.py +147 -0
  18. oaken_cli-0.1.0/src/oaken_cli/commands/schedule_cmd.py +138 -0
  19. oaken_cli-0.1.0/src/oaken_cli/commands/system_cmd.py +408 -0
  20. oaken_cli-0.1.0/src/oaken_cli/daemon.py +99 -0
  21. oaken_cli-0.1.0/src/oaken_cli/discovery.py +184 -0
  22. oaken_cli-0.1.0/src/oaken_cli/exits.py +74 -0
  23. oaken_cli-0.1.0/src/oaken_cli/http.py +252 -0
  24. oaken_cli-0.1.0/src/oaken_cli/inprocess.py +119 -0
  25. oaken_cli-0.1.0/src/oaken_cli/main.py +840 -0
  26. oaken_cli-0.1.0/src/oaken_cli/output.py +114 -0
  27. oaken_cli-0.1.0/src/oaken_cli/sanitize.py +77 -0
  28. oaken_cli-0.1.0/src/oaken_cli/tests/__init__.py +1 -0
  29. oaken_cli-0.1.0/src/oaken_cli/tests/conftest.py +97 -0
  30. oaken_cli-0.1.0/src/oaken_cli/tests/test_cli_accounts.py +204 -0
  31. oaken_cli-0.1.0/src/oaken_cli/tests/test_cli_commands.py +290 -0
  32. oaken_cli-0.1.0/src/oaken_cli/tests/test_cli_e2e.py +113 -0
  33. oaken_cli-0.1.0/src/oaken_cli/tests/test_cli_http.py +279 -0
  34. oaken_cli-0.1.0/src/oaken_cli/tests/test_cli_mcp.py +228 -0
  35. oaken_cli-0.1.0/src/oaken_cli/tests/test_cli_unit.py +102 -0
  36. oaken_cli-0.1.0/src/oaken_cli/tests/test_cli_usage.py +48 -0
  37. oaken_cli-0.1.0/src/oaken_cli/tests/test_version.py +12 -0
@@ -0,0 +1,241 @@
1
+ # Byte-compiled / optimized / DLL files
2
+ __pycache__/
3
+ *.py[codz]
4
+ *$py.class
5
+
6
+ # C extensions
7
+ *.so
8
+
9
+ # Distribution / packaging
10
+ .Python
11
+ build/
12
+ develop-eggs/
13
+ dist/
14
+ downloads/
15
+ eggs/
16
+ .eggs/
17
+ lib/
18
+ lib64/
19
+ parts/
20
+ sdist/
21
+ var/
22
+ wheels/
23
+ share/python-wheels/
24
+ *.egg-info/
25
+ .installed.cfg
26
+ *.egg
27
+ MANIFEST
28
+
29
+ # PyInstaller
30
+ # Usually these files are written by a python script from a template
31
+ # before PyInstaller builds the exe, so as to inject date/other infos into it.
32
+ *.manifest
33
+ *.spec
34
+
35
+ # Installer logs
36
+ pip-log.txt
37
+ pip-delete-this-directory.txt
38
+
39
+ # Unit test / coverage reports
40
+ htmlcov/
41
+ .tox/
42
+ .nox/
43
+ .coverage
44
+ .coverage.*
45
+ .cache
46
+ nosetests.xml
47
+ coverage.xml
48
+ *.cover
49
+ *.py.cover
50
+ *.lcov
51
+ .hypothesis/
52
+ .pytest_cache/
53
+ cover/
54
+
55
+ # Translations
56
+ *.mo
57
+ *.pot
58
+
59
+ # Django stuff:
60
+ *.log
61
+ local_settings.py
62
+ db.sqlite3
63
+ db.sqlite3-journal
64
+
65
+ # Flask stuff:
66
+ instance/
67
+ .webassets-cache
68
+
69
+ # Scrapy stuff:
70
+ .scrapy
71
+
72
+ # Sphinx documentation
73
+ docs/_build/
74
+
75
+ # PyBuilder
76
+ .pybuilder/
77
+ target/
78
+
79
+ # Jupyter Notebook
80
+ .ipynb_checkpoints
81
+
82
+ # IPython
83
+ profile_default/
84
+ ipython_config.py
85
+
86
+ # pyenv
87
+ # For a library or package, you might want to ignore these files since the code is
88
+ # intended to run in multiple environments; otherwise, check them in:
89
+ # .python-version
90
+
91
+ # pipenv
92
+ # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
93
+ # However, in case of collaboration, if having platform-specific dependencies or dependencies
94
+ # having no cross-platform support, pipenv may install dependencies that don't work, or not
95
+ # install all needed dependencies.
96
+ # Pipfile.lock
97
+
98
+ # uv
99
+ # Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
100
+ # This is especially recommended for binary packages to ensure reproducibility, and is more
101
+ # commonly ignored for libraries.
102
+ # uv.lock
103
+
104
+ # poetry
105
+ # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
106
+ # This is especially recommended for binary packages to ensure reproducibility, and is more
107
+ # commonly ignored for libraries.
108
+ # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
109
+ # poetry.lock
110
+ # poetry.toml
111
+
112
+ # pdm
113
+ # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
114
+ # pdm recommends including project-wide configuration in pdm.toml, but excluding .pdm-python.
115
+ # https://pdm-project.org/en/latest/usage/project/#working-with-version-control
116
+ # pdm.lock
117
+ # pdm.toml
118
+ .pdm-python
119
+ .pdm-build/
120
+
121
+ # pixi
122
+ # Similar to Pipfile.lock, it is generally recommended to include pixi.lock in version control.
123
+ # pixi.lock
124
+ # Pixi creates a virtual environment in the .pixi directory, just like venv module creates one
125
+ # in the .venv directory. It is recommended not to include this directory in version control.
126
+ .pixi/*
127
+ !.pixi/config.toml
128
+
129
+ # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
130
+ __pypackages__/
131
+
132
+ # Celery stuff
133
+ celerybeat-schedule*
134
+ celerybeat.pid
135
+
136
+ # Redis
137
+ *.rdb
138
+ *.aof
139
+ *.pid
140
+
141
+ # RabbitMQ
142
+ mnesia/
143
+ rabbitmq/
144
+ rabbitmq-data/
145
+
146
+ # ActiveMQ
147
+ activemq-data/
148
+
149
+ # SageMath parsed files
150
+ *.sage.py
151
+
152
+ # Environments
153
+ .env
154
+ .envrc
155
+ .venv
156
+ env/
157
+ venv/
158
+ ENV/
159
+ env.bak/
160
+ venv.bak/
161
+
162
+ # Spyder project settings
163
+ .spyderproject
164
+ .spyproject
165
+
166
+ # Rope project settings
167
+ .ropeproject
168
+
169
+ # mkdocs/Zensical documentation
170
+ /site
171
+
172
+ # mypy
173
+ .mypy_cache/
174
+ .dmypy.json
175
+ dmypy.json
176
+
177
+ # Pyre type checker
178
+ .pyre/
179
+
180
+ # pytype static type analyzer
181
+ .pytype/
182
+
183
+ # Cython debug symbols
184
+ cython_debug/
185
+
186
+ # PyCharm
187
+ # JetBrains specific template is maintained in a separate JetBrains.gitignore that can
188
+ # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
189
+ # and can be added to the global gitignore or merged into this file. For a more nuclear
190
+ # option (not recommended) you can uncomment the following to ignore the entire idea folder.
191
+ # .idea/
192
+
193
+ # Abstra
194
+ # Abstra is an AI-powered process automation framework.
195
+ # Ignore directories containing user credentials, local state, and settings.
196
+ # Learn more at https://abstra.io/docs
197
+ .abstra/
198
+
199
+ # Visual Studio Code
200
+ # Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore that
201
+ # can be found at https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore
202
+ # and can be added to the global gitignore or merged into this file. However, if you prefer, you
203
+ # could uncomment the following to ignore the entire vscode folder
204
+ # .vscode/
205
+ # Temporary file for partial code execution
206
+ tempCodeRunnerFile.py
207
+
208
+ # Ruff stuff:
209
+ .ruff_cache/
210
+
211
+ # PyPI configuration file
212
+ .pypirc
213
+
214
+ # Marimo
215
+ marimo/_static/
216
+ marimo/_lsp/
217
+ __marimo__/
218
+
219
+ # Streamlit
220
+ .streamlit/secrets.toml
221
+
222
+ # --- Oaken Mail: never commit data, mail or secrets ---
223
+ *.db
224
+ *.db-*
225
+ *.sqlite*
226
+ *.eml
227
+ *.mbox
228
+ .env.*
229
+ !.env.example
230
+ token*.json
231
+ credentials*.json
232
+ client_secret*.json
233
+ node_modules/
234
+ site/
235
+ .claude/settings.local.json
236
+
237
+ # Synthetic test fixtures are committed (no real mail; see docs/architecture/parsing.md).
238
+ !src/oaken/mail/tests/fixtures/**/*.eml
239
+
240
+ # A real config.toml holds local settings and must never be committed (use config.example.toml).
241
+ config.toml
@@ -0,0 +1,28 @@
1
+ Metadata-Version: 2.5
2
+ Name: oaken-cli
3
+ Version: 0.1.0
4
+ Summary: The oaken command line: a thin client of the Oaken Mail core daemon.
5
+ License-Expression: Apache-2.0
6
+ Keywords: ai,cli,email,local-first,mail,mcp,oaken
7
+ Classifier: Development Status :: 4 - Beta
8
+ Classifier: Environment :: Console
9
+ Classifier: Intended Audience :: End Users/Desktop
10
+ Classifier: License :: OSI Approved :: Apache Software License
11
+ Classifier: Operating System :: OS Independent
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Topic :: Communications :: Email
14
+ Classifier: Topic :: Utilities
15
+ Requires-Python: >=3.11
16
+ Requires-Dist: oaken
17
+ Requires-Dist: rich>=13
18
+ Description-Content-Type: text/markdown
19
+
20
+ # oaken-cli
21
+
22
+ The `oaken` command: a thin client of the Oaken Mail core daemon. Every command is one operation
23
+ of core's registry, called over the loopback HTTP API. See the CLI reference on the docs site and
24
+ `docs/architecture/daemon.md`.
25
+
26
+ ```text
27
+ uv run oaken --help
28
+ ```
@@ -0,0 +1,9 @@
1
+ # oaken-cli
2
+
3
+ The `oaken` command: a thin client of the Oaken Mail core daemon. Every command is one operation
4
+ of core's registry, called over the loopback HTTP API. See the CLI reference on the docs site and
5
+ `docs/architecture/daemon.md`.
6
+
7
+ ```text
8
+ uv run oaken --help
9
+ ```
@@ -0,0 +1,43 @@
1
+ [project]
2
+ name = "oaken-cli"
3
+ dynamic = ["version"] # one source of truth: src/oaken/__init__.py (the whole product shares it)
4
+ description = "The oaken command line: a thin client of the Oaken Mail core daemon."
5
+ readme = "README.md"
6
+ license = "Apache-2.0"
7
+ requires-python = ">=3.11"
8
+ keywords = ["oaken", "email", "mail", "cli", "mcp", "local-first", "ai"]
9
+ classifiers = [
10
+ "Development Status :: 4 - Beta",
11
+ "Intended Audience :: End Users/Desktop",
12
+ "License :: OSI Approved :: Apache Software License",
13
+ "Operating System :: OS Independent",
14
+ "Environment :: Console",
15
+ "Programming Language :: Python :: 3",
16
+ "Topic :: Communications :: Email",
17
+ "Topic :: Utilities",
18
+ ]
19
+ dependencies = [
20
+ "oaken",
21
+ "rich>=13",
22
+ ]
23
+
24
+ [project.scripts]
25
+ oaken = "oaken_cli:main"
26
+
27
+ [tool.uv.sources]
28
+ oaken = { workspace = true }
29
+
30
+ [build-system]
31
+ requires = ["hatchling"]
32
+ build-backend = "hatchling.build"
33
+
34
+ # The product releases in lockstep (ADR 0014); every distribution reads its version from the one
35
+ # place oaken declares it, so a single bump moves them all and they can never drift.
36
+ [tool.hatch.version]
37
+ source = "regex"
38
+ path = "../../src/oaken/__init__.py"
39
+ pattern = '__version__ = "(?P<version>[^"]+)"'
40
+
41
+ [tool.hatch.build.targets.wheel]
42
+ packages = ["src/oaken_cli"]
43
+ exclude = ["**/tests/**"]
@@ -0,0 +1,14 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ """The Oaken command-line client.
3
+
4
+ The CLI is a *thin* client: it holds no business logic. Every command is a call to a registry
5
+ operation, reached through an :class:`~oaken_cli.client.ApiClient`. In normal use that client
6
+ speaks HTTP to the local core daemon (prompt 16); in tests it wraps the in-process
7
+ ``CoreClient`` directly, so the whole CLI is exercised without a socket.
8
+
9
+ The entry point is :func:`oaken_cli.main.main`, wired as the ``oaken`` console script.
10
+ """
11
+
12
+ from oaken_cli.main import main
13
+
14
+ __all__ = ["main"]
@@ -0,0 +1,10 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ """Run the CLI as ``python -m oaken_cli``. The console script ``oaken`` is the normal entry point;
3
+ this mirror is handy for ``python -m`` invocation and for measuring startup without the wrapper."""
4
+
5
+ from __future__ import annotations
6
+
7
+ from oaken_cli.main import _console_main
8
+
9
+ if __name__ == "__main__":
10
+ _console_main()
@@ -0,0 +1,144 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ """The seam between the CLI and core: a small, transport-neutral client.
3
+
4
+ The CLI never imports core's HTTP wiring or its in-process ``CoreClient`` directly. It programs
5
+ against :class:`ApiClient`, an abstract interface with exactly the operations a thin client needs:
6
+
7
+ * :meth:`ApiClient.call` — run one registry operation (``POST /v1/ops/{name}`` over HTTP);
8
+ * :meth:`ApiClient.list_ops` — the operation catalogue (``GET /v1/ops``), which drives both the
9
+ generic ``oaken op NAME`` fallback and ``--help`` text;
10
+ * :meth:`ApiClient.stream_events` — the SSE event stream (``GET /v1/events``), for ``oaken watch``;
11
+ * :meth:`ApiClient.health` — a liveness probe (``GET /v1/health``) used by ``oaken status``.
12
+
13
+ Two implementations exist (or will):
14
+
15
+ * :class:`~oaken_cli.inprocess.InProcessApiClient` wraps the in-process ``CoreClient``. It needs no
16
+ daemon and no socket, so the CLI's whole command surface is testable today (prompt 17).
17
+ * ``HttpApiClient`` speaks to the running daemon. **It is a stub until prompt 16** builds the
18
+ server; see :mod:`oaken_cli.http`.
19
+
20
+ The two results a mutation can have — it ran, or it needs confirming first — are modelled by
21
+ :class:`CallResult`, which mirrors core's own ``CallResult`` without importing it, so the HTTP
22
+ decoder and the in-process adapter produce the same shape.
23
+ """
24
+
25
+ from __future__ import annotations
26
+
27
+ from abc import ABC, abstractmethod
28
+ from collections.abc import AsyncIterator, Mapping
29
+ from dataclasses import dataclass, field
30
+ from typing import Any
31
+
32
+
33
+ @dataclass(frozen=True)
34
+ class OpInfo:
35
+ """One row of the operation catalogue, as a thin client needs it.
36
+
37
+ This is the decoded form of a ``/v1/ops`` entry. It is deliberately a plain dataclass, not a
38
+ pydantic model: the CLI must not import the registry to describe an operation, so that a
39
+ registry change never forces a CLI release (the generic ``oaken op NAME`` fallback is built
40
+ from exactly these fields).
41
+ """
42
+
43
+ name: str
44
+ summary: str
45
+ permission: str # "read" | "mutate" | "send"
46
+ marks_read: bool = False
47
+ streaming: bool = False
48
+ input_schema: Mapping[str, Any] = field(default_factory=dict)
49
+ output_schema: Mapping[str, Any] = field(default_factory=dict)
50
+
51
+
52
+ @dataclass(frozen=True)
53
+ class Proposal:
54
+ """What a mutation *would* change, returned when it has not been confirmed.
55
+
56
+ The CLI shows this to the person and, for an explicit ``--yes`` or an interactive confirm,
57
+ calls again with ``confirmation_token``. Mirrors core's gate ``Proposal``; ``changes`` may be a
58
+ prefix of the whole, and ``total_changes`` is always the real count.
59
+ """
60
+
61
+ operation: str
62
+ summary: str
63
+ changes: list[dict[str, Any]] = field(default_factory=list)
64
+ total_changes: int = 0
65
+ confirmation_token: str = ""
66
+ expires_at: str = ""
67
+
68
+
69
+ @dataclass(frozen=True)
70
+ class CallResult:
71
+ """The outcome of :meth:`ApiClient.call`: either ``output`` (it ran) or ``proposal`` (confirm
72
+ first). ``request_id`` ties the call to an audit row and to the daemon's logs."""
73
+
74
+ request_id: str
75
+ output: Any | None = None
76
+ proposal: Proposal | None = None
77
+
78
+ @property
79
+ def executed(self) -> bool:
80
+ return self.proposal is None
81
+
82
+
83
+ @dataclass(frozen=True)
84
+ class Event:
85
+ """One line of the SSE stream: a typed event name, its JSON data, and the stream id used to
86
+ resume with ``Last-Event-ID`` after a reconnect."""
87
+
88
+ id: str
89
+ event: str
90
+ data: Mapping[str, Any]
91
+
92
+
93
+ class ApiClient(ABC):
94
+ """The thin-client contract. An implementation decides *how* it reaches core (HTTP to the
95
+ daemon, or the in-process registry); the CLI only ever uses these methods.
96
+
97
+ Lifetime: an ``ApiClient`` is an async context manager so an HTTP implementation can hold a
98
+ connection pool and close it cleanly. The in-process implementation needs no teardown but
99
+ honours the same protocol.
100
+ """
101
+
102
+ async def __aenter__(self) -> ApiClient:
103
+ return self
104
+
105
+ async def __aexit__(self, *exc: object) -> None:
106
+ await self.aclose()
107
+
108
+ async def aclose(self) -> None: # noqa: B027 — an optional concrete hook, not abstract
109
+ """Release any resources (HTTP pool, etc.). The default does nothing."""
110
+
111
+ @abstractmethod
112
+ async def health(self) -> Mapping[str, Any]:
113
+ """Liveness only. Never requires a working operation; used by ``oaken status`` to tell a
114
+ running-but-degraded core from one that is down. Raises :class:`CoreUnavailable` when core
115
+ cannot be reached at all."""
116
+
117
+ @abstractmethod
118
+ async def list_ops(self) -> list[OpInfo]:
119
+ """The operations this caller may invoke, each with its schemas. Drives the generic
120
+ ``oaken op NAME`` fallback and the auto-derived ``--help``."""
121
+
122
+ @abstractmethod
123
+ async def call(
124
+ self,
125
+ name: str,
126
+ payload: Mapping[str, Any] | None = None,
127
+ *,
128
+ confirm: bool = False,
129
+ confirmation_token: str | None = None,
130
+ timeout: float | None = None, # noqa: ASYNC109
131
+ ) -> CallResult:
132
+ """Run one operation. ``timeout`` (seconds) replaces the transport's default wait for the
133
+ answer, for the few operations that are slow by nature, such as ``ask`` on a local model.
134
+
135
+ ``confirm=True`` is the CLI saying *the person at the terminal asked for this exact
136
+ action*; it is how a human-class caller pre-confirms a mutation. A mutation that is neither
137
+ confirmed nor accompanied by a ``confirmation_token`` comes back as a :class:`Proposal` and
138
+ changes nothing.
139
+ """
140
+
141
+ @abstractmethod
142
+ def stream_events(self, last_event_id: str | None = None) -> AsyncIterator[Event]:
143
+ """Yield events as they arrive, resuming after ``last_event_id`` when given. Returns an
144
+ async iterator (not a coroutine) so ``oaken watch`` can ``async for`` over it."""
@@ -0,0 +1,3 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ """CLI command handlers. Each module holds thin handlers that call registry operations through an
3
+ :class:`~oaken_cli.client.ApiClient`. Imported lazily by :mod:`oaken_cli.main`."""