Metadata-Version: 2.4
Name: mcp-nexus-sdk
Version: 1.3.0
Summary: Headless Python SDK + CLI for the Nexus skill platform (bundles nexus_core)
Author: MCP Nexus
License: MIT
Classifier: Programming Language :: Python :: 3
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: OS Independent
Requires-Python: >=3.10
Description-Content-Type: text/markdown
Requires-Dist: requests>=2.31
Requires-Dist: httpx>=0.27
Requires-Dist: pydantic>=2.5
Requires-Dist: packaging>=23.0
Requires-Dist: cryptography>=42.0
Provides-Extra: legacy-zip
Requires-Dist: pyzipper>=0.3.6; extra == "legacy-zip"
Provides-Extra: keyring
Requires-Dist: keyring>=24.0; extra == "keyring"
Provides-Extra: test
Requires-Dist: pytest>=8; extra == "test"
Requires-Dist: respx>=0.21; extra == "test"

# Nexus SDK

Headless Python library + `nexus` CLI that lets a customer's own client (e.g.
OPENCLAW) drive the Nexus Runner's two user-facing surfaces — **chat** (run
skills, including the agentic chat loop) and the **skill library** (browse /
install / uninstall / sync / balance) — **without the Electron Runner installed**.
It shares the Runner's data dir, `.ocskill` formats, license/offline-token flow
and shared Python env via the bundled `nexus_core` package (the same code the
Runner's `api.py` wraps in FastAPI).

Custom-skill **authoring** (the Runner's 工作台) is intentionally **not** part of
the SDK — authoring stays in the Runner GUI / the dev `sdk/build/` scripts.

## Layout

```
core_service/
  nexus_runner/        Electron app — api.py (FastAPI) + electron/ + src/ (Vue);
                       runner.py / installer.py / bootstrap.py / license_client.py /
                       token_cache.py / migrate.py / skill_review.py / paths.py /
                       runtime_env.py are thin re-exports of nexus_core.
  sdk/
    pyproject.toml     packages nexus_sdk + nexus_core  →  pip install ./core_service/sdk
    README.md          this file
    nexus_core/        GUI-/HTTP-free core:
                         paths · package (read/pack .ocskill: free tar.gz / paid
                         OCSK-v2 AES-256-GCM / legacy ZIP + SHA256SUMS) ·
                         package_verify (Ed25519) · runtime_env · bootstrap
                         (shared <data>/python/ + on-demand skill-dep installs) ·
                         installer · license_client · token_cache · migrate ·
                         skill_review · runner (run_skill: online/offline +
                         license + decrypt + exec) · registry (catalog /
                         my-skills / balance / install-token / download) ·
                         agent_loop (headless chat loop + prompt-skill loop) · config
    nexus_sdk/         SkillClient (high-level facade) · cli.py (`nexus`) ·
                         tools.py (expose skills as tool specs for your own agent loop) ·
                         legacy SkillLoader / SkillExecutor (back-compat)
    tests/             pytest suite (no network / no Runner)
    build/             dev/platform build scripts (not part of the published SDK)
```

Data dir: `NEXUS_DATA_DIR` if set (Electron passes `app.getPath('userData')`
= `%APPDATA%\Nexus` on Windows), else `%APPDATA%\Nexus`, else `~/.nexus` —
see `nexus_core.paths`.

## Install

```bash
pip install ./core_service/sdk                # nexus_sdk + nexus_core + the `nexus` CLI
pip install "./core_service/sdk[keyring]"     # + OS secret store for the API key
pip install "./core_service/sdk[legacy-zip]"  # + pyzipper for old AES-ZIP .ocskill
```

## Library

```python
from nexus_sdk import SkillClient
c = SkillClient(api_key="sk_live_...")          # or NEXUS_API_KEY env / `nexus config set`

# chat — agentic: the LLM picks/installs/runs skills via tools
res = c.chat("merge these two excels by 科目",
             ask_handler=lambda prompt, schema: my_app.ask_user(prompt, schema))
print(res["reply"]); print(res["tool_calls"])

# or run a skill directly / a prompt skill
c.run("excel-merge-filter", inputs={"input_0": "/a.xlsx"}, params={"mode": "and"})
c.run_prompt("code-review", "review this diff: ...")

# skill library
c.catalog("excel")            # marketplace listing
c.install("excel-merge-filter")   # download + license(if paid) + decrypt + install + pip deps
c.uninstall("excel-merge-filter")
c.list_installed(); c.my_skills(); c.sync()
c.balance()                   # license balance / offline-token inventory
c.prefetch("bank-match", count=10)
```

`nexus_core` is usable directly too (`from nexus_core.runner import run_skill_sync`, etc.).

### Integrating chat into your client (OPENCLAW)

`c.chat(message, *, history=None, ask_handler=None, skill_hints=None, max_turns=8,
locale="zh", on_delta=None, ref_table=None, llm_base_url=None, llm_api_key=None, model=None)`
runs one agentic turn. The LLM sees five client-side tools — `question` / `list_skills`
/ `load_skill` / `run_skill` / `read_ref` — under a progressive-disclosure system
prompt; `run_skill` is auto-resolved via this client (installs on demand) and `question`
is routed to your `ask_handler(prompt, schema, choices) -> str` (omit it and such a turn
yields an error the LLM works around). `on_delta(text)` streams assistant tokens; a
`ref_table` keeps file paths / secrets / large outputs out of the LLM context. Returns
`{"reply", "messages", "tool_calls"}`; pass `messages` back as `history`.

**The SDK ships no model.** By default the loop calls the platform LLM proxy (metered,
billed to your API key) at `<server>/api/v1/chat/completions` — the same path skills hit.
`NEXUS_LLM_URL` (server root) overrides `server_url` for this. To run it against **your
own** OpenAI-compatible LLM instead, pass `llm_base_url=` (e.g. `https://api.openai.com/v1`)
+ `llm_api_key=` + `model=` (that key is used only for the LLM call, never for skill licensing).

### Using Nexus skills as tools in YOUR own agent loop

If you already have an agent framework and just want the skills as callable tools
(no Nexus chat loop), use the adapter:

```python
from nexus_sdk import SkillClient, skill_tool_specs, dispatch_tool_call
c = SkillClient()
tools = skill_tool_specs(c, query="excel")             # OpenAI function-tool specs
# register `tools` in your own LLM call; when the model emits e.g.
#   name="nexus__excel-merge-filter", arguments={"input_0": "...", "mode": "and"}:
result = dispatch_tool_call(c, name, arguments,
                            max_output_bytes=8_000,    # optional — cap context bloat from big results
                            confirm_handler=lambda slug, info, args, reason: True)  # optional gate for confirm_required skills
```

`dispatch_tool_call` returns `{ok: True, slug, output}` on success (or
`{ok, slug, output_ref, output_preview, truncated}` when capped; pass a
`ref_table=` to keep the full output reachable). Cloud-execution-mode skills are
NOT auto-installed (manifest comes from the catalog; `client.run` handles cloud
routing). `confirm_required` skills are flagged in the return; omit
`confirm_handler` for back-compat (runs anyway, returns `confirm_required: true`).

**Anthropic (Claude) format** — pass `flavor="anthropic"` and feed results back as `tool_result`:

```python
import anthropic, json
client = anthropic.Anthropic()                          # uses ANTHROPIC_API_KEY env
nexus = SkillClient()
tools = skill_tool_specs(nexus, query="excel", flavor="anthropic")   # [{name, description, input_schema}, ...]

msg = client.messages.create(model="claude-opus-4-7", max_tokens=2048, tools=tools,
                             messages=[{"role": "user", "content": "merge these two excels by 科目"}])
# the model may emit a tool_use block:
for block in msg.content:
    if block.type == "tool_use":
        out = dispatch_tool_call(nexus, block.name, block.input, max_output_bytes=8000)
        tool_result = {"type": "tool_result", "tool_use_id": block.id,
                       "content": json.dumps(out, ensure_ascii=False, default=str),
                       "is_error": not out.get("ok", False)}
        # feed `tool_result` back as the next user-turn content; loop until no tool_use blocks
```

Not ported from the Runner's renderer (`src/engine/agent-loop.ts`): the fixed-rules /
TF-IDF chat layer, the quota pre-charge UI, the `confirm_required` "type CONFIRM" gate.

## CLI

```bash
nexus chat "merge these two excels by 科目" [--interactive] [--stream] [--turns 8] [--locale zh|en]
nexus chat "..." --llm-base-url https://api.openai.com/v1 --llm-api-key sk-... --model gpt-4o   # BYO LLM
nexus run <slug> --inputs '{"input_0":"/a.xlsx"}' --params '{"mode":"and"}'
nexus run-prompt <slug> "your message"  [--llm-base-url ... --llm-api-key ... --model ...]
nexus install <slug>  |  uninstall <slug>  |  list  |  sync
nexus search [query]  |  balance
nexus env provision   |  env clear   |  prefetch <slug> --count 10
nexus config show  |  config set api_key=sk_live_...  |  config set server_url=...
nexus migrate export <bundle> --pass <p>  |  migrate import <bundle> --pass <p>
nexus version
```

All commands print JSON to stdout (errors → JSON on stderr, exit 1).

## .ocskill format

| Format | Magic | When | Payload |
|---|---|---|---|
| plain tar.gz | `1f 8b` | free skills | `skill.json`, `main.py`/`SKILL.md`, `requirements.txt`, `assets/*`, `SHA256SUMS` |
| OCSK-v2 | `OCSK`(4B) + nonce(12B) + AES-256-GCM(tar.gz) | paid skills | same payload inside; per-skill key = HMAC-SHA256(build_secret, slug), delivered by the license API |
| legacy ZIP / AES-ZIP | `PK\x03\x04` | old packages | same files; AES-ZIP needs the key as password (`mcp-nexus-sdk[legacy-zip]`) |

`.ocskill` packages carry skill source + `requirements.txt` only — never wheels.
Python deps are pip-installed on demand into the shared `<data>/python/` env,
version-pinned project-wide via `skill_packages/constraints.txt`. No per-skill venvs.

## Testing

Two layers — `pytest` for unit tests (offline, no network), and a runnable script
for end-to-end smoke against a live backend.

### Unit tests (offline, ~2s)

```bash
cd core_service/sdk
pip install -e ".[test]"     # installs pytest + respx + the SDK editable
pytest -q                    # 44 tests; no network, no Runner, no API key needed
```

These cover: agent_loop's tool dispatch + streaming SSE assembly, ref-table,
skill_tools (partition/validate/resolve_for_run/confirm_info), tools adapter
(specs + dispatch + confirm gate + max_output_bytes), CLI subcommands,
config/migrate round-trips, package isolation (no `nexus_runner` / fastapi /
electron imports leak into the SDK).

### End-to-end smoke (live backend; `tests/integration_smoke.py`)

A standalone script — not collected by pytest — that walks every SDK surface
against a live backend and prints PASS / SKIP / FAIL per step. Uses an isolated
`NEXUS_DATA_DIR` under the system temp dir so it cannot pollute your real
Runner install at `%APPDATA%\Nexus`.

```bash
# minimal (no network, no API key) — proves import / version / env / confirm gate / CLI
python core_service/sdk/tests/integration_smoke.py

# add live catalog + balance + tool-spec generation
NEXUS_API_KEY=sk_live_... NEXUS_SERVER_URL=https://mcp-nexus.online \
  python core_service/sdk/tests/integration_smoke.py

# add: actually install a free skill, run it, then uninstall
NEXUS_API_KEY=sk_live_... \
  python core_service/sdk/tests/integration_smoke.py --with-install \
    --skill action-item-extractor

# add: exercise the agentic chat loop through the platform proxy (consumes LLM quota)
NEXUS_API_KEY=sk_live_... \
  python core_service/sdk/tests/integration_smoke.py --with-install --with-chat

# add: also exercise BYO LLM (your own OpenAI-compatible endpoint)
NEXUS_API_KEY=sk_live_... \
  NEXUS_SMOKE_BYO_BASE_URL=https://api.openai.com/v1 \
  NEXUS_SMOKE_BYO_API_KEY=sk-... \
  NEXUS_SMOKE_BYO_MODEL=gpt-4o-mini \
  python core_service/sdk/tests/integration_smoke.py --with-chat
```

Steps covered, in order: `import + __version__`, `SkillClient` instantiation
into an isolated data dir, shared Python env provisioning (`ensure_env`),
live `catalog()`, `balance()`, tool-spec generation (OpenAI + Anthropic
flavors), `install` + `run` + `uninstall` of a free skill, the `confirm_handler`
gate (offline, with a fake client), platform-proxy `chat()`, BYO-LLM `chat()`,
and `python -m nexus_sdk.cli version` as a CLI smoke. Each step prints its
own elapsed time; exit code 0 iff zero FAIL.

Tunable env vars: `NEXUS_API_KEY` · `NEXUS_SERVER_URL` · `NEXUS_SMOKE_FREE_SLUG`
(default `action-item-extractor`) · `NEXUS_SMOKE_RUN_INPUTS` / `NEXUS_SMOKE_RUN_PARAMS`
(JSON for the run step) · `NEXUS_SMOKE_BYO_BASE_URL` / `NEXUS_SMOKE_BYO_API_KEY` /
`NEXUS_SMOKE_BYO_MODEL`. CLI flags: `--with-install`, `--with-chat`,
`--skill <slug>`, `--keep-data-dir`.

### Isolated install (verifies `pip install ./core_service/sdk` works clean)

```bash
python -m venv /tmp/mcp-nexus-sdk-test && /tmp/mcp-nexus-sdk-test/Scripts/activate   # bash: source ...
pip install ./core_service/sdk
nexus version
python -c "from nexus_sdk import SkillClient, skill_tool_specs, dispatch_tool_call; print('ok')"
python core_service/sdk/tests/integration_smoke.py     # works without the editable install too
```

The CI matrix (`.github/workflows/sdk.yml`) already runs this clean install
across 3 OSes × Python 3.10–3.13.

## Notes

- **Shared Python env**: skill deps install into `<data>/python/` (provisioned on
  demand — copies a bundled CPython if shipped, else downloads python-build-standalone),
  exactly like the Runner. Set `NEXUS_PYTHON=<path>` to use a specific interpreter.
- **Local Runner**: if a Runner is running on `localhost:7432`, the legacy
  `SkillLoader` prefers its HTTP API; `SkillClient` always works locally via `nexus_core`.
- The vestigial `nexus_sdk/setup.py` is superseded by `core_service/sdk/pyproject.toml`.
