diff --git a/README.md b/README.md index ac2bd2c..3bb74be 100644 --- a/README.md +++ b/README.md @@ -29,7 +29,7 @@ Full documentation: [onepin.ai/docs](https://onepin.ai/docs) ## Authentication -Mint an API key at [app.onepin.ai/settings/api-keys](https://app.onepin.ai/settings/api-keys), then: +Mint an API key at [app.onepin.ai/workspace/~/settings/api](https://app.onepin.ai/workspace/~/settings/api), then: ```bash onepin login @@ -524,7 +524,7 @@ A TTS API returns whatever it generates. Onepin gates the result. You set accura ### Do I need a Onepin account? -Yes. Mint an API key at [app.onepin.ai/settings/api-keys](https://app.onepin.ai/settings/api-keys), then run `onepin login`. See [Authentication](#authentication). +Yes. Mint an API key at [app.onepin.ai/workspace/~/settings/api](https://app.onepin.ai/workspace/~/settings/api), then run `onepin login`. See [Authentication](#authentication). ### Can my AI coding agent run Onepin? diff --git a/examples/README.md b/examples/README.md index 0b53361..8050356 100644 --- a/examples/README.md +++ b/examples/README.md @@ -5,7 +5,7 @@ Runnable snippets for the Onepin Python SDK. Each reads your API key from the ```sh pip install onepin -export ONEPIN_API_KEY="op_..." # create one at https://app.onepin.ai/settings/api-keys +export ONEPIN_API_KEY="op_..." # create one at https://app.onepin.ai/workspace/~/settings/api python examples/quickstart.py ``` diff --git a/src/onepin/_cli/__init__.py b/src/onepin/_cli/__init__.py index c95e34b..4a7fd15 100644 --- a/src/onepin/_cli/__init__.py +++ b/src/onepin/_cli/__init__.py @@ -1,4 +1,4 @@ -"""OnePin CLI -- hand-rolled Typer CLI atop the Fern-generated SDK.""" +"""Onepin CLI -- hand-rolled Typer CLI atop the Fern-generated SDK.""" from __future__ import annotations diff --git a/src/onepin/_cli/_ctx.py b/src/onepin/_cli/_ctx.py index 30a04a8..3ac98a1 100644 --- a/src/onepin/_cli/_ctx.py +++ b/src/onepin/_cli/_ctx.py @@ -268,9 +268,9 @@ def _classify_other_error(exc: Exception) -> tuple[str | None, str]: from pydantic import ValidationError if isinstance(exc, httpx.ConnectError): - return "NETWORK_ERROR", "Could not connect to the OnePin API." + return "NETWORK_ERROR", "Could not connect to the Onepin API." if isinstance(exc, httpx.TimeoutException): - return "TIMEOUT", "The request to the OnePin API timed out." + return "TIMEOUT", "The request to the Onepin API timed out." if isinstance(exc, httpx.TransportError): return "NETWORK_ERROR", "A network transport error occurred." if isinstance(exc, json.JSONDecodeError): diff --git a/src/onepin/_cli/_http.py b/src/onepin/_cli/_http.py index 21d1301..8702fbf 100644 --- a/src/onepin/_cli/_http.py +++ b/src/onepin/_cli/_http.py @@ -118,7 +118,7 @@ def _call_whoami(key: str, base_url: str, timeout: float = 10.0, *, verbose: boo Args: key: API key to authenticate with. - base_url: Base URL of the OnePin API (no trailing slash). + base_url: Base URL of the Onepin API (no trailing slash). timeout: Request timeout in seconds. verbose: If True, log the request and response status to stderr. diff --git a/src/onepin/_cli/_skill/__init__.py b/src/onepin/_cli/_skill/__init__.py index 248814c..7e3f243 100644 --- a/src/onepin/_cli/_skill/__init__.py +++ b/src/onepin/_cli/_skill/__init__.py @@ -1,4 +1,4 @@ -"""Bundled OnePin agent skill (the ``onepin/`` folder is package data, not Python). +"""Bundled Onepin agent skill (the ``onepin/`` folder is package data, not Python). This package exists only so ``importlib.resources.files("onepin._cli._skill")`` can resolve the bundled ``onepin/SKILL.md`` (+ ``reference.md``) in both wheel and zipimport layouts. diff --git a/src/onepin/_cli/_skill/onepin/SKILL.md b/src/onepin/_cli/_skill/onepin/SKILL.md index 5ad47cd..3b23290 100644 --- a/src/onepin/_cli/_skill/onepin/SKILL.md +++ b/src/onepin/_cli/_skill/onepin/SKILL.md @@ -1,15 +1,15 @@ --- name: onepin description: >- - Use when the user mentions OnePin or wants to operate a OnePin voice-workflow + Use when the user mentions Onepin or wants to operate a Onepin voice-workflow workspace from the terminal — list / inspect / run workflows, check run status, - browse voices or templates, inspect usage. OnePin is an AI voice-workflow + browse voices or templates, inspect usage. Onepin is an AI voice-workflow platform with a `onepin` CLI; this skill drives it safely. --- -# OnePin +# Onepin -Drive a OnePin voice-workflow workspace through the `onepin` CLI. The CLI is the only +Drive a Onepin voice-workflow workspace through the `onepin` CLI. The CLI is the only integration surface — this skill teaches its contract, not a frozen command list. ## Golden rules @@ -33,7 +33,7 @@ integration surface — this skill teaches its contract, not a frozen command li 1. `onepin --version` — if "command not found", tell the user to `pip install onepin`, then stop. 2. `onepin --json whoami` — confirms authentication. On success note `workspace_id`, `scopes`. - Unauthenticated (`NOT_LOGGED_IN` / `INVALID_API_KEY`) → tell the user to run `onepin login` - (mint a key at https://app.onepin.ai/settings/api-keys), then stop. + (mint a key at https://app.onepin.ai/workspace/~/settings/api), then stop. - Pass `--workspace ` (or set `ONEPIN_WORKSPACE_ID`) only if the user has more than one workspace and the wrong one is active. 3. If `onepin schema` errors or the command is missing, the CLI is old — suggest diff --git a/src/onepin/_cli/_skill/onepin/reference.md b/src/onepin/_cli/_skill/onepin/reference.md index c5d10b2..497a0b3 100644 --- a/src/onepin/_cli/_skill/onepin/reference.md +++ b/src/onepin/_cli/_skill/onepin/reference.md @@ -1,4 +1,4 @@ -# OnePin CLI reference +# Onepin CLI reference `onepin schema` (JSON manifest of every command: `path`, `args`, `options`, `destructive`) is the **authoritative** source — when this file and `schema` disagree, trust `schema`. This file is a diff --git a/src/onepin/_cli/commands/auth.py b/src/onepin/_cli/commands/auth.py index 4b8b235..f5bb082 100644 --- a/src/onepin/_cli/commands/auth.py +++ b/src/onepin/_cli/commands/auth.py @@ -12,7 +12,7 @@ from onepin._cli.auth.credentials import delete_credentials, write_credentials _DEFAULT_BASE_URL = "https://api.onepin.ai" -_DASHBOARD_URL = "https://app.onepin.ai/settings/api-keys" +_DASHBOARD_URL = "https://app.onepin.ai/workspace/~/settings/api" def _resolve_login_base_url(local_flag: str | None) -> str: diff --git a/src/onepin/_cli/commands/skill.py b/src/onepin/_cli/commands/skill.py index ac7d924..a74123b 100644 --- a/src/onepin/_cli/commands/skill.py +++ b/src/onepin/_cli/commands/skill.py @@ -1,6 +1,6 @@ """``onepin skill install / path / uninstall`` -- manage the bundled cross-tool agent skill. -These commands materialize the OnePin agent skill (an open `Agent Skills `_ +These commands materialize the Onepin agent skill (an open `Agent Skills `_ ``SKILL.md`` folder bundled in the wheel) into each AI coding tool's skills directory, so the skill is usable from Claude Code (as ``/onepin``), Cursor, OpenAI Codex, Gemini CLI, and Copilot. @@ -67,7 +67,7 @@ def install( force: bool = typer.Option(False, "--force", help="Overwrite existing skill files."), json_output_local: bool = typer.Option(False, "--json", help="Emit JSON instead of text."), ) -> None: - """Install the OnePin agent skill into your AI coding tool(s). + """Install the Onepin agent skill into your AI coding tool(s). Writes the bundled ``SKILL.md`` folder into each target's skills directory. In Claude Code the skill is then invocable as ``/onepin``; other tools load it on demand by relevance. Refuses to @@ -94,7 +94,7 @@ def install( render_json({"ok": True, "command": "/onepin", "targets": written}) return for labels, directory in targets: - typer.echo(f"Installed OnePin skill → {directory} ({', '.join(labels)})") + typer.echo(f"Installed Onepin skill → {directory} ({', '.join(labels)})") typer.echo("In Claude Code, invoke it with /onepin; other tools load it by relevance.") typer.echo("If your tool is already running, restart it (or run /reload-plugins) to pick up a new skills dir.") @@ -105,7 +105,7 @@ def path( project: bool = typer.Option(False, "--project", help="Show project paths instead of HOME."), json_output_local: bool = typer.Option(False, "--json", help="Emit JSON instead of text."), ) -> None: - """Show where the OnePin skill is (or would be) installed, without writing anything.""" + """Show where the Onepin skill is (or would be) installed, without writing anything.""" json_on = output_json(json_output_local) try: targets = _target_dirs(_select_tools(tool, all_tools), project) @@ -129,7 +129,7 @@ def uninstall( yes: bool = typer.Option(False, "--yes", "-y", help="Skip the confirmation prompt."), json_output_local: bool = typer.Option(False, "--json", help="Emit JSON instead of text."), ) -> None: - """Remove the installed OnePin skill folder(s). Idempotent; only deletes the ``onepin/`` leaf dir.""" + """Remove the installed Onepin skill folder(s). Idempotent; only deletes the ``onepin/`` leaf dir.""" json_on = output_json(json_output_local) try: targets = _target_dirs(_select_tools(tool, all_tools), project) @@ -141,7 +141,7 @@ def uninstall( "CONFIRMATION_REQUIRED", "Pass --yes to remove the skill (no interactive prompt under --json).", ) - typer.confirm(f"Remove the OnePin skill from {len(present)} location(s)?", abort=True, err=True) + typer.confirm(f"Remove the Onepin skill from {len(present)} location(s)?", abort=True, err=True) removed: list[dict[str, object]] = [] for labels, directory in present: @@ -158,7 +158,7 @@ def uninstall( typer.echo("Nothing to remove.") return for entry in removed: - typer.echo(f"Removed OnePin skill ← {entry['path']}") + typer.echo(f"Removed Onepin skill ← {entry['path']}") # === helpers ============================================================================= @@ -210,13 +210,13 @@ def _bundled_skill_files() -> list[tuple[str, bytes]]: try: entries = sorted(root.iterdir(), key=lambda entry: entry.name) except (FileNotFoundError, NotADirectoryError) as exc: - raise CliError("SKILL_PAYLOAD_MISSING", "The bundled OnePin skill is missing from the package.") from exc + raise CliError("SKILL_PAYLOAD_MISSING", "The bundled Onepin skill is missing from the package.") from exc for entry in entries: if not entry.is_file() or entry.name.endswith(".py"): continue files.append((entry.name, entry.read_bytes())) if not files: - raise CliError("SKILL_PAYLOAD_MISSING", "The bundled OnePin skill is missing from the package.") + raise CliError("SKILL_PAYLOAD_MISSING", "The bundled Onepin skill is missing from the package.") return files diff --git a/src/onepin/_cli/main.py b/src/onepin/_cli/main.py index 59f08e1..2e70a4b 100644 --- a/src/onepin/_cli/main.py +++ b/src/onepin/_cli/main.py @@ -1,4 +1,4 @@ -"""OnePin CLI entry point -- `onepin = "onepin._cli.main:app"`.""" +"""Onepin CLI entry point -- `onepin = "onepin._cli.main:app"`.""" from __future__ import annotations diff --git a/src/onepin/_version_gate.py b/src/onepin/_version_gate.py index 0d2001d..78e6953 100644 --- a/src/onepin/_version_gate.py +++ b/src/onepin/_version_gate.py @@ -1,6 +1,6 @@ """Client-side SDK version gate (hand-written; preserved across ``fern generate`` via ``.fernignore``). -The OnePin API advertises the minimum SDK version it still accepts via the +The Onepin API advertises the minimum SDK version it still accepts via the ``X-OnePin-Required-Version`` response header (and enforces it with HTTP 426). This module reads that header off every response and stops the caller when the installed ``onepin`` package is older than the floor. diff --git a/tests/cli/test_cli_login.py b/tests/cli/test_cli_login.py index 9d07fad..6e44cf8 100644 --- a/tests/cli/test_cli_login.py +++ b/tests/cli/test_cli_login.py @@ -161,7 +161,7 @@ def test_401_exits_1_with_invalid_api_key_message(self, tmp_home: Path) -> None: assert result.exit_code == 1 assert "INVALID_API_KEY" in result.output - assert "app.onepin.ai/settings/api-keys" in result.output + assert "app.onepin.ai/workspace/~/settings/api" in result.output @respx.mock def test_network_error_exits_1_with_network_error_message(self, tmp_home: Path) -> None: diff --git a/tests/unit/test_skill.py b/tests/unit/test_skill.py index d780cb1..aa625f8 100644 --- a/tests/unit/test_skill.py +++ b/tests/unit/test_skill.py @@ -68,6 +68,31 @@ def test_bundled_skill_files_missing_raises(monkeypatch: pytest.MonkeyPatch) -> assert exc.value.code == "SKILL_PAYLOAD_MISSING" +def test_bundled_skill_files_empty_payload_raises(monkeypatch: pytest.MonkeyPatch) -> None: + """Dir resolves but yields nothing shippable (all .py / non-files) -> SKILL_PAYLOAD_MISSING.""" + import importlib.resources + + class _Entry: + def __init__(self, name: str, is_file: bool) -> None: + self.name = name + self._is_file = is_file + + def is_file(self) -> bool: + return self._is_file + + class _Root: + def joinpath(self, *_parts: str) -> _Root: + return self + + def iterdir(self) -> list[_Entry]: + return [_Entry("helper.py", is_file=True), _Entry("nested", is_file=False)] + + monkeypatch.setattr(importlib.resources, "files", lambda _pkg: _Root()) + with pytest.raises(CliError) as exc: + skill._bundled_skill_files() + assert exc.value.code == "SKILL_PAYLOAD_MISSING" + + def test_write_maps_oserror_to_write_failed(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: def boom(dest: Path, content: bytes, *, force: bool) -> None: raise OSError("disk full")