Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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?

Expand Down
2 changes: 1 addition & 1 deletion examples/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
```

Expand Down
2 changes: 1 addition & 1 deletion src/onepin/_cli/__init__.py
Original file line number Diff line number Diff line change
@@ -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

Expand Down
4 changes: 2 additions & 2 deletions src/onepin/_cli/_ctx.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
2 changes: 1 addition & 1 deletion src/onepin/_cli/_http.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
2 changes: 1 addition & 1 deletion src/onepin/_cli/_skill/__init__.py
Original file line number Diff line number Diff line change
@@ -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.
Expand Down
10 changes: 5 additions & 5 deletions src/onepin/_cli/_skill/onepin/SKILL.md
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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 <uuid>` (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
Expand Down
2 changes: 1 addition & 1 deletion src/onepin/_cli/_skill/onepin/reference.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down
2 changes: 1 addition & 1 deletion src/onepin/_cli/commands/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
18 changes: 9 additions & 9 deletions src/onepin/_cli/commands/skill.py
Original file line number Diff line number Diff line change
@@ -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 <https://agentskills.io>`_
These commands materialize the Onepin agent skill (an open `Agent Skills <https://agentskills.io>`_
``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.

Expand Down Expand Up @@ -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
Expand All @@ -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.")

Expand All @@ -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)
Expand All @@ -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)
Expand All @@ -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:
Expand All @@ -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 =============================================================================
Expand Down Expand Up @@ -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


Expand Down
2 changes: 1 addition & 1 deletion src/onepin/_cli/main.py
Original file line number Diff line number Diff line change
@@ -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

Expand Down
2 changes: 1 addition & 1 deletion src/onepin/_version_gate.py
Original file line number Diff line number Diff line change
@@ -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.
Expand Down
2 changes: 1 addition & 1 deletion tests/cli/test_cli_login.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
25 changes: 25 additions & 0 deletions tests/unit/test_skill.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
Loading