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
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
"""
|
|
2
|
+
CLI self-description system — generates machine-readable manifests
|
|
3
|
+
from argparse parsers for agent discovery.
|
|
4
|
+
|
|
5
|
+
Agents call `escli --describe` to get a compact JSON manifest of all
|
|
6
|
+
commands, or `escli <command> --schema` for per-command detail.
|
|
7
|
+
|
|
8
|
+
Token budget: --describe returns ~300-500 tokens (compact index).
|
|
9
|
+
Per-command --schema returns ~100-200 tokens (full detail for one command).
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import argparse
|
|
13
|
+
import json
|
|
14
|
+
from typing import Any
|
|
15
|
+
|
|
16
|
+
from escli import __version__
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def generate_manifest(parser: argparse.ArgumentParser) -> dict:
|
|
20
|
+
"""Generate the compact CLI manifest for --describe."""
|
|
21
|
+
commands = []
|
|
22
|
+
# Walk the subparsers
|
|
23
|
+
for action in parser._subparsers._actions:
|
|
24
|
+
if isinstance(action, argparse._SubParsersAction):
|
|
25
|
+
# Build a help text lookup from choices_actions
|
|
26
|
+
help_map: dict[str, str] = {}
|
|
27
|
+
for choice_action in getattr(action, '_choices_actions', []):
|
|
28
|
+
help_map[choice_action.dest] = choice_action.help or ""
|
|
29
|
+
|
|
30
|
+
for name, subparser in action.choices.items():
|
|
31
|
+
if _is_alias(name, action):
|
|
32
|
+
continue
|
|
33
|
+
cmd = _describe_command(name, subparser, help_map.get(name, ""))
|
|
34
|
+
if cmd:
|
|
35
|
+
commands.append(cmd)
|
|
36
|
+
|
|
37
|
+
return {
|
|
38
|
+
"name": "escli",
|
|
39
|
+
"version": __version__,
|
|
40
|
+
"description": "Eightstate CLI — AI services from the command line",
|
|
41
|
+
"agent_instructions": {
|
|
42
|
+
"structured_output": "Use --json for all commands to get {ok, data, error, meta} envelope on stdout",
|
|
43
|
+
"quiet_mode": "Use --quiet to suppress stderr progress messages",
|
|
44
|
+
"auth": "Run 'escli auth login' for browser OAuth, or set service-specific env vars",
|
|
45
|
+
"exit_codes": {
|
|
46
|
+
"0": "success",
|
|
47
|
+
"1": "error",
|
|
48
|
+
"2": "usage/validation",
|
|
49
|
+
"4": "auth failure",
|
|
50
|
+
"5": "not found",
|
|
51
|
+
"8": "transient/retryable",
|
|
52
|
+
},
|
|
53
|
+
},
|
|
54
|
+
"commands": commands,
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def generate_command_schema(name: str, parser: argparse.ArgumentParser) -> dict | None:
|
|
59
|
+
"""Generate detailed schema for a single command (--schema flag)."""
|
|
60
|
+
# Find the subparser for this command
|
|
61
|
+
for action in parser._subparsers._actions:
|
|
62
|
+
if isinstance(action, argparse._SubParsersAction):
|
|
63
|
+
if name in action.choices:
|
|
64
|
+
subparser = action.choices[name]
|
|
65
|
+
return _describe_command_detail(name, subparser)
|
|
66
|
+
return None
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _is_alias(name: str, action: argparse._SubParsersAction) -> bool:
|
|
70
|
+
"""Check if a subparser name is an alias (shares parser with another name)."""
|
|
71
|
+
parser = action.choices[name]
|
|
72
|
+
for other_name, other_parser in action.choices.items():
|
|
73
|
+
if other_parser is parser and other_name != name and len(other_name) > len(name):
|
|
74
|
+
return True # This name is shorter = alias
|
|
75
|
+
return False
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _describe_command(name: str, parser: argparse.ArgumentParser, help_text: str = "") -> dict | None:
|
|
79
|
+
"""Generate compact command entry for the manifest."""
|
|
80
|
+
desc = help_text or _first_line(parser.description or "") or name
|
|
81
|
+
|
|
82
|
+
# Check for subcommands
|
|
83
|
+
subcommands = []
|
|
84
|
+
for action in parser._subparsers._actions if parser._subparsers else []:
|
|
85
|
+
if isinstance(action, argparse._SubParsersAction):
|
|
86
|
+
for sub_name, sub_parser in action.choices.items():
|
|
87
|
+
if not _is_alias(sub_name, action):
|
|
88
|
+
subcommands.append(sub_name)
|
|
89
|
+
|
|
90
|
+
entry: dict[str, Any] = {
|
|
91
|
+
"name": name,
|
|
92
|
+
"description": desc,
|
|
93
|
+
}
|
|
94
|
+
if subcommands:
|
|
95
|
+
entry["subcommands"] = subcommands
|
|
96
|
+
return entry
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def _describe_command_detail(name: str, parser: argparse.ArgumentParser) -> dict:
|
|
100
|
+
"""Generate full command schema with args, flags, examples."""
|
|
101
|
+
args = []
|
|
102
|
+
flags = []
|
|
103
|
+
|
|
104
|
+
for action in parser._actions:
|
|
105
|
+
if isinstance(action, (argparse._HelpAction, argparse._VersionAction)):
|
|
106
|
+
continue
|
|
107
|
+
if isinstance(action, argparse._SubParsersAction):
|
|
108
|
+
continue
|
|
109
|
+
|
|
110
|
+
if action.option_strings:
|
|
111
|
+
# It's a flag
|
|
112
|
+
flag: dict[str, Any] = {
|
|
113
|
+
"names": action.option_strings,
|
|
114
|
+
"type": _type_name(action),
|
|
115
|
+
"required": action.required,
|
|
116
|
+
}
|
|
117
|
+
if action.default is not None and action.default != argparse.SUPPRESS:
|
|
118
|
+
flag["default"] = str(action.default) if action.default is not argparse.SUPPRESS else None
|
|
119
|
+
if action.help and action.help != argparse.SUPPRESS:
|
|
120
|
+
flag["description"] = action.help
|
|
121
|
+
if action.choices:
|
|
122
|
+
flag["choices"] = list(action.choices)
|
|
123
|
+
flags.append(flag)
|
|
124
|
+
else:
|
|
125
|
+
# Positional argument
|
|
126
|
+
arg: dict[str, Any] = {
|
|
127
|
+
"name": action.dest,
|
|
128
|
+
"type": _type_name(action),
|
|
129
|
+
"required": action.nargs not in ('?', '*'),
|
|
130
|
+
}
|
|
131
|
+
if action.help and action.help != argparse.SUPPRESS:
|
|
132
|
+
arg["description"] = action.help
|
|
133
|
+
if action.nargs in ('+', '*'):
|
|
134
|
+
arg["multiple"] = True
|
|
135
|
+
args.append(arg)
|
|
136
|
+
|
|
137
|
+
result: dict[str, Any] = {
|
|
138
|
+
"command": name,
|
|
139
|
+
"description": _first_line(parser.description or ""),
|
|
140
|
+
}
|
|
141
|
+
if args:
|
|
142
|
+
result["arguments"] = args
|
|
143
|
+
if flags:
|
|
144
|
+
result["flags"] = flags
|
|
145
|
+
|
|
146
|
+
# Extract examples from epilog
|
|
147
|
+
if parser.epilog:
|
|
148
|
+
examples = _extract_examples(parser.epilog)
|
|
149
|
+
if examples:
|
|
150
|
+
result["examples"] = examples
|
|
151
|
+
|
|
152
|
+
return result
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def _type_name(action: argparse.Action) -> str:
|
|
156
|
+
"""Get a human/machine-readable type name for an action."""
|
|
157
|
+
if isinstance(action, (argparse._StoreTrueAction, argparse._StoreFalseAction)):
|
|
158
|
+
return "bool"
|
|
159
|
+
if action.type is int:
|
|
160
|
+
return "int"
|
|
161
|
+
if action.type is float:
|
|
162
|
+
return "float"
|
|
163
|
+
if action.choices:
|
|
164
|
+
return "enum"
|
|
165
|
+
if action.nargs in ('+', '*'):
|
|
166
|
+
return "string[]"
|
|
167
|
+
return "string"
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def _first_line(text: str) -> str:
|
|
171
|
+
"""Get the first non-empty line of text."""
|
|
172
|
+
for line in text.strip().split('\n'):
|
|
173
|
+
line = line.strip()
|
|
174
|
+
if line:
|
|
175
|
+
return line
|
|
176
|
+
return ""
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def _extract_examples(epilog: str) -> list[str]:
|
|
180
|
+
"""Extract example commands from epilog text."""
|
|
181
|
+
examples = []
|
|
182
|
+
for line in epilog.split('\n'):
|
|
183
|
+
line = line.strip()
|
|
184
|
+
if line.startswith('escli '):
|
|
185
|
+
examples.append(line)
|
|
186
|
+
return examples[:5] # Cap at 5 examples
|
escli/services/output.py
ADDED
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Structured output envelope and error types for agent consumption.
|
|
3
|
+
|
|
4
|
+
Every --json response uses the standard envelope:
|
|
5
|
+
{"ok": true, "data": {...}, "error": null, "meta": {...}}
|
|
6
|
+
{"ok": false, "data": null, "error": {...}, "meta": {...}}
|
|
7
|
+
|
|
8
|
+
Error objects include machine-readable codes and remediation hints.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import json as jsonlib
|
|
12
|
+
import sys
|
|
13
|
+
import time
|
|
14
|
+
from typing import Any
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class CliError(Exception):
|
|
18
|
+
"""Structured CLI error with code, category, and remediation."""
|
|
19
|
+
|
|
20
|
+
def __init__(
|
|
21
|
+
self,
|
|
22
|
+
code: str,
|
|
23
|
+
message: str,
|
|
24
|
+
category: str = "user", # user, system, transient
|
|
25
|
+
exit_code: int = 1,
|
|
26
|
+
retryable: bool = False,
|
|
27
|
+
remediation: list[dict] | None = None,
|
|
28
|
+
details: dict | None = None,
|
|
29
|
+
):
|
|
30
|
+
super().__init__(message)
|
|
31
|
+
self.code = code
|
|
32
|
+
self.category = category
|
|
33
|
+
self.exit_code = exit_code
|
|
34
|
+
self.retryable = retryable
|
|
35
|
+
self.remediation = remediation or []
|
|
36
|
+
self.details = details
|
|
37
|
+
|
|
38
|
+
def to_dict(self) -> dict:
|
|
39
|
+
d: dict = {
|
|
40
|
+
"code": self.code,
|
|
41
|
+
"category": self.category,
|
|
42
|
+
"message": str(self),
|
|
43
|
+
"retryable": self.retryable,
|
|
44
|
+
}
|
|
45
|
+
if self.remediation:
|
|
46
|
+
d["remediation"] = self.remediation
|
|
47
|
+
if self.details:
|
|
48
|
+
d["details"] = self.details
|
|
49
|
+
return d
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
# ── Common errors ────────────────────────────────────────────────
|
|
53
|
+
|
|
54
|
+
class AuthError(CliError):
|
|
55
|
+
def __init__(self, message: str = "not authenticated", **kwargs):
|
|
56
|
+
super().__init__(
|
|
57
|
+
code="cli.auth.required",
|
|
58
|
+
message=message,
|
|
59
|
+
category="user",
|
|
60
|
+
exit_code=4,
|
|
61
|
+
remediation=[{"action": "run", "command": "escli auth login"}],
|
|
62
|
+
**kwargs,
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class NotFoundError(CliError):
|
|
67
|
+
def __init__(self, resource: str, **kwargs):
|
|
68
|
+
super().__init__(
|
|
69
|
+
code="cli.not_found",
|
|
70
|
+
message=f"not found: {resource}",
|
|
71
|
+
category="user",
|
|
72
|
+
exit_code=5,
|
|
73
|
+
**kwargs,
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
class ValidationError(CliError):
|
|
78
|
+
def __init__(self, message: str, **kwargs):
|
|
79
|
+
super().__init__(
|
|
80
|
+
code="cli.validation",
|
|
81
|
+
message=message,
|
|
82
|
+
category="user",
|
|
83
|
+
exit_code=2,
|
|
84
|
+
**kwargs,
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
class TransientError(CliError):
|
|
89
|
+
def __init__(self, message: str, retry_after: int | None = None, **kwargs):
|
|
90
|
+
remediation = [{"action": "retry"}]
|
|
91
|
+
if retry_after:
|
|
92
|
+
remediation[0]["backoff_seconds"] = retry_after
|
|
93
|
+
super().__init__(
|
|
94
|
+
code="cli.transient",
|
|
95
|
+
message=message,
|
|
96
|
+
category="transient",
|
|
97
|
+
exit_code=8,
|
|
98
|
+
retryable=True,
|
|
99
|
+
remediation=remediation,
|
|
100
|
+
**kwargs,
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
class ApiError(CliError):
|
|
105
|
+
def __init__(self, status: int, message: str, **kwargs):
|
|
106
|
+
retryable = status in (429, 500, 502, 503, 504)
|
|
107
|
+
category = "transient" if retryable else "system"
|
|
108
|
+
code = {
|
|
109
|
+
401: "cli.auth.invalid",
|
|
110
|
+
403: "cli.auth.forbidden",
|
|
111
|
+
404: "cli.not_found",
|
|
112
|
+
429: "cli.rate_limited",
|
|
113
|
+
}.get(status, f"cli.api.{status}")
|
|
114
|
+
|
|
115
|
+
remediation = []
|
|
116
|
+
if status == 401:
|
|
117
|
+
remediation = [{"action": "run", "command": "escli auth login"}]
|
|
118
|
+
elif status == 429:
|
|
119
|
+
remediation = [{"action": "retry", "backoff_seconds": 30}]
|
|
120
|
+
elif status >= 500:
|
|
121
|
+
remediation = [{"action": "retry", "backoff_seconds": 5}]
|
|
122
|
+
|
|
123
|
+
super().__init__(
|
|
124
|
+
code=code,
|
|
125
|
+
message=message,
|
|
126
|
+
category=category,
|
|
127
|
+
exit_code=8 if retryable else 1,
|
|
128
|
+
retryable=retryable,
|
|
129
|
+
remediation=remediation,
|
|
130
|
+
**kwargs,
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
# ── Envelope ─────────────────────────────────────────────────────
|
|
135
|
+
|
|
136
|
+
def success(data: Any = None, meta: dict | None = None) -> dict:
|
|
137
|
+
"""Build a success envelope."""
|
|
138
|
+
return {"ok": True, "data": data, "error": None, "meta": meta or {}}
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def failure(error: CliError | dict | str, meta: dict | None = None) -> dict:
|
|
142
|
+
"""Build a failure envelope."""
|
|
143
|
+
if isinstance(error, CliError):
|
|
144
|
+
err = error.to_dict()
|
|
145
|
+
elif isinstance(error, dict):
|
|
146
|
+
err = error
|
|
147
|
+
else:
|
|
148
|
+
err = {"code": "cli.error", "category": "system", "message": str(error), "retryable": False}
|
|
149
|
+
return {"ok": False, "data": None, "error": err, "meta": meta or {}}
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def emit_json(envelope: dict):
|
|
153
|
+
"""Print a JSON envelope to stdout."""
|
|
154
|
+
print(jsonlib.dumps(envelope))
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def emit_error(error: CliError, use_json: bool = False) -> int:
|
|
158
|
+
"""Print an error and return the exit code."""
|
|
159
|
+
if use_json:
|
|
160
|
+
emit_json(failure(error))
|
|
161
|
+
else:
|
|
162
|
+
print(f" ✗ {error}", file=sys.stderr)
|
|
163
|
+
for r in error.remediation:
|
|
164
|
+
if r.get("command"):
|
|
165
|
+
print(f" → run: {r['command']}", file=sys.stderr)
|
|
166
|
+
elif r.get("hint"):
|
|
167
|
+
print(f" → {r['hint']}", file=sys.stderr)
|
|
168
|
+
return error.exit_code
|