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.
- oaken_cli-0.1.0/.gitignore +241 -0
- oaken_cli-0.1.0/PKG-INFO +28 -0
- oaken_cli-0.1.0/README.md +9 -0
- oaken_cli-0.1.0/pyproject.toml +43 -0
- oaken_cli-0.1.0/src/oaken_cli/__init__.py +14 -0
- oaken_cli-0.1.0/src/oaken_cli/__main__.py +10 -0
- oaken_cli-0.1.0/src/oaken_cli/client.py +144 -0
- oaken_cli-0.1.0/src/oaken_cli/commands/__init__.py +3 -0
- oaken_cli-0.1.0/src/oaken_cli/commands/account_cmd.py +349 -0
- oaken_cli-0.1.0/src/oaken_cli/commands/agent_cmd.py +207 -0
- oaken_cli-0.1.0/src/oaken_cli/commands/completion.py +64 -0
- oaken_cli-0.1.0/src/oaken_cli/commands/content_cmd.py +266 -0
- oaken_cli-0.1.0/src/oaken_cli/commands/daemon_cmd.py +99 -0
- oaken_cli-0.1.0/src/oaken_cli/commands/mail_cmd.py +349 -0
- oaken_cli-0.1.0/src/oaken_cli/commands/mcp_cmd.py +263 -0
- oaken_cli-0.1.0/src/oaken_cli/commands/operation_cmd.py +162 -0
- oaken_cli-0.1.0/src/oaken_cli/commands/runtime.py +147 -0
- oaken_cli-0.1.0/src/oaken_cli/commands/schedule_cmd.py +138 -0
- oaken_cli-0.1.0/src/oaken_cli/commands/system_cmd.py +408 -0
- oaken_cli-0.1.0/src/oaken_cli/daemon.py +99 -0
- oaken_cli-0.1.0/src/oaken_cli/discovery.py +184 -0
- oaken_cli-0.1.0/src/oaken_cli/exits.py +74 -0
- oaken_cli-0.1.0/src/oaken_cli/http.py +252 -0
- oaken_cli-0.1.0/src/oaken_cli/inprocess.py +119 -0
- oaken_cli-0.1.0/src/oaken_cli/main.py +840 -0
- oaken_cli-0.1.0/src/oaken_cli/output.py +114 -0
- oaken_cli-0.1.0/src/oaken_cli/sanitize.py +77 -0
- oaken_cli-0.1.0/src/oaken_cli/tests/__init__.py +1 -0
- oaken_cli-0.1.0/src/oaken_cli/tests/conftest.py +97 -0
- oaken_cli-0.1.0/src/oaken_cli/tests/test_cli_accounts.py +204 -0
- oaken_cli-0.1.0/src/oaken_cli/tests/test_cli_commands.py +290 -0
- oaken_cli-0.1.0/src/oaken_cli/tests/test_cli_e2e.py +113 -0
- oaken_cli-0.1.0/src/oaken_cli/tests/test_cli_http.py +279 -0
- oaken_cli-0.1.0/src/oaken_cli/tests/test_cli_mcp.py +228 -0
- oaken_cli-0.1.0/src/oaken_cli/tests/test_cli_unit.py +102 -0
- oaken_cli-0.1.0/src/oaken_cli/tests/test_cli_usage.py +48 -0
- 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
|
oaken_cli-0.1.0/PKG-INFO
ADDED
|
@@ -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."""
|