deepsieve-cli 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.
- deepsieve_cli/__init__.py +0 -0
- deepsieve_cli/__main__.py +5 -0
- deepsieve_cli/client.py +149 -0
- deepsieve_cli/config.py +116 -0
- deepsieve_cli/main.py +531 -0
- deepsieve_cli-0.1.0.dist-info/METADATA +78 -0
- deepsieve_cli-0.1.0.dist-info/RECORD +9 -0
- deepsieve_cli-0.1.0.dist-info/WHEEL +4 -0
- deepsieve_cli-0.1.0.dist-info/entry_points.txt +2 -0
|
File without changes
|
deepsieve_cli/client.py
ADDED
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
"""HTTP client for `/v1`, plus the device-authorization login flow.
|
|
2
|
+
|
|
3
|
+
Deliberately a thin httpx wrapper rather than a dependency on the generated
|
|
4
|
+
SDK: the CLI needs the unauthenticated device endpoints before any credential
|
|
5
|
+
exists, and keeping the dependency surface at httpx keeps `uvx deepsieve`
|
|
6
|
+
startup fast. The same choice the stdio MCP package made.
|
|
7
|
+
|
|
8
|
+
Errors are surfaced as the typed `/v1` envelope so the CLI can print the
|
|
9
|
+
message the API actually gave — a 403 must never render as "something failed".
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import time
|
|
15
|
+
from dataclasses import dataclass
|
|
16
|
+
from typing import Any
|
|
17
|
+
|
|
18
|
+
import httpx
|
|
19
|
+
|
|
20
|
+
USER_AGENT = "deepsieve-cli"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class ApiError(Exception):
|
|
24
|
+
def __init__(self, code: str, message: str, *, request_id: str | None = None) -> None:
|
|
25
|
+
self.code = code
|
|
26
|
+
self.message = message
|
|
27
|
+
self.request_id = request_id
|
|
28
|
+
super().__init__(message)
|
|
29
|
+
|
|
30
|
+
def __str__(self) -> str:
|
|
31
|
+
tail = f" (request id {self.request_id})" if self.request_id else ""
|
|
32
|
+
return f"{self.message}{tail}"
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@dataclass
|
|
36
|
+
class DeviceAuthorization:
|
|
37
|
+
device_code: str
|
|
38
|
+
user_code: str
|
|
39
|
+
verification_uri: str
|
|
40
|
+
verification_uri_complete: str
|
|
41
|
+
expires_in: int
|
|
42
|
+
interval: int
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class Client:
|
|
46
|
+
def __init__(self, origin: str, api_key: str | None = None, timeout: float = 60.0) -> None:
|
|
47
|
+
self.origin = origin.rstrip("/")
|
|
48
|
+
self.api_key = api_key
|
|
49
|
+
self._timeout = timeout
|
|
50
|
+
|
|
51
|
+
def _headers(self, extra: dict[str, str] | None = None) -> dict[str, str]:
|
|
52
|
+
h = {"User-Agent": USER_AGENT, **(extra or {})}
|
|
53
|
+
if self.api_key:
|
|
54
|
+
h["Authorization"] = f"Bearer {self.api_key}"
|
|
55
|
+
return h
|
|
56
|
+
|
|
57
|
+
def request(
|
|
58
|
+
self,
|
|
59
|
+
method: str,
|
|
60
|
+
path: str,
|
|
61
|
+
*,
|
|
62
|
+
params: dict[str, Any] | None = None,
|
|
63
|
+
json_body: dict[str, Any] | None = None,
|
|
64
|
+
headers: dict[str, str] | None = None,
|
|
65
|
+
raw: bool = False,
|
|
66
|
+
) -> Any:
|
|
67
|
+
url = f"{self.origin}{path}"
|
|
68
|
+
try:
|
|
69
|
+
with httpx.Client(timeout=self._timeout, follow_redirects=True) as c:
|
|
70
|
+
r = c.request(
|
|
71
|
+
method, url, params=params, json=json_body, headers=self._headers(headers)
|
|
72
|
+
)
|
|
73
|
+
except httpx.RequestError as exc:
|
|
74
|
+
raise ApiError("network_error", f"Could not reach {self.origin}: {exc}") from exc
|
|
75
|
+
|
|
76
|
+
if raw and r.is_success:
|
|
77
|
+
return r.text
|
|
78
|
+
try:
|
|
79
|
+
body = r.json()
|
|
80
|
+
except ValueError:
|
|
81
|
+
if r.is_success:
|
|
82
|
+
return {"raw": r.text}
|
|
83
|
+
raise ApiError("http_error", f"HTTP {r.status_code} from {path}") from None
|
|
84
|
+
if not r.is_success:
|
|
85
|
+
err = (body or {}).get("error") or {}
|
|
86
|
+
raise ApiError(
|
|
87
|
+
err.get("code", f"http_{r.status_code}"),
|
|
88
|
+
err.get("message", f"HTTP {r.status_code} from {path}"),
|
|
89
|
+
request_id=err.get("request_id"),
|
|
90
|
+
)
|
|
91
|
+
return body
|
|
92
|
+
|
|
93
|
+
# ── device authorization ────────────────────────────────────────────────
|
|
94
|
+
|
|
95
|
+
def start_device_authorization(
|
|
96
|
+
self, client_name: str, preset: str = "cli"
|
|
97
|
+
) -> DeviceAuthorization:
|
|
98
|
+
# Ask for the CLI preset, not the broad `agent` one. The consent screen
|
|
99
|
+
# lists what it asks for, and asking a security-attentive developer to
|
|
100
|
+
# approve "manage webhook endpoints" for a tool that has no webhook
|
|
101
|
+
# command is how you lose their trust at the first screen.
|
|
102
|
+
body = self.request(
|
|
103
|
+
"POST",
|
|
104
|
+
"/v1/auth/device/code",
|
|
105
|
+
json_body={"client_name": client_name, "preset": preset},
|
|
106
|
+
)
|
|
107
|
+
return DeviceAuthorization(
|
|
108
|
+
device_code=body["device_code"],
|
|
109
|
+
user_code=body["user_code"],
|
|
110
|
+
verification_uri=body["verification_uri"],
|
|
111
|
+
verification_uri_complete=body.get("verification_uri_complete")
|
|
112
|
+
or body["verification_uri"],
|
|
113
|
+
expires_in=int(body.get("expires_in", 600)),
|
|
114
|
+
interval=int(body.get("interval", 5)),
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
def poll_for_credential(
|
|
118
|
+
self, auth: DeviceAuthorization, *, on_tick=None, sleep=time.sleep
|
|
119
|
+
) -> dict[str, Any]:
|
|
120
|
+
"""Poll until approved, denied, or expired.
|
|
121
|
+
|
|
122
|
+
Honours the server's `interval`, and backs off on `slow_down` as
|
|
123
|
+
RFC 8628 requires — a CLI that ignores that is how a device flow turns
|
|
124
|
+
into a self-inflicted denial of service.
|
|
125
|
+
"""
|
|
126
|
+
interval = auth.interval
|
|
127
|
+
deadline = time.monotonic() + auth.expires_in
|
|
128
|
+
while time.monotonic() < deadline:
|
|
129
|
+
sleep(interval)
|
|
130
|
+
if on_tick:
|
|
131
|
+
on_tick()
|
|
132
|
+
try:
|
|
133
|
+
return self.request(
|
|
134
|
+
"POST", "/v1/auth/device/token", json_body={"device_code": auth.device_code}
|
|
135
|
+
)
|
|
136
|
+
except ApiError as exc:
|
|
137
|
+
if exc.code == "authorization_pending":
|
|
138
|
+
continue
|
|
139
|
+
if exc.code == "slow_down":
|
|
140
|
+
interval += 5
|
|
141
|
+
continue
|
|
142
|
+
# A shared limiter bucket or a transient blip must not abort a
|
|
143
|
+
# sign-in the human is in the middle of approving: back off and
|
|
144
|
+
# keep waiting until the authorization itself expires.
|
|
145
|
+
if exc.code in ("rate_limited", "network_error") or exc.code.startswith("http_5"):
|
|
146
|
+
interval = min(interval + 5, 30)
|
|
147
|
+
continue
|
|
148
|
+
raise
|
|
149
|
+
raise ApiError("expired_token", "Timed out waiting for approval. Run login again.")
|
deepsieve_cli/config.py
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
"""Profiles and credential storage.
|
|
2
|
+
|
|
3
|
+
A profile is `(origin, api_key)` under a name. That is what makes it safe to
|
|
4
|
+
work against staging and production side by side: they are separate profiles,
|
|
5
|
+
never a global "current login" a developer has to remember the state of.
|
|
6
|
+
|
|
7
|
+
Credentials live in a 0600 file under the user's config directory, never in
|
|
8
|
+
shell history and never in the repo. `DEEPSIEVE_API_KEY` in the environment
|
|
9
|
+
always wins, so CI needs no login and no file.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import json
|
|
15
|
+
import os
|
|
16
|
+
import stat
|
|
17
|
+
from dataclasses import dataclass
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
|
|
20
|
+
DEFAULT_ORIGIN = "https://deepsieve.ai"
|
|
21
|
+
ENV_KEY = "DEEPSIEVE_API_KEY"
|
|
22
|
+
ENV_ORIGIN = "DEEPSIEVE_BASE_URL"
|
|
23
|
+
ENV_PROFILE = "DEEPSIEVE_PROFILE"
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def config_path() -> Path:
|
|
27
|
+
"""`$XDG_CONFIG_HOME/deepsieve/config.json`, or the OS equivalent."""
|
|
28
|
+
override = os.environ.get("DEEPSIEVE_CONFIG")
|
|
29
|
+
if override:
|
|
30
|
+
return Path(override)
|
|
31
|
+
base = os.environ.get("XDG_CONFIG_HOME")
|
|
32
|
+
root = Path(base) if base else Path.home() / ".config"
|
|
33
|
+
return root / "deepsieve" / "config.json"
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@dataclass
|
|
37
|
+
class Profile:
|
|
38
|
+
name: str
|
|
39
|
+
origin: str
|
|
40
|
+
api_key: str | None = None
|
|
41
|
+
key_name: str | None = None
|
|
42
|
+
scopes: list[str] | None = None
|
|
43
|
+
|
|
44
|
+
@property
|
|
45
|
+
def is_env_credential(self) -> bool:
|
|
46
|
+
return bool(os.environ.get(ENV_KEY)) and self.api_key == os.environ.get(ENV_KEY)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _read() -> dict:
|
|
50
|
+
path = config_path()
|
|
51
|
+
if not path.exists():
|
|
52
|
+
return {"profiles": {}}
|
|
53
|
+
try:
|
|
54
|
+
data = json.loads(path.read_text())
|
|
55
|
+
except (OSError, ValueError):
|
|
56
|
+
return {"profiles": {}}
|
|
57
|
+
data.setdefault("profiles", {})
|
|
58
|
+
return data
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _write(data: dict) -> None:
|
|
62
|
+
path = config_path()
|
|
63
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
64
|
+
# chmod BEFORE the secret is written, so it is never briefly world-readable.
|
|
65
|
+
tmp = path.with_suffix(".tmp")
|
|
66
|
+
tmp.touch(mode=0o600, exist_ok=True)
|
|
67
|
+
os.chmod(tmp, stat.S_IRUSR | stat.S_IWUSR)
|
|
68
|
+
tmp.write_text(json.dumps(data, indent=2) + "\n")
|
|
69
|
+
tmp.replace(path)
|
|
70
|
+
os.chmod(path, stat.S_IRUSR | stat.S_IWUSR)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def profile_name(explicit: str | None = None) -> str:
|
|
74
|
+
return explicit or os.environ.get(ENV_PROFILE) or "default"
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def load_profile(name: str | None = None) -> Profile:
|
|
78
|
+
"""Resolve a profile, with environment variables taking precedence.
|
|
79
|
+
|
|
80
|
+
Env-first is what makes the same binary work in CI without a login step —
|
|
81
|
+
and it means a compromised config file cannot silently override an
|
|
82
|
+
explicitly-provided CI credential.
|
|
83
|
+
"""
|
|
84
|
+
resolved = profile_name(name)
|
|
85
|
+
stored = _read()["profiles"].get(resolved, {})
|
|
86
|
+
return Profile(
|
|
87
|
+
name=resolved,
|
|
88
|
+
origin=(os.environ.get(ENV_ORIGIN) or stored.get("origin") or DEFAULT_ORIGIN).rstrip("/"),
|
|
89
|
+
api_key=os.environ.get(ENV_KEY) or stored.get("api_key"),
|
|
90
|
+
key_name=stored.get("key_name"),
|
|
91
|
+
scopes=stored.get("scopes"),
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def save_profile(profile: Profile) -> None:
|
|
96
|
+
data = _read()
|
|
97
|
+
data["profiles"][profile.name] = {
|
|
98
|
+
"origin": profile.origin,
|
|
99
|
+
"api_key": profile.api_key,
|
|
100
|
+
"key_name": profile.key_name,
|
|
101
|
+
"scopes": profile.scopes,
|
|
102
|
+
}
|
|
103
|
+
_write(data)
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def forget_profile(name: str) -> bool:
|
|
107
|
+
data = _read()
|
|
108
|
+
if name not in data["profiles"]:
|
|
109
|
+
return False
|
|
110
|
+
del data["profiles"][name]
|
|
111
|
+
_write(data)
|
|
112
|
+
return True
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def list_profiles() -> dict[str, dict]:
|
|
116
|
+
return _read()["profiles"]
|
deepsieve_cli/main.py
ADDED
|
@@ -0,0 +1,531 @@
|
|
|
1
|
+
"""`deepsieve` — the DeepSieve CLI.
|
|
2
|
+
|
|
3
|
+
Scope is deliberate. This is the operator's tool: sign in, start and watch
|
|
4
|
+
research, read the cited dataset, wire an agent up. It does NOT manage billing,
|
|
5
|
+
organizations, API keys, or edit an active Blueprint — those are decisions a
|
|
6
|
+
human should make in the app, and a terminal (or an agent driving one) is the
|
|
7
|
+
wrong place to make them irreversibly. The credential it holds is ceilinged at
|
|
8
|
+
the `agent` preset, so the restraint is enforced server-side too, not just by
|
|
9
|
+
the absence of a subcommand.
|
|
10
|
+
|
|
11
|
+
Output: human-readable tables on a TTY, `--json` for pipes and agents. Exit
|
|
12
|
+
codes are stable — 0 success, 1 failure, 2 usage, 3 auth, 4 still-running.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import argparse
|
|
18
|
+
import json
|
|
19
|
+
import os
|
|
20
|
+
import re
|
|
21
|
+
import sys
|
|
22
|
+
import time
|
|
23
|
+
import uuid
|
|
24
|
+
import webbrowser
|
|
25
|
+
from typing import Any
|
|
26
|
+
|
|
27
|
+
from . import config
|
|
28
|
+
from .client import ApiError, Client
|
|
29
|
+
|
|
30
|
+
EXIT_OK, EXIT_FAIL, EXIT_USAGE, EXIT_AUTH, EXIT_PENDING = 0, 1, 2, 3, 4
|
|
31
|
+
|
|
32
|
+
CLIENT_NAME = "DeepSieve CLI"
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
# ── output helpers ───────────────────────────────────────────────────────────
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _tty() -> bool:
|
|
39
|
+
return sys.stdout.isatty()
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def out(data: Any, as_json: bool, human: str | None = None) -> None:
|
|
43
|
+
if as_json or human is None:
|
|
44
|
+
print(json.dumps(data, indent=2, default=str))
|
|
45
|
+
else:
|
|
46
|
+
print(human)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def err(message: str) -> None:
|
|
50
|
+
print(f"error: {message}", file=sys.stderr)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _table(rows: list[list[str]], headers: list[str]) -> str:
|
|
54
|
+
widths = [len(h) for h in headers]
|
|
55
|
+
for r in rows:
|
|
56
|
+
for i, cell in enumerate(r):
|
|
57
|
+
widths[i] = max(widths[i], len(cell))
|
|
58
|
+
line = " ".join(h.upper().ljust(widths[i]) for i, h in enumerate(headers))
|
|
59
|
+
body = [" ".join(c.ljust(widths[i]) for i, c in enumerate(r)) for r in rows]
|
|
60
|
+
return "\n".join([line, *body])
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _client(args) -> tuple[Client, config.Profile]:
|
|
64
|
+
prof = config.load_profile(getattr(args, "profile", None))
|
|
65
|
+
return Client(prof.origin, prof.api_key), prof
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _require_auth(args) -> tuple[Client, config.Profile]:
|
|
69
|
+
client, prof = _client(args)
|
|
70
|
+
if not prof.api_key:
|
|
71
|
+
err(
|
|
72
|
+
f"not signed in for profile '{prof.name}'. Run: deepsieve login"
|
|
73
|
+
+ (f" --profile {prof.name}" if prof.name != "default" else "")
|
|
74
|
+
)
|
|
75
|
+
raise SystemExit(EXIT_AUTH)
|
|
76
|
+
return client, prof
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
# ── commands ─────────────────────────────────────────────────────────────────
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def cmd_login(args) -> int:
|
|
83
|
+
origin = (args.origin or os.environ.get(config.ENV_ORIGIN) or "").rstrip("/")
|
|
84
|
+
name = config.profile_name(args.profile)
|
|
85
|
+
if not origin:
|
|
86
|
+
existing = config.load_profile(name)
|
|
87
|
+
origin = existing.origin
|
|
88
|
+
client = Client(origin)
|
|
89
|
+
|
|
90
|
+
if args.api_key:
|
|
91
|
+
# Headless path. Verify before storing, so a typo fails now rather than
|
|
92
|
+
# on the next command with a confusing 401.
|
|
93
|
+
client.api_key = args.api_key
|
|
94
|
+
try:
|
|
95
|
+
me = client.request("GET", "/v1/me")
|
|
96
|
+
except ApiError as exc:
|
|
97
|
+
err(f"that key did not work against {origin}: {exc}")
|
|
98
|
+
return EXIT_AUTH
|
|
99
|
+
config.save_profile(
|
|
100
|
+
config.Profile(
|
|
101
|
+
name=name,
|
|
102
|
+
origin=origin,
|
|
103
|
+
api_key=args.api_key,
|
|
104
|
+
key_name="(provided)",
|
|
105
|
+
scopes=me.get("scopes"),
|
|
106
|
+
)
|
|
107
|
+
)
|
|
108
|
+
out({"signed_in": True, "profile": name, "origin": origin}, args.json,
|
|
109
|
+
f"Signed in to {origin} as profile '{name}'.")
|
|
110
|
+
return EXIT_OK
|
|
111
|
+
|
|
112
|
+
try:
|
|
113
|
+
auth = client.start_device_authorization(CLIENT_NAME)
|
|
114
|
+
except ApiError as exc:
|
|
115
|
+
err(str(exc))
|
|
116
|
+
return EXIT_FAIL
|
|
117
|
+
|
|
118
|
+
print(f"\n Your code: {auth.user_code}\n")
|
|
119
|
+
print(f" Open: {auth.verification_uri_complete}\n")
|
|
120
|
+
if not args.no_browser:
|
|
121
|
+
try:
|
|
122
|
+
webbrowser.open(auth.verification_uri_complete)
|
|
123
|
+
except Exception: # noqa: BLE001 — a headless box has no browser; the URL is printed
|
|
124
|
+
pass
|
|
125
|
+
print(" Waiting for approval…", end="", flush=True)
|
|
126
|
+
|
|
127
|
+
try:
|
|
128
|
+
cred = client.poll_for_credential(auth, on_tick=lambda: print(".", end="", flush=True))
|
|
129
|
+
except ApiError as exc:
|
|
130
|
+
print()
|
|
131
|
+
err(str(exc))
|
|
132
|
+
return EXIT_AUTH
|
|
133
|
+
print()
|
|
134
|
+
|
|
135
|
+
config.save_profile(
|
|
136
|
+
config.Profile(
|
|
137
|
+
name=name,
|
|
138
|
+
origin=origin,
|
|
139
|
+
api_key=cred["api_key"],
|
|
140
|
+
key_name=cred.get("key_name"),
|
|
141
|
+
scopes=cred.get("scopes"),
|
|
142
|
+
)
|
|
143
|
+
)
|
|
144
|
+
print(f"\n Signed in to {origin} as profile '{name}'.")
|
|
145
|
+
print(f" Credential: {cred.get('key_name')} — revoke any time at {origin}/settings/api-keys\n")
|
|
146
|
+
return EXIT_OK
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def cmd_logout(args) -> int:
|
|
150
|
+
name = config.profile_name(args.profile)
|
|
151
|
+
removed = config.forget_profile(name)
|
|
152
|
+
msg = (
|
|
153
|
+
f"Signed out of profile '{name}'. The API key still exists — revoke it at "
|
|
154
|
+
"/settings/api-keys if it should stop working."
|
|
155
|
+
if removed
|
|
156
|
+
else f"No stored credential for profile '{name}'."
|
|
157
|
+
)
|
|
158
|
+
out({"signed_out": removed, "profile": name}, args.json, msg)
|
|
159
|
+
return EXIT_OK
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def cmd_whoami(args) -> int:
|
|
163
|
+
client, prof = _require_auth(args)
|
|
164
|
+
try:
|
|
165
|
+
me = client.request("GET", "/v1/me")
|
|
166
|
+
except ApiError as exc:
|
|
167
|
+
err(str(exc))
|
|
168
|
+
return EXIT_AUTH
|
|
169
|
+
if args.json:
|
|
170
|
+
out(me, True)
|
|
171
|
+
return EXIT_OK
|
|
172
|
+
print(f"profile {prof.name}")
|
|
173
|
+
print(f"origin {prof.origin}")
|
|
174
|
+
print(f"org {me.get('org_id')}")
|
|
175
|
+
print(f"workspace {me.get('workspace_schema')}")
|
|
176
|
+
print(f"role {me.get('role')}")
|
|
177
|
+
print(f"scopes {', '.join(me.get('scopes') or []) or '(full)'}")
|
|
178
|
+
return EXIT_OK
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def cmd_profiles(args) -> int:
|
|
182
|
+
profiles = config.list_profiles()
|
|
183
|
+
active = config.profile_name(args.profile)
|
|
184
|
+
if args.json:
|
|
185
|
+
out({k: {"origin": v.get("origin"), "active": k == active} for k, v in profiles.items()}, True)
|
|
186
|
+
return EXIT_OK
|
|
187
|
+
if not profiles:
|
|
188
|
+
print("No profiles yet. Run: deepsieve login")
|
|
189
|
+
return EXIT_OK
|
|
190
|
+
rows = [
|
|
191
|
+
[("* " if k == active else " ") + k, v.get("origin", ""), v.get("key_name") or ""]
|
|
192
|
+
for k, v in sorted(profiles.items())
|
|
193
|
+
]
|
|
194
|
+
print(_table(rows, ["profile", "origin", "credential"]))
|
|
195
|
+
return EXIT_OK
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def cmd_runs_list(args) -> int:
|
|
199
|
+
client, _ = _require_auth(args)
|
|
200
|
+
params: dict[str, Any] = {"limit": args.limit}
|
|
201
|
+
if args.status:
|
|
202
|
+
params["status"] = args.status
|
|
203
|
+
try:
|
|
204
|
+
body = client.request("GET", "/v1/research/runs", params=params)
|
|
205
|
+
except ApiError as exc:
|
|
206
|
+
err(str(exc))
|
|
207
|
+
return EXIT_FAIL
|
|
208
|
+
if args.json:
|
|
209
|
+
out(body, True)
|
|
210
|
+
return EXIT_OK
|
|
211
|
+
rows = [
|
|
212
|
+
[r.get("id", "")[:8], r.get("status", ""), str(r.get("created_at", ""))[:19],
|
|
213
|
+
(r.get("title") or r.get("query") or "")[:48]]
|
|
214
|
+
for r in body.get("data", [])
|
|
215
|
+
]
|
|
216
|
+
print(_table(rows, ["id", "status", "created", "title"]) if rows else "No runs yet.")
|
|
217
|
+
return EXIT_OK
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def cmd_runs_get(args) -> int:
|
|
221
|
+
client, _ = _require_auth(args)
|
|
222
|
+
try:
|
|
223
|
+
body = client.request("GET", f"/v1/research/runs/{args.run_id}")
|
|
224
|
+
except ApiError as exc:
|
|
225
|
+
err(str(exc))
|
|
226
|
+
return EXIT_FAIL
|
|
227
|
+
if args.json:
|
|
228
|
+
out(body, True)
|
|
229
|
+
else:
|
|
230
|
+
print(f"id {body.get('id')}")
|
|
231
|
+
print(f"status {body.get('status')} (done={body.get('done')})")
|
|
232
|
+
if body.get("error"):
|
|
233
|
+
print(f"error {body['error'].get('message')}")
|
|
234
|
+
if not body.get("done"):
|
|
235
|
+
return EXIT_PENDING
|
|
236
|
+
return EXIT_OK if body.get("status") == "completed" else EXIT_FAIL
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
def cmd_runs_create(args) -> int:
|
|
240
|
+
client, prof = _require_auth(args)
|
|
241
|
+
if bool(args.query) == bool(args.seeds):
|
|
242
|
+
err("pass exactly one of --query or --seeds")
|
|
243
|
+
return EXIT_USAGE
|
|
244
|
+
|
|
245
|
+
body: dict[str, Any] = {"depth": args.depth, "dry_run": args.dry_run, "monitored": args.monitored}
|
|
246
|
+
if args.query:
|
|
247
|
+
body["query"] = args.query
|
|
248
|
+
if args.seeds:
|
|
249
|
+
body["seeds"] = [s.strip() for s in args.seeds.split(",") if s.strip()]
|
|
250
|
+
|
|
251
|
+
# A real run spends money. Confirm interactively unless told not to — and
|
|
252
|
+
# never silently in a pipe, where nobody is watching.
|
|
253
|
+
if not args.dry_run and not args.yes:
|
|
254
|
+
if not _tty():
|
|
255
|
+
err("refusing to start a billable run non-interactively; pass --yes (or --dry-run)")
|
|
256
|
+
return EXIT_USAGE
|
|
257
|
+
print(f"This starts a REAL research run on {prof.origin} — it spends credits "
|
|
258
|
+
f"and takes 15-60 minutes.")
|
|
259
|
+
if input("Continue? [y/N] ").strip().lower() not in ("y", "yes"):
|
|
260
|
+
print("Cancelled.")
|
|
261
|
+
return EXIT_OK
|
|
262
|
+
|
|
263
|
+
try:
|
|
264
|
+
created = client.request(
|
|
265
|
+
"POST",
|
|
266
|
+
"/v1/research/runs",
|
|
267
|
+
json_body=body,
|
|
268
|
+
headers={"Idempotency-Key": args.idempotency_key or uuid.uuid4().hex},
|
|
269
|
+
)
|
|
270
|
+
except ApiError as exc:
|
|
271
|
+
err(str(exc))
|
|
272
|
+
return EXIT_FAIL
|
|
273
|
+
|
|
274
|
+
if args.json and not args.wait:
|
|
275
|
+
out(created, True)
|
|
276
|
+
return EXIT_OK
|
|
277
|
+
run_id = created.get("id")
|
|
278
|
+
print(f"started run {run_id} ({'dry run' if args.dry_run else 'billable'})")
|
|
279
|
+
if not args.wait:
|
|
280
|
+
print(f"poll with: deepsieve runs get {run_id}")
|
|
281
|
+
return EXIT_OK
|
|
282
|
+
return _wait(client, run_id, args)
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
def cmd_runs_wait(args) -> int:
|
|
286
|
+
client, _ = _require_auth(args)
|
|
287
|
+
return _wait(client, args.run_id, args)
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
def _wait(client: Client, run_id: str, args) -> int:
|
|
291
|
+
"""Poll to completion, honouring Retry-After semantics via a fixed floor."""
|
|
292
|
+
deadline = time.monotonic() + args.timeout
|
|
293
|
+
while time.monotonic() < deadline:
|
|
294
|
+
try:
|
|
295
|
+
body = client.request("GET", f"/v1/research/runs/{run_id}")
|
|
296
|
+
except ApiError as exc:
|
|
297
|
+
err(str(exc))
|
|
298
|
+
return EXIT_FAIL
|
|
299
|
+
if body.get("done"):
|
|
300
|
+
if args.json:
|
|
301
|
+
out(body, True)
|
|
302
|
+
else:
|
|
303
|
+
print(f"run {run_id} finished: {body.get('status')}")
|
|
304
|
+
if body.get("error"):
|
|
305
|
+
print(f" {body['error'].get('message')}")
|
|
306
|
+
return EXIT_OK if body.get("status") == "completed" else EXIT_FAIL
|
|
307
|
+
if not args.json and _tty():
|
|
308
|
+
print(f" status: {body.get('status')}…", end="\r", flush=True)
|
|
309
|
+
time.sleep(args.interval)
|
|
310
|
+
err(f"still running after {args.timeout}s; poll with: deepsieve runs get {run_id}")
|
|
311
|
+
return EXIT_PENDING
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
def cmd_runs_cancel(args) -> int:
|
|
315
|
+
client, _ = _require_auth(args)
|
|
316
|
+
try:
|
|
317
|
+
body = client.request("POST", f"/v1/research/runs/{args.run_id}/cancel")
|
|
318
|
+
except ApiError as exc:
|
|
319
|
+
err(str(exc))
|
|
320
|
+
return EXIT_FAIL
|
|
321
|
+
out(body, args.json, f"cancellation requested for {args.run_id}")
|
|
322
|
+
return EXIT_OK
|
|
323
|
+
|
|
324
|
+
|
|
325
|
+
def cmd_data_catalog(args) -> int:
|
|
326
|
+
client, _ = _require_auth(args)
|
|
327
|
+
try:
|
|
328
|
+
body = client.request("GET", "/v1/data")
|
|
329
|
+
except ApiError as exc:
|
|
330
|
+
err(str(exc))
|
|
331
|
+
return EXIT_FAIL
|
|
332
|
+
if args.json:
|
|
333
|
+
out(body, True)
|
|
334
|
+
return EXIT_OK
|
|
335
|
+
for ent in body.get("entities", []):
|
|
336
|
+
cols = ", ".join(c.get("key", "") for c in ent.get("columns", []))
|
|
337
|
+
print(f"{ent.get('key')} ({ent.get('label', '')})\n {cols}\n")
|
|
338
|
+
return EXIT_OK
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
def cmd_data_get(args) -> int:
|
|
342
|
+
client, _ = _require_auth(args)
|
|
343
|
+
params: dict[str, Any] = {"limit": args.limit, "receipts": args.receipts}
|
|
344
|
+
if args.cursor:
|
|
345
|
+
params["cursor"] = args.cursor
|
|
346
|
+
if args.updated_since:
|
|
347
|
+
params["updated_since"] = args.updated_since
|
|
348
|
+
try:
|
|
349
|
+
body = client.request("GET", f"/v1/data/{args.entity}", params=params)
|
|
350
|
+
except ApiError as exc:
|
|
351
|
+
err(str(exc))
|
|
352
|
+
return EXIT_FAIL
|
|
353
|
+
out(body, True) # rows are structured either way; --json only stills the notes
|
|
354
|
+
if body.get("truncated") and not args.json:
|
|
355
|
+
print(
|
|
356
|
+
"\nnote: this response is a PREVIEW, not the full dataset "
|
|
357
|
+
f"(preview_row_cap={body.get('preview_row_cap')}).",
|
|
358
|
+
file=sys.stderr,
|
|
359
|
+
)
|
|
360
|
+
return EXIT_OK
|
|
361
|
+
|
|
362
|
+
|
|
363
|
+
def _remote_server_name(prof) -> str:
|
|
364
|
+
"""The MCP server name this deployment tells agents to register.
|
|
365
|
+
|
|
366
|
+
Read from the deployment's own published setup payload, which is
|
|
367
|
+
unauthenticated and carries the canonical registration line. That matters:
|
|
368
|
+
the obvious source (`/api/system/status`) requires a session, so it 401s in
|
|
369
|
+
exactly the situation this command exists for — wiring up an agent before
|
|
370
|
+
anyone has signed in.
|
|
371
|
+
|
|
372
|
+
The fallback is the brand default, never a slice of the hostname: emitting
|
|
373
|
+
"staging" for staging.example.com drops the brand and collides with every
|
|
374
|
+
other deployment whose first label happens to match.
|
|
375
|
+
"""
|
|
376
|
+
try:
|
|
377
|
+
body = Client(prof.origin).request("GET", "/setup.md", raw=True)
|
|
378
|
+
m = re.search(r"claude mcp add --transport http (\S+) ", body or "")
|
|
379
|
+
if m:
|
|
380
|
+
return m.group(1)
|
|
381
|
+
except Exception: # noqa: BLE001 — naming must never block printing config
|
|
382
|
+
pass
|
|
383
|
+
return "deepsieve"
|
|
384
|
+
|
|
385
|
+
|
|
386
|
+
def cmd_setup_mcp(args) -> int:
|
|
387
|
+
"""Print (or install) the MCP registration for this profile's origin."""
|
|
388
|
+
_, prof = _client(args)
|
|
389
|
+
url = f"{prof.origin}/mcp"
|
|
390
|
+
# Ask the deployment what it calls itself rather than assuming "deepsieve":
|
|
391
|
+
# hardcoding it here would print a staging registration that overwrites the
|
|
392
|
+
# production entry — the exact collision the env-aware naming fixed.
|
|
393
|
+
name = args.name or _remote_server_name(prof)
|
|
394
|
+
if args.json:
|
|
395
|
+
out({"mcpServers": {name: {"url": url}}}, True)
|
|
396
|
+
return EXIT_OK
|
|
397
|
+
print(f"Register the {name} MCP server (browser login, no key needed):\n")
|
|
398
|
+
print(f" claude mcp add --transport http {name} {url}\n")
|
|
399
|
+
print("Or add to your agent's MCP config:\n")
|
|
400
|
+
print(json.dumps({"mcpServers": {name: {"url": url}}}, indent=2))
|
|
401
|
+
print(f"\nFull setup, including skills and rules: {prof.origin}/setup.md")
|
|
402
|
+
return EXIT_OK
|
|
403
|
+
|
|
404
|
+
|
|
405
|
+
# ── parser ───────────────────────────────────────────────────────────────────
|
|
406
|
+
|
|
407
|
+
|
|
408
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
409
|
+
# The global flags live on a shared PARENT parser as well as the root, so
|
|
410
|
+
# both `deepsieve --json runs list` and `deepsieve runs list --json` work.
|
|
411
|
+
# argparse subparsers do not inherit root options, and the docs promise
|
|
412
|
+
# "add --json to any command" — the trailing form is the one people type.
|
|
413
|
+
# SUPPRESS is load-bearing: without it the subparser re-applies its own
|
|
414
|
+
# default and CLOBBERS a value given before the subcommand, so
|
|
415
|
+
# `deepsieve --json data get x` would silently print human output.
|
|
416
|
+
common = argparse.ArgumentParser(add_help=False)
|
|
417
|
+
common.add_argument(
|
|
418
|
+
"--profile",
|
|
419
|
+
default=argparse.SUPPRESS,
|
|
420
|
+
help="named profile (default: $DEEPSIEVE_PROFILE or 'default')",
|
|
421
|
+
)
|
|
422
|
+
common.add_argument(
|
|
423
|
+
"--json",
|
|
424
|
+
action="store_true",
|
|
425
|
+
default=argparse.SUPPRESS,
|
|
426
|
+
help="machine-readable output",
|
|
427
|
+
)
|
|
428
|
+
|
|
429
|
+
p = argparse.ArgumentParser(
|
|
430
|
+
prog="deepsieve",
|
|
431
|
+
description="Run cited deep research from your terminal.",
|
|
432
|
+
epilog="Docs: https://deepsieve.ai/developers/cli",
|
|
433
|
+
parents=[common],
|
|
434
|
+
)
|
|
435
|
+
sub = p.add_subparsers(dest="command", required=True)
|
|
436
|
+
|
|
437
|
+
lg = sub.add_parser("login", help="sign in via your browser (device flow)", parents=[common])
|
|
438
|
+
lg.add_argument("--origin", help="deployment URL (e.g. https://staging.deepsieve.ai)")
|
|
439
|
+
lg.add_argument("--api-key", help="headless: use a key instead of the browser flow")
|
|
440
|
+
lg.add_argument("--no-browser", action="store_true", help="print the URL, don't open it")
|
|
441
|
+
lg.set_defaults(func=cmd_login)
|
|
442
|
+
|
|
443
|
+
lo = sub.add_parser("logout", help="forget this profile's stored credential", parents=[common])
|
|
444
|
+
lo.set_defaults(func=cmd_logout)
|
|
445
|
+
|
|
446
|
+
wa = sub.add_parser("whoami", help="show the resolved identity, workspace and scopes", parents=[common])
|
|
447
|
+
wa.set_defaults(func=cmd_whoami)
|
|
448
|
+
|
|
449
|
+
pr = sub.add_parser("profiles", help="list configured profiles", parents=[common])
|
|
450
|
+
pr.set_defaults(func=cmd_profiles)
|
|
451
|
+
|
|
452
|
+
runs = sub.add_parser("runs", help="research runs", parents=[common]).add_subparsers(dest="sub", required=True)
|
|
453
|
+
|
|
454
|
+
rl = runs.add_parser("list", help="recent runs", parents=[common])
|
|
455
|
+
rl.add_argument("--status")
|
|
456
|
+
rl.add_argument("--limit", type=int, default=20)
|
|
457
|
+
rl.set_defaults(func=cmd_runs_list)
|
|
458
|
+
|
|
459
|
+
rg = runs.add_parser("get", help="one run's status", parents=[common])
|
|
460
|
+
rg.add_argument("run_id")
|
|
461
|
+
rg.set_defaults(func=cmd_runs_get)
|
|
462
|
+
|
|
463
|
+
rc = runs.add_parser("create", help="start a run", parents=[common])
|
|
464
|
+
rc.add_argument("--query")
|
|
465
|
+
rc.add_argument("--seeds", help="comma-separated names or URLs")
|
|
466
|
+
rc.add_argument("--depth", default="standard", choices=["standard", "max"])
|
|
467
|
+
rc.add_argument("--dry-run", action="store_true", help="free simulated run (~15s)")
|
|
468
|
+
rc.add_argument("--no-monitor", dest="monitored", action="store_false",
|
|
469
|
+
help="one-off; don't bill ongoing freshness")
|
|
470
|
+
rc.add_argument("--wait", action="store_true", help="poll until it finishes")
|
|
471
|
+
rc.add_argument("--interval", type=int, default=15)
|
|
472
|
+
rc.add_argument("--timeout", type=int, default=3600)
|
|
473
|
+
rc.add_argument("--idempotency-key")
|
|
474
|
+
rc.add_argument("-y", "--yes", action="store_true", help="skip the billable-run confirmation")
|
|
475
|
+
rc.set_defaults(func=cmd_runs_create)
|
|
476
|
+
|
|
477
|
+
rw = runs.add_parser("wait", help="poll an existing run to completion", parents=[common])
|
|
478
|
+
rw.add_argument("run_id")
|
|
479
|
+
rw.add_argument("--interval", type=int, default=15)
|
|
480
|
+
rw.add_argument("--timeout", type=int, default=3600)
|
|
481
|
+
rw.set_defaults(func=cmd_runs_wait)
|
|
482
|
+
|
|
483
|
+
rx = runs.add_parser("cancel", help="cancel an in-flight run", parents=[common])
|
|
484
|
+
rx.add_argument("run_id")
|
|
485
|
+
rx.set_defaults(func=cmd_runs_cancel)
|
|
486
|
+
|
|
487
|
+
data = sub.add_parser("data", help="your cited dataset", parents=[common]).add_subparsers(dest="sub", required=True)
|
|
488
|
+
|
|
489
|
+
dc = data.add_parser("catalog", help="entities and columns (never guess these)", parents=[common])
|
|
490
|
+
dc.set_defaults(func=cmd_data_catalog)
|
|
491
|
+
|
|
492
|
+
dg = data.add_parser("get", help="rows for one entity", parents=[common])
|
|
493
|
+
dg.add_argument("entity")
|
|
494
|
+
dg.add_argument("--limit", type=int, default=25)
|
|
495
|
+
dg.add_argument("--cursor")
|
|
496
|
+
dg.add_argument("--updated-since")
|
|
497
|
+
dg.add_argument("--receipts", action="store_true", help="include per-cell citations")
|
|
498
|
+
dg.set_defaults(func=cmd_data_get)
|
|
499
|
+
|
|
500
|
+
setup = sub.add_parser("setup", help="connect other tools", parents=[common]).add_subparsers(
|
|
501
|
+
dest="sub", required=True
|
|
502
|
+
)
|
|
503
|
+
sm = setup.add_parser("mcp", help="MCP server registration for this origin", parents=[common])
|
|
504
|
+
sm.add_argument("--name", help="server name to register")
|
|
505
|
+
sm.set_defaults(func=cmd_setup_mcp)
|
|
506
|
+
|
|
507
|
+
return p
|
|
508
|
+
|
|
509
|
+
|
|
510
|
+
def main(argv: list[str] | None = None) -> int:
|
|
511
|
+
parser = build_parser()
|
|
512
|
+
args = parser.parse_args(argv)
|
|
513
|
+
# SUPPRESS means the attrs only exist when passed; normalise once here so
|
|
514
|
+
# every command can read them unconditionally.
|
|
515
|
+
for attr, default in (("json", False), ("profile", None)):
|
|
516
|
+
if not hasattr(args, attr):
|
|
517
|
+
setattr(args, attr, default)
|
|
518
|
+
try:
|
|
519
|
+
return args.func(args)
|
|
520
|
+
except SystemExit as exc: # _require_auth and friends
|
|
521
|
+
return int(exc.code) if exc.code is not None else EXIT_FAIL
|
|
522
|
+
except KeyboardInterrupt:
|
|
523
|
+
print()
|
|
524
|
+
return EXIT_FAIL
|
|
525
|
+
except ApiError as exc:
|
|
526
|
+
err(str(exc))
|
|
527
|
+
return EXIT_FAIL
|
|
528
|
+
|
|
529
|
+
|
|
530
|
+
if __name__ == "__main__": # pragma: no cover
|
|
531
|
+
sys.exit(main())
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: deepsieve-cli
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: DeepSieve CLI — run cited deep research from your terminal
|
|
5
|
+
Requires-Python: >=3.11
|
|
6
|
+
Requires-Dist: httpx>=0.27
|
|
7
|
+
Description-Content-Type: text/markdown
|
|
8
|
+
|
|
9
|
+
# deepsieve-cli
|
|
10
|
+
|
|
11
|
+
Run cited deep research from your terminal.
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
uv tool install deepsieve-cli # or: pipx install deepsieve-cli
|
|
15
|
+
deepsieve login
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
`login` opens your browser, you approve a short code, and the CLI stores a
|
|
19
|
+
scoped API key. Nothing to copy, nothing pasted into your shell history. The
|
|
20
|
+
credential appears in **Settings → API keys** on your deployment and can be
|
|
21
|
+
revoked there at any time.
|
|
22
|
+
|
|
23
|
+
## Commands
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
deepsieve whoami # identity, workspace, scopes
|
|
27
|
+
deepsieve runs create --query "..." --dry-run # free simulated run (~15s)
|
|
28
|
+
deepsieve runs create --query "..." --wait # real run: spends credits
|
|
29
|
+
deepsieve runs list
|
|
30
|
+
deepsieve runs get <id> # exit 4 while still running
|
|
31
|
+
deepsieve runs cancel <id>
|
|
32
|
+
deepsieve data catalog # entities + columns (never guess)
|
|
33
|
+
deepsieve data get companies --receipts # rows with per-cell citations
|
|
34
|
+
deepsieve setup mcp # MCP registration for this origin
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Add `--json` to anything for machine-readable output.
|
|
38
|
+
|
|
39
|
+
## Profiles: several deployments side by side
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
deepsieve login --profile staging --origin https://staging.deepsieve.ai
|
|
43
|
+
deepsieve --profile staging runs list
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Each profile is its own `(origin, credential)` pair, so staging and production
|
|
47
|
+
never overwrite one another. `DEEPSIEVE_PROFILE` sets the default.
|
|
48
|
+
|
|
49
|
+
## CI and containers
|
|
50
|
+
|
|
51
|
+
Skip `login` entirely — set `DEEPSIEVE_API_KEY` (and `DEEPSIEVE_BASE_URL` if not
|
|
52
|
+
production). Environment variables take precedence over any stored profile.
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
DEEPSIEVE_API_KEY=ds_live_... deepsieve --json data get companies
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## What it deliberately cannot do
|
|
59
|
+
|
|
60
|
+
No billing, no organization or member management, no API-key management, and it
|
|
61
|
+
cannot edit an active Blueprint. Those are decisions for a human in the app —
|
|
62
|
+
and the restriction is enforced server-side by the credential's scopes, not just
|
|
63
|
+
by the absence of a subcommand.
|
|
64
|
+
|
|
65
|
+
Starting a real (billable) run asks for confirmation, and refuses outright in a
|
|
66
|
+
non-interactive shell unless you pass `--yes`.
|
|
67
|
+
|
|
68
|
+
## Exit codes
|
|
69
|
+
|
|
70
|
+
| Code | Meaning |
|
|
71
|
+
|---|---|
|
|
72
|
+
| 0 | success |
|
|
73
|
+
| 1 | failure |
|
|
74
|
+
| 2 | usage error |
|
|
75
|
+
| 3 | not authenticated |
|
|
76
|
+
| 4 | run still in progress |
|
|
77
|
+
|
|
78
|
+
Full docs: <https://deepsieve.ai/developers/cli>
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
deepsieve_cli/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
+
deepsieve_cli/__main__.py,sha256=I0WVsFDX16-IxsgTiX1tUcx9ypuiTkokIaLK2fU5RpI,53
|
|
3
|
+
deepsieve_cli/client.py,sha256=jcQAZvMm_mLkC8iIBkSSP_94zY5ry8bVdXQwOw6NkYo,5679
|
|
4
|
+
deepsieve_cli/config.py,sha256=YygQfIs0u03BXuU-NQvL0GaDhkmRd2_uvOpis4i74xw,3453
|
|
5
|
+
deepsieve_cli/main.py,sha256=8hL5eFWM420imOIRE7NAulMRCakxHgMauDhdr3fTRVI,19937
|
|
6
|
+
deepsieve_cli-0.1.0.dist-info/METADATA,sha256=JYciuIPwIqA1aPYHOqDwbRukx6ajq5B7tfdBIR0STd0,2515
|
|
7
|
+
deepsieve_cli-0.1.0.dist-info/WHEEL,sha256=lCkmxWfQsSc9CfIClYeavTdQeEX2toPqufh9gI35EQA,87
|
|
8
|
+
deepsieve_cli-0.1.0.dist-info/entry_points.txt,sha256=yJCcmxJr4LGXwLTa8j4QfSM_rNx_0fvE0cttFyi9OhM,54
|
|
9
|
+
deepsieve_cli-0.1.0.dist-info/RECORD,,
|