opensell-cli 0.1.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.
- opensell_cli/__init__.py +0 -0
- opensell_cli/catalog.py +39 -0
- opensell_cli/cli.py +76 -0
- opensell_cli/execute.py +26 -0
- opensell_cli/output.py +34 -0
- opensell_cli/schema_to_click.py +38 -0
- opensell_cli-0.1.0.dist-info/METADATA +10 -0
- opensell_cli-0.1.0.dist-info/RECORD +10 -0
- opensell_cli-0.1.0.dist-info/WHEEL +4 -0
- opensell_cli-0.1.0.dist-info/entry_points.txt +2 -0
opensell_cli/__init__.py
ADDED
|
File without changes
|
opensell_cli/catalog.py
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# opensell_cli/catalog.py
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import json
|
|
5
|
+
|
|
6
|
+
import click
|
|
7
|
+
from aixianyu_mcp.registry import TOOL_REGISTRY
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def build_catalog() -> dict:
|
|
11
|
+
"""The CLI's machine-readable surface — the analog of MCP list_tools."""
|
|
12
|
+
commands = []
|
|
13
|
+
for name, spec in TOOL_REGISTRY.items():
|
|
14
|
+
props = spec.input_schema.get("properties", {})
|
|
15
|
+
required = set(spec.input_schema.get("required", []))
|
|
16
|
+
commands.append(
|
|
17
|
+
{
|
|
18
|
+
"command": name.replace("_", "-"),
|
|
19
|
+
"tool": name,
|
|
20
|
+
"scope": spec.scope,
|
|
21
|
+
"tier": spec.tier,
|
|
22
|
+
"description": spec.description,
|
|
23
|
+
"args": [
|
|
24
|
+
{
|
|
25
|
+
"name": pname,
|
|
26
|
+
"type": pspec.get("type", "string"),
|
|
27
|
+
"required": pname in required,
|
|
28
|
+
"default": pspec.get("default"),
|
|
29
|
+
}
|
|
30
|
+
for pname, pspec in props.items()
|
|
31
|
+
],
|
|
32
|
+
}
|
|
33
|
+
)
|
|
34
|
+
return {"commands": commands}
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@click.command("catalog", help="Emit the full command surface as JSON (for agents).")
|
|
38
|
+
def catalog_command() -> None:
|
|
39
|
+
click.echo(json.dumps(build_catalog(), ensure_ascii=False, indent=2))
|
opensell_cli/cli.py
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# opensell_cli/cli.py
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import asyncio
|
|
5
|
+
import json
|
|
6
|
+
|
|
7
|
+
import click
|
|
8
|
+
from aixianyu_mcp.errors import MCPToolError
|
|
9
|
+
from aixianyu_mcp.registry import TOOL_REGISTRY, ToolSpec
|
|
10
|
+
|
|
11
|
+
from .catalog import catalog_command
|
|
12
|
+
from .execute import run_tool
|
|
13
|
+
from .output import emit_error, emit_success
|
|
14
|
+
from .schema_to_click import options_for_schema
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _decode_object_args(spec: ToolSpec, args: dict) -> dict:
|
|
18
|
+
"""Return a copy of args with any type:object property (passed as a JSON
|
|
19
|
+
string) decoded. Malformed JSON becomes a clean Click usage error."""
|
|
20
|
+
props = spec.input_schema.get("properties", {})
|
|
21
|
+
out = dict(args)
|
|
22
|
+
for name, pspec in props.items():
|
|
23
|
+
if pspec.get("type") == "object" and isinstance(out.get(name), str):
|
|
24
|
+
try:
|
|
25
|
+
out[name] = json.loads(out[name])
|
|
26
|
+
except json.JSONDecodeError as exc:
|
|
27
|
+
raise click.BadParameter(
|
|
28
|
+
f"must be a valid JSON object ({exc})",
|
|
29
|
+
param_hint=f"--{name.replace('_', '-')}",
|
|
30
|
+
)
|
|
31
|
+
return out
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _make_command(tool_name: str, spec: ToolSpec) -> click.Command:
|
|
35
|
+
def callback(**kwargs):
|
|
36
|
+
ctx = click.get_current_context()
|
|
37
|
+
args = {k: v for k, v in kwargs.items() if v is not None}
|
|
38
|
+
args = _decode_object_args(spec, args)
|
|
39
|
+
try:
|
|
40
|
+
result = asyncio.run(
|
|
41
|
+
run_tool(
|
|
42
|
+
tool_name,
|
|
43
|
+
args,
|
|
44
|
+
base_url=ctx.obj["base_url"],
|
|
45
|
+
token=ctx.obj["token"],
|
|
46
|
+
)
|
|
47
|
+
)
|
|
48
|
+
emit_success(result)
|
|
49
|
+
except MCPToolError as err:
|
|
50
|
+
emit_error(err)
|
|
51
|
+
|
|
52
|
+
return click.Command(
|
|
53
|
+
name=tool_name.replace("_", "-"),
|
|
54
|
+
params=options_for_schema(spec.input_schema),
|
|
55
|
+
callback=callback,
|
|
56
|
+
help=f"{spec.description}\n\nRequires scope: {spec.scope}",
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def build_cli() -> click.Group:
|
|
61
|
+
@click.group()
|
|
62
|
+
@click.option("--token", envvar="AIXIANYU_AGENT_TOKEN", default="")
|
|
63
|
+
@click.option("--base-url", envvar="AIXIANYU_BASE_URL", default=None)
|
|
64
|
+
@click.pass_context
|
|
65
|
+
def cli(ctx: click.Context, token: str, base_url: str | None):
|
|
66
|
+
"""OPENSELL marketplace CLI for AI agents."""
|
|
67
|
+
ctx.obj = {"token": token, "base_url": base_url}
|
|
68
|
+
|
|
69
|
+
for name, spec in TOOL_REGISTRY.items():
|
|
70
|
+
cli.add_command(_make_command(name, spec))
|
|
71
|
+
cli.add_command(catalog_command)
|
|
72
|
+
return cli
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def main() -> None:
|
|
76
|
+
build_cli()()
|
opensell_cli/execute.py
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# opensell_cli/execute.py
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import json
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from aixianyu_mcp.errors import MCPError, MCPToolError
|
|
8
|
+
from aixianyu_mcp.rest_client import AixianyuRESTClient
|
|
9
|
+
from aixianyu_mcp.server import TOOL_HANDLERS
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
async def run_tool(name: str, args: dict, *, base_url: str | None, token: str) -> Any:
|
|
13
|
+
"""Execute a registry tool by reusing the MCP handler, then unwrap the
|
|
14
|
+
TextContent result back into a plain Python object.
|
|
15
|
+
|
|
16
|
+
`name` is the snake_case registry/tool name (e.g. "get_item").
|
|
17
|
+
Raises MCPToolError on backend errors (handled by the caller).
|
|
18
|
+
"""
|
|
19
|
+
client = AixianyuRESTClient(base_url=base_url, agent_token=token)
|
|
20
|
+
if name == "ping":
|
|
21
|
+
return await client.ping()
|
|
22
|
+
handler = TOOL_HANDLERS.get(name)
|
|
23
|
+
if handler is None:
|
|
24
|
+
raise MCPToolError(MCPError.SERVER_ERROR)
|
|
25
|
+
blocks = await handler(client, args)
|
|
26
|
+
return json.loads(blocks[0].text)
|
opensell_cli/output.py
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# opensell_cli/output.py
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import json
|
|
5
|
+
import sys
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from aixianyu_mcp.errors import MCPToolError
|
|
9
|
+
|
|
10
|
+
# Stable exit codes per MCPError name. Covers every enum member so a newly
|
|
11
|
+
# added error can never fall through to an undefined code.
|
|
12
|
+
EXIT_CODES: dict[str, int] = {
|
|
13
|
+
"INVALID_ARGS": 1,
|
|
14
|
+
"UNAUTHORIZED": 2,
|
|
15
|
+
"INSUFFICIENT_SCOPE": 3,
|
|
16
|
+
"AGENT_SANDBOX_LIMIT": 4,
|
|
17
|
+
"INSUFFICIENT_BALANCE": 5,
|
|
18
|
+
"RATE_LIMITED": 6,
|
|
19
|
+
"ITEM_NOT_FOUND": 7,
|
|
20
|
+
"CONVERSATION_NOT_FOUND": 7,
|
|
21
|
+
"SERVER_ERROR": 8,
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def emit_success(payload: Any) -> None:
|
|
26
|
+
"""Print the tool payload as JSON to stdout and exit 0."""
|
|
27
|
+
sys.stdout.write(json.dumps(payload, ensure_ascii=False) + "\n")
|
|
28
|
+
raise SystemExit(0)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def emit_error(err: MCPToolError) -> None:
|
|
32
|
+
"""Print the normalized error to stderr and exit with its mapped code."""
|
|
33
|
+
sys.stderr.write(json.dumps(err.to_dict(), ensure_ascii=False) + "\n")
|
|
34
|
+
raise SystemExit(EXIT_CODES.get(err.error.name, 8))
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# opensell_cli/schema_to_click.py
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import click
|
|
5
|
+
|
|
6
|
+
_SCALAR = {"integer": click.INT, "number": click.FLOAT, "string": click.STRING}
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def options_for_schema(schema: dict) -> list[click.Option]:
|
|
10
|
+
"""Build one click.Option per JSON-Schema property.
|
|
11
|
+
|
|
12
|
+
- property name `item_id` -> flag `--item-id` (Click maps it back to the
|
|
13
|
+
`item_id` callback kwarg automatically).
|
|
14
|
+
- integer/number/string -> matching Click types; enum -> click.Choice;
|
|
15
|
+
boolean -> is_flag; object -> STRING (a JSON string, decoded at dispatch).
|
|
16
|
+
- names in `required` are required unless they declare a `default`.
|
|
17
|
+
"""
|
|
18
|
+
props = schema.get("properties", {})
|
|
19
|
+
required = set(schema.get("required", []))
|
|
20
|
+
options: list[click.Option] = []
|
|
21
|
+
for name, spec in props.items():
|
|
22
|
+
flag = "--" + name.replace("_", "-")
|
|
23
|
+
help_text = spec.get("description", "")
|
|
24
|
+
if spec.get("type") == "boolean":
|
|
25
|
+
options.append(click.Option([flag], is_flag=True, help=help_text))
|
|
26
|
+
continue
|
|
27
|
+
kwargs: dict = {"help": help_text, "required": name in required}
|
|
28
|
+
if "default" in spec:
|
|
29
|
+
kwargs["default"] = spec["default"]
|
|
30
|
+
kwargs["required"] = False
|
|
31
|
+
if "enum" in spec:
|
|
32
|
+
kwargs["type"] = click.Choice(spec["enum"])
|
|
33
|
+
elif spec.get("type") == "object":
|
|
34
|
+
kwargs["type"] = click.STRING
|
|
35
|
+
else:
|
|
36
|
+
kwargs["type"] = _SCALAR.get(spec.get("type"), click.STRING)
|
|
37
|
+
options.append(click.Option([flag], **kwargs))
|
|
38
|
+
return options
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: opensell-cli
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: OPENSELL marketplace CLI for AI agents
|
|
5
|
+
Requires-Python: >=3.12
|
|
6
|
+
Requires-Dist: aixianyu-mcp
|
|
7
|
+
Requires-Dist: click>=8.1
|
|
8
|
+
Provides-Extra: dev
|
|
9
|
+
Requires-Dist: pytest-asyncio>=0.23.0; extra == 'dev'
|
|
10
|
+
Requires-Dist: pytest>=8.2.0; extra == 'dev'
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
opensell_cli/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
+
opensell_cli/catalog.py,sha256=CAKXQYxCdfqkAY-yKNvtS4zpOUlLTMeZntV-CNqms5o,1274
|
|
3
|
+
opensell_cli/cli.py,sha256=6rguZ-bROEuy4XG56tLWOcW6xCuEyxjUN2Gg6v-myQo,2497
|
|
4
|
+
opensell_cli/execute.py,sha256=4Kv-rveF6H-6iSGI01SuuKTyCUpbXL8p5CiUA51u0iI,937
|
|
5
|
+
opensell_cli/output.py,sha256=YCKE7pynGv7rk2JeZelizEj3brDagoOvppmdDDGeorE,1013
|
|
6
|
+
opensell_cli/schema_to_click.py,sha256=ao79n_tXw3udJ1MFBvFF66gECnELzUYWz4YjpZvSrtw,1559
|
|
7
|
+
opensell_cli-0.1.0.dist-info/METADATA,sha256=aRxpVTxwXhyuNV8JassHAo1MLUVbtRMHdYzo5DKYWXc,301
|
|
8
|
+
opensell_cli-0.1.0.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
|
|
9
|
+
opensell_cli-0.1.0.dist-info/entry_points.txt,sha256=EzNEBt2PpKdkmBsNyI4J6tRsuXVHHCAzdnX_54Fo-d8,51
|
|
10
|
+
opensell_cli-0.1.0.dist-info/RECORD,,
|