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/commands/docs.py
ADDED
|
@@ -0,0 +1,354 @@
|
|
|
1
|
+
"""
|
|
2
|
+
escli docs — library documentation search via Context7 API.
|
|
3
|
+
|
|
4
|
+
Usage:
|
|
5
|
+
escli docs search <library> Search for libraries
|
|
6
|
+
escli docs get <library> <query> Resolve + fetch docs (one-shot)
|
|
7
|
+
escli docs fetch <library-id> <query> Fetch docs by library ID
|
|
8
|
+
|
|
9
|
+
Flags:
|
|
10
|
+
--tokens N Max tokens to return
|
|
11
|
+
--page N Page number (1-10)
|
|
12
|
+
--topic T Focus on specific topic
|
|
13
|
+
--format F Output format: auto|text|json|markdown
|
|
14
|
+
--refresh Bypass cache
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
import argparse
|
|
18
|
+
import hashlib
|
|
19
|
+
import json
|
|
20
|
+
import os
|
|
21
|
+
import pathlib
|
|
22
|
+
import sys
|
|
23
|
+
import time
|
|
24
|
+
|
|
25
|
+
from ..services.credentials import get_key_for_service, report_key
|
|
26
|
+
|
|
27
|
+
CTX7_API = "https://context7.com/api"
|
|
28
|
+
CACHE_DIR = pathlib.Path(os.environ.get("ESCLI_CACHE_DIR", pathlib.Path.home() / ".escli" / "cache"))
|
|
29
|
+
CACHE_TTL_SEARCH = 86400 # 24h
|
|
30
|
+
CACHE_TTL_RESOLVE = 604800 # 7d
|
|
31
|
+
CACHE_TTL_DOCS = 86400 # 24h
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
# ── Cache ────────────────────────────────────────────────────────
|
|
35
|
+
|
|
36
|
+
def _cache_path(layer: str, key: str) -> pathlib.Path:
|
|
37
|
+
h = hashlib.sha256(key.encode()).hexdigest()
|
|
38
|
+
return CACHE_DIR / layer / f"{h}.json"
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _cache_read(layer: str, key: str, ttl: int) -> dict | None:
|
|
42
|
+
path = _cache_path(layer, key)
|
|
43
|
+
if not path.exists():
|
|
44
|
+
return None
|
|
45
|
+
try:
|
|
46
|
+
data = json.loads(path.read_text())
|
|
47
|
+
if time.time() > data.get("expires_at", 0):
|
|
48
|
+
path.unlink(missing_ok=True)
|
|
49
|
+
return None
|
|
50
|
+
return data.get("payload")
|
|
51
|
+
except (json.JSONDecodeError, OSError):
|
|
52
|
+
path.unlink(missing_ok=True)
|
|
53
|
+
return None
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _cache_write(layer: str, key: str, payload: dict, ttl: int):
|
|
57
|
+
path = _cache_path(layer, key)
|
|
58
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
59
|
+
envelope = {
|
|
60
|
+
"created_at": time.time(),
|
|
61
|
+
"expires_at": time.time() + ttl,
|
|
62
|
+
"payload": payload,
|
|
63
|
+
}
|
|
64
|
+
path.write_text(json.dumps(envelope))
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
# ── HTTP ─────────────────────────────────────────────────────────
|
|
68
|
+
|
|
69
|
+
def _get_api_key() -> str | None:
|
|
70
|
+
return get_key_for_service("context7", "CONTEXT7_API_KEY")
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _request(url: str, api_key: str | None = None) -> dict:
|
|
74
|
+
import httpx
|
|
75
|
+
headers = {"User-Agent": "escli-docs/1.0", "X-Context7-Source": "cli"}
|
|
76
|
+
if api_key:
|
|
77
|
+
headers["Authorization"] = f"Bearer {api_key}"
|
|
78
|
+
|
|
79
|
+
resp = httpx.get(url, headers=headers, timeout=30, follow_redirects=True)
|
|
80
|
+
|
|
81
|
+
# Report rate state back to gate
|
|
82
|
+
if api_key:
|
|
83
|
+
remaining = resp.headers.get("ratelimit-remaining")
|
|
84
|
+
limit = resp.headers.get("ratelimit-limit")
|
|
85
|
+
reset = resp.headers.get("ratelimit-reset")
|
|
86
|
+
tier = resp.headers.get("context7-quota-tier")
|
|
87
|
+
report_key(
|
|
88
|
+
"context7",
|
|
89
|
+
api_key[-8:],
|
|
90
|
+
resp.status_code,
|
|
91
|
+
remaining=int(remaining) if remaining else None,
|
|
92
|
+
limit=int(limit) if limit else None,
|
|
93
|
+
reset=reset,
|
|
94
|
+
tier=tier,
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
if resp.status_code == 429:
|
|
98
|
+
print(" ✗ rate limited. try again later.", file=sys.stderr)
|
|
99
|
+
sys.exit(1)
|
|
100
|
+
if resp.status_code == 404:
|
|
101
|
+
return {"_status": 404}
|
|
102
|
+
resp.raise_for_status()
|
|
103
|
+
return resp.json()
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
# ── Commands ─────────────────────────────────────────────────────
|
|
107
|
+
|
|
108
|
+
def cmd_search(args):
|
|
109
|
+
"""Search for libraries by name."""
|
|
110
|
+
import urllib.parse
|
|
111
|
+
name = args.library_name
|
|
112
|
+
query = getattr(args, "query", None) or name
|
|
113
|
+
refresh = getattr(args, "refresh", False)
|
|
114
|
+
limit = getattr(args, "limit", 10) or 10
|
|
115
|
+
|
|
116
|
+
cache_key = f"search:{name.lower()}:{query.lower()}"
|
|
117
|
+
if not refresh:
|
|
118
|
+
cached = _cache_read("search", cache_key, CACHE_TTL_SEARCH)
|
|
119
|
+
if cached:
|
|
120
|
+
_print_search(cached, args, from_cache=True)
|
|
121
|
+
return 0
|
|
122
|
+
|
|
123
|
+
api_key = _get_api_key()
|
|
124
|
+
url = f"{CTX7_API}/v2/libs/search?libraryName={urllib.parse.quote(name)}&query={urllib.parse.quote(query)}"
|
|
125
|
+
data = _request(url, api_key)
|
|
126
|
+
|
|
127
|
+
if data.get("_status") == 404:
|
|
128
|
+
if args.json:
|
|
129
|
+
print(json.dumps({"success": False, "error": f"no libraries found for: {name}"}))
|
|
130
|
+
else:
|
|
131
|
+
print(f" ✗ no libraries found for: {name}", file=sys.stderr)
|
|
132
|
+
return 1
|
|
133
|
+
|
|
134
|
+
_cache_write("search", cache_key, data, CACHE_TTL_SEARCH)
|
|
135
|
+
|
|
136
|
+
# Client-side limit
|
|
137
|
+
if "results" in data:
|
|
138
|
+
data["results"] = data["results"][:limit]
|
|
139
|
+
|
|
140
|
+
_print_search(data, args)
|
|
141
|
+
return 0
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def _print_search(data, args, from_cache=False):
|
|
145
|
+
results = data.get("results", [])
|
|
146
|
+
if args.json:
|
|
147
|
+
print(json.dumps({"success": True, "cached": from_cache, "results": results, "count": len(results)}))
|
|
148
|
+
return
|
|
149
|
+
|
|
150
|
+
if not results:
|
|
151
|
+
print(" No results found.", file=sys.stderr)
|
|
152
|
+
return
|
|
153
|
+
|
|
154
|
+
cache_label = " (cached)" if from_cache else ""
|
|
155
|
+
if not args.quiet:
|
|
156
|
+
print(f"\n Search results{cache_label}:\n", file=sys.stderr)
|
|
157
|
+
for r in results:
|
|
158
|
+
state = r.get("state", "")
|
|
159
|
+
stars = r.get("stars", "")
|
|
160
|
+
print(f" {r.get('id', ''):<40} {r.get('title', '')}", file=sys.stderr)
|
|
161
|
+
print(f" state={state} stars={stars} {r.get('description', '')[:80]}", file=sys.stderr)
|
|
162
|
+
print(file=sys.stderr)
|
|
163
|
+
|
|
164
|
+
# Pipe-friendly: TSV to stdout
|
|
165
|
+
if args.quiet:
|
|
166
|
+
for r in results:
|
|
167
|
+
print(f"{r.get('id', '')}\t{r.get('title', '')}\t{r.get('state', '')}")
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def cmd_get(args):
|
|
171
|
+
"""One-shot: resolve library name to ID, then fetch docs."""
|
|
172
|
+
import urllib.parse
|
|
173
|
+
name = args.library_name
|
|
174
|
+
query = args.query
|
|
175
|
+
refresh = getattr(args, "refresh", False)
|
|
176
|
+
|
|
177
|
+
# Resolve
|
|
178
|
+
resolve_key = f"resolve:{name.lower()}"
|
|
179
|
+
library_id = None
|
|
180
|
+
if not refresh:
|
|
181
|
+
cached = _cache_read("resolve", resolve_key, CACHE_TTL_RESOLVE)
|
|
182
|
+
if cached:
|
|
183
|
+
library_id = cached.get("id")
|
|
184
|
+
|
|
185
|
+
if not library_id:
|
|
186
|
+
api_key = _get_api_key()
|
|
187
|
+
url = f"{CTX7_API}/v2/libs/search?libraryName={urllib.parse.quote(name)}&query={urllib.parse.quote(query)}"
|
|
188
|
+
data = _request(url, api_key)
|
|
189
|
+
|
|
190
|
+
if data.get("_status") == 404:
|
|
191
|
+
if args.json:
|
|
192
|
+
print(json.dumps({"success": False, "error": f"no libraries found for: {name}"}))
|
|
193
|
+
else:
|
|
194
|
+
print(f" ✗ no libraries found for: {name}", file=sys.stderr)
|
|
195
|
+
return 1
|
|
196
|
+
|
|
197
|
+
results = data.get("results", [])
|
|
198
|
+
finalized = [r for r in results if r.get("state") == "finalized"]
|
|
199
|
+
pick = finalized[0] if finalized else (results[0] if results else None)
|
|
200
|
+
|
|
201
|
+
if not pick:
|
|
202
|
+
if args.json:
|
|
203
|
+
print(json.dumps({"success": False, "error": f"no results for: {name}"}))
|
|
204
|
+
else:
|
|
205
|
+
print(f" ✗ no results for: {name}", file=sys.stderr)
|
|
206
|
+
return 1
|
|
207
|
+
|
|
208
|
+
library_id = pick["id"]
|
|
209
|
+
_cache_write("resolve", resolve_key, {"id": library_id, "name": name}, CACHE_TTL_RESOLVE)
|
|
210
|
+
|
|
211
|
+
if not args.quiet:
|
|
212
|
+
print(f" → resolved: {library_id}", file=sys.stderr)
|
|
213
|
+
|
|
214
|
+
# Fetch docs
|
|
215
|
+
return _fetch_docs(library_id, query, args)
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def cmd_fetch(args):
|
|
219
|
+
"""Fetch docs directly by library ID."""
|
|
220
|
+
return _fetch_docs(args.library_id, args.query, args)
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def _fetch_docs(library_id: str, query: str, args) -> int:
|
|
224
|
+
import urllib.parse
|
|
225
|
+
tokens = getattr(args, "tokens", None)
|
|
226
|
+
page = getattr(args, "page", None)
|
|
227
|
+
topic = getattr(args, "topic", None)
|
|
228
|
+
refresh = getattr(args, "refresh", False)
|
|
229
|
+
|
|
230
|
+
cache_key = f"docs:{library_id}:{query}:{tokens}:{page}:{topic}"
|
|
231
|
+
if not refresh:
|
|
232
|
+
cached = _cache_read("docs", cache_key, CACHE_TTL_DOCS)
|
|
233
|
+
if cached:
|
|
234
|
+
_print_docs(cached, args, library_id, query, from_cache=True)
|
|
235
|
+
return 0
|
|
236
|
+
|
|
237
|
+
api_key = _get_api_key()
|
|
238
|
+
url = f"{CTX7_API}/v2/context?libraryId={urllib.parse.quote(library_id)}&query={urllib.parse.quote(query)}&type=txt"
|
|
239
|
+
|
|
240
|
+
if tokens:
|
|
241
|
+
url += f"&tokens={tokens}"
|
|
242
|
+
if page:
|
|
243
|
+
url += f"&page={page}"
|
|
244
|
+
if topic:
|
|
245
|
+
url += f"&topic={urllib.parse.quote(topic)}"
|
|
246
|
+
|
|
247
|
+
resp = _request_raw(url, api_key)
|
|
248
|
+
|
|
249
|
+
if resp.status_code == 404:
|
|
250
|
+
if args.json:
|
|
251
|
+
print(json.dumps({"success": False, "error": f"library not found: {library_id}"}))
|
|
252
|
+
else:
|
|
253
|
+
print(f" ✗ library not found: {library_id}", file=sys.stderr)
|
|
254
|
+
return 1
|
|
255
|
+
|
|
256
|
+
body = resp.text
|
|
257
|
+
_cache_write("docs", cache_key, {"body": body}, CACHE_TTL_DOCS)
|
|
258
|
+
_print_docs({"body": body}, args, library_id, query)
|
|
259
|
+
return 0
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
def _request_raw(url: str, api_key: str | None = None):
|
|
263
|
+
import httpx
|
|
264
|
+
headers = {"User-Agent": "escli-docs/1.0", "X-Context7-Source": "cli"}
|
|
265
|
+
if api_key:
|
|
266
|
+
headers["Authorization"] = f"Bearer {api_key}"
|
|
267
|
+
|
|
268
|
+
resp = httpx.get(url, headers=headers, timeout=30, follow_redirects=True)
|
|
269
|
+
|
|
270
|
+
if api_key:
|
|
271
|
+
remaining = resp.headers.get("ratelimit-remaining")
|
|
272
|
+
limit = resp.headers.get("ratelimit-limit")
|
|
273
|
+
reset = resp.headers.get("ratelimit-reset")
|
|
274
|
+
tier = resp.headers.get("context7-quota-tier")
|
|
275
|
+
report_key(
|
|
276
|
+
"context7", api_key[-8:], resp.status_code,
|
|
277
|
+
remaining=int(remaining) if remaining else None,
|
|
278
|
+
limit=int(limit) if limit else None,
|
|
279
|
+
reset=reset, tier=tier,
|
|
280
|
+
)
|
|
281
|
+
|
|
282
|
+
if resp.status_code == 429:
|
|
283
|
+
print(" ✗ rate limited. try again later.", file=sys.stderr)
|
|
284
|
+
sys.exit(1)
|
|
285
|
+
|
|
286
|
+
return resp
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
def _print_docs(data, args, library_id, query, from_cache=False):
|
|
290
|
+
body = data.get("body", "")
|
|
291
|
+
if args.json:
|
|
292
|
+
print(json.dumps({
|
|
293
|
+
"success": True,
|
|
294
|
+
"cached": from_cache,
|
|
295
|
+
"library_id": library_id,
|
|
296
|
+
"query": query,
|
|
297
|
+
"content": body,
|
|
298
|
+
}))
|
|
299
|
+
else:
|
|
300
|
+
print(body)
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
# ── Parser ───────────────────────────────────────────────────────
|
|
304
|
+
|
|
305
|
+
def register(subparsers):
|
|
306
|
+
"""Register the docs subcommand group."""
|
|
307
|
+
F = argparse.RawDescriptionHelpFormatter
|
|
308
|
+
|
|
309
|
+
docs_p = subparsers.add_parser(
|
|
310
|
+
"docs", aliases=["d"], help="Library documentation search (Context7)",
|
|
311
|
+
formatter_class=F,
|
|
312
|
+
epilog="""subcommands:
|
|
313
|
+
search <library> Search for libraries
|
|
314
|
+
get <library> <query> Resolve + fetch docs (one-shot)
|
|
315
|
+
fetch <library-id> <query> Fetch docs by library ID
|
|
316
|
+
|
|
317
|
+
examples:
|
|
318
|
+
escli docs search react
|
|
319
|
+
escli docs get next.js "app router middleware"
|
|
320
|
+
escli docs get react "useEffect cleanup" --tokens 5000
|
|
321
|
+
escli --json --quiet docs get express "error handling"
|
|
322
|
+
""")
|
|
323
|
+
|
|
324
|
+
docs_subs = docs_p.add_subparsers(dest="docs_command", metavar="subcommand")
|
|
325
|
+
|
|
326
|
+
# search
|
|
327
|
+
search_p = docs_subs.add_parser("search", aliases=["s"], help="Search for libraries")
|
|
328
|
+
search_p.add_argument("library_name", help="Library name to search for")
|
|
329
|
+
search_p.add_argument("query", nargs="?", default=None, help="Optional search query")
|
|
330
|
+
search_p.add_argument("--limit", type=int, default=10, help="Limit results (default: 10)")
|
|
331
|
+
search_p.add_argument("--refresh", action="store_true", help="Bypass cache")
|
|
332
|
+
search_p.set_defaults(func=cmd_search)
|
|
333
|
+
|
|
334
|
+
# get (one-shot: resolve + fetch)
|
|
335
|
+
get_p = docs_subs.add_parser("get", aliases=["g"], help="Resolve + fetch docs")
|
|
336
|
+
get_p.add_argument("library_name", help="Library name")
|
|
337
|
+
get_p.add_argument("query", help="Documentation query")
|
|
338
|
+
get_p.add_argument("--tokens", type=int, default=None, help="Max tokens to return")
|
|
339
|
+
get_p.add_argument("--page", type=int, default=None, help="Page number (1-10)")
|
|
340
|
+
get_p.add_argument("--topic", default=None, help="Focus on specific topic")
|
|
341
|
+
get_p.add_argument("--refresh", action="store_true", help="Bypass cache")
|
|
342
|
+
get_p.set_defaults(func=cmd_get)
|
|
343
|
+
|
|
344
|
+
# fetch (by library ID)
|
|
345
|
+
fetch_p = docs_subs.add_parser("fetch", aliases=["f"], help="Fetch docs by library ID")
|
|
346
|
+
fetch_p.add_argument("library_id", help="Library ID (e.g. /facebook/react)")
|
|
347
|
+
fetch_p.add_argument("query", help="Documentation query")
|
|
348
|
+
fetch_p.add_argument("--tokens", type=int, default=None, help="Max tokens to return")
|
|
349
|
+
fetch_p.add_argument("--page", type=int, default=None, help="Page number (1-10)")
|
|
350
|
+
fetch_p.add_argument("--topic", default=None, help="Focus on specific topic")
|
|
351
|
+
fetch_p.add_argument("--refresh", action="store_true", help="Bypass cache")
|
|
352
|
+
fetch_p.set_defaults(func=cmd_fetch)
|
|
353
|
+
|
|
354
|
+
return docs_p
|