eightstatecli 0.4.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.
- eightstatecli-0.4.0.dist-info/METADATA +177 -0
- eightstatecli-0.4.0.dist-info/RECORD +18 -0
- eightstatecli-0.4.0.dist-info/WHEEL +4 -0
- eightstatecli-0.4.0.dist-info/entry_points.txt +2 -0
- eightstatecli-0.4.0.dist-info/licenses/LICENSE +21 -0
- escli/__init__.py +837 -0
- escli/__main__.py +5 -0
- escli/commands/__init__.py +0 -0
- escli/commands/audio.py +438 -0
- escli/commands/docs.py +354 -0
- escli/commands/research.py +597 -0
- escli/commands/search.py +286 -0
- escli/commands/social.py +243 -0
- escli/commands/usage.py +428 -0
- escli/services/__init__.py +0 -0
- escli/services/credentials.py +117 -0
- escli/services/describe.py +186 -0
- escli/services/output.py +168 -0
escli/__init__.py
ADDED
|
@@ -0,0 +1,837 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
escli — Eightstate CLI
|
|
4
|
+
|
|
5
|
+
A unified CLI for Eightstate AI services. Designed for both human operators
|
|
6
|
+
and AI agents (Claude Code, Codex, etc.).
|
|
7
|
+
|
|
8
|
+
AGENT INSTRUCTIONS:
|
|
9
|
+
- Use --json for all commands to get structured JSON output to stdout
|
|
10
|
+
- Use --quiet to suppress stderr progress messages
|
|
11
|
+
- Auth: set ESCLI_API_KEY env var OR run: escli auth login --key <key>
|
|
12
|
+
- Generate: escli --json --quiet image gen "prompt" -s <size> -o output.png
|
|
13
|
+
- Edit: escli --json --quiet image edit "instruction" -i input.png -o output.png
|
|
14
|
+
- Docs: escli --json --quiet docs get react "useEffect cleanup"
|
|
15
|
+
- Audio: escli --json --quiet audio transcribe file.mp3 --speakers
|
|
16
|
+
- JSON output schema: {"success": bool, ...}
|
|
17
|
+
- Exit codes: 0=success, 1=error, 2=usage
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
__version__ = "0.4.0"
|
|
21
|
+
|
|
22
|
+
import argparse
|
|
23
|
+
import base64
|
|
24
|
+
import json as jsonlib
|
|
25
|
+
import os
|
|
26
|
+
import pathlib
|
|
27
|
+
import sys
|
|
28
|
+
import time
|
|
29
|
+
from datetime import datetime
|
|
30
|
+
|
|
31
|
+
# ─────────────────────────────────────────────────────────────────────
|
|
32
|
+
# Branding
|
|
33
|
+
# ─────────────────────────────────────────────────────────────────────
|
|
34
|
+
BANNER_COMPACT = r"""
|
|
35
|
+
┌─────────────────────────────────┐
|
|
36
|
+
│ escli v""" + __version__ + r""" │
|
|
37
|
+
│ eightstate command line │
|
|
38
|
+
└─────────────────────────────────┘"""
|
|
39
|
+
|
|
40
|
+
# Box drawing
|
|
41
|
+
BOX_H = "─"; BOX_V = "│"; BOX_TL = "┌"; BOX_TR = "┐"; BOX_BL = "└"; BOX_BR = "┘"
|
|
42
|
+
BOX_T = "├"; BOX_B = "┤"
|
|
43
|
+
DOT = "·"; BULLET = "▸"; CHECK = "✓"; CROSS = "✗"; ARROW = "→"
|
|
44
|
+
BLOCK = "█"; SHADE = "░"; PIPE = "│"
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def box(title: str, lines: list[str], width: int = 45) -> str:
|
|
48
|
+
inner = width - 4
|
|
49
|
+
out = [f" {BOX_TL}{BOX_H * (width - 2)}{BOX_TR}",
|
|
50
|
+
f" {BOX_V} {title:<{inner}}{BOX_V}",
|
|
51
|
+
f" {BOX_T}{BOX_H * (width - 2)}{BOX_B}"]
|
|
52
|
+
for line in lines:
|
|
53
|
+
out.append(f" {BOX_V} {line[:inner]:<{inner}}{BOX_V}")
|
|
54
|
+
out.append(f" {BOX_BL}{BOX_H * (width - 2)}{BOX_BR}")
|
|
55
|
+
return "\n".join(out)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
# ─────────────────────────────────────────────────────────────────────
|
|
59
|
+
# Paths & Defaults
|
|
60
|
+
# ─────────────────────────────────────────────────────────────────────
|
|
61
|
+
CONFIG_DIR = pathlib.Path(os.environ.get("ESCLI_CONFIG_DIR", pathlib.Path.home() / ".escli"))
|
|
62
|
+
CONFIG_FILE = CONFIG_DIR / "config.json"
|
|
63
|
+
ENDPOINT = "https://ai.eightstate.co/v1"
|
|
64
|
+
|
|
65
|
+
GATE_URL = os.environ.get("ESCLI_GATE_URL", "https://internal.eightstate.co")
|
|
66
|
+
|
|
67
|
+
DEFAULTS = {
|
|
68
|
+
"model": "gpt-image-2", "quality": "high", "size": "1024x1024",
|
|
69
|
+
"format": "png", "out_dir": ".", "timeout": 300,
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
SIZE_ALIASES = {
|
|
73
|
+
"square": "1024x1024", "sq": "1024x1024",
|
|
74
|
+
"landscape": "1536x1024", "land": "1536x1024", "ls": "1536x1024", "wide": "1536x1024",
|
|
75
|
+
"portrait": "1024x1536", "port": "1024x1536", "tall": "1024x1536",
|
|
76
|
+
"1024x1024": "1024x1024", "1536x1024": "1536x1024", "1024x1536": "1024x1536",
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
VALID_QUALITIES = ["low", "medium", "high"]
|
|
80
|
+
VALID_FORMATS = ["png", "jpeg", "jpg", "webp"]
|
|
81
|
+
VALID_BACKGROUNDS = ["auto", "transparent"]
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
# ─────────────────────────────────────────────────────────────────────
|
|
85
|
+
# Config store
|
|
86
|
+
# ─────────────────────────────────────────────────────────────────────
|
|
87
|
+
def _load_config() -> dict:
|
|
88
|
+
if CONFIG_FILE.exists():
|
|
89
|
+
try:
|
|
90
|
+
return jsonlib.loads(CONFIG_FILE.read_text())
|
|
91
|
+
except (jsonlib.JSONDecodeError, OSError):
|
|
92
|
+
return {}
|
|
93
|
+
return {}
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def _save_config(cfg: dict):
|
|
97
|
+
CONFIG_DIR.mkdir(parents=True, exist_ok=True)
|
|
98
|
+
CONFIG_FILE.write_text(jsonlib.dumps(cfg, indent=2) + "\n")
|
|
99
|
+
CONFIG_FILE.chmod(0o600)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def get_active_profile_name() -> str:
|
|
103
|
+
return _load_config().get("active_profile", "default")
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def get_profile(name: str = None) -> dict:
|
|
107
|
+
cfg = _load_config()
|
|
108
|
+
if name is None:
|
|
109
|
+
name = cfg.get("active_profile", "default")
|
|
110
|
+
profile = cfg.get("profiles", {}).get(name, {})
|
|
111
|
+
return {
|
|
112
|
+
"name": name,
|
|
113
|
+
"api_key": profile.get("api_key") or os.environ.get("ESCLI_API_KEY", ""),
|
|
114
|
+
"endpoint": profile.get("endpoint") or os.environ.get("ESCLI_BASE_URL", ENDPOINT),
|
|
115
|
+
"label": profile.get("label", name),
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def set_profile(name, api_key, label=None, endpoint=None):
|
|
120
|
+
cfg = _load_config()
|
|
121
|
+
cfg.setdefault("profiles", {})[name] = {
|
|
122
|
+
"api_key": api_key, "endpoint": endpoint or ENDPOINT,
|
|
123
|
+
"label": label or name, "created": datetime.now().isoformat(),
|
|
124
|
+
}
|
|
125
|
+
cfg["active_profile"] = name
|
|
126
|
+
_save_config(cfg)
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def delete_profile(name):
|
|
130
|
+
cfg = _load_config()
|
|
131
|
+
profiles = cfg.get("profiles", {})
|
|
132
|
+
if name not in profiles:
|
|
133
|
+
return False
|
|
134
|
+
del profiles[name]
|
|
135
|
+
if cfg.get("active_profile") == name:
|
|
136
|
+
remaining = list(profiles.keys())
|
|
137
|
+
cfg["active_profile"] = remaining[0] if remaining else ""
|
|
138
|
+
_save_config(cfg)
|
|
139
|
+
return True
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def list_profiles():
|
|
143
|
+
cfg = _load_config()
|
|
144
|
+
active = cfg.get("active_profile", "default")
|
|
145
|
+
return [{
|
|
146
|
+
"name": n, "label": d.get("label", n),
|
|
147
|
+
"key": f"{d.get('api_key','')[:8]}...{d.get('api_key','')[-4:]}" if len(d.get("api_key","")) > 12 else "***",
|
|
148
|
+
"endpoint": d.get("endpoint", ENDPOINT),
|
|
149
|
+
"active": n == active, "created": d.get("created", ""),
|
|
150
|
+
} for n, d in cfg.get("profiles", {}).items()]
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def resolve_api_key(args):
|
|
154
|
+
if getattr(args, "_explicit_api_key", None):
|
|
155
|
+
return args._explicit_api_key
|
|
156
|
+
p = get_profile()
|
|
157
|
+
return p["api_key"] or os.environ.get("ESCLI_API_KEY", "")
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def resolve_endpoint(args):
|
|
161
|
+
if getattr(args, "_explicit_base_url", None):
|
|
162
|
+
return args._explicit_base_url
|
|
163
|
+
return get_profile().get("endpoint", ENDPOINT)
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
# ─────────────────────────────────────────────────────────────────────
|
|
167
|
+
# Helpers
|
|
168
|
+
# ─────────────────────────────────────────────────────────────────────
|
|
169
|
+
def slugify(text, max_len=40):
|
|
170
|
+
slug = "".join(c if c.isalnum() or c == " " else "" for c in text.lower().strip())
|
|
171
|
+
return "-".join(slug.split())[:max_len] or "image"
|
|
172
|
+
|
|
173
|
+
def auto_filename(prompt, ext="png"):
|
|
174
|
+
return f"{datetime.now().strftime('%Y%m%d-%H%M%S')}-{slugify(prompt)}.{ext}"
|
|
175
|
+
|
|
176
|
+
def log(msg, quiet=False):
|
|
177
|
+
if not quiet:
|
|
178
|
+
print(msg, file=sys.stderr)
|
|
179
|
+
|
|
180
|
+
def resolve_size(value):
|
|
181
|
+
key = value.lower().strip()
|
|
182
|
+
if key in SIZE_ALIASES:
|
|
183
|
+
return SIZE_ALIASES[key]
|
|
184
|
+
print(f" {CROSS} invalid size: {value}", file=sys.stderr)
|
|
185
|
+
sys.exit(2)
|
|
186
|
+
|
|
187
|
+
def mask_key(key):
|
|
188
|
+
return f"{key[:8]}...{key[-4:]}" if len(key) > 12 else "***"
|
|
189
|
+
|
|
190
|
+
def require_openai():
|
|
191
|
+
try:
|
|
192
|
+
from openai import OpenAI
|
|
193
|
+
return OpenAI
|
|
194
|
+
except ImportError:
|
|
195
|
+
print(f" {CROSS} missing: openai SDK. pip install openai", file=sys.stderr)
|
|
196
|
+
sys.exit(1)
|
|
197
|
+
|
|
198
|
+
def require_httpx():
|
|
199
|
+
try:
|
|
200
|
+
import httpx
|
|
201
|
+
return httpx
|
|
202
|
+
except ImportError:
|
|
203
|
+
print(f" {CROSS} missing: httpx. pip install httpx", file=sys.stderr)
|
|
204
|
+
sys.exit(1)
|
|
205
|
+
|
|
206
|
+
def open_file(path):
|
|
207
|
+
import subprocess
|
|
208
|
+
if sys.platform == "darwin":
|
|
209
|
+
subprocess.run(["open", str(path)], check=False)
|
|
210
|
+
elif sys.platform == "linux":
|
|
211
|
+
subprocess.run(["xdg-open", str(path)], check=False)
|
|
212
|
+
elif sys.platform == "win32":
|
|
213
|
+
os.startfile(str(path))
|
|
214
|
+
|
|
215
|
+
def ensure_authed(args):
|
|
216
|
+
api_key, base_url = resolve_api_key(args), resolve_endpoint(args)
|
|
217
|
+
if not api_key:
|
|
218
|
+
log(f"\n {CROSS} not authenticated\n {ARROW} run: escli auth login\n", getattr(args, 'quiet', False))
|
|
219
|
+
sys.exit(1)
|
|
220
|
+
return api_key, base_url
|
|
221
|
+
|
|
222
|
+
def validate_key(api_key, endpoint, timeout=15):
|
|
223
|
+
OpenAI = require_openai()
|
|
224
|
+
try:
|
|
225
|
+
client = OpenAI(base_url=endpoint, api_key=api_key, timeout=timeout)
|
|
226
|
+
return True, f"{len(client.models.list().data)} models"
|
|
227
|
+
except Exception as e:
|
|
228
|
+
err = str(e)
|
|
229
|
+
if "401" in err or "Unauthorized" in err:
|
|
230
|
+
return False, "invalid key"
|
|
231
|
+
if "403" in err or "Forbidden" in err:
|
|
232
|
+
return False, "not authorized"
|
|
233
|
+
return False, f"connection error: {err[:80]}"
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
# ─────────────────────────────────────────────────────────────────────
|
|
237
|
+
# Commands
|
|
238
|
+
# ─────────────────────────────────────────────────────────────────────
|
|
239
|
+
|
|
240
|
+
def cmd_auth_login(args):
|
|
241
|
+
# Legacy: --key flag for direct API key login (image proxy)
|
|
242
|
+
if getattr(args, "key", None):
|
|
243
|
+
return _legacy_key_login(args)
|
|
244
|
+
|
|
245
|
+
# Device flow via gate + Clerk
|
|
246
|
+
log("", args.quiet)
|
|
247
|
+
log(BANNER_COMPACT, args.quiet)
|
|
248
|
+
log("", args.quiet)
|
|
249
|
+
log(f" {BULLET} logging in via eightstate...", args.quiet)
|
|
250
|
+
|
|
251
|
+
httpx = require_httpx()
|
|
252
|
+
try:
|
|
253
|
+
resp = httpx.post(f"{GATE_URL}/api/auth/device", timeout=10)
|
|
254
|
+
resp.raise_for_status()
|
|
255
|
+
data = resp.json()
|
|
256
|
+
except Exception as e:
|
|
257
|
+
if args.json: print(jsonlib.dumps({"success": False, "error": f"failed to reach gate: {e}"}))
|
|
258
|
+
else: log(f" {CROSS} failed to reach {GATE_URL}: {e}", args.quiet)
|
|
259
|
+
return 1
|
|
260
|
+
|
|
261
|
+
device_code = data["device_code"]
|
|
262
|
+
user_code = data["user_code"]
|
|
263
|
+
verification_url = data["verification_url"]
|
|
264
|
+
interval = data.get("interval", 5)
|
|
265
|
+
|
|
266
|
+
log(f" {DOT} opening browser...", args.quiet)
|
|
267
|
+
log(f" {DOT} confirm code: {user_code}", args.quiet)
|
|
268
|
+
log("", args.quiet)
|
|
269
|
+
|
|
270
|
+
# Open browser
|
|
271
|
+
import subprocess, webbrowser
|
|
272
|
+
try:
|
|
273
|
+
webbrowser.open(verification_url)
|
|
274
|
+
except Exception:
|
|
275
|
+
log(f" {ARROW} open this URL: {verification_url}", args.quiet)
|
|
276
|
+
|
|
277
|
+
log(f" {SHADE * 30} waiting for auth...", args.quiet)
|
|
278
|
+
|
|
279
|
+
# Poll
|
|
280
|
+
import time as _time
|
|
281
|
+
deadline = _time.time() + 900 # 15 min max
|
|
282
|
+
while _time.time() < deadline:
|
|
283
|
+
_time.sleep(interval)
|
|
284
|
+
try:
|
|
285
|
+
poll_resp = httpx.post(
|
|
286
|
+
f"{GATE_URL}/api/auth/poll",
|
|
287
|
+
json={"device_code": device_code},
|
|
288
|
+
timeout=10,
|
|
289
|
+
)
|
|
290
|
+
poll_data = poll_resp.json()
|
|
291
|
+
except Exception:
|
|
292
|
+
continue
|
|
293
|
+
|
|
294
|
+
if poll_data.get("status") == "authorized":
|
|
295
|
+
cli_token = poll_data["token"]
|
|
296
|
+
# Store the CLI token
|
|
297
|
+
profile_name = getattr(args, "profile", None) or "default"
|
|
298
|
+
cfg = _load_config()
|
|
299
|
+
cfg.setdefault("profiles", {})[profile_name] = {
|
|
300
|
+
"cli_token": cli_token,
|
|
301
|
+
"endpoint": GATE_URL,
|
|
302
|
+
"label": getattr(args, "label", None) or profile_name,
|
|
303
|
+
"created": datetime.now().isoformat(),
|
|
304
|
+
}
|
|
305
|
+
cfg["active_profile"] = profile_name
|
|
306
|
+
_save_config(cfg)
|
|
307
|
+
|
|
308
|
+
if args.json:
|
|
309
|
+
print(jsonlib.dumps({"success": True, "profile": profile_name, "gate": GATE_URL}))
|
|
310
|
+
else:
|
|
311
|
+
log("", args.quiet)
|
|
312
|
+
log(box(f"{CHECK} authenticated", [
|
|
313
|
+
f"profile {profile_name}",
|
|
314
|
+
f"gate {GATE_URL}",
|
|
315
|
+
f"config {CONFIG_FILE}",
|
|
316
|
+
]), args.quiet)
|
|
317
|
+
log("", args.quiet)
|
|
318
|
+
return 0
|
|
319
|
+
|
|
320
|
+
if poll_data.get("status") == "expired":
|
|
321
|
+
if args.json: print(jsonlib.dumps({"success": False, "error": "session expired"}))
|
|
322
|
+
else: log(f" {CROSS} session expired. try again.", args.quiet)
|
|
323
|
+
return 1
|
|
324
|
+
|
|
325
|
+
if args.json: print(jsonlib.dumps({"success": False, "error": "timed out"}))
|
|
326
|
+
else: log(f" {CROSS} timed out waiting for auth.", args.quiet)
|
|
327
|
+
return 1
|
|
328
|
+
|
|
329
|
+
|
|
330
|
+
def _legacy_key_login(args):
|
|
331
|
+
"""Legacy login with a direct API key (for image proxy)."""
|
|
332
|
+
endpoint = getattr(args, "endpoint", None) or ENDPOINT
|
|
333
|
+
api_key = args.key
|
|
334
|
+
profile_name = getattr(args, "profile", None) or "default"
|
|
335
|
+
label = getattr(args, "label", None) or profile_name
|
|
336
|
+
|
|
337
|
+
log(f" {DOT} validating against {endpoint}...", args.quiet)
|
|
338
|
+
ok, msg = validate_key(api_key, endpoint)
|
|
339
|
+
if not ok:
|
|
340
|
+
if args.json: print(jsonlib.dumps({"success": False, "error": msg}))
|
|
341
|
+
else: log(f" {CROSS} {msg}", args.quiet)
|
|
342
|
+
return 1
|
|
343
|
+
|
|
344
|
+
set_profile(profile_name, api_key, label=label, endpoint=endpoint)
|
|
345
|
+
masked = mask_key(api_key)
|
|
346
|
+
if args.json:
|
|
347
|
+
print(jsonlib.dumps({"success": True, "profile": profile_name, "key": masked,
|
|
348
|
+
"endpoint": endpoint, "models": msg, "config_path": str(CONFIG_FILE)}))
|
|
349
|
+
else:
|
|
350
|
+
log("", args.quiet)
|
|
351
|
+
log(box(f"{CHECK} authenticated", [f"profile {profile_name}", f"key {masked}",
|
|
352
|
+
f"models {msg}", f"config {CONFIG_FILE}"]), args.quiet)
|
|
353
|
+
log("", args.quiet)
|
|
354
|
+
return 0
|
|
355
|
+
|
|
356
|
+
|
|
357
|
+
def cmd_auth_logout(args):
|
|
358
|
+
profile_name = getattr(args, "profile", None) or get_active_profile_name()
|
|
359
|
+
if getattr(args, "all", False):
|
|
360
|
+
if CONFIG_FILE.exists(): CONFIG_FILE.unlink()
|
|
361
|
+
if args.json: print(jsonlib.dumps({"success": True, "message": "all profiles removed"}))
|
|
362
|
+
else: log(f" {CHECK} all profiles removed", args.quiet)
|
|
363
|
+
return 0
|
|
364
|
+
deleted = delete_profile(profile_name)
|
|
365
|
+
if args.json: print(jsonlib.dumps({"success": True, "profile": profile_name, "deleted": deleted}))
|
|
366
|
+
else: log(f" {CHECK if deleted else CROSS} profile '{profile_name}' {'removed' if deleted else 'not found'}", args.quiet)
|
|
367
|
+
return 0
|
|
368
|
+
|
|
369
|
+
|
|
370
|
+
def cmd_auth_status(args):
|
|
371
|
+
profile = get_profile()
|
|
372
|
+
has_key = bool(profile["api_key"])
|
|
373
|
+
if args.json:
|
|
374
|
+
r = {"authenticated": has_key, "profile": profile["name"], "endpoint": profile["endpoint"],
|
|
375
|
+
"config_path": str(CONFIG_FILE)}
|
|
376
|
+
if has_key: r["key"] = mask_key(profile["api_key"])
|
|
377
|
+
print(jsonlib.dumps(r))
|
|
378
|
+
else:
|
|
379
|
+
if has_key:
|
|
380
|
+
print(box(f"{CHECK} authenticated", [f"profile {profile['name']}",
|
|
381
|
+
f"key {mask_key(profile['api_key'])}",
|
|
382
|
+
f"endpoint {profile['endpoint']}"]))
|
|
383
|
+
else: print(f"\n {CROSS} not authenticated\n {ARROW} run: escli auth login\n")
|
|
384
|
+
return 0
|
|
385
|
+
|
|
386
|
+
|
|
387
|
+
def cmd_auth_profiles(args):
|
|
388
|
+
profiles = list_profiles()
|
|
389
|
+
if args.json:
|
|
390
|
+
print(jsonlib.dumps({"success": True, "profiles": profiles, "count": len(profiles)})); return 0
|
|
391
|
+
if not profiles: print(f" {CROSS} no profiles. run: escli auth login"); return 0
|
|
392
|
+
print(f"\n {'':2}{'PROFILE':<16} {'KEY':<24} {'ENDPOINT'}")
|
|
393
|
+
print(f" {BOX_H * 65}")
|
|
394
|
+
for p in profiles:
|
|
395
|
+
print(f" {BULLET if p['active'] else ' '} {p['name']:<15} {p['key']:<24} {p['endpoint']}")
|
|
396
|
+
print(f"\n {len(profiles)} profile(s) {DOT} active: {get_active_profile_name()}\n")
|
|
397
|
+
return 0
|
|
398
|
+
|
|
399
|
+
|
|
400
|
+
def cmd_auth_switch(args):
|
|
401
|
+
target = args.profile_name
|
|
402
|
+
cfg = _load_config()
|
|
403
|
+
profiles = cfg.get("profiles", {})
|
|
404
|
+
if target not in profiles:
|
|
405
|
+
if args.json: print(jsonlib.dumps({"success": False, "error": f"profile '{target}' not found"}))
|
|
406
|
+
else: log(f" {CROSS} profile '{target}' not found", args.quiet)
|
|
407
|
+
return 1
|
|
408
|
+
cfg["active_profile"] = target; _save_config(cfg)
|
|
409
|
+
if args.json: print(jsonlib.dumps({"success": True, "active_profile": target}))
|
|
410
|
+
else: log(f" {CHECK} switched {ARROW} {target}", args.quiet)
|
|
411
|
+
return 0
|
|
412
|
+
|
|
413
|
+
|
|
414
|
+
def cmd_image_generate(args):
|
|
415
|
+
api_key, base_url = ensure_authed(args)
|
|
416
|
+
OpenAI = require_openai()
|
|
417
|
+
client = OpenAI(base_url=base_url, api_key=api_key, timeout=args.timeout)
|
|
418
|
+
prompt = " ".join(args.prompt)
|
|
419
|
+
if not prompt: print(f" {CROSS} prompt required", file=sys.stderr); sys.exit(1)
|
|
420
|
+
|
|
421
|
+
size = resolve_size(args.size)
|
|
422
|
+
ext = args.format if args.format != "jpg" else "jpeg"
|
|
423
|
+
out_ext = args.format if args.format != "jpeg" else "jpg"
|
|
424
|
+
out_path = pathlib.Path(args.output) if args.output else pathlib.Path(args.out_dir) / auto_filename(prompt, out_ext)
|
|
425
|
+
out_path.parent.mkdir(parents=True, exist_ok=True)
|
|
426
|
+
|
|
427
|
+
log("", args.quiet)
|
|
428
|
+
log(f" {BULLET} prompt {prompt}", args.quiet)
|
|
429
|
+
log(f" {DOT} size {size} {DOT} quality {args.quality} {DOT} format {args.format}", args.quiet)
|
|
430
|
+
log(f" {DOT} model {args.model}", args.quiet)
|
|
431
|
+
log(f" {DOT} output {out_path}", args.quiet)
|
|
432
|
+
log("", args.quiet)
|
|
433
|
+
log(f" {SHADE * 30} generating...", args.quiet)
|
|
434
|
+
|
|
435
|
+
extra_body = {}
|
|
436
|
+
if args.format and args.format != "png": extra_body["output_format"] = ext
|
|
437
|
+
if args.background: extra_body["background"] = args.background
|
|
438
|
+
if args.compression is not None: extra_body["output_compression"] = args.compression
|
|
439
|
+
if args.moderation: extra_body["moderation"] = args.moderation
|
|
440
|
+
|
|
441
|
+
t0 = time.time()
|
|
442
|
+
try:
|
|
443
|
+
kwargs = dict(model=args.model, prompt=prompt, size=size, quality=args.quality, response_format="b64_json")
|
|
444
|
+
if extra_body: kwargs["extra_body"] = extra_body
|
|
445
|
+
resp = client.images.generate(**kwargs)
|
|
446
|
+
except Exception as e:
|
|
447
|
+
if args.json: print(jsonlib.dumps({"error": str(e), "success": False}))
|
|
448
|
+
else: log(f" {CROSS} {e}", args.quiet)
|
|
449
|
+
sys.exit(1)
|
|
450
|
+
|
|
451
|
+
elapsed = round(time.time() - t0, 1)
|
|
452
|
+
raw = base64.b64decode(resp.data[0].b64_json)
|
|
453
|
+
out_path.write_bytes(raw)
|
|
454
|
+
revised = getattr(resp.data[0], "revised_prompt", None)
|
|
455
|
+
kb = len(raw) / 1024
|
|
456
|
+
|
|
457
|
+
if args.json:
|
|
458
|
+
result = {"success": True, "path": str(out_path.resolve()), "size_bytes": len(raw),
|
|
459
|
+
"elapsed_seconds": elapsed, "prompt": prompt, "model": args.model,
|
|
460
|
+
"image_size": size, "quality": args.quality}
|
|
461
|
+
if revised: result["revised_prompt"] = revised
|
|
462
|
+
print(jsonlib.dumps(result))
|
|
463
|
+
else:
|
|
464
|
+
log(f" {BLOCK * 30} done", args.quiet)
|
|
465
|
+
log("", args.quiet)
|
|
466
|
+
log(f" {CHECK} {out_path}", args.quiet)
|
|
467
|
+
log(f" {elapsed}s {DOT} {kb:.0f}kb {DOT} {size}", args.quiet)
|
|
468
|
+
if revised: log(f" revised: {revised[:80]}...", args.quiet)
|
|
469
|
+
log("", args.quiet)
|
|
470
|
+
if args.quiet: print(str(out_path.resolve()))
|
|
471
|
+
|
|
472
|
+
if args.open: open_file(out_path)
|
|
473
|
+
return 0
|
|
474
|
+
|
|
475
|
+
|
|
476
|
+
def cmd_image_edit(args):
|
|
477
|
+
api_key, base_url = ensure_authed(args)
|
|
478
|
+
httpx = require_httpx()
|
|
479
|
+
prompt = " ".join(args.prompt)
|
|
480
|
+
if not prompt: print(f" {CROSS} prompt required", file=sys.stderr); sys.exit(1)
|
|
481
|
+
|
|
482
|
+
input_path = pathlib.Path(args.input)
|
|
483
|
+
if not input_path.exists(): print(f" {CROSS} not found: {input_path}", file=sys.stderr); sys.exit(1)
|
|
484
|
+
|
|
485
|
+
size = resolve_size(args.size)
|
|
486
|
+
img_b64 = base64.b64encode(input_path.read_bytes()).decode()
|
|
487
|
+
mime = {".png": "image/png", ".jpg": "image/jpeg", ".jpeg": "image/jpeg",
|
|
488
|
+
".webp": "image/webp"}.get(input_path.suffix.lower(), "image/png")
|
|
489
|
+
out_ext = args.format if args.format != "jpeg" else "jpg"
|
|
490
|
+
out_path = pathlib.Path(args.output) if args.output else pathlib.Path(args.out_dir) / auto_filename(prompt, out_ext)
|
|
491
|
+
out_path.parent.mkdir(parents=True, exist_ok=True)
|
|
492
|
+
|
|
493
|
+
log("", args.quiet)
|
|
494
|
+
log(f" {BULLET} input {input_path}", args.quiet)
|
|
495
|
+
log(f" {BULLET} prompt {prompt}", args.quiet)
|
|
496
|
+
log(f" {DOT} size {size} {DOT} quality {args.quality}", args.quiet)
|
|
497
|
+
log(f" {DOT} output {out_path}", args.quiet)
|
|
498
|
+
log("", args.quiet)
|
|
499
|
+
log(f" {SHADE * 30} editing...", args.quiet)
|
|
500
|
+
|
|
501
|
+
body = {"model": args.model, "prompt": prompt,
|
|
502
|
+
"images": [{"image_url": f"data:{mime};base64,{img_b64}"}],
|
|
503
|
+
"size": size, "quality": args.quality, "response_format": "b64_json"}
|
|
504
|
+
ext = args.format if args.format != "jpg" else "jpeg"
|
|
505
|
+
if args.format and args.format != "png": body["output_format"] = ext
|
|
506
|
+
if args.compression is not None: body["output_compression"] = args.compression
|
|
507
|
+
if args.fidelity: body["input_fidelity"] = args.fidelity
|
|
508
|
+
|
|
509
|
+
t0 = time.time()
|
|
510
|
+
try:
|
|
511
|
+
r = httpx.post(f"{base_url}/images/edits",
|
|
512
|
+
headers={"Authorization": f"Bearer {api_key}", "Content-Type": "application/json"},
|
|
513
|
+
json=body, timeout=args.timeout)
|
|
514
|
+
r.raise_for_status(); data = r.json()
|
|
515
|
+
except Exception as e:
|
|
516
|
+
if args.json: print(jsonlib.dumps({"error": str(e), "success": False}))
|
|
517
|
+
else: log(f" {CROSS} {e}", args.quiet)
|
|
518
|
+
sys.exit(1)
|
|
519
|
+
|
|
520
|
+
elapsed = round(time.time() - t0, 1)
|
|
521
|
+
raw = base64.b64decode(data["data"][0]["b64_json"])
|
|
522
|
+
out_path.write_bytes(raw); kb = len(raw) / 1024
|
|
523
|
+
|
|
524
|
+
if args.json:
|
|
525
|
+
print(jsonlib.dumps({"success": True, "path": str(out_path.resolve()), "size_bytes": len(raw),
|
|
526
|
+
"elapsed_seconds": elapsed, "prompt": prompt, "input": str(input_path.resolve()),
|
|
527
|
+
"model": args.model, "image_size": size, "quality": args.quality}))
|
|
528
|
+
else:
|
|
529
|
+
log(f" {BLOCK * 30} done", args.quiet); log("", args.quiet)
|
|
530
|
+
log(f" {CHECK} {out_path}", args.quiet)
|
|
531
|
+
log(f" {elapsed}s {DOT} {kb:.0f}kb {DOT} {size}", args.quiet); log("", args.quiet)
|
|
532
|
+
if args.quiet: print(str(out_path.resolve()))
|
|
533
|
+
|
|
534
|
+
if args.open: open_file(out_path)
|
|
535
|
+
return 0
|
|
536
|
+
|
|
537
|
+
|
|
538
|
+
def cmd_models(args):
|
|
539
|
+
api_key, base_url = ensure_authed(args)
|
|
540
|
+
OpenAI = require_openai()
|
|
541
|
+
try:
|
|
542
|
+
models = OpenAI(base_url=base_url, api_key=api_key, timeout=30).models.list()
|
|
543
|
+
except Exception as e:
|
|
544
|
+
if args.json: print(jsonlib.dumps({"error": str(e), "success": False}))
|
|
545
|
+
else: print(f" {CROSS} {e}", file=sys.stderr)
|
|
546
|
+
sys.exit(1)
|
|
547
|
+
model_list = sorted([m.id for m in models.data])
|
|
548
|
+
if args.json:
|
|
549
|
+
print(jsonlib.dumps({"success": True, "models": model_list, "count": len(model_list)}))
|
|
550
|
+
else:
|
|
551
|
+
print(f"\n {'MODEL':<40}"); print(f" {BOX_H * 40}")
|
|
552
|
+
for m in model_list: print(f" {m}")
|
|
553
|
+
print(f"\n {len(model_list)} models available\n")
|
|
554
|
+
return 0
|
|
555
|
+
|
|
556
|
+
|
|
557
|
+
def cmd_version(args):
|
|
558
|
+
if args.json: print(jsonlib.dumps({"version": __version__, "endpoint": ENDPOINT}))
|
|
559
|
+
else: print(BANNER_COMPACT)
|
|
560
|
+
return 0
|
|
561
|
+
|
|
562
|
+
|
|
563
|
+
# ─────────────────────────────────────────────────────────────────────
|
|
564
|
+
# Parser — with full agent-readable help on every subcommand
|
|
565
|
+
# ─────────────────────────────────────────────────────────────────────
|
|
566
|
+
def build_parser():
|
|
567
|
+
F = argparse.RawDescriptionHelpFormatter
|
|
568
|
+
|
|
569
|
+
root = argparse.ArgumentParser(prog="escli", description=BANNER_COMPACT + "\n", formatter_class=F, epilog=f"""quickstart:
|
|
570
|
+
escli auth login {DOT} authenticate
|
|
571
|
+
escli image gen "a sunset over the ocean" {DOT} generate
|
|
572
|
+
escli models {DOT} list models
|
|
573
|
+
|
|
574
|
+
examples:
|
|
575
|
+
escli i g "landscape" -s wide -q high --open
|
|
576
|
+
escli i e "repaint as watercolor" -i photo.jpg --open
|
|
577
|
+
escli --json --quiet i g "a logo" {DOT} agent mode
|
|
578
|
+
|
|
579
|
+
shortcuts:
|
|
580
|
+
i g {ARROW} image generate a {ARROW} auth
|
|
581
|
+
i e {ARROW} image edit m {ARROW} models
|
|
582
|
+
|
|
583
|
+
sizes:
|
|
584
|
+
square {PIPE} sq 1024x1024 (default)
|
|
585
|
+
landscape {PIPE} wide 1536x1024
|
|
586
|
+
portrait {PIPE} tall 1024x1536
|
|
587
|
+
|
|
588
|
+
agent usage:
|
|
589
|
+
{DOT} always pass --json for structured output to stdout
|
|
590
|
+
{DOT} pass --quiet to suppress stderr progress messages
|
|
591
|
+
{DOT} set ESCLI_API_KEY env var to skip auth login
|
|
592
|
+
{DOT} output JSON schema:
|
|
593
|
+
{{"success": true, "path": "/abs/path.png", "size_bytes": 123456,
|
|
594
|
+
"elapsed_seconds": 18.4, "prompt": "...", "model": "gpt-image-2",
|
|
595
|
+
"image_size": "1024x1024", "quality": "high"}}
|
|
596
|
+
{DOT} on error: {{"success": false, "error": "message"}}
|
|
597
|
+
{DOT} exit codes: 0=success, 1=error, 2=usage
|
|
598
|
+
|
|
599
|
+
env vars:
|
|
600
|
+
ESCLI_API_KEY API key (overrides stored profile)
|
|
601
|
+
ESCLI_BASE_URL API endpoint (default: {ENDPOINT})
|
|
602
|
+
ESCLI_IMAGE_MODEL Default model (default: gpt-image-2)
|
|
603
|
+
ESCLI_IMAGE_QUALITY Default quality: low | medium | high (default: high)
|
|
604
|
+
ESCLI_IMAGE_SIZE Default size (default: 1024x1024)
|
|
605
|
+
ESCLI_IMAGE_FORMAT Default format: png | jpeg | webp (default: png)
|
|
606
|
+
ESCLI_OUT_DIR Output directory (default: .)
|
|
607
|
+
ESCLI_TIMEOUT Timeout in seconds (default: 300)
|
|
608
|
+
ESCLI_CONFIG_DIR Config directory (default: ~/.escli)
|
|
609
|
+
|
|
610
|
+
config: ~/.escli/config.json (600 permissions, owner-only)
|
|
611
|
+
""")
|
|
612
|
+
root.add_argument("-v", "--version", action="store_true", help="Show version")
|
|
613
|
+
root.add_argument("--json", action="store_true", default=False, help="Structured JSON output (agent-friendly)")
|
|
614
|
+
root.add_argument("--quiet", action="store_true", default=False, help="Suppress stderr progress, print only result to stdout")
|
|
615
|
+
root.add_argument("--describe", action="store_true", default=False, help="Machine-readable JSON manifest of all commands (for agent discovery)")
|
|
616
|
+
root.add_argument("--base-url", dest="_explicit_base_url", default=None, help=argparse.SUPPRESS)
|
|
617
|
+
root.add_argument("--api-key", dest="_explicit_api_key", default=None, help=argparse.SUPPRESS)
|
|
618
|
+
root.add_argument("--timeout", type=int, default=int(os.environ.get("ESCLI_TIMEOUT", "300")), help=argparse.SUPPRESS)
|
|
619
|
+
|
|
620
|
+
subs = root.add_subparsers(dest="command", metavar="command")
|
|
621
|
+
|
|
622
|
+
# ── auth ──────────────────────────────────────────────────────────
|
|
623
|
+
auth_p = subs.add_parser("auth", aliases=["a"], help="Authentication and profiles", formatter_class=F,
|
|
624
|
+
epilog=f"""subcommands:
|
|
625
|
+
login {DOT} authenticate with API key
|
|
626
|
+
logout {DOT} remove credentials
|
|
627
|
+
status {DOT} show current profile (alias: whoami)
|
|
628
|
+
profiles {DOT} list all profiles (alias: ls)
|
|
629
|
+
switch {DOT} switch active profile (alias: use)
|
|
630
|
+
|
|
631
|
+
agent examples:
|
|
632
|
+
escli auth login --key sk-xxx {DOT} non-interactive login
|
|
633
|
+
escli --json auth status {DOT} check if authenticated
|
|
634
|
+
ESCLI_API_KEY=sk-xxx escli --json models {DOT} skip login entirely
|
|
635
|
+
""")
|
|
636
|
+
auth_subs = auth_p.add_subparsers(dest="auth_command", metavar="subcommand")
|
|
637
|
+
|
|
638
|
+
login_p = auth_subs.add_parser("login", aliases=["l"], help="Authenticate with an API key", formatter_class=F,
|
|
639
|
+
epilog=f"""examples:
|
|
640
|
+
escli auth login {DOT} interactive prompt
|
|
641
|
+
escli auth login --key sk-xxx {DOT} non-interactive (agents/CI)
|
|
642
|
+
escli auth login -k sk-xxx -p work {DOT} named profile
|
|
643
|
+
echo sk-xxx | escli auth login {DOT} pipe key from stdin
|
|
644
|
+
|
|
645
|
+
agent: escli --json auth login --key sk-xxx
|
|
646
|
+
{ARROW} {{"success": true, "profile": "default", "key": "sk-xxx...xxx", "models": "28 models"}}
|
|
647
|
+
""")
|
|
648
|
+
login_p.add_argument("--key", "-k", default=None, help="API key (omit for interactive prompt)")
|
|
649
|
+
login_p.add_argument("--profile", "-p", default="default", help="Profile name (default: default)")
|
|
650
|
+
login_p.add_argument("--label", default=None, help="Human-readable label for this profile")
|
|
651
|
+
login_p.add_argument("--endpoint", default=None, help=f"API endpoint (default: {ENDPOINT})")
|
|
652
|
+
login_p.set_defaults(func=cmd_auth_login)
|
|
653
|
+
|
|
654
|
+
logout_p = auth_subs.add_parser("logout", help="Remove stored credentials")
|
|
655
|
+
logout_p.add_argument("--profile", "-p", default=None, help="Profile to remove (default: active)")
|
|
656
|
+
logout_p.add_argument("--all", action="store_true", help="Remove ALL profiles")
|
|
657
|
+
logout_p.set_defaults(func=cmd_auth_logout)
|
|
658
|
+
|
|
659
|
+
auth_subs.add_parser("status", aliases=["s", "whoami"], help="Show current auth status").set_defaults(func=cmd_auth_status)
|
|
660
|
+
auth_subs.add_parser("profiles", aliases=["ls", "list"], help="List all profiles").set_defaults(func=cmd_auth_profiles)
|
|
661
|
+
|
|
662
|
+
switch_p = auth_subs.add_parser("switch", aliases=["use"], help="Switch active profile")
|
|
663
|
+
switch_p.add_argument("profile_name", help="Profile to activate")
|
|
664
|
+
switch_p.set_defaults(func=cmd_auth_switch)
|
|
665
|
+
|
|
666
|
+
# ── image ─────────────────────────────────────────────────────────
|
|
667
|
+
image_p = subs.add_parser("image", aliases=["img", "i"], help="Image generation and editing", formatter_class=F,
|
|
668
|
+
epilog=f"""subcommands:
|
|
669
|
+
generate (gen, g) {DOT} generate image from text prompt
|
|
670
|
+
edit (e) {DOT} edit existing image with instruction
|
|
671
|
+
|
|
672
|
+
quick reference:
|
|
673
|
+
escli i g "prompt" -s wide -q high --open {DOT} generate landscape
|
|
674
|
+
escli i e "instruction" -i img.png --open {DOT} edit image
|
|
675
|
+
|
|
676
|
+
sizes: square(1024x1024) landscape(1536x1024) portrait(1024x1536)
|
|
677
|
+
aliases: sq, wide, tall, land, port, ls
|
|
678
|
+
""")
|
|
679
|
+
image_subs = image_p.add_subparsers(dest="image_command", metavar="subcommand")
|
|
680
|
+
|
|
681
|
+
def add_image_flags(p, include_input=False):
|
|
682
|
+
p.add_argument("prompt", nargs="+", help="Text prompt describing desired image")
|
|
683
|
+
p.add_argument("-s", "--size", default=os.environ.get("ESCLI_IMAGE_SIZE", DEFAULTS["size"]),
|
|
684
|
+
help="Image size: square|sq|landscape|wide|ls|portrait|tall|port or WxH (default: square = 1024x1024)")
|
|
685
|
+
p.add_argument("-q", "--quality", default=os.environ.get("ESCLI_IMAGE_QUALITY", DEFAULTS["quality"]),
|
|
686
|
+
choices=VALID_QUALITIES, help="Quality: low (fast, ~15s) | medium (~25s) | high (best, ~40s) (default: high)")
|
|
687
|
+
p.add_argument("-f", "--format", default=os.environ.get("ESCLI_IMAGE_FORMAT", DEFAULTS["format"]),
|
|
688
|
+
choices=VALID_FORMATS, help="Output format: png | jpeg | jpg | webp (default: png)")
|
|
689
|
+
p.add_argument("-m", "--model", default=os.environ.get("ESCLI_IMAGE_MODEL", DEFAULTS["model"]),
|
|
690
|
+
help="Model to use (default: gpt-image-2)")
|
|
691
|
+
p.add_argument("-o", "--output", default=None,
|
|
692
|
+
help="Output file path. If omitted, auto-generates: YYYYMMDD-HHMMSS-slugified-prompt.png")
|
|
693
|
+
p.add_argument("-d", "--out-dir", default=os.environ.get("ESCLI_OUT_DIR", DEFAULTS["out_dir"]),
|
|
694
|
+
help="Output directory when -o not set (default: current dir)")
|
|
695
|
+
p.add_argument("--open", action="store_true", help="Open image in default viewer after generating")
|
|
696
|
+
p.add_argument("--compression", type=int, default=None, metavar="0-100",
|
|
697
|
+
help="Output compression level for jpeg/webp (0=min, 100=max)")
|
|
698
|
+
p.add_argument("--moderation", default=None, choices=["auto", "low"],
|
|
699
|
+
help="Content moderation: auto (default) | low (permissive)")
|
|
700
|
+
if include_input:
|
|
701
|
+
p.add_argument("-i", "--input", required=True,
|
|
702
|
+
help="Path to input image file to edit (png, jpg, webp)")
|
|
703
|
+
p.add_argument("--fidelity", default=None, choices=["low", "high"],
|
|
704
|
+
help="How closely to preserve the input image: low (more creative) | high (more faithful)")
|
|
705
|
+
|
|
706
|
+
gen_p = image_subs.add_parser("generate", aliases=["gen", "g"], help="Generate an image from a text prompt",
|
|
707
|
+
formatter_class=F, epilog=f"""sizes (any of these work for -s):
|
|
708
|
+
square, sq {ARROW} 1024x1024 (default)
|
|
709
|
+
landscape, wide, ls {ARROW} 1536x1024 (good for wallpapers, banners)
|
|
710
|
+
portrait, tall, port {ARROW} 1024x1536 (good for phone mockups, posters)
|
|
711
|
+
1024x1024 {ARROW} also accepts raw WxH values
|
|
712
|
+
|
|
713
|
+
quality:
|
|
714
|
+
low {DOT} fastest (~15s), lower detail
|
|
715
|
+
medium {DOT} balanced (~25s)
|
|
716
|
+
high {DOT} best quality (~40s), most detail
|
|
717
|
+
|
|
718
|
+
examples:
|
|
719
|
+
escli i g "a sunset over the ocean"
|
|
720
|
+
escli i g "oil painting of a mountain" -s wide -q high --open
|
|
721
|
+
escli i g "phone app mockup" -s portrait -o mockup.png
|
|
722
|
+
escli i g "logo design" -f webp --compression 80
|
|
723
|
+
escli i g "product photo" -s sq -q high -d ./output/
|
|
724
|
+
|
|
725
|
+
agent examples:
|
|
726
|
+
escli --json --quiet i g "a logo" -s sq -q low -o /tmp/logo.png
|
|
727
|
+
|
|
728
|
+
stdout JSON on success:
|
|
729
|
+
{{"success": true, "path": "/abs/path/logo.png", "size_bytes": 123456,
|
|
730
|
+
"elapsed_seconds": 18.4, "prompt": "a logo", "model": "gpt-image-2",
|
|
731
|
+
"image_size": "1024x1024", "quality": "low",
|
|
732
|
+
"revised_prompt": "A simple clean logo design..."}}
|
|
733
|
+
|
|
734
|
+
stdout JSON on error:
|
|
735
|
+
{{"success": false, "error": "error message"}}
|
|
736
|
+
""")
|
|
737
|
+
add_image_flags(gen_p)
|
|
738
|
+
gen_p.add_argument("--background", default=None, choices=VALID_BACKGROUNDS,
|
|
739
|
+
help="Background: auto (default) | transparent (removes background)")
|
|
740
|
+
gen_p.set_defaults(func=cmd_image_generate)
|
|
741
|
+
|
|
742
|
+
edit_p = image_subs.add_parser("edit", aliases=["e"], help="Edit an existing image with an instruction",
|
|
743
|
+
formatter_class=F, epilog=f"""how it works:
|
|
744
|
+
Takes an input image (-i) and applies the prompt as an edit instruction.
|
|
745
|
+
The model understands the image content and modifies it accordingly.
|
|
746
|
+
|
|
747
|
+
examples:
|
|
748
|
+
escli i e "make it snow" -i summer.jpg --open
|
|
749
|
+
escli i e "repaint as oil painting" -i photo.jpg -q high -o painted.png
|
|
750
|
+
escli i e "remove the background" -i product.png
|
|
751
|
+
escli i e "add sunglasses to the person" -i portrait.jpg -s sq
|
|
752
|
+
|
|
753
|
+
agent example:
|
|
754
|
+
escli --json --quiet i e "make it blue" -i input.png -o output.png
|
|
755
|
+
|
|
756
|
+
stdout JSON on success:
|
|
757
|
+
{{"success": true, "path": "/abs/path/output.png", "size_bytes": 123456,
|
|
758
|
+
"elapsed_seconds": 22.7, "prompt": "make it blue",
|
|
759
|
+
"input": "/abs/path/input.png", "model": "gpt-image-2",
|
|
760
|
+
"image_size": "1024x1024", "quality": "high"}}
|
|
761
|
+
|
|
762
|
+
supported input formats: png, jpg, jpeg, webp
|
|
763
|
+
""")
|
|
764
|
+
add_image_flags(edit_p, include_input=True)
|
|
765
|
+
edit_p.set_defaults(func=cmd_image_edit)
|
|
766
|
+
|
|
767
|
+
# ── models ────────────────────────────────────────────────────────
|
|
768
|
+
subs.add_parser("models", aliases=["m"], help="List available models", formatter_class=F,
|
|
769
|
+
epilog=f"""lists all models available on the API endpoint.
|
|
770
|
+
|
|
771
|
+
agent example:
|
|
772
|
+
escli --json models
|
|
773
|
+
{ARROW} {{"success": true, "models": ["gpt-image-2", "gpt-5.4-mini", ...], "count": 28}}
|
|
774
|
+
""").set_defaults(func=cmd_models)
|
|
775
|
+
|
|
776
|
+
# ── docs (Context7) ──────────────────────────────────────────────
|
|
777
|
+
from .commands.docs import register as register_docs
|
|
778
|
+
register_docs(subs)
|
|
779
|
+
|
|
780
|
+
# ── audio (AssemblyAI) ───────────────────────────────────────────
|
|
781
|
+
from .commands.audio import register as register_audio
|
|
782
|
+
register_audio(subs)
|
|
783
|
+
|
|
784
|
+
# ── search + fetch (Parallel.ai) ────────────────────────────────
|
|
785
|
+
from .commands.search import register as register_search
|
|
786
|
+
register_search(subs)
|
|
787
|
+
|
|
788
|
+
# ── research (Parallel Task API) ────────────────────────────────
|
|
789
|
+
from .commands.research import register as register_research
|
|
790
|
+
register_research(subs)
|
|
791
|
+
|
|
792
|
+
# ── social (Tavily) ─────────────────────────────────────────────
|
|
793
|
+
from .commands.social import register as register_social
|
|
794
|
+
register_social(subs)
|
|
795
|
+
|
|
796
|
+
# ── usage (gate analytics) ──────────────────────────────────────
|
|
797
|
+
from .commands.usage import register as register_usage
|
|
798
|
+
register_usage(subs)
|
|
799
|
+
|
|
800
|
+
# ── version ───────────────────────────────────────────────────────
|
|
801
|
+
subs.add_parser("version", help="Show version info").set_defaults(func=cmd_version)
|
|
802
|
+
|
|
803
|
+
return root
|
|
804
|
+
|
|
805
|
+
|
|
806
|
+
# ─────────────────────────────────────────────────────────────────────
|
|
807
|
+
# Main
|
|
808
|
+
# ─────────────────────────────────────────────────────────────────────
|
|
809
|
+
def main():
|
|
810
|
+
parser = build_parser()
|
|
811
|
+
args = parser.parse_args()
|
|
812
|
+
for attr in ("json", "quiet"):
|
|
813
|
+
if not hasattr(args, attr): setattr(args, attr, False)
|
|
814
|
+
|
|
815
|
+
# Self-description: machine-readable manifest for agent discovery
|
|
816
|
+
if getattr(args, "describe", False):
|
|
817
|
+
from .services.describe import generate_manifest
|
|
818
|
+
import json as _json
|
|
819
|
+
print(_json.dumps(generate_manifest(parser), indent=2))
|
|
820
|
+
return 0
|
|
821
|
+
|
|
822
|
+
if getattr(args, "version", False) and not args.command: return cmd_version(args)
|
|
823
|
+
if not args.command: parser.print_help(); return 0
|
|
824
|
+
if args.command in ("auth", "a") and not getattr(args, "auth_command", None):
|
|
825
|
+
print(f" usage: escli auth <login{PIPE}logout{PIPE}status{PIPE}profiles{PIPE}switch>", file=sys.stderr); return 2
|
|
826
|
+
if args.command in ("image", "img", "i") and not getattr(args, "image_command", None):
|
|
827
|
+
print(f" usage: escli image <generate{PIPE}edit>", file=sys.stderr); return 2
|
|
828
|
+
if args.command in ("docs", "d") and not getattr(args, "docs_command", None):
|
|
829
|
+
print(f" usage: escli docs <search{PIPE}get{PIPE}fetch>", file=sys.stderr); return 2
|
|
830
|
+
if args.command in ("audio", "au") and not getattr(args, "audio_command", None):
|
|
831
|
+
print(f" usage: escli audio <transcribe{PIPE}status{PIPE}get{PIPE}list>", file=sys.stderr); return 2
|
|
832
|
+
if hasattr(args, "func"): return args.func(args)
|
|
833
|
+
parser.print_help(); return 0
|
|
834
|
+
|
|
835
|
+
|
|
836
|
+
if __name__ == "__main__":
|
|
837
|
+
sys.exit(main() or 0)
|