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.
File without changes
@@ -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()()
@@ -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,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.29.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ opensell = opensell_cli.cli:main