eightstatecli 0.4.1__tar.gz → 0.4.3__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.
- {eightstatecli-0.4.1 → eightstatecli-0.4.3}/PKG-INFO +1 -1
- {eightstatecli-0.4.1 → eightstatecli-0.4.3}/pyproject.toml +1 -1
- {eightstatecli-0.4.1 → eightstatecli-0.4.3}/src/escli/__init__.py +25 -25
- {eightstatecli-0.4.1 → eightstatecli-0.4.3}/src/escli/commands/audio.py +18 -18
- {eightstatecli-0.4.1 → eightstatecli-0.4.3}/src/escli/commands/docs.py +8 -8
- {eightstatecli-0.4.1 → eightstatecli-0.4.3}/src/escli/commands/research.py +163 -17
- {eightstatecli-0.4.1 → eightstatecli-0.4.3}/src/escli/commands/search.py +7 -7
- {eightstatecli-0.4.1 → eightstatecli-0.4.3}/src/escli/commands/social.py +7 -7
- {eightstatecli-0.4.1 → eightstatecli-0.4.3}/src/escli/commands/usage.py +4 -4
- {eightstatecli-0.4.1 → eightstatecli-0.4.3}/.gitignore +0 -0
- {eightstatecli-0.4.1 → eightstatecli-0.4.3}/LICENSE +0 -0
- {eightstatecli-0.4.1 → eightstatecli-0.4.3}/README.md +0 -0
- {eightstatecli-0.4.1 → eightstatecli-0.4.3}/src/escli/__main__.py +0 -0
- {eightstatecli-0.4.1 → eightstatecli-0.4.3}/src/escli/commands/__init__.py +0 -0
- {eightstatecli-0.4.1 → eightstatecli-0.4.3}/src/escli/services/__init__.py +0 -0
- {eightstatecli-0.4.1 → eightstatecli-0.4.3}/src/escli/services/credentials.py +0 -0
- {eightstatecli-0.4.1 → eightstatecli-0.4.3}/src/escli/services/describe.py +0 -0
- {eightstatecli-0.4.1 → eightstatecli-0.4.3}/src/escli/services/output.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: eightstatecli
|
|
3
|
-
Version: 0.4.
|
|
3
|
+
Version: 0.4.3
|
|
4
4
|
Summary: Eightstate CLI — unified AI services from the command line
|
|
5
5
|
Project-URL: Homepage, https://github.com/eight-state/eightstate
|
|
6
6
|
Project-URL: Repository, https://github.com/eight-state/eightstate
|
|
@@ -17,7 +17,7 @@ AGENT INSTRUCTIONS:
|
|
|
17
17
|
- Exit codes: 0=success, 1=error, 2=usage
|
|
18
18
|
"""
|
|
19
19
|
|
|
20
|
-
__version__ = "0.4.
|
|
20
|
+
__version__ = "0.4.3"
|
|
21
21
|
|
|
22
22
|
import argparse
|
|
23
23
|
import base64
|
|
@@ -718,7 +718,7 @@ config: ~/.escli/config.json (600 permissions, owner-only)
|
|
|
718
718
|
subs = root.add_subparsers(dest="command", metavar="command")
|
|
719
719
|
|
|
720
720
|
# ── auth ──────────────────────────────────────────────────────────
|
|
721
|
-
auth_p = subs.add_parser("auth", aliases=["a"], help="
|
|
721
|
+
auth_p = subs.add_parser("auth", aliases=["a"], help="Manage API auth and profiles", formatter_class=F,
|
|
722
722
|
epilog=f"""subcommands:
|
|
723
723
|
login {DOT} authenticate with API key
|
|
724
724
|
logout {DOT} remove credentials
|
|
@@ -733,7 +733,7 @@ agent examples:
|
|
|
733
733
|
""")
|
|
734
734
|
auth_subs = auth_p.add_subparsers(dest="auth_command", metavar="subcommand")
|
|
735
735
|
|
|
736
|
-
login_p = auth_subs.add_parser("login", aliases=["l"], help="Authenticate
|
|
736
|
+
login_p = auth_subs.add_parser("login", aliases=["l"], help="Authenticate via browser or API key", formatter_class=F,
|
|
737
737
|
epilog=f"""examples:
|
|
738
738
|
escli auth login {DOT} interactive prompt
|
|
739
739
|
escli auth login --key sk-xxx {DOT} non-interactive (agents/CI)
|
|
@@ -745,24 +745,24 @@ agent: escli --json auth login --key sk-xxx
|
|
|
745
745
|
""")
|
|
746
746
|
login_p.add_argument("--key", "-k", default=None, help="API key (omit for interactive prompt)")
|
|
747
747
|
login_p.add_argument("--profile", "-p", default="default", help="Profile name (default: default)")
|
|
748
|
-
login_p.add_argument("--label", default=None, help="
|
|
749
|
-
login_p.add_argument("--endpoint", default=None, help=
|
|
748
|
+
login_p.add_argument("--label", default=None, help="Profile label")
|
|
749
|
+
login_p.add_argument("--endpoint", default=None, help="API endpoint")
|
|
750
750
|
login_p.set_defaults(func=cmd_auth_login)
|
|
751
751
|
|
|
752
752
|
logout_p = auth_subs.add_parser("logout", help="Remove stored credentials")
|
|
753
753
|
logout_p.add_argument("--profile", "-p", default=None, help="Profile to remove (default: active)")
|
|
754
|
-
logout_p.add_argument("--all", action="store_true", help="Remove
|
|
754
|
+
logout_p.add_argument("--all", action="store_true", help="Remove all profiles")
|
|
755
755
|
logout_p.set_defaults(func=cmd_auth_logout)
|
|
756
756
|
|
|
757
|
-
auth_subs.add_parser("status", aliases=["s", "whoami"], help="Show current auth
|
|
758
|
-
auth_subs.add_parser("profiles", aliases=["ls", "list"], help="List all profiles").set_defaults(func=cmd_auth_profiles)
|
|
757
|
+
auth_subs.add_parser("status", aliases=["s", "whoami"], help="Show current profile and auth state").set_defaults(func=cmd_auth_status)
|
|
758
|
+
auth_subs.add_parser("profiles", aliases=["ls", "list"], help="List all stored profiles").set_defaults(func=cmd_auth_profiles)
|
|
759
759
|
|
|
760
|
-
switch_p = auth_subs.add_parser("switch", aliases=["use"], help="Switch active profile")
|
|
760
|
+
switch_p = auth_subs.add_parser("switch", aliases=["use"], help="Switch the active profile")
|
|
761
761
|
switch_p.add_argument("profile_name", help="Profile to activate")
|
|
762
762
|
switch_p.set_defaults(func=cmd_auth_switch)
|
|
763
763
|
|
|
764
764
|
# ── image ─────────────────────────────────────────────────────────
|
|
765
|
-
image_p = subs.add_parser("image", aliases=["img", "i"], help="
|
|
765
|
+
image_p = subs.add_parser("image", aliases=["img", "i"], help="Generate or edit images from prompts", formatter_class=F,
|
|
766
766
|
epilog=f"""subcommands:
|
|
767
767
|
generate (gen, g) {DOT} generate image from text prompt
|
|
768
768
|
edit (e) {DOT} edit existing image with instruction
|
|
@@ -777,29 +777,29 @@ sizes: square(1024x1024) landscape(1536x1024) portrait(1024x1536)
|
|
|
777
777
|
image_subs = image_p.add_subparsers(dest="image_command", metavar="subcommand")
|
|
778
778
|
|
|
779
779
|
def add_image_flags(p, include_input=False):
|
|
780
|
-
p.add_argument("prompt", nargs="+", help="Text prompt
|
|
780
|
+
p.add_argument("prompt", nargs="+", help="Text prompt")
|
|
781
781
|
p.add_argument("-s", "--size", default=os.environ.get("ESCLI_IMAGE_SIZE", DEFAULTS["size"]),
|
|
782
|
-
help="
|
|
782
|
+
help="Size: sq|wide|tall or WxH (default: 1024x1024)")
|
|
783
783
|
p.add_argument("-q", "--quality", default=os.environ.get("ESCLI_IMAGE_QUALITY", DEFAULTS["quality"]),
|
|
784
|
-
choices=VALID_QUALITIES, help="Quality: low
|
|
784
|
+
choices=VALID_QUALITIES, help="Quality: low|medium|high (default: high)")
|
|
785
785
|
p.add_argument("-f", "--format", default=os.environ.get("ESCLI_IMAGE_FORMAT", DEFAULTS["format"]),
|
|
786
|
-
choices=VALID_FORMATS, help="
|
|
786
|
+
choices=VALID_FORMATS, help="Format: png|jpeg|webp (default: png)")
|
|
787
787
|
p.add_argument("-m", "--model", default=os.environ.get("ESCLI_IMAGE_MODEL", DEFAULTS["model"]),
|
|
788
|
-
help="Model
|
|
788
|
+
help="Model (default: gpt-image-2)")
|
|
789
789
|
p.add_argument("-o", "--output", default=None,
|
|
790
|
-
help="Output
|
|
790
|
+
help="Output path (default: auto-named in --out-dir)")
|
|
791
791
|
p.add_argument("-d", "--out-dir", default=os.environ.get("ESCLI_OUT_DIR", DEFAULTS["out_dir"]),
|
|
792
|
-
help="Output
|
|
793
|
-
p.add_argument("--open", action="store_true", help="Open
|
|
792
|
+
help="Output dir (default: .)")
|
|
793
|
+
p.add_argument("--open", action="store_true", help="Open result after writing")
|
|
794
794
|
p.add_argument("--compression", type=int, default=None, metavar="0-100",
|
|
795
|
-
help="
|
|
795
|
+
help="JPEG/WebP compression 0-100")
|
|
796
796
|
p.add_argument("--moderation", default=None, choices=["auto", "low"],
|
|
797
|
-
help="
|
|
797
|
+
help="Moderation: auto|low")
|
|
798
798
|
if include_input:
|
|
799
799
|
p.add_argument("-i", "--input", required=True,
|
|
800
|
-
help="
|
|
800
|
+
help="Input image (png/jpg/webp)")
|
|
801
801
|
p.add_argument("--fidelity", default=None, choices=["low", "high"],
|
|
802
|
-
help="
|
|
802
|
+
help="Fidelity to input: low|high")
|
|
803
803
|
|
|
804
804
|
gen_p = image_subs.add_parser("generate", aliases=["gen", "g"], help="Generate an image from a text prompt",
|
|
805
805
|
formatter_class=F, epilog=f"""sizes (any of these work for -s):
|
|
@@ -834,7 +834,7 @@ agent examples:
|
|
|
834
834
|
""")
|
|
835
835
|
add_image_flags(gen_p)
|
|
836
836
|
gen_p.add_argument("--background", default=None, choices=VALID_BACKGROUNDS,
|
|
837
|
-
help="Background: auto
|
|
837
|
+
help="Background: auto|transparent")
|
|
838
838
|
gen_p.set_defaults(func=cmd_image_generate)
|
|
839
839
|
|
|
840
840
|
edit_p = image_subs.add_parser("edit", aliases=["e"], help="Edit an existing image with an instruction",
|
|
@@ -863,7 +863,7 @@ supported input formats: png, jpg, jpeg, webp
|
|
|
863
863
|
edit_p.set_defaults(func=cmd_image_edit)
|
|
864
864
|
|
|
865
865
|
# ── models ────────────────────────────────────────────────────────
|
|
866
|
-
subs.add_parser("models", aliases=["m"], help="List available
|
|
866
|
+
subs.add_parser("models", aliases=["m"], help="List models available on the endpoint", formatter_class=F,
|
|
867
867
|
epilog=f"""lists all models available on the API endpoint.
|
|
868
868
|
|
|
869
869
|
agent example:
|
|
@@ -896,7 +896,7 @@ agent example:
|
|
|
896
896
|
register_usage(subs)
|
|
897
897
|
|
|
898
898
|
# ── version ───────────────────────────────────────────────────────
|
|
899
|
-
subs.add_parser("version", help="
|
|
899
|
+
subs.add_parser("version", help="Print escli version").set_defaults(func=cmd_version)
|
|
900
900
|
|
|
901
901
|
return root
|
|
902
902
|
|
|
@@ -394,7 +394,7 @@ def register(subparsers):
|
|
|
394
394
|
F = argparse.RawDescriptionHelpFormatter
|
|
395
395
|
|
|
396
396
|
audio_p = subparsers.add_parser(
|
|
397
|
-
"audio", aliases=["au"], help="
|
|
397
|
+
"audio", aliases=["au"], help="Transcribe audio files or URLs",
|
|
398
398
|
formatter_class=F,
|
|
399
399
|
epilog="""subcommands:
|
|
400
400
|
transcribe <file-or-url> Transcribe audio with speaker diarization
|
|
@@ -413,42 +413,42 @@ examples:
|
|
|
413
413
|
audio_subs = audio_p.add_subparsers(dest="audio_command", metavar="subcommand")
|
|
414
414
|
|
|
415
415
|
# transcribe
|
|
416
|
-
tr_p = audio_subs.add_parser("transcribe", aliases=["t"], help="Transcribe audio")
|
|
416
|
+
tr_p = audio_subs.add_parser("transcribe", aliases=["t"], help="Transcribe an audio file or URL")
|
|
417
417
|
tr_p.add_argument("source", help="Audio file path or URL")
|
|
418
|
-
tr_p.add_argument("-o", "--output", default=None, help="
|
|
419
|
-
tr_p.add_argument("--format", choices=["text", "json", "srt", "vtt"], default="text", help="Output format")
|
|
418
|
+
tr_p.add_argument("-o", "--output", default=None, help="Output file")
|
|
419
|
+
tr_p.add_argument("--format", choices=["text", "json", "srt", "vtt"], default="text", help="Output format: text|json|srt|vtt")
|
|
420
420
|
# Speaker diarization
|
|
421
|
-
tr_p.add_argument("--speakers", action="store_true", help="
|
|
421
|
+
tr_p.add_argument("--speakers", action="store_true", help="Speaker diarization")
|
|
422
422
|
tr_p.add_argument("--speakers-expected", type=int, default=None, metavar="N", help="Expected speaker count (1-20)")
|
|
423
|
-
tr_p.add_argument("--speaker-names", default=None, metavar="A,B,C", help="Identify speakers by name")
|
|
423
|
+
tr_p.add_argument("--speaker-names", default=None, metavar="A,B,C", help="Identify speakers by name (comma-separated)")
|
|
424
424
|
# Audio intelligence
|
|
425
|
-
tr_p.add_argument("--sentiment", action="store_true", help="
|
|
426
|
-
tr_p.add_argument("--chapters", action="store_true", help="
|
|
427
|
-
tr_p.add_argument("--entities", action="store_true", help="
|
|
428
|
-
tr_p.add_argument("--summarize", action="store_true", help="
|
|
429
|
-
tr_p.add_argument("--highlights", action="store_true", help="
|
|
430
|
-
tr_p.add_argument("--topics", action="store_true", help="
|
|
431
|
-
tr_p.add_argument("--content-safety", action="store_true", help="
|
|
425
|
+
tr_p.add_argument("--sentiment", action="store_true", help="Sentiment analysis")
|
|
426
|
+
tr_p.add_argument("--chapters", action="store_true", help="Auto chapters")
|
|
427
|
+
tr_p.add_argument("--entities", action="store_true", help="Entity detection")
|
|
428
|
+
tr_p.add_argument("--summarize", action="store_true", help="Summarization")
|
|
429
|
+
tr_p.add_argument("--highlights", action="store_true", help="Auto highlights")
|
|
430
|
+
tr_p.add_argument("--topics", action="store_true", help="IAB topic detection")
|
|
431
|
+
tr_p.add_argument("--content-safety", action="store_true", help="Content safety detection")
|
|
432
432
|
# Transcription options
|
|
433
433
|
tr_p.add_argument("--language", default=None, metavar="CODE", help="Language code (default: auto-detect)")
|
|
434
434
|
tr_p.add_argument("--dual-channel", action="store_true", help="Dual channel transcription")
|
|
435
435
|
tr_p.add_argument("--multichannel", action="store_true", help="Multichannel transcription")
|
|
436
|
-
tr_p.add_argument("--word-boost", default=None, metavar="W1,W2", help="Boost accuracy for words")
|
|
436
|
+
tr_p.add_argument("--word-boost", default=None, metavar="W1,W2", help="Boost accuracy for words (comma-separated)")
|
|
437
437
|
tr_p.add_argument("--disfluencies", action="store_true", help="Include filler words")
|
|
438
438
|
tr_p.add_argument("--filter-profanity", action="store_true", help="Filter profanity")
|
|
439
439
|
tr_p.add_argument("--redact-pii", action="store_true", help="Redact PII")
|
|
440
440
|
tr_p.set_defaults(func=cmd_transcribe)
|
|
441
441
|
|
|
442
442
|
# status
|
|
443
|
-
st_p = audio_subs.add_parser("status", aliases=["s"], help="Check transcript status")
|
|
443
|
+
st_p = audio_subs.add_parser("status", aliases=["s"], help="Check transcript job status")
|
|
444
444
|
st_p.add_argument("transcript_id", help="Transcript ID")
|
|
445
445
|
st_p.set_defaults(func=cmd_status)
|
|
446
446
|
|
|
447
447
|
# get
|
|
448
|
-
get_p = audio_subs.add_parser("get", aliases=["g"], help="Fetch completed transcript")
|
|
448
|
+
get_p = audio_subs.add_parser("get", aliases=["g"], help="Fetch a completed transcript")
|
|
449
449
|
get_p.add_argument("transcript_id", help="Transcript ID")
|
|
450
|
-
get_p.add_argument("--format", choices=["text", "json", "srt", "vtt"], default="text", help="Output format")
|
|
451
|
-
get_p.add_argument("-o", "--output", default=None, help="
|
|
450
|
+
get_p.add_argument("--format", choices=["text", "json", "srt", "vtt"], default="text", help="Output format: text|json|srt|vtt")
|
|
451
|
+
get_p.add_argument("-o", "--output", default=None, help="Output file")
|
|
452
452
|
get_p.set_defaults(func=cmd_get)
|
|
453
453
|
|
|
454
454
|
# list
|
|
@@ -333,7 +333,7 @@ def register(subparsers):
|
|
|
333
333
|
F = argparse.RawDescriptionHelpFormatter
|
|
334
334
|
|
|
335
335
|
docs_p = subparsers.add_parser(
|
|
336
|
-
"docs", aliases=["d"], help="
|
|
336
|
+
"docs", aliases=["d"], help="Fetch up-to-date library docs",
|
|
337
337
|
formatter_class=F,
|
|
338
338
|
epilog="""subcommands:
|
|
339
339
|
search <library> Search for libraries
|
|
@@ -350,30 +350,30 @@ examples:
|
|
|
350
350
|
docs_subs = docs_p.add_subparsers(dest="docs_command", metavar="subcommand")
|
|
351
351
|
|
|
352
352
|
# search
|
|
353
|
-
search_p = docs_subs.add_parser("search", aliases=["s"], help="Search for libraries")
|
|
354
|
-
search_p.add_argument("library_name", help="Library name
|
|
353
|
+
search_p = docs_subs.add_parser("search", aliases=["s"], help="Search for libraries by name")
|
|
354
|
+
search_p.add_argument("library_name", help="Library name")
|
|
355
355
|
search_p.add_argument("query", nargs="?", default=None, help="Optional search query")
|
|
356
|
-
search_p.add_argument("--limit", type=int, default=10, help="
|
|
356
|
+
search_p.add_argument("--limit", type=int, default=10, help="Max results (default: 10)")
|
|
357
357
|
search_p.add_argument("--refresh", action="store_true", help="Bypass cache")
|
|
358
358
|
search_p.set_defaults(func=cmd_search)
|
|
359
359
|
|
|
360
360
|
# get (one-shot: resolve + fetch)
|
|
361
|
-
get_p = docs_subs.add_parser("get", aliases=["g"], help="Resolve
|
|
361
|
+
get_p = docs_subs.add_parser("get", aliases=["g"], help="Resolve a library and fetch docs in one shot")
|
|
362
362
|
get_p.add_argument("library_name", help="Library name")
|
|
363
363
|
get_p.add_argument("query", help="Documentation query")
|
|
364
364
|
get_p.add_argument("--tokens", type=int, default=None, help="Max tokens to return")
|
|
365
365
|
get_p.add_argument("--page", type=int, default=None, help="Page number (1-10)")
|
|
366
|
-
get_p.add_argument("--topic", default=None, help="Focus on
|
|
366
|
+
get_p.add_argument("--topic", default=None, help="Focus on a topic")
|
|
367
367
|
get_p.add_argument("--refresh", action="store_true", help="Bypass cache")
|
|
368
368
|
get_p.set_defaults(func=cmd_get)
|
|
369
369
|
|
|
370
370
|
# fetch (by library ID)
|
|
371
|
-
fetch_p = docs_subs.add_parser("fetch", aliases=["f"], help="Fetch docs by library ID")
|
|
371
|
+
fetch_p = docs_subs.add_parser("fetch", aliases=["f"], help="Fetch docs by exact library ID")
|
|
372
372
|
fetch_p.add_argument("library_id", help="Library ID (e.g. /facebook/react)")
|
|
373
373
|
fetch_p.add_argument("query", help="Documentation query")
|
|
374
374
|
fetch_p.add_argument("--tokens", type=int, default=None, help="Max tokens to return")
|
|
375
375
|
fetch_p.add_argument("--page", type=int, default=None, help="Page number (1-10)")
|
|
376
|
-
fetch_p.add_argument("--topic", default=None, help="Focus on
|
|
376
|
+
fetch_p.add_argument("--topic", default=None, help="Focus on a topic")
|
|
377
377
|
fetch_p.add_argument("--refresh", action="store_true", help="Bypass cache")
|
|
378
378
|
fetch_p.set_defaults(func=cmd_fetch)
|
|
379
379
|
|
|
@@ -27,13 +27,27 @@ import urllib.request
|
|
|
27
27
|
import urllib.error
|
|
28
28
|
from datetime import datetime, timezone
|
|
29
29
|
|
|
30
|
-
from ..services.credentials import
|
|
30
|
+
from ..services.credentials import (
|
|
31
|
+
get_key_for_service,
|
|
32
|
+
call_with_retry,
|
|
33
|
+
vend_key,
|
|
34
|
+
ServiceCallError,
|
|
35
|
+
CONFIG_DIR,
|
|
36
|
+
)
|
|
31
37
|
|
|
32
38
|
API_BASE = "https://api.parallel.ai"
|
|
33
39
|
SSE_BETA = "events-sse-2025-07-24"
|
|
34
40
|
MAX_POLL_WAIT = 7200 # 2 hours (ultra8x can take up to 2h)
|
|
35
41
|
POLL_INTERVAL = 10
|
|
36
42
|
|
|
43
|
+
# Local cache mapping run_id → fingerprint of the issuing key.
|
|
44
|
+
# Used so --status / --result vend the same key that created the run.
|
|
45
|
+
_RUN_OWNERS_FILE = CONFIG_DIR / "parallel-run-owners.json"
|
|
46
|
+
|
|
47
|
+
# Max times we'll re-vend trying to land on the issuing fingerprint
|
|
48
|
+
# before giving up and using whatever we got.
|
|
49
|
+
_STICKY_VEND_ATTEMPTS = 6
|
|
50
|
+
|
|
37
51
|
PROCESSORS = [
|
|
38
52
|
"lite", "base", "core", "core2x", "pro", "ultra",
|
|
39
53
|
"ultra2x", "ultra4x", "ultra8x",
|
|
@@ -42,6 +56,70 @@ PROCESSORS = [
|
|
|
42
56
|
]
|
|
43
57
|
|
|
44
58
|
|
|
59
|
+
# ── Run-owner cache ──────────────────────────────────────────────
|
|
60
|
+
|
|
61
|
+
def _load_run_owners() -> dict:
|
|
62
|
+
if _RUN_OWNERS_FILE.exists():
|
|
63
|
+
try:
|
|
64
|
+
return json.loads(_RUN_OWNERS_FILE.read_text())
|
|
65
|
+
except (json.JSONDecodeError, OSError):
|
|
66
|
+
return {}
|
|
67
|
+
return {}
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _remember_run_owner(run_id: str, fingerprint: str) -> None:
|
|
71
|
+
"""Persist {run_id -> fingerprint} so later --status/--result can reuse the key."""
|
|
72
|
+
if not run_id or not fingerprint or fingerprint == "env":
|
|
73
|
+
return
|
|
74
|
+
try:
|
|
75
|
+
CONFIG_DIR.mkdir(parents=True, exist_ok=True)
|
|
76
|
+
owners = _load_run_owners()
|
|
77
|
+
owners[run_id] = fingerprint
|
|
78
|
+
# Cap entries (keep last 500) to bound file size
|
|
79
|
+
if len(owners) > 500:
|
|
80
|
+
owners = dict(list(owners.items())[-500:])
|
|
81
|
+
_RUN_OWNERS_FILE.write_text(json.dumps(owners))
|
|
82
|
+
except OSError:
|
|
83
|
+
pass
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _recall_run_owner(run_id: str) -> str | None:
|
|
87
|
+
return _load_run_owners().get(run_id)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _get_key_for_run(run_id: str) -> str | None:
|
|
91
|
+
"""
|
|
92
|
+
Vend a key suitable for operating on run_id.
|
|
93
|
+
|
|
94
|
+
If PARALLEL_API_KEY is set, use it (single-key mode).
|
|
95
|
+
Otherwise vend from gate. If we remember the issuing fingerprint for this
|
|
96
|
+
run, retry vending until it matches (gate is headroom-rotated, so we may
|
|
97
|
+
have to vend a few times). If we can't land on the right key, fall back
|
|
98
|
+
to whatever vended and warn — caller will see a 404 if it's the wrong org.
|
|
99
|
+
"""
|
|
100
|
+
env_key = os.environ.get("PARALLEL_API_KEY")
|
|
101
|
+
if env_key:
|
|
102
|
+
return env_key
|
|
103
|
+
|
|
104
|
+
target = _recall_run_owner(run_id)
|
|
105
|
+
last_key = None
|
|
106
|
+
for _ in range(_STICKY_VEND_ATTEMPTS):
|
|
107
|
+
result = vend_key("parallel")
|
|
108
|
+
if not result:
|
|
109
|
+
return None
|
|
110
|
+
last_key = result["api_key"]
|
|
111
|
+
if target is None or result["fingerprint"] == target:
|
|
112
|
+
return last_key
|
|
113
|
+
|
|
114
|
+
if target is not None:
|
|
115
|
+
print(
|
|
116
|
+
f" [warn] could not vend issuing key for {run_id} "
|
|
117
|
+
f"(wanted fingerprint {target[:8]}…); response may 404",
|
|
118
|
+
file=sys.stderr,
|
|
119
|
+
)
|
|
120
|
+
return last_key
|
|
121
|
+
|
|
122
|
+
|
|
45
123
|
def _get_api_key() -> str:
|
|
46
124
|
key = get_key_for_service("parallel", "PARALLEL_API_KEY")
|
|
47
125
|
if not key:
|
|
@@ -87,7 +165,24 @@ def _api_request(method: str, path: str, api_key: str,
|
|
|
87
165
|
|
|
88
166
|
# ── SSE streaming ────────────────────────────────────────────────
|
|
89
167
|
|
|
168
|
+
def _sse_error_message(payload: dict) -> str:
|
|
169
|
+
"""Pull a readable message out of a Parallel error payload."""
|
|
170
|
+
err = payload.get("error") if isinstance(payload, dict) else None
|
|
171
|
+
if isinstance(err, dict):
|
|
172
|
+
return err.get("message") or err.get("detail") or json.dumps(err)
|
|
173
|
+
return payload.get("message") or json.dumps(payload)
|
|
174
|
+
|
|
175
|
+
|
|
90
176
|
def _stream_sse(api_key: str, run_id: str, quiet: bool = False) -> dict | None:
|
|
177
|
+
"""
|
|
178
|
+
Stream task progress over SSE.
|
|
179
|
+
|
|
180
|
+
Returns:
|
|
181
|
+
dict — task output (terminal state: completed)
|
|
182
|
+
None — stream ended without terminal state; caller should poll
|
|
183
|
+
Exits:
|
|
184
|
+
2 — definitive error (task failed, run not found, wrong-org key)
|
|
185
|
+
"""
|
|
91
186
|
url = f"{API_BASE}/v1beta/tasks/runs/{run_id}/events"
|
|
92
187
|
seen = set()
|
|
93
188
|
|
|
@@ -103,20 +198,45 @@ def _stream_sse(api_key: str, run_id: str, quiet: bool = False) -> dict | None:
|
|
|
103
198
|
|
|
104
199
|
try:
|
|
105
200
|
resp = urllib.request.urlopen(req, timeout=600)
|
|
201
|
+
except urllib.error.HTTPError as e:
|
|
202
|
+
try:
|
|
203
|
+
err_payload = json.loads(e.read().decode(errors="replace"))
|
|
204
|
+
msg = _sse_error_message(err_payload)
|
|
205
|
+
except Exception:
|
|
206
|
+
msg = str(e)
|
|
207
|
+
print(f" ✗ SSE error ({e.code}): {msg}", file=sys.stderr)
|
|
208
|
+
sys.exit(2)
|
|
106
209
|
except Exception as e:
|
|
107
210
|
if not quiet:
|
|
108
|
-
print(f" [sse] failed: {e}", file=sys.stderr)
|
|
211
|
+
print(f" [sse] connect failed: {e}", file=sys.stderr)
|
|
109
212
|
return None
|
|
110
213
|
|
|
214
|
+
# Detect non-SSE responses (HTTP 200 + JSON error body).
|
|
215
|
+
# Parallel returns this when the API key doesn't own the run_id, or
|
|
216
|
+
# when the run_id is malformed. The Content-Type is still
|
|
217
|
+
# `text/event-stream` so we can't rely on the header — instead we
|
|
218
|
+
# buffer non-SSE lines and, if we exit the loop without ever seeing
|
|
219
|
+
# a real SSE event, treat the buffer as a JSON error body.
|
|
220
|
+
saw_sse_event = False
|
|
221
|
+
non_sse_buffer: list[str] = []
|
|
222
|
+
|
|
111
223
|
try:
|
|
112
224
|
for raw_line in resp:
|
|
113
225
|
line = raw_line.decode("utf-8", errors="replace").rstrip("\n\r")
|
|
226
|
+
if not line:
|
|
227
|
+
continue
|
|
114
228
|
if not line.startswith("data: "):
|
|
229
|
+
# SSE comments start with ':' and field lines may be
|
|
230
|
+
# `event:` / `id:` / `retry:` — ignore those, but capture
|
|
231
|
+
# anything that looks like raw content for error-detect.
|
|
232
|
+
if line[:1] not in (":",) and not line.startswith(("event:", "id:", "retry:")):
|
|
233
|
+
non_sse_buffer.append(line)
|
|
115
234
|
continue
|
|
116
235
|
try:
|
|
117
236
|
event = json.loads(line[6:])
|
|
118
237
|
except json.JSONDecodeError:
|
|
119
238
|
continue
|
|
239
|
+
saw_sse_event = True
|
|
120
240
|
|
|
121
241
|
etype = event.get("type", "")
|
|
122
242
|
|
|
@@ -144,12 +264,27 @@ def _stream_sse(api_key: str, run_id: str, quiet: bool = False) -> dict | None:
|
|
|
144
264
|
sys.exit(2)
|
|
145
265
|
|
|
146
266
|
elif etype == "error":
|
|
147
|
-
|
|
267
|
+
msg = _sse_error_message(event)
|
|
268
|
+
print(f" ✗ SSE error event: {msg}", file=sys.stderr)
|
|
269
|
+
sys.exit(2)
|
|
148
270
|
except Exception:
|
|
149
271
|
pass
|
|
150
272
|
finally:
|
|
151
273
|
resp.close()
|
|
152
274
|
|
|
275
|
+
# If the stream produced zero SSE events but had body content,
|
|
276
|
+
# it's almost certainly an error response (wrong-org key, bad run_id).
|
|
277
|
+
# Surface it and bail rather than silently reconnecting 25 times.
|
|
278
|
+
if not saw_sse_event and non_sse_buffer:
|
|
279
|
+
body = "\n".join(non_sse_buffer)
|
|
280
|
+
try:
|
|
281
|
+
payload = json.loads(body)
|
|
282
|
+
msg = _sse_error_message(payload)
|
|
283
|
+
except json.JSONDecodeError:
|
|
284
|
+
msg = body[:300]
|
|
285
|
+
print(f" ✗ SSE refused: {msg}", file=sys.stderr)
|
|
286
|
+
sys.exit(2)
|
|
287
|
+
|
|
153
288
|
return None
|
|
154
289
|
|
|
155
290
|
|
|
@@ -345,10 +480,15 @@ def cmd_run(args):
|
|
|
345
480
|
if getattr(args, "follow_up", None):
|
|
346
481
|
body["previous_interaction_id"] = args.follow_up
|
|
347
482
|
|
|
348
|
-
# Submit with retry — vend key, submit task, retry on 402/429
|
|
483
|
+
# Submit with retry — vend key, submit task, retry on 402/429.
|
|
484
|
+
# Capture the issuing VendedKey so SSE + polling use the SAME key.
|
|
485
|
+
# Without this, gate may vend a different org's key for the followup
|
|
486
|
+
# GET/SSE calls, causing the streamed run_id to look "not found".
|
|
349
487
|
headers = {"parallel-beta": SSE_BETA}
|
|
488
|
+
captured: dict = {"key": None}
|
|
350
489
|
|
|
351
490
|
def submit(key):
|
|
491
|
+
captured["key"] = key
|
|
352
492
|
return _api_request("POST", "/v1/tasks/runs", key.api_key,
|
|
353
493
|
body=body, extra_headers=headers, raise_on_error=True)
|
|
354
494
|
|
|
@@ -385,11 +525,17 @@ def cmd_run(args):
|
|
|
385
525
|
print(" ✗ no Parallel API key. Set PARALLEL_API_KEY or add one via the dashboard.", file=sys.stderr)
|
|
386
526
|
return 1
|
|
387
527
|
|
|
388
|
-
#
|
|
389
|
-
|
|
528
|
+
# Reuse the SAME key that submitted the task for SSE + polling.
|
|
529
|
+
vended = captured["key"]
|
|
530
|
+
api_key = vended.api_key if vended is not None else _get_api_key()
|
|
390
531
|
run_id = task["run_id"]
|
|
391
532
|
created_at = task.get("created_at", "")
|
|
392
533
|
|
|
534
|
+
# Remember which gate-vended key owns this run, so later
|
|
535
|
+
# `escli research --status/--result <run_id>` can vend the same one.
|
|
536
|
+
if vended is not None:
|
|
537
|
+
_remember_run_owner(run_id, vended.fingerprint)
|
|
538
|
+
|
|
393
539
|
if not args.quiet:
|
|
394
540
|
print(f" · run_id: {run_id}", file=sys.stderr)
|
|
395
541
|
|
|
@@ -436,7 +582,7 @@ def cmd_run(args):
|
|
|
436
582
|
|
|
437
583
|
def cmd_status(args):
|
|
438
584
|
"""Check task run status."""
|
|
439
|
-
api_key = _get_api_key()
|
|
585
|
+
api_key = _get_key_for_run(args.run_id) or _get_api_key()
|
|
440
586
|
result = _api_request("GET", f"/v1/tasks/runs/{args.run_id}", api_key)
|
|
441
587
|
|
|
442
588
|
if args.json:
|
|
@@ -511,7 +657,7 @@ def cmd_processors(args):
|
|
|
511
657
|
|
|
512
658
|
def cmd_result(args):
|
|
513
659
|
"""Fetch completed task result."""
|
|
514
|
-
api_key = _get_api_key()
|
|
660
|
+
api_key = _get_key_for_run(args.run_id) or _get_api_key()
|
|
515
661
|
|
|
516
662
|
# Check status first
|
|
517
663
|
run = _api_request("GET", f"/v1/tasks/runs/{args.run_id}", api_key)
|
|
@@ -554,7 +700,7 @@ def register(subparsers):
|
|
|
554
700
|
F = argparse.RawDescriptionHelpFormatter
|
|
555
701
|
|
|
556
702
|
p = subparsers.add_parser(
|
|
557
|
-
"research", aliases=["r"], help="
|
|
703
|
+
"research", aliases=["r"], help="Run deep web research tasks",
|
|
558
704
|
formatter_class=F,
|
|
559
705
|
epilog="""modes:
|
|
560
706
|
escli research "query" -o report.md Run a research task (default)
|
|
@@ -591,31 +737,31 @@ output modes:
|
|
|
591
737
|
mode_g.add_argument("--result", default=None, metavar="RUN_ID", help="Fetch completed task result")
|
|
592
738
|
|
|
593
739
|
# Run options
|
|
594
|
-
p.add_argument("-o", "--output", default=None, help="Output
|
|
740
|
+
p.add_argument("-o", "--output", default=None, help="Output path (md or json)")
|
|
595
741
|
p.add_argument("-p", "--processor", default="pro", choices=PROCESSORS, help="Processor tier (default: pro)")
|
|
596
742
|
|
|
597
743
|
# Output schema
|
|
598
744
|
schema_g = p.add_argument_group("output schema")
|
|
599
|
-
schema_g.add_argument("--text", action="store_true", help="Markdown report
|
|
745
|
+
schema_g.add_argument("--text", action="store_true", help="Markdown report output")
|
|
600
746
|
schema_g.add_argument("--schema", default=None, metavar="FILE", help="JSON Schema file for structured output")
|
|
601
|
-
schema_g.add_argument("--output-schema", default=None, metavar="STR", help="Inline
|
|
747
|
+
schema_g.add_argument("--output-schema", default=None, metavar="STR", help="Inline schema description")
|
|
602
748
|
|
|
603
749
|
# Input
|
|
604
750
|
input_g = p.add_argument_group("input")
|
|
605
|
-
input_g.add_argument("--input-json", default=None, metavar="JSON", help="JSON
|
|
606
|
-
input_g.add_argument("--input-file", default=None, metavar="FILE", help="JSON file
|
|
751
|
+
input_g.add_argument("--input-json", default=None, metavar="JSON", help="JSON input (instead of text)")
|
|
752
|
+
input_g.add_argument("--input-file", default=None, metavar="FILE", help="JSON input file")
|
|
607
753
|
|
|
608
754
|
# Source policy
|
|
609
755
|
source_g = p.add_argument_group("source policy")
|
|
610
756
|
source_g.add_argument("--include-domains", default=None, metavar="D1,D2", help="Only use these domains")
|
|
611
757
|
source_g.add_argument("--exclude-domains", default=None, metavar="D1,D2", help="Exclude these domains")
|
|
612
|
-
source_g.add_argument("--after-date", default=None, metavar="YYYY-MM-DD", help="
|
|
758
|
+
source_g.add_argument("--after-date", default=None, metavar="YYYY-MM-DD", help="Filter content after this date")
|
|
613
759
|
|
|
614
760
|
# Advanced
|
|
615
761
|
adv_g = p.add_argument_group("advanced")
|
|
616
|
-
adv_g.add_argument("--location", default=None, metavar="CC", help="ISO country code for geo-
|
|
762
|
+
adv_g.add_argument("--location", default=None, metavar="CC", help="ISO country code for geo-targeting")
|
|
617
763
|
adv_g.add_argument("--metadata", default=None, help="Metadata as JSON or key=val,key=val")
|
|
618
|
-
adv_g.add_argument("--follow-up", default=None, metavar="RUN_ID", help="Follow
|
|
764
|
+
adv_g.add_argument("--follow-up", default=None, metavar="RUN_ID", help="Follow up on a previous run")
|
|
619
765
|
adv_g.add_argument("--no-basis", action="store_true", help="Exclude citations and reasoning")
|
|
620
766
|
|
|
621
767
|
p.set_defaults(func=_dispatch)
|
|
@@ -268,7 +268,7 @@ def register(subparsers):
|
|
|
268
268
|
|
|
269
269
|
# search
|
|
270
270
|
search_p = subparsers.add_parser(
|
|
271
|
-
"search", aliases=["s"], help="
|
|
271
|
+
"search", aliases=["s"], help="Search the web, return ranked excerpts",
|
|
272
272
|
formatter_class=F,
|
|
273
273
|
epilog="""examples:
|
|
274
274
|
escli search "latest cloudflare workers features"
|
|
@@ -278,14 +278,14 @@ def register(subparsers):
|
|
|
278
278
|
Uses the free Parallel Search MCP. No API key needed.
|
|
279
279
|
Add a Parallel API key for higher rate limits.
|
|
280
280
|
""")
|
|
281
|
-
search_p.add_argument("objective", nargs="+", help="
|
|
281
|
+
search_p.add_argument("objective", nargs="+", help="Search objective")
|
|
282
282
|
search_p.add_argument("--queries", nargs="+", default=None, metavar="Q",
|
|
283
|
-
help="
|
|
283
|
+
help="Override queries (default: objective)")
|
|
284
284
|
search_p.set_defaults(func=cmd_search)
|
|
285
285
|
|
|
286
286
|
# fetch
|
|
287
287
|
fetch_p = subparsers.add_parser(
|
|
288
|
-
"fetch", aliases=["f"], help="Extract
|
|
288
|
+
"fetch", aliases=["f"], help="Extract clean content from URLs",
|
|
289
289
|
formatter_class=F,
|
|
290
290
|
epilog="""examples:
|
|
291
291
|
escli fetch https://docs.parallel.ai/search/search-quickstart
|
|
@@ -295,9 +295,9 @@ Add a Parallel API key for higher rate limits.
|
|
|
295
295
|
|
|
296
296
|
Uses the free Parallel Extract MCP. No API key needed.
|
|
297
297
|
""")
|
|
298
|
-
fetch_p.add_argument("urls", nargs="+", help="URLs to extract
|
|
299
|
-
fetch_p.add_argument("--objective", default=None, help="Focus
|
|
300
|
-
fetch_p.add_argument("--full", action="store_true", help="Return full
|
|
298
|
+
fetch_p.add_argument("urls", nargs="+", help="URLs to extract")
|
|
299
|
+
fetch_p.add_argument("--objective", default=None, help="Focus on this topic")
|
|
300
|
+
fetch_p.add_argument("--full", action="store_true", help="Return full content (not excerpts)")
|
|
301
301
|
fetch_p.set_defaults(func=cmd_fetch)
|
|
302
302
|
|
|
303
303
|
return search_p
|
|
@@ -215,7 +215,7 @@ def register(subparsers):
|
|
|
215
215
|
F = argparse.RawDescriptionHelpFormatter
|
|
216
216
|
|
|
217
217
|
p = subparsers.add_parser(
|
|
218
|
-
"social", help="
|
|
218
|
+
"social", help="Search posts across social platforms",
|
|
219
219
|
formatter_class=F,
|
|
220
220
|
epilog="""platforms:
|
|
221
221
|
reddit, x, tiktok, linkedin, instagram, facebook, youtube, combined (default)
|
|
@@ -235,11 +235,11 @@ time ranges:
|
|
|
235
235
|
year Last 12 months
|
|
236
236
|
""")
|
|
237
237
|
|
|
238
|
-
p.add_argument("query", nargs="+", help="
|
|
238
|
+
p.add_argument("query", nargs="+", help="Search query")
|
|
239
239
|
p.add_argument("--platform", "-p", default=None, metavar="P",
|
|
240
|
-
help=f"Platform(s)
|
|
240
|
+
help=f"Platform(s), comma-separated ({', '.join(VALID_PLATFORMS)})")
|
|
241
241
|
p.add_argument("--time", "-t", default=None, choices=VALID_TIME_RANGES, metavar="T",
|
|
242
|
-
help="Time range: day
|
|
242
|
+
help="Time range: day|week|month|year")
|
|
243
243
|
p.add_argument("--max-results", "-n", type=int, default=10, metavar="N",
|
|
244
244
|
help="Max results (1-20, default: 10)")
|
|
245
245
|
p.add_argument("--depth", default="advanced", choices=VALID_DEPTHS,
|
|
@@ -247,11 +247,11 @@ time ranges:
|
|
|
247
247
|
p.add_argument("--answer", "-a", action="store_true",
|
|
248
248
|
help="Include AI-synthesized answer")
|
|
249
249
|
p.add_argument("--raw", action="store_true",
|
|
250
|
-
help="Include full post content
|
|
250
|
+
help="Include full post content")
|
|
251
251
|
p.add_argument("--images", action="store_true",
|
|
252
|
-
help="Include images
|
|
252
|
+
help="Include images")
|
|
253
253
|
p.add_argument("--country", default=None, metavar="CC",
|
|
254
|
-
help="Country for geo-
|
|
254
|
+
help="Country for geo-targeting (e.g. 'united states')")
|
|
255
255
|
p.set_defaults(func=cmd_social)
|
|
256
256
|
|
|
257
257
|
return p
|
|
@@ -390,7 +390,7 @@ def register(subparsers):
|
|
|
390
390
|
F = argparse.RawDescriptionHelpFormatter
|
|
391
391
|
|
|
392
392
|
p = subparsers.add_parser(
|
|
393
|
-
"usage", aliases=["u"], help="API usage and spend
|
|
393
|
+
"usage", aliases=["u"], help="Show API usage and spend",
|
|
394
394
|
formatter_class=F,
|
|
395
395
|
epilog=f"""examples:
|
|
396
396
|
escli usage {DOT} summary, last 30 days
|
|
@@ -418,11 +418,11 @@ auth: requires `escli auth login` (uses CLI token, not API key).
|
|
|
418
418
|
p.add_argument("--days", type=int, default=30, metavar="N",
|
|
419
419
|
help="Window size in days (default: 30)")
|
|
420
420
|
p.add_argument("--service", default=None, metavar="NAME",
|
|
421
|
-
help="Filter to
|
|
421
|
+
help="Filter to one service (parallel|openai|assemblyai|...)")
|
|
422
422
|
p.add_argument("--daily", action="store_true",
|
|
423
|
-
help="
|
|
423
|
+
help="Day-by-day breakdown")
|
|
424
424
|
p.add_argument("--pricing", action="store_true",
|
|
425
|
-
help="Show current pricing rules
|
|
425
|
+
help="Show current pricing rules")
|
|
426
426
|
p.set_defaults(func=cmd_usage)
|
|
427
427
|
|
|
428
428
|
return p
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|