cyborgy 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.
- cyborgy-0.1.0/MANIFEST.in +1 -0
- cyborgy-0.1.0/PKG-INFO +55 -0
- cyborgy-0.1.0/README.md +33 -0
- cyborgy-0.1.0/cyborgy/__init__.py +3 -0
- cyborgy-0.1.0/cyborgy/api.py +311 -0
- cyborgy-0.1.0/cyborgy/auth.py +48 -0
- cyborgy-0.1.0/cyborgy/cli.py +527 -0
- cyborgy-0.1.0/cyborgy/config.py +103 -0
- cyborgy-0.1.0/cyborgy/install.py +350 -0
- cyborgy-0.1.0/cyborgy/mcp_server.py +757 -0
- cyborgy-0.1.0/cyborgy/project.py +98 -0
- cyborgy-0.1.0/cyborgy/worker.py +1443 -0
- cyborgy-0.1.0/cyborgy.egg-info/PKG-INFO +55 -0
- cyborgy-0.1.0/cyborgy.egg-info/SOURCES.txt +18 -0
- cyborgy-0.1.0/cyborgy.egg-info/dependency_links.txt +1 -0
- cyborgy-0.1.0/cyborgy.egg-info/entry_points.txt +5 -0
- cyborgy-0.1.0/cyborgy.egg-info/requires.txt +3 -0
- cyborgy-0.1.0/cyborgy.egg-info/top_level.txt +1 -0
- cyborgy-0.1.0/pyproject.toml +43 -0
- cyborgy-0.1.0/setup.cfg +4 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
prune tests
|
cyborgy-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: cyborgy
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Extend the claude and codex CLIs: skills marketplace, memory, and extension APIs via the Cyborgy MCP.
|
|
5
|
+
Author: Kizuna Intelligence
|
|
6
|
+
Project-URL: Homepage, https://github.com/kizuna-intelligence/cyborgy
|
|
7
|
+
Project-URL: Repository, https://github.com/kizuna-intelligence/cyborgy
|
|
8
|
+
Keywords: cyborgy,codex,claude,mcp,skills,agents
|
|
9
|
+
Classifier: Development Status :: 3 - Alpha
|
|
10
|
+
Classifier: Environment :: Console
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Classifier: Topic :: Software Development
|
|
17
|
+
Requires-Python: >=3.10
|
|
18
|
+
Description-Content-Type: text/markdown
|
|
19
|
+
Requires-Dist: httpx>=0.27
|
|
20
|
+
Requires-Dist: mcp>=1.2
|
|
21
|
+
Requires-Dist: pynacl>=1.5
|
|
22
|
+
|
|
23
|
+
# Cyborgy CLI
|
|
24
|
+
|
|
25
|
+
`cyborgy` extends coding-agent CLIs such as Claude Code and Codex with the
|
|
26
|
+
Cyborgy MCP, workspace roles, skills, memory, extension APIs, and temporary
|
|
27
|
+
tunnels.
|
|
28
|
+
|
|
29
|
+
This package provides:
|
|
30
|
+
|
|
31
|
+
- `cyborgy`: the main CLI.
|
|
32
|
+
- `cyborgy-mcp`: the MCP server used by supported coding agents.
|
|
33
|
+
- `cyborgy-dev` and `cyborgy-mcp-dev`: development-environment entry points.
|
|
34
|
+
|
|
35
|
+
## Install
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
pip install cyborgy
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Basic usage
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
cyborgy login
|
|
45
|
+
cyborgy mcp install --target codex
|
|
46
|
+
cyborgy codex exec "summarize this repository"
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
The CLI stores login state locally, installs the Cyborgy MCP configuration into
|
|
50
|
+
the current repository, and syncs the skills assigned to that workspace.
|
|
51
|
+
|
|
52
|
+
## Status
|
|
53
|
+
|
|
54
|
+
This is an alpha package. The public CLI surface is expected to evolve with the
|
|
55
|
+
Cyborgy service.
|
cyborgy-0.1.0/README.md
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# Cyborgy CLI
|
|
2
|
+
|
|
3
|
+
`cyborgy` extends coding-agent CLIs such as Claude Code and Codex with the
|
|
4
|
+
Cyborgy MCP, workspace roles, skills, memory, extension APIs, and temporary
|
|
5
|
+
tunnels.
|
|
6
|
+
|
|
7
|
+
This package provides:
|
|
8
|
+
|
|
9
|
+
- `cyborgy`: the main CLI.
|
|
10
|
+
- `cyborgy-mcp`: the MCP server used by supported coding agents.
|
|
11
|
+
- `cyborgy-dev` and `cyborgy-mcp-dev`: development-environment entry points.
|
|
12
|
+
|
|
13
|
+
## Install
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
pip install cyborgy
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Basic usage
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
cyborgy login
|
|
23
|
+
cyborgy mcp install --target codex
|
|
24
|
+
cyborgy codex exec "summarize this repository"
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
The CLI stores login state locally, installs the Cyborgy MCP configuration into
|
|
28
|
+
the current repository, and syncs the skills assigned to that workspace.
|
|
29
|
+
|
|
30
|
+
## Status
|
|
31
|
+
|
|
32
|
+
This is an alpha package. The public CLI surface is expected to evolve with the
|
|
33
|
+
Cyborgy service.
|
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
"""Thin HTTP client over the Cyborgy backend. Shared by the CLI and the MCP."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import httpx
|
|
5
|
+
|
|
6
|
+
from . import config
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class ApiError(RuntimeError):
|
|
10
|
+
pass
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _media_body(url, question, data, filename, mime_type) -> dict:
|
|
14
|
+
body: dict = {"url": url or "", "question": question}
|
|
15
|
+
if data:
|
|
16
|
+
body["data"] = data
|
|
17
|
+
if filename:
|
|
18
|
+
body["filename"] = filename
|
|
19
|
+
if mime_type:
|
|
20
|
+
body["mime_type"] = mime_type
|
|
21
|
+
return body
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class Api:
|
|
25
|
+
def __init__(self, token: str | None = None, base_url: str | None = None) -> None:
|
|
26
|
+
self.base_url = (base_url or config.api_url()).rstrip("/")
|
|
27
|
+
self.token = token if token is not None else config.get_token()
|
|
28
|
+
|
|
29
|
+
def _headers(self) -> dict:
|
|
30
|
+
return {"Authorization": f"Bearer {self.token}"} if self.token else {}
|
|
31
|
+
|
|
32
|
+
def _req(self, method: str, path: str, **kw) -> httpx.Response:
|
|
33
|
+
kw.setdefault("timeout", 120)
|
|
34
|
+
resp = httpx.request(method, f"{self.base_url}{path}", headers=self._headers(), **kw)
|
|
35
|
+
if resp.status_code >= 400:
|
|
36
|
+
raise ApiError(f"{method} {path} -> {resp.status_code}: {resp.text}")
|
|
37
|
+
return resp
|
|
38
|
+
|
|
39
|
+
# --- auth (device flow) ---
|
|
40
|
+
def device_start(self, name: str = "", host: str = "") -> dict:
|
|
41
|
+
return self._req("POST", "/auth/device/start", json={"name": name, "host": host}).json()
|
|
42
|
+
|
|
43
|
+
def device_token(self, device_code: str) -> httpx.Response:
|
|
44
|
+
# caller inspects status (428 = keep polling); don't raise here.
|
|
45
|
+
return httpx.post(f"{self.base_url}/auth/device/token", json={"device_code": device_code}, timeout=30)
|
|
46
|
+
|
|
47
|
+
def me(self) -> dict:
|
|
48
|
+
return self._req("GET", "/auth/me").json()
|
|
49
|
+
|
|
50
|
+
# --- ephemeral tunnels ---
|
|
51
|
+
def tunnel_config(self) -> dict:
|
|
52
|
+
return self._req("POST", "/me/tunnel/config").json()
|
|
53
|
+
|
|
54
|
+
# --- skills ---
|
|
55
|
+
def search_skills(self, q: str | None = None, tag: str | None = None) -> list[dict]:
|
|
56
|
+
params = {k: v for k, v in {"q": q, "tag": tag}.items() if v}
|
|
57
|
+
return self._req("GET", "/skills", params=params).json()
|
|
58
|
+
|
|
59
|
+
def get_skill(self, skill_id: str) -> dict:
|
|
60
|
+
return self._req("GET", f"/skills/{skill_id}").json()
|
|
61
|
+
|
|
62
|
+
def get_skill_files(self, skill_id: str) -> dict:
|
|
63
|
+
return self._req("GET", f"/skills/{skill_id}/files").json()
|
|
64
|
+
|
|
65
|
+
def create_skill(self, payload: dict) -> dict:
|
|
66
|
+
return self._req("POST", "/skills", json=payload).json()
|
|
67
|
+
|
|
68
|
+
def update_skill(self, skill_id: str, patch: dict) -> dict:
|
|
69
|
+
return self._req("PUT", f"/skills/{skill_id}", json=patch).json()
|
|
70
|
+
|
|
71
|
+
def put_skill_files(self, skill_id: str, files: list[dict], message: str = "update") -> dict:
|
|
72
|
+
return self._req("PUT", f"/skills/{skill_id}/files", json={"files": files, "message": message}).json()
|
|
73
|
+
|
|
74
|
+
def delete_skill(self, skill_id: str) -> None:
|
|
75
|
+
self._req("DELETE", f"/skills/{skill_id}")
|
|
76
|
+
|
|
77
|
+
# --- my skills ---
|
|
78
|
+
def my_skills(self) -> list[dict]:
|
|
79
|
+
return self._req("GET", "/me/skills").json()
|
|
80
|
+
|
|
81
|
+
def my_owned(self) -> list[dict]:
|
|
82
|
+
return self._req("GET", "/me/skills/owned").json()
|
|
83
|
+
|
|
84
|
+
def add_favorite(self, skill_id: str) -> None:
|
|
85
|
+
self._req("PUT", f"/me/skills/{skill_id}")
|
|
86
|
+
|
|
87
|
+
# --- api catalog ---
|
|
88
|
+
def list_apis(self, q: str | None = None, tag: str | None = None) -> list[dict]:
|
|
89
|
+
params = {k: v for k, v in {"q": q, "tag": tag}.items() if v}
|
|
90
|
+
return self._req("GET", "/apis", params=params).json()
|
|
91
|
+
|
|
92
|
+
def get_api(self, api_id: str) -> dict:
|
|
93
|
+
return self._req("GET", f"/apis/{api_id}").json()
|
|
94
|
+
|
|
95
|
+
def my_apis(self) -> list[dict]:
|
|
96
|
+
return self._req("GET", "/me/apis").json()
|
|
97
|
+
|
|
98
|
+
def my_owned_apis(self) -> list[dict]:
|
|
99
|
+
return self._req("GET", "/me/apis/owned").json()
|
|
100
|
+
|
|
101
|
+
def create_api(self, payload: dict) -> dict:
|
|
102
|
+
return self._req("POST", "/apis", json=payload).json()
|
|
103
|
+
|
|
104
|
+
def update_api(self, api_id: str, patch: dict) -> dict:
|
|
105
|
+
return self._req("PUT", f"/apis/{api_id}", json=patch).json()
|
|
106
|
+
|
|
107
|
+
def add_api_favorite(self, api_id: str) -> None:
|
|
108
|
+
self._req("PUT", f"/me/apis/{api_id}")
|
|
109
|
+
|
|
110
|
+
def call_api(self, path: str, body: dict) -> dict:
|
|
111
|
+
"""Invoke an API by its backend path (e.g. /media/audio/ask)."""
|
|
112
|
+
return self._req("POST", path, json=body).json()
|
|
113
|
+
|
|
114
|
+
def api_job(self, path: str, job_id: str) -> dict:
|
|
115
|
+
"""Fetch an async API job by the API's backend path."""
|
|
116
|
+
return self._req("GET", f"{path.rstrip('/')}/{job_id}").json()
|
|
117
|
+
|
|
118
|
+
# --- memory ---
|
|
119
|
+
def memory_add(self, text: str, metadata: dict | None = None) -> dict:
|
|
120
|
+
return self._req("POST", "/memory", json={"text": text, "metadata": metadata or {}}).json()
|
|
121
|
+
|
|
122
|
+
def memory_search(self, q: str, limit: int = 10) -> list[dict]:
|
|
123
|
+
return self._req("GET", "/memory/search", params={"q": q, "limit": limit}).json()
|
|
124
|
+
|
|
125
|
+
# --- roles & workspaces ---
|
|
126
|
+
def list_roles(self) -> list[dict]:
|
|
127
|
+
return self._req("GET", "/me/roles").json()
|
|
128
|
+
|
|
129
|
+
def create_role(self, payload: dict) -> dict:
|
|
130
|
+
return self._req("POST", "/me/roles", json=payload).json()
|
|
131
|
+
|
|
132
|
+
def get_role(self, role_id: str) -> dict:
|
|
133
|
+
return self._req("GET", f"/me/roles/{role_id}").json()
|
|
134
|
+
|
|
135
|
+
def update_role(self, role_id: str, patch: dict) -> dict:
|
|
136
|
+
role = self.get_role(role_id)
|
|
137
|
+
payload = {
|
|
138
|
+
"name": role.get("name", ""),
|
|
139
|
+
"description": role.get("description", ""),
|
|
140
|
+
"parent_role_ids": role.get("parent_role_ids") or [],
|
|
141
|
+
"skill_ids": role.get("skill_ids") or [],
|
|
142
|
+
}
|
|
143
|
+
payload.update(patch)
|
|
144
|
+
return self._req("PUT", f"/me/roles/{role_id}", json=payload).json()
|
|
145
|
+
|
|
146
|
+
def delete_role(self, role_id: str) -> None:
|
|
147
|
+
self._req("DELETE", f"/me/roles/{role_id}")
|
|
148
|
+
|
|
149
|
+
def get_workspace(self, machine: str, path: str) -> dict:
|
|
150
|
+
return self._req("GET", "/me/workspace", params={"machine": machine, "path": path}).json()
|
|
151
|
+
|
|
152
|
+
def set_workspace(self, machine: str, path: str, role_ids: list[str], supervisor_role_ids: list[str] | None = None,
|
|
153
|
+
code_review_role_ids: list[str] | None = None) -> dict:
|
|
154
|
+
current = None
|
|
155
|
+
if supervisor_role_ids is None:
|
|
156
|
+
current = self.get_workspace(machine, path)
|
|
157
|
+
supervisor_role_ids = current.get("supervisor_role_ids") or []
|
|
158
|
+
if code_review_role_ids is None:
|
|
159
|
+
current = current or self.get_workspace(machine, path)
|
|
160
|
+
code_review_role_ids = current.get("code_review_role_ids") or []
|
|
161
|
+
return self._req("PUT", "/me/workspace",
|
|
162
|
+
json={"machine_id": machine, "path": path, "role_ids": role_ids,
|
|
163
|
+
"supervisor_role_ids": supervisor_role_ids,
|
|
164
|
+
"code_review_role_ids": code_review_role_ids}).json()
|
|
165
|
+
|
|
166
|
+
def list_workspaces(self) -> list[dict]:
|
|
167
|
+
return self._req("GET", "/me/workspaces").json()
|
|
168
|
+
|
|
169
|
+
def list_repository_role_bindings(self) -> list[dict]:
|
|
170
|
+
return self._req("GET", "/me/repository-role-bindings").json()
|
|
171
|
+
|
|
172
|
+
def set_repository_role_binding(self, github_repository: str, role_ids: list[str],
|
|
173
|
+
supervisor_role_ids: list[str] | None = None,
|
|
174
|
+
code_review_role_ids: list[str] | None = None) -> dict:
|
|
175
|
+
current = None
|
|
176
|
+
if supervisor_role_ids is None:
|
|
177
|
+
current = next((b for b in self.list_repository_role_bindings()
|
|
178
|
+
if b.get("github_repository") == github_repository), {})
|
|
179
|
+
supervisor_role_ids = current.get("supervisor_role_ids") or []
|
|
180
|
+
if code_review_role_ids is None:
|
|
181
|
+
current = current or next((b for b in self.list_repository_role_bindings()
|
|
182
|
+
if b.get("github_repository") == github_repository), {})
|
|
183
|
+
code_review_role_ids = current.get("code_review_role_ids") or []
|
|
184
|
+
return self._req("PUT", "/me/repository-role-binding",
|
|
185
|
+
json={"github_repository": github_repository, "role_ids": role_ids,
|
|
186
|
+
"supervisor_role_ids": supervisor_role_ids,
|
|
187
|
+
"code_review_role_ids": code_review_role_ids}).json()
|
|
188
|
+
|
|
189
|
+
def resolve_workspace(self, machine: str, path: str, github_repository: str | None = None) -> dict:
|
|
190
|
+
params = {"machine": machine, "path": path}
|
|
191
|
+
if github_repository:
|
|
192
|
+
params["github_repository"] = github_repository
|
|
193
|
+
return self._req("GET", "/me/workspace/resolve", params=params).json()
|
|
194
|
+
|
|
195
|
+
# --- AI chat / coordination ---
|
|
196
|
+
def ai_chat_post(self, channel: str, message: str, agent_id: str = "") -> dict:
|
|
197
|
+
return self._req("POST", "/me/ai-chat/messages",
|
|
198
|
+
json={"channel": channel, "message": message, "agent_id": agent_id}).json()
|
|
199
|
+
|
|
200
|
+
def ai_chat_list(self, channel: str = "work", since: str = "", until: str = "", limit: int = 100) -> list[dict]:
|
|
201
|
+
params = {"channel": channel, "limit": limit}
|
|
202
|
+
if since:
|
|
203
|
+
params["since"] = since
|
|
204
|
+
if until:
|
|
205
|
+
params["until"] = until
|
|
206
|
+
return self._req("GET", "/me/ai-chat/messages", params=params).json()
|
|
207
|
+
|
|
208
|
+
# --- worker (remote task execution) ---
|
|
209
|
+
def worker_register(self, name: str, host: str, root: str = "", dirs: list[str] | None = None,
|
|
210
|
+
convos: list[dict] | None = None, accounts: list[dict] | None = None,
|
|
211
|
+
public_key: str = "", models: dict | None = None) -> dict:
|
|
212
|
+
return self._req("POST", "/worker/register",
|
|
213
|
+
json={"name": name, "host": host, "root": root, "dirs": dirs or [],
|
|
214
|
+
"convos": convos or [], "accounts": accounts or [], "public_key": public_key,
|
|
215
|
+
"models": models or {}}).json()
|
|
216
|
+
|
|
217
|
+
def worker_poll(self, worker_id: str) -> list[dict]:
|
|
218
|
+
return self._req("GET", "/worker/poll", params={"worker_id": worker_id}, timeout=40).json()
|
|
219
|
+
|
|
220
|
+
# --- cloud login (browser/code re-auth driven from the web) ---
|
|
221
|
+
def cloud_login_get(self, cl_id: str) -> dict:
|
|
222
|
+
return self._req("GET", f"/worker/cloud-login/{cl_id}").json()
|
|
223
|
+
|
|
224
|
+
def cloud_login_update(self, cl_id: str, *, status: str | None = None, url: str | None = None,
|
|
225
|
+
code: str | None = None, needs_code: bool | None = None, message: str | None = None) -> None:
|
|
226
|
+
body: dict = {}
|
|
227
|
+
for k, v in {"status": status, "url": url, "code": code, "needs_code": needs_code, "message": message}.items():
|
|
228
|
+
if v is not None:
|
|
229
|
+
body[k] = v
|
|
230
|
+
self._req("POST", f"/worker/cloud-login/{cl_id}", json=body)
|
|
231
|
+
|
|
232
|
+
# --- viewer (files shown in the web) ---
|
|
233
|
+
def push_view(self, session_id: str, name: str, mime: str, data: str) -> dict:
|
|
234
|
+
return self._req("POST", "/me/views",
|
|
235
|
+
json={"session_id": session_id, "name": name, "mime": mime, "data": data}).json()
|
|
236
|
+
|
|
237
|
+
def list_views(self, session_id: str | None = None) -> list[dict]:
|
|
238
|
+
params = {"session_id": session_id} if session_id else {}
|
|
239
|
+
return self._req("GET", "/me/views", params=params).json()
|
|
240
|
+
|
|
241
|
+
def session_output(self, session_id: str, *, text: str | None = None,
|
|
242
|
+
review_text: str | None = None,
|
|
243
|
+
status: str | None = None, exit_code: int | None = None,
|
|
244
|
+
claude_session_id: str | None = None,
|
|
245
|
+
review_session_id: str | None = None, review_status: str | None = None,
|
|
246
|
+
review_feedback: str | None = None, review_round: int | None = None,
|
|
247
|
+
completion_summary: str | None = None, completion_detail: str | None = None,
|
|
248
|
+
completion_status: str | None = None) -> None:
|
|
249
|
+
body: dict = {}
|
|
250
|
+
if text is not None:
|
|
251
|
+
body["text"] = text
|
|
252
|
+
if review_text is not None:
|
|
253
|
+
body["review_text"] = review_text
|
|
254
|
+
if status is not None:
|
|
255
|
+
body["status"] = status
|
|
256
|
+
if exit_code is not None:
|
|
257
|
+
body["exit_code"] = exit_code
|
|
258
|
+
if claude_session_id is not None:
|
|
259
|
+
body["claude_session_id"] = claude_session_id
|
|
260
|
+
if review_session_id is not None:
|
|
261
|
+
body["review_session_id"] = review_session_id
|
|
262
|
+
if review_status is not None:
|
|
263
|
+
body["review_status"] = review_status
|
|
264
|
+
if review_feedback is not None:
|
|
265
|
+
body["review_feedback"] = review_feedback
|
|
266
|
+
if review_round is not None:
|
|
267
|
+
body["review_round"] = review_round
|
|
268
|
+
if completion_summary is not None:
|
|
269
|
+
body["completion_summary"] = completion_summary
|
|
270
|
+
if completion_detail is not None:
|
|
271
|
+
body["completion_detail"] = completion_detail
|
|
272
|
+
if completion_status is not None:
|
|
273
|
+
body["completion_status"] = completion_status
|
|
274
|
+
self._req("POST", f"/worker/sessions/{session_id}/output", json=body)
|
|
275
|
+
|
|
276
|
+
def notify_work_complete(self, session_id: str, one_line_summary: str, detailed_summary: str,
|
|
277
|
+
status: str = "complete") -> None:
|
|
278
|
+
self.session_output(
|
|
279
|
+
session_id,
|
|
280
|
+
completion_summary=one_line_summary,
|
|
281
|
+
completion_detail=detailed_summary,
|
|
282
|
+
completion_status=status,
|
|
283
|
+
)
|
|
284
|
+
|
|
285
|
+
# --- media ---
|
|
286
|
+
def ask_audio(self, url: str | None, question: str, *, data: str | None = None,
|
|
287
|
+
filename: str | None = None, mime_type: str | None = None) -> dict:
|
|
288
|
+
return self._req("POST", "/media/audio/ask", json=_media_body(url, question, data, filename, mime_type)).json()
|
|
289
|
+
|
|
290
|
+
def ask_video(self, url: str | None, question: str, *, data: str | None = None,
|
|
291
|
+
filename: str | None = None, mime_type: str | None = None) -> dict:
|
|
292
|
+
return self._req("POST", "/media/video/ask", json=_media_body(url, question, data, filename, mime_type)).json()
|
|
293
|
+
|
|
294
|
+
# --- generative media ---
|
|
295
|
+
def image_gen(self, prompt: str, n: int = 1, aspect_ratio: str | None = None) -> dict:
|
|
296
|
+
return self._req("POST", "/media/image/generate",
|
|
297
|
+
json={"prompt": prompt, "n": n, "aspect_ratio": aspect_ratio or ""}).json()
|
|
298
|
+
|
|
299
|
+
def tts(self, text: str, voice: str | None = None, language: str | None = None) -> dict:
|
|
300
|
+
return self._req("POST", "/media/tts",
|
|
301
|
+
json={"text": text, "voice": voice or "", "language": language or ""}).json()
|
|
302
|
+
|
|
303
|
+
def music_gen(self, prompt: str, negative_prompt: str | None = None, seconds: int = 30) -> dict:
|
|
304
|
+
return self._req("POST", "/media/music/generate",
|
|
305
|
+
json={"prompt": prompt, "negative_prompt": negative_prompt or "", "seconds": seconds}).json()
|
|
306
|
+
|
|
307
|
+
def video_gen(self, prompt: str, seconds: int = 8) -> dict:
|
|
308
|
+
return self._req("POST", "/media/video/generate", json={"prompt": prompt, "seconds": seconds}).json()
|
|
309
|
+
|
|
310
|
+
def media_job(self, job_id: str) -> dict:
|
|
311
|
+
return self._req("GET", f"/media/jobs/{job_id}").json()
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"""Device-authorization login, Claude-CLI style."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import sys
|
|
5
|
+
import time
|
|
6
|
+
import webbrowser
|
|
7
|
+
|
|
8
|
+
from . import config
|
|
9
|
+
from .api import Api
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def login(open_browser: bool = True, name: str = "", host: str = "") -> dict:
|
|
13
|
+
api = Api(token=None)
|
|
14
|
+
start = api.device_start(name, host) # name/host let the web approve this worker per-worker
|
|
15
|
+
url = start["verification_uri_complete"]
|
|
16
|
+
|
|
17
|
+
print("\n To sign in, open this URL and approve the code:\n")
|
|
18
|
+
print(f" {start['verification_uri']}")
|
|
19
|
+
print(f" code: {start['user_code']}\n")
|
|
20
|
+
if open_browser:
|
|
21
|
+
try:
|
|
22
|
+
webbrowser.open(url)
|
|
23
|
+
except Exception: # noqa: BLE001 - headless is fine
|
|
24
|
+
pass
|
|
25
|
+
|
|
26
|
+
interval = start.get("interval", 5)
|
|
27
|
+
deadline = time.time() + start.get("expires_in", 900)
|
|
28
|
+
print(" Waiting for approval", end="", flush=True)
|
|
29
|
+
while time.time() < deadline:
|
|
30
|
+
resp = api.device_token(start["device_code"])
|
|
31
|
+
if resp.status_code == 200:
|
|
32
|
+
token = resp.json()["access_token"]
|
|
33
|
+
config.set_token(token)
|
|
34
|
+
me = Api(token=token).me()
|
|
35
|
+
print(f"\n ✓ Logged in as {me.get('email') or me.get('uid')}\n")
|
|
36
|
+
return me
|
|
37
|
+
if resp.status_code == 428: # authorization_pending
|
|
38
|
+
print(".", end="", flush=True)
|
|
39
|
+
time.sleep(interval)
|
|
40
|
+
continue
|
|
41
|
+
print(f"\n login failed: {resp.status_code} {resp.text}", file=sys.stderr)
|
|
42
|
+
break
|
|
43
|
+
raise SystemExit("login timed out or failed")
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def ensure_login() -> None:
|
|
47
|
+
if config.get_token() is None:
|
|
48
|
+
login()
|