5dive 0.1.0__py3-none-any.whl
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.
- 5dive-0.1.0.dist-info/METADATA +95 -0
- 5dive-0.1.0.dist-info/RECORD +6 -0
- 5dive-0.1.0.dist-info/WHEEL +4 -0
- 5dive-0.1.0.dist-info/licenses/LICENSE +21 -0
- fivedive/__init__.py +14 -0
- fivedive/client.py +120 -0
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: 5dive
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Python client for the 5dive CLI — read the task queue, agents and org chart of a 5dive box.
|
|
5
|
+
Project-URL: Homepage, https://5dive.ai
|
|
6
|
+
Project-URL: Source, https://github.com/5dive-ai/5dive
|
|
7
|
+
Project-URL: Issues, https://github.com/5dive-ai/5dive/issues
|
|
8
|
+
Author: 5dive
|
|
9
|
+
License: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: 5dive,agents,automation,cli,multi-agent
|
|
12
|
+
Classifier: Development Status :: 3 - Alpha
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
17
|
+
Requires-Python: >=3.9
|
|
18
|
+
Description-Content-Type: text/markdown
|
|
19
|
+
|
|
20
|
+
# 5dive
|
|
21
|
+
|
|
22
|
+
Python client for the [5dive](https://5dive.ai) CLI — read the task queue, agent
|
|
23
|
+
seats and org chart of a 5dive box from Python.
|
|
24
|
+
|
|
25
|
+
5dive runs a team of AI coding agents on your own machine. State lives on that
|
|
26
|
+
box and the CLI is the interface with a stable contract over it, so this package
|
|
27
|
+
is a thin, dependency-free wrapper over `5dive <verb> --json` rather than an HTTP
|
|
28
|
+
client.
|
|
29
|
+
|
|
30
|
+
## Install
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
pip install 5dive
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
The distribution is named `5dive`; the import name is `fivedive`, because a
|
|
37
|
+
Python identifier cannot begin with a digit.
|
|
38
|
+
|
|
39
|
+
## Use
|
|
40
|
+
|
|
41
|
+
```python
|
|
42
|
+
from fivedive import FiveDive
|
|
43
|
+
|
|
44
|
+
fd = FiveDive()
|
|
45
|
+
|
|
46
|
+
for t in fd.tasks("--status=todo"):
|
|
47
|
+
print(t["ident"], t["assignee"], t["title"])
|
|
48
|
+
|
|
49
|
+
print(fd.task("DIVE-3903")["status"])
|
|
50
|
+
|
|
51
|
+
for a in fd.agents():
|
|
52
|
+
print(a["name"], a["active"])
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Anything the CLI can answer in JSON is reachable, whether or not this package
|
|
56
|
+
has a named helper for it:
|
|
57
|
+
|
|
58
|
+
```python
|
|
59
|
+
fd.raw("org", "tree")
|
|
60
|
+
fd.raw("task", "ls", "--assignee=main")
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## Errors are raised, not returned
|
|
64
|
+
|
|
65
|
+
Every `--json` verb answers in one envelope:
|
|
66
|
+
|
|
67
|
+
```json
|
|
68
|
+
{"ok": true, "data": {...}}
|
|
69
|
+
{"ok": false, "error": {"code": 4, "class": "not_found", "message": "no such task: NOPE-1"}}
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
An unchecked `ok` is the bug this package exists to prevent, so the false branch
|
|
73
|
+
raises instead of handing back a dict you have to remember to test:
|
|
74
|
+
|
|
75
|
+
```python
|
|
76
|
+
from fivedive import FiveDive, FiveDiveError, CliNotFound
|
|
77
|
+
|
|
78
|
+
try:
|
|
79
|
+
fd.task("NOPE-1")
|
|
80
|
+
except FiveDiveError as e:
|
|
81
|
+
print(e.err_class, e.code, e) # not_found 4 no such task: NOPE-1
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
Branch on `err_class`, not on message text — the class is the stable contract.
|
|
85
|
+
`CliNotFound` is raised when the `5dive` binary is not on `PATH`.
|
|
86
|
+
|
|
87
|
+
## Requirements
|
|
88
|
+
|
|
89
|
+
Python 3.9+, no third-party dependencies, and a 5dive box with the CLI
|
|
90
|
+
installed. Get the CLI at [5dive.ai](https://5dive.ai) or
|
|
91
|
+
[github.com/5dive-ai/5dive](https://github.com/5dive-ai/5dive).
|
|
92
|
+
|
|
93
|
+
## Licence
|
|
94
|
+
|
|
95
|
+
MIT.
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
fivedive/__init__.py,sha256=TgyYOkwsX4um9RRjNFnPAqkRgkMnMVixqiFS9bKmGdA,408
|
|
2
|
+
fivedive/client.py,sha256=IBmFhrgbkJqarBfB-LCGyglYdJZBnsJBP-8HbPsxHTE,4829
|
|
3
|
+
5dive-0.1.0.dist-info/METADATA,sha256=jr5prAYjjsm2eRYq7PPn8n85YG8zlxPHjdGMSW4MUlI,2648
|
|
4
|
+
5dive-0.1.0.dist-info/WHEEL,sha256=zOwg4jB6zX2kU910N-cMawjivD6tO8NEWvE12je1bVk,87
|
|
5
|
+
5dive-0.1.0.dist-info/licenses/LICENSE,sha256=TOFzH6yOF3dLJJDk_P5xWlCy-5-MhO0WzA-cXO07ARg,1062
|
|
6
|
+
5dive-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 5dive
|
|
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.
|
fivedive/__init__.py
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"""Python client for the 5dive CLI.
|
|
2
|
+
|
|
3
|
+
The distribution is named ``5dive``; the import name is ``fivedive`` because a
|
|
4
|
+
Python identifier cannot begin with a digit.
|
|
5
|
+
|
|
6
|
+
from fivedive import FiveDive
|
|
7
|
+
for t in FiveDive().tasks():
|
|
8
|
+
print(t["ident"], t["status"])
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from .client import FiveDive, FiveDiveError, CliNotFound
|
|
12
|
+
|
|
13
|
+
__all__ = ["FiveDive", "FiveDiveError", "CliNotFound"]
|
|
14
|
+
__version__ = "0.1.0"
|
fivedive/client.py
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
"""A thin, dependency-free wrapper over the local ``5dive`` CLI's JSON mode.
|
|
2
|
+
|
|
3
|
+
WHY THIS IS A WRAPPER AND NOT AN HTTP CLIENT: 5dive's state lives on the box the
|
|
4
|
+
agents run on, and the CLI is the only interface with a stable contract over it.
|
|
5
|
+
Every ``--json`` verb answers in one envelope::
|
|
6
|
+
|
|
7
|
+
{"ok": true, "data": {...}}
|
|
8
|
+
{"ok": false, "error": {"code": 4, "class": "not_found", "message": "..."}}
|
|
9
|
+
|
|
10
|
+
so the whole job of this module is to run the binary, parse that envelope, and
|
|
11
|
+
raise on the false branch instead of handing back a dict the caller has to
|
|
12
|
+
remember to check. An unchecked ``ok`` is the bug this exists to prevent.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import json
|
|
18
|
+
import shutil
|
|
19
|
+
import subprocess
|
|
20
|
+
from typing import Any, Dict, List, Optional, Sequence
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class FiveDiveError(RuntimeError):
|
|
24
|
+
"""The CLI answered with ``ok: false``.
|
|
25
|
+
|
|
26
|
+
Carries the machine-readable fields so callers can branch on ``err_class``
|
|
27
|
+
rather than matching on message text, which is not a stable contract.
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
def __init__(self, message: str, code: Optional[int] = None, err_class: Optional[str] = None):
|
|
31
|
+
super().__init__(message)
|
|
32
|
+
self.code = code
|
|
33
|
+
self.err_class = err_class
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class CliNotFound(FiveDiveError):
|
|
37
|
+
"""The ``5dive`` binary is not on PATH."""
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class FiveDive:
|
|
41
|
+
"""Run 5dive CLI verbs and return parsed JSON.
|
|
42
|
+
|
|
43
|
+
:param binary: path to the CLI, if it is not simply ``5dive`` on PATH.
|
|
44
|
+
:param timeout: seconds before a call is abandoned. ``None`` waits forever,
|
|
45
|
+
which is rarely what you want from a library.
|
|
46
|
+
"""
|
|
47
|
+
|
|
48
|
+
def __init__(self, binary: str = "5dive", timeout: Optional[float] = 30.0):
|
|
49
|
+
self.binary = binary
|
|
50
|
+
self.timeout = timeout
|
|
51
|
+
|
|
52
|
+
# -- the one place a subprocess is run -------------------------------
|
|
53
|
+
def raw(self, *args: str) -> Any:
|
|
54
|
+
"""Run ``5dive <args> --json`` and return the ``data`` payload.
|
|
55
|
+
|
|
56
|
+
``--json`` is appended only when the caller has not already passed it,
|
|
57
|
+
so ``raw("task", "ls", "--json")`` and ``raw("task", "ls")`` agree.
|
|
58
|
+
"""
|
|
59
|
+
argv: List[str] = [self.binary, *args]
|
|
60
|
+
if "--json" not in argv:
|
|
61
|
+
argv.append("--json")
|
|
62
|
+
|
|
63
|
+
if shutil.which(self.binary) is None and "/" not in self.binary:
|
|
64
|
+
raise CliNotFound(f"{self.binary!r} is not on PATH — is this a 5dive box?")
|
|
65
|
+
|
|
66
|
+
try:
|
|
67
|
+
proc = subprocess.run(argv, capture_output=True, text=True, timeout=self.timeout)
|
|
68
|
+
except FileNotFoundError as exc: # binary named by path, but absent
|
|
69
|
+
raise CliNotFound(f"cannot execute {self.binary!r}: {exc}") from exc
|
|
70
|
+
except subprocess.TimeoutExpired as exc:
|
|
71
|
+
raise FiveDiveError(f"{' '.join(argv)} timed out after {self.timeout}s") from exc
|
|
72
|
+
|
|
73
|
+
# A non-zero exit still carries a JSON envelope on the error path, so
|
|
74
|
+
# parse BEFORE judging the status: the envelope's message is better than
|
|
75
|
+
# "exited 4", and falling back to stderr only when there is no envelope
|
|
76
|
+
# keeps a genuine crash legible instead of masking it as a parse error.
|
|
77
|
+
payload = None
|
|
78
|
+
if proc.stdout.strip():
|
|
79
|
+
try:
|
|
80
|
+
payload = json.loads(proc.stdout)
|
|
81
|
+
except json.JSONDecodeError:
|
|
82
|
+
payload = None
|
|
83
|
+
|
|
84
|
+
if payload is None:
|
|
85
|
+
detail = (proc.stderr or proc.stdout or "").strip() or f"exited {proc.returncode}"
|
|
86
|
+
raise FiveDiveError(f"{' '.join(argv)}: no JSON envelope — {detail}")
|
|
87
|
+
|
|
88
|
+
if not payload.get("ok", False):
|
|
89
|
+
err = payload.get("error") or {}
|
|
90
|
+
raise FiveDiveError(
|
|
91
|
+
err.get("message", "unknown error"),
|
|
92
|
+
code=err.get("code"),
|
|
93
|
+
err_class=err.get("class"),
|
|
94
|
+
)
|
|
95
|
+
return payload.get("data")
|
|
96
|
+
|
|
97
|
+
# -- convenience readers ---------------------------------------------
|
|
98
|
+
def tasks(self, *flags: str) -> List[Dict[str, Any]]:
|
|
99
|
+
"""The task queue. Extra flags pass straight through, e.g.
|
|
100
|
+
``tasks("--status=todo", "--assignee=main")``."""
|
|
101
|
+
data = self.raw("task", "ls", *flags) or {}
|
|
102
|
+
return data.get("tasks", [])
|
|
103
|
+
|
|
104
|
+
def task(self, ident: str) -> Dict[str, Any]:
|
|
105
|
+
"""One task by ident, e.g. ``task("DIVE-3903")``."""
|
|
106
|
+
return self.raw("task", "show", ident) or {}
|
|
107
|
+
|
|
108
|
+
def agents(self, *flags: str) -> List[Dict[str, Any]]:
|
|
109
|
+
"""The agent seats on this box."""
|
|
110
|
+
data = self.raw("agent", "list", *flags)
|
|
111
|
+
if isinstance(data, list):
|
|
112
|
+
return data
|
|
113
|
+
return (data or {}).get("agents", [])
|
|
114
|
+
|
|
115
|
+
def version(self) -> str:
|
|
116
|
+
"""The CLI's version string (does not use the JSON envelope)."""
|
|
117
|
+
proc = subprocess.run(
|
|
118
|
+
[self.binary, "--version"], capture_output=True, text=True, timeout=self.timeout
|
|
119
|
+
)
|
|
120
|
+
return proc.stdout.strip()
|