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.
@@ -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,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.32.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -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()