@adaai/cli
v0.1.0
Published
Pi extensions for ada.ai: an OpenAI-compatible gateway provider with browser device-flow login and dynamic model discovery.
Readme
@adaai/cli
Pi extensions for ada.ai — an OpenAI-compatible LLM gateway. This package adds Ada AI as a first-class pi provider with two ways to authenticate and a dynamic, self-refreshing model catalog.
The ada.ai backend lives in
../infra. Seeinfra/docs/api-reference.mdandinfra/docs/cli-auth.mdfor the gateway and device-flow contracts this extension implements.
What it does
- Registers the
adaprovider — an OpenAI-compatible gateway. Every upstream is served over/v1/chat/completions, so the provider uses pi-ai's built-inopenai-completionsstream implementation. (/v1/messagesis an Anthropic translation shim and is not used.) - Two auth paths to the same
sk-rc-…proxy key:- Ambient key — set
ADA_API_KEY(a key you mint in the dashboard) and pi is ready immediately, no stored credential. - Device flow —
/login adamints a key through your browser without the CLI ever touching your session.
- Ambient key — set
- Dynamic catalog. The gateway's
/v1/modelsreturns the enabled invoker names; each is enriched with capability metadata (reasoning, context window, cost, …) from pi-ai's built-in catalog so pi's UI and request construction behave correctly per model. - Keeps fresh across sessions and exposes
/adato force a refresh.
Install
Important:
npm installalone does not register a pi package. pi discovers extensions only from packages listed insettings.packages(project.pi/settings.jsonor global~/.pi/agent/settings.json) — there is nonode_modulesscan for thepifield. Usepi install(or a-eflag) to register.
Recommended: local-path install (loads from disk, no copy)
# from the project where you run pi — registers in .pi/settings.json
pi install ./ada-my-pi -lpi adds the path to project settings without copying and loads the
extension in place, so source edits to extensions/*.ts are live on the next
/reload (after the project is trusted). Use pi install ./ada-my-pi
(without -l) to register globally instead.
Quick one-off test (no registration)
pi -e ./extensions/ada-provider.ts-e loads the single extension for this run only; it is not persisted. Authenticate with export ADA_API_KEY=sk-rc-… (the device flow isn't deployed in production yet).
Manual (pin the path in settings)
Add to .pi/settings.json:
{ "packages": ["./ada-my-pi"] }After the project is trusted, pi installs any missing npm deps and loads the extension on startup.
Published (once released)
pi install npm:@adaai/cliRun pi extensions to confirm ada is loaded.
Requires
@earendil-works/pi-coding-agentand@earendil-works/pi-aias peers (provided by your pi installation).
Authenticate
Option A — ambient API key (works today; no stored credential)
export ADA_API_KEY=sk-rc-…Mint the key on the dashboard's Keys page (see infra/docs/usage.md §3).
On startup the extension fetches /v1/models once (8s timeout) so models are
available immediately — to pi --list-models, to interactive startup, and
without waiting for the first session refresh. Failures are non-fatal (logged
to stderr); use /ada to repopulate.
Option B — device flow (browser login)
The
/auth/device/*routes requireREDIS_URLon the gateway (seeinfra/docs/cli-auth.mdandbackend/serving-api/proxy/main.go). If/login adareturns404 page not found, your gateway doesn't have Redis configured — use Option A (ADA_API_KEY) instead.
/login adaPi opens the flow: it POST /auth/device/code, prints a user code and the
verification URL, then polls /auth/device/token until you approve in your
browser (or deny, or it expires). The approval page shows an organization
picker — choose the organization that owns the upstreams you want this CLI
to reach (Personal for your own). The minted sk-rc-… key is scoped to that
organization, stored as an OAuth credential with a far-future expiry (device
keys don't rotate). Pi then auto-refreshes the catalog.
Sign out with /logout ada.
Use
Once authenticated, Ada AI is just another provider:
pi --list-models # includes ada models
pi -m ada:auto # the ada routing invoker
pi -m ada:gpt-5 "hello" # a specific model served by adaInside a session, /model lists providers and /ada force-refreshes the
catalog:
/ada # → "Refreshed Ada AI: N models."Knowledge-base slash commands (ada:*)
The ada-kb extension exposes the knowledge-base skills as slash commands
you can type in a session (they call the shared client directly, so they
behave exactly like the CLI scripts). Read results render in the transcript
and stay available as context for follow-ups; writes and errors notify.
First set your credentials (see skills/lib/AUTH.md). The
simplest path is an API key, which works for both /v1/* and the knowledge
base:
export ADA_API_KEY=sk-rc-… # mint at https://ada.ai/keys or via /login adaAlternatively, reuse your browser session cookies:
export ADA_SESSION_JWT=eyJ… # the rb_session cookie
export ADA_STYTCH_SESSION_TOKEN=… # the 7-day rb_stytch_session cookie (auto-refreshes)Then bind a repo to a knowledge-base project and use it:
/ada:help list the ada:* commands
/ada:projects [--org <id>] list knowledge-base projects
/ada:bind "Eng Notes" --create bind this repo (creates the project)
/ada:show show this repo's binding
/ada:unbind remove this repo's binding
/ada:list list documents in the bound project
/ada:get auth-model fetch one document with its content
/ada:upsert auth-model --title "Auth model" --content "Decided [[kb-architecture]]."
/ada:backlinks auth-model which notes link TO this slug
/ada:resolve session-x one hop of outbound links (dangling flagged)For multi-line content use --file <path>. ada:upsert creates when the
slug is new and updates in place when it exists (omitted fields are left
unchanged). /ada (refresh) and the ada:* commands coexist — refresh
updates the model catalog, the KB commands use your Stytch session.
Knowledge-base tools (kb_*)
The same extension also registers LLM-callable tools so the agent can read
and write the knowledge base itself during a turn (same shared client, so
identical behavior). Binding stays user-owned via /ada:bind; the tools
operate on the bound project but accept an explicit project_id to target
one the user names. The agent calls these directly — no / prefix:
| Tool | What it does |
| --- | --- |
| kb_show_binding | Show this repo's binding (no network). |
| kb_list_projects | List knowledge-base projects. |
| kb_list_documents | List document metadata in a project. |
| kb_get_document | Fetch one document by slug-or-id, with content. |
| kb_upsert_document | Create or update a note by slug. |
| kb_backlinks | Which notes link TO a slug (scans the project). |
| kb_resolve_links | One hop of outbound links from a note (dangling flagged). |
Use /tools to enable/disable them interactively. Auth is the same as the
ada:* commands: ADA_API_KEY (recommended) or ADA_SESSION_JWT +
ADA_STYTCH_SESSION_TOKEN.
Skills
This package also ships project-management skills that use Ada AI's
knowledge base — org-scoped projects of inter-connected markdown documents
linked with Obsidian-style [[slug]] wikilinks (see
../docs/content/docs/knowledge-base.mdx).
They are registered under pi.skills and load once the package is installed:
| Skill | What it does |
| --- | --- |
| project-binding | Bind this git repo to one knowledge-base project; writes .pi/kb-binding.json. |
| kb-summarize | Summarize the current transcript into the bound project as [[slug]] notes. |
| kb-retrieve | List docs, fetch by slug/id, follow links outward, compute backlinks. |
The knowledge base lives under /me/*. It accepts a user-scoped sk-rc-…
API key (ADA_API_KEY, the same one /v1/* uses) or a Stytch session
JWT — the rest of /me/* (invokers, usage, org/key management) stays
session-only. Export ADA_API_KEY for the simplest path, or
ADA_SESSION_JWT + ADA_STYTCH_SESSION_TOKEN and the shared client refreshes
the JWT automatically. See skills/README.md and
skills/lib/AUTH.md for setup and the scripts.
Troubleshooting: /login ada succeeded but no models appear
After /login ada, pi auto-refreshes the catalog asynchronously and shows
only a generic line ("… but its model catalog could not be refreshed; using
cached models") if it fails — the cause is dropped from the UI. Run /ada
for the actual error:
- 0 models —
/v1/modelsauthorized your key but returned an empty list, which means the key's organization has no enabled upstreams or invokers (ListModelsForScopeinbackend/serving-api/proxy/upstream_router.goqueries the org the key is scoped to, and no invoker is auto-seeded). The device flow's organization picker (Option B below) lets you scope the minted key to the org that owns your upstreams — pick that org when you approve. If you approved with Personal by mistake,/logout adaand/login adaagain, choosing the right organization. - HTTP 401/403 — the proxy key was rejected. Re-run
/login ada, or set a freshADA_API_KEY(mint at https://ada.ai/keys).
The full message is also written to pi's log ([ada] model refresh failed: …).
Configuration
| Env var | Default | Purpose |
| --- | --- | --- |
| ADA_API_KEY | — | Ambient proxy key (sk-rc-…). Skips the device flow and the persisted catalog (the bootstrap fetch is authoritative for env-var users). Also authenticates the knowledge base (/me/projects*), so KB skills/tools work with no session. |
| ADA_API_BASE | https://api.ada.ai | Gateway base URL for all /auth/* and /v1/* traffic. ada-branded (mirrors ADA_API_KEY); override for non-production backends. |
How the catalog works
/v1/models returns only model ids in the OpenAI shape. For each id the
extension looks up the built-in catalog (by id) and copies api-agnostic
capability facts — reasoning, input, contextWindow, maxTokens,
cost — so pi can do context accounting, pick token limits, and render the
model list correctly.
Two deliberate choices:
thinkingLevelMapis copied only from OpenAI-family matches. That map uses openaireasoning_effortvalues, which is exactly what ada receives over/v1/chat/completions. A non-OpenAI match (e.g. an Anthropic model id) carries provider-native thinking values that must not be forwarded over an openai-completions request — leaving the map unset lets pi use the openai-completions defaults.compat.supportsStrictMode = false. Ada aggregates arbitrary upstreams behind one OpenAI-compatible endpoint. Strict JSON-schema tools are OpenAI-specific and may be rejected by non-OpenAI upstreams, while non-strict tools are universally accepted. SetsupportsStrictMode: trueper model viamodelOverrideswhen you know the upstream supports it.
auto is ada's routing invoker (a virtual model), distinct from the
OpenRouter model of the same id, so it gets clean ada-side defaults and the
display name Ada Auto.
Architecture
src/
index.ts # programmatic entry (re-exports for tests/embedding)
ada/
config.ts # base URLs, provider id/name, tunables, fallback model
catalog.ts # /v1/models fetch + built-in catalog enrichment
device-flow.ts # browser device authorization (RFC 8628-style polling)
provider.ts # the Provider<"openai-completions"> factory
extensions/
ada-provider.ts # pi extension factory: registerProvider + /ada + session_start
ada-kb.ts # pi extension factory: ada:* slash commands + kb_* LLM tools
skills/
README.md # project-management skills overview + auth setup
lib/kb-client.mjs # shared zero-dep knowledge-base client (auth, refresh, link graph)
lib/kb-client.d.mts # TypeScript declarations for the shared client
lib/AUTH.md # how to obtain Ada AI session credentials for /me/*
project-binding/ # bind this repo to a knowledge-base project (.pi/kb-binding.json)
kb-summarize/ # write the transcript into the bound project as [[slug]] notes
kb-retrieve/ # list/fetch docs, resolve links, compute backlinks
test/
run-all.mjs # runs every suite
smoke.mjs # provider smoke test
skills-smoke.mjs # kb-client + skill CLI scripts (mocked fetch, no network)
ada-kb-commands.mjs # ada:* command registration + arg/validation (no network)
ada-kb-e2e.mjs # ada:* commands against a local mock serving-api
ada-kb-tools.mjs # kb_* tool registration + validation (no network)
ada-kb-tools-e2e.mjs # kb_* tools against a local mock serving-apiThe extension factory is declared in package.json under pi.extensions. To
add more extensions to this package, drop a new .ts file in extensions/
and append its path to the pi.extensions array — each entry is loaded as its
own factory.
Programmatic use
import { createAdaProvider, loginAda, fetchAdaModels } from "@adaai/cli";
const provider = createAdaProvider(await fetchAdaModels(process.env.ADA_API_KEY!, undefined));
// provider.id === "ada"; provider.getModels(), provider.stream(...), ...Develop
npm install
npm run check # tsc --noEmit
npm test # node test/smoke.mjs (no network; mocks /v1 + device endpoints)The smoke test loads the extension under jiti with pi's real getAliases()
map (e.g. @earendil-works/pi-ai → dist/compat.js), so it catches imports
that resolve in plain Node but break under pi's extension loader.
