withoneai/cli(opens in a new tab)
Language: TypeScript
A command-line tool to give your agents access to any app, create workflows and manage your One account.
- stars
- 414
- watchers
- 1
- forks
- 9
- open issues
- 13
- Last updated
- 10/29/2026
Language: TypeScript
A command-line tool to give your agents access to any app, create workflows and manage your One account.
Website · Docs · Dashboard · Changelog · X · LinkedIn
One gives your AI agent authenticated access to 1000+ platforms - Gmail, Slack, Shopify, HubSpot, Stripe, Notion, and everything else - through a single interface. No API keys to juggle, no OAuth flows to build, no request formats to memorize. Connect a platform once, and your agent can search for actions, read the docs, and execute API calls in seconds.
npx @withone/cli@latest init
Or install globally:
npm install -g @withone/cli one init
one init walks you through setup: authenticate via browser or enter your API key, pick your AI agents, and you're done. The MCP server gets installed automatically.
Or authenticate directly:
one login # Opens browser for authentication (global or per-directory) one logout # Clear credentials (with scope picker and confirmation)
The browser consent page names the key and tags it with where this CLI is installed (scope, project path, machine, OS user, CLI version, chosen harnesses, and the per-install device id unless telemetry is off) so you can find and revoke it from Settings → API keys. The terminal prints exactly what will be sent before the browser opens.
Requires Node.js 22.23.4+, 24.22.1+ or 26.12.1+. Older Node versions trust the compromised eMudhra intermediate CA certificates (October 2026), so a network attacker could intercept the CLI's traffic and your API key. On them the CLI refuses to run and prints the command that updates Node for how you installed it. Node 18, 20, 23 and 25 are end-of-life and never got the fix.
If you can't update Node yet, ONE_ALLOW_UNPATCHED_NODE=1 runs the CLI anyway,
with a security warning on every run. You remain exposed while it is set.
# Connect a platform one add gmail # See what you're connected to one list # Find the action, with its docs one actions find gmail "send an email" --task "email a hello to a contact" # Execute it one actions execute gmail <actionId> <connectionKey> \ -d '{"to": "jane@example.com", "subject": "Hello", "body": "Sent from my AI agent"}'
That's it. Four commands to go from zero to sending an email through Gmail's API - fully authenticated, correctly formatted, without touching a single OAuth token.
Chain actions across platforms into reusable workflows:
# Create a flow that looks up a Stripe customer and sends a Gmail welcome email one flow create welcome-customer --definition '{ "key": "welcome-customer", "name": "Welcome New Customer", "version": "1", "inputs": { "stripeKey": { "type": "string", "required": true, "connection": { "platform": "stripe" } }, "gmailKey": { "type": "string", "required": true, "connection": { "platform": "gmail" } }, "email": { "type": "string", "required": true } }, "steps": [ { "id": "find", "name": "Find customer", "type": "action", "action": { "platform": "stripe", "actionId": "<actionId>", "connectionKey": "$.input.stripeKey", "data": { "query": "email:'\''{{$.input.email}}'\''" } } }, { "id": "send", "name": "Send email", "type": "action", "if": "$.steps.find.response.data.length > 0", "action": { "platform": "gmail", "actionId": "<actionId>", "connectionKey": "$.input.gmailKey", "data": { "to": "{{$.input.email}}", "subject": "Welcome!", "body": "Thanks for joining." } } } ] }' # Validate it one flow validate welcome-customer # Run it - connection keys auto-resolve if you have one connection per platform one flow execute welcome-customer -i email=jane@example.com
Workflows live under .one/flows/<key>/flow.json with an optional lib/ subfolder for .mjs code modules — create new flows in this folder layout. Flows can be organized into subdirectory groups: .one/flows/<group>/<key>/flow.json. Reference them as group/key or just the bare key if unique. (The legacy single-file layout .one/flows/<key>.flow.json is deprecated but still loads for backward compatibility.) Flows support conditions, loops, while loops, parallel steps, transforms, sub-flows, pagination, bash steps, and external .mjs code modules. Run one guide flows for the full reference.
Your AI Agent
↓
One CLI
↓
One API (api.withone.ai/v1/passthrough)
↓
Gmail / Slack / Shopify / HubSpot / Stripe / ...
Every API call routes through One's passthrough proxy. One injects the right credentials, handles rate limiting, and normalizes responses. You never see or manage raw OAuth tokens - your connection key is all you need.
Set up your API key and install the MCP server into your AI agents.
one init
Supports Claude Code, Claude Desktop, Cursor, Windsurf, Codex, and Kiro.
Global vs. project scope. one init is interactive and asks where the setup should live:
~/.one/config.json) — applies to every folder. Best when you only need one workspace / API key.~/.one/projects/<slug>/config.json) — scoped to the current project, stored under your home directory so secrets never land in git. Use this when different projects need different API keys, connections, or access control.When you run one in a project, it uses the project config if one exists and falls back to the global config otherwise. Use one config path to see which config is active and the full resolution order.
In a monorepo, the project root is the nearest ancestor with .one/, .git, or package.json — checked in that order. Run mkdir .one in a nested subproject to make it its own project root (so the config is keyed by the nested dir's slug instead of the monorepo's). Your home directory itself is never treated as a project root — ~/.one is the CLI's own config directory, and a dotfiles .git or stray package.json in $HOME shouldn't turn everything under home into one project. Scope that applies to all of home is the global config.
If you've already set up, one init shows your current status for the active scope and lets you update your key, install to more agents, or reconfigure.
Agent-driven setup (no prompts). Pass --auth and one init runs end-to-end without any terminal interaction — handy when an AI agent is onboarding you. It saves the key, auto-installs the One skill, and skips the connect step (one add later).
one init --auth browser # opens a login window; you authenticate, the window closes — done one init --auth manual --api-key sk_live_... # headless / CI, no browser
| Flag | What it does |
|---|---|
-y |
Skip confirmations |
-g |
Non-interactive: write the One config globally (~/.one/config.json) |
-p |
Non-interactive: write the One config for this project (~/.one/projects/<slug>/config.json) |
--auth <browser|manual> |
Run setup with no prompts. browser opens a login window; manual uses --api-key. Scope from -g/-p (default global). |
--api-key <key> |
API key for --auth manual (sk_live_… / sk_test_…) |
--openai-key <key> |
Optional OpenAI key for one mem semantic search |
ONE_APP_URL (env) |
Dashboard origin the browser flows open (default https://app.withone.ai); use http://localhost:4202 against a local frontend. |
ONE_API_BASE (env) |
API origin, overriding apiBase in config (default https://api.withone.ai); use http://localhost:5005 against a local backend. |
Both overrides apply to the current process only and are never written to config, so a key minted against a local backend lands in your normal config and a cron job or MCP server without the variable would send it to the hosted API. Pair them with ONE_HOME (see below) to keep a local stack's credentials in a separate profile.
Connect a new platform via OAuth.
one add shopify
one add hubspot
one add gmail
one add gmail --tag work # tag the connectionOpens your browser, you authorize, done. The CLI polls until the connection is live. Platform names are lowercase (with dashes for multi-word names) - run one platforms to see them all.
| Flag | What it does |
|---|---|
--tag <name> |
Tag the new connection once it's created. Lets sync/flow profiles target a specific connection via "connection": { "platform": "gmail", "tag": "work" } when you have several connections for one platform (e.g. personal vs work Gmail). |
List your active connections with their status, connection keys, and the access this key has on each.
one list
● gmail operational live::gmail::default::abc123
● slack operational live::slack::default::def456
● shopify operational live::shopify::default::ghi789
You need the connection key (rightmost column) when executing actions.
Access reporting. Every connection carries an access field: what this key may run there. The CLI reads it from One (GET /v1/access/me, the access chosen on the consent page or later in the dashboard) and narrows it by any limits you set with one config — the server's answer is the ceiling, local settings can only lower it.
| Policy | Means | level |
|---|---|---|
{"policy": "full"} |
Every action on the connection | full (Full access) |
{"policy": "methods", "methods": ["GET"]} |
Only actions with those methods | read-only, read-write (GET, POST, PUT, PATCH), or custom |
{"policy": "actions", "actions": [{"actionId": "...", "title": "...", "method": "..."}]} |
Only these specific actions | custom, or none when empty |
A connection the key does not reach is not listed. When the policy is actions, those actions are exactly what may run — actions load <actionId> reads their docs, no find needed.
// one --agent list { "total": 2, "showing": 2, "connections": [ { "platform": "gmail", "state": "operational", "key": "live::gmail::default::abc123", "access": { "policy": "methods", "methods": ["GET"], "level": "read-only" } } ], "accessSource": "server" }
accessSource is "server" when One reported the key's access and "local" when it could not (an older API); then access shows only the CLI's own settings, the human table labels the column "Access (CLI settings only)", and accessHint says One may allow less. knowledgeOnly: true appears when knowledge-only mode is on (execution disabled), and unresolvedActionIds lists action ids that could not be looked up.
This mirrors the access field on the One MCP server's list_one_integrations tool, so both surfaces report access the same way.
Remove a connection by its key.
one connection delete live::gmail::default::abc123
one connection rm live::gmail::default::abc123 # aliasShows the connection details and asks for confirmation before deleting. Use --force to skip the confirmation prompt.
| Option | What it does |
|---|---|
-f, --force |
Skip confirmation prompt |
Browse all 1000+ available platforms.
one platforms # all platforms one platforms -c "CRM" # filter by category one platforms --json # machine-readable output
Find the action for every operation a task needs, across platforms, with its documentation, in one call: parameters, validation rules, request/response structure, and platform-specific quirks. One's decision model picks the action for each intent.
one actions find gmail "send an email" one actions find hubspot "find a contact by email" gmail "send an email" --task "email a follow-up to a CRM contact" one actions find shopify "list products" -t knowledge
"send a message to a channel"), without its data: IDs, names and message text make the search miss. Up to 10 pairs. Name the platform before every intent, even when they share one (stripe "list charges" stripe "list refunds"); a "platform" containing a space is refused as an intent that slipped out of its pair.--task describes the whole job in one line, in general terms. Agents are asked to send it with every find, since it helps tell similar actions apart.-t knowledge searches the catalog for writing code and building flows and relays, which includes the platform's own (passthrough) endpoints; the default is the catalog for running actions now.--ai-model names the model running the CLI (e.g. claude-sonnet-5), recorded with the find beside the CLI and the harness that launched it, so the account's usage can report which model asked. Agents are asked to send it with every find.--task or --ai-model carries a hint field (a dim line for humans) asking for them on the next find. Both stay optional.--no-knowledge lists the chosen actions (actionId, title, method, path) without fetching their documentation; load it later with one actions load <actionId>.Each intent's answer gives the action(s) to use with their docs, any actions also needed beside them, a substitute when the model was unsure (use one or the other, never both), and a few alternatives, then says once how to execute. Always read the docs before executing. The access settings from one config apply: an action they refuse is never offered.
A platform name no active platform has (clickup for click-up) is answered with selector: "unknown_platform" and a suggestions list of the platforms it may have meant, closest first, instead of being searched. Find again with the right name; one platforms lists them all.
Large documents come back as a digest: request-building sections in full (method/URL, headers, description, enforcement rules, required and optional parameters, sample request, gotchas, error handling), then a notice naming what was left out with the exact command to load it. In knowledge-only mode each pick comes back whole, with how to call it from code.
More of an action's documentation: the digest by default (how to read an alternative from a find answer), or a section a digest left out, the whole document, or its table of contents. Up to 10 action ids.
one --agent actions load <actionId> --section "Response Fields" # one section, by heading or id one --agent actions load <actionId> --section response,examples # several, by alias one --agent actions load <actionId> --full # whole document one --agent actions load <actionId> --toc # every section id, no document
Loads are served from the local cache once a doc is cached.
Execute an API action on a connected platform.
# Simple GET one actions execute shopify <actionId> <connectionKey> # POST with data one actions execute hubspot <actionId> <connectionKey> \ -d '{"properties": {"email": "jane@example.com", "firstname": "Jane"}}' # With path variables one actions execute shopify <actionId> <connectionKey> \ --path-vars '{"order_id": "12345"}' # With query params one actions execute stripe <actionId> <connectionKey> \ --query-params '{"limit": "10"}'
| Option | What it does |
|---|---|
-d, --data <json> |
Request body (POST, PUT, PATCH) |
--path-vars <json> |
Replace {variables} in the URL path |
--query-params <json> |
Query string parameters |
--headers <json> |
Additional request headers |
--form-data |
Send as multipart/form-data |
--form-url-encoded |
Send as application/x-www-form-urlencoded |
--dry-run |
Show the request without executing it |
--mock |
Return example response without making an API call |
--skip-validation |
Skip input validation against the action schema |
--output <path> |
Save response to a file (for genuine binary downloads — PDFs, images). Text responses (text/plain, HTML, CSV, XML) render inline automatically. |
--no-cache |
Bypass the cached action details and re-fetch them; the fresh details still refresh the cache (execution itself is never cached) |
The CLI validates required parameters (path variables, query params, body fields) against the action schema before executing. Missing params return a clear error with the flag name and description. Pass --skip-validation to bypass.
Execute multiple actions concurrently with --parallel, separating each action with --:
one --agent actions execute --parallel \ gmail send-email conn123 -d '{"to":"a@b.com","subject":"Hi","body":"Hello"}' \ -- slack post-message conn456 -d '{"channel":"#general","text":"Done"}' \ -- google-sheets append-row conn789 -d '{"values":["x","y"]}'
Each segment follows the same format: <platform> <actionId> <connectionKey> [-d ...] [--path-vars ...] [--query-params ...]. All segments are validated upfront before any execution starts. Results are collected via Promise.allSettled — if one fails, the rest still complete.
| Option | What it does |
|---|---|
--parallel |
Enable parallel mode |
--max-concurrency <n> |
Max concurrent actions per batch (default: 5) |
Agent-mode output includes parallel: true, per-action status/durationMs/response/_preflight ({"cache":"hit"|"miss"}), plus totalDurationMs, succeeded, and failed counts.
Manage the local cache of action documentation. actions find and actions load read each action's details (docs, method, path, validation schema) through the cache, so repeated reads serve instantly from disk, and actions execute reuses them for its preflight lookup, so a find followed by execute costs a single API round trip for the execute.
one cache list # List all cached entries with age and status one cache list --expired # Show only expired entries one cache clear # Clear all cached data one cache clear <actionId> # Clear a specific entry one cache update-all # Re-fetch fresh data for all cached entries
Find, load and execute also support cache flags:
one actions find gmail "send an email" --no-cache # Fetch the docs fresh one actions load <actionId> --no-cache # Skip cache, fetch fresh one actions load <actionId> --cache-status # Check cache status one actions load <actionId> --section response # Served from the cached doc, no fetch one actions execute gmail <actionId> <key> --no-cache # Fresh action-details lookup
Default TTL is 1 hour. Configure via ONE_CACHE_TTL environment variable or cacheTtl in ~/.one/config.json.
Note: execution responses are never cached — the action always runs live. Only action metadata (docs, method, path, schema) is cached, and agent-mode execute output reports it via "_preflight": {"cache": "hit"|"miss"}.
One ships a local memory store (a real Postgres process bootstrapped on demand via the bundled embedded-postgres plugin, with a postgres plugin for remote/self-hosted) that backs both user-authored notes and synced platform data. Zero-config — the first one mem call on a fresh machine auto-initializes the cluster at ~/.one/pg/cluster/ and writes a daemon PID file so subsequent CLI invocations reuse it. The daemon listens on port 5434, moving to the next free port if another program already holds it; pin a port with one mem config set embedded-postgres.port <n>.
# User memories — works immediately on a new install one mem add note '{"content":"Design review is Thursday"}' --tags work --weight 7 one mem update <id> '{"status":"done"}' # merges into data; refreshes searchable_text one mem search "design review" # hybrid FTS + semantic (if key set) one mem list note --limit 20 # Merge keys (`keys[]`) are a first-class column (unique across ACTIVE records; # archiving frees them). Manage them after creation with `mem key` — NOT `mem update`. one mem key <id> --add email:x@y.com # also --remove / --set <csv> # Find every record tied to a person/company, grouped by type. Spans BOTH columns: # `keys[]` (this record IS the entity — drives merge) and `identity_keys[]` (this # record INVOLVES the entity — Gmail thread participants, calendar attendees; never merges). one mem find-by-key email:jane@acme.com # every record carrying the key one mem find-by-key email:jane@acme.com --type gmail/gmailThreads one mem find-by-key email:jane@acme.com email:bob@acme.com # intersection — records with BOTH # Backfill searchable_text for rows that landed NULL or with UUID-noise-heavy text # (drops ids/timestamps, leads with name/title/email). No embedding provider needed. one mem reindex --searchable --type attio/attioPeople # Listing synced platform rows — type is positional and namespaced as <platform>/<model>; # there is NO --platform flag and NO platform column in the schema. one mem list "gmail/threads" one mem list "attio/attioPeople" --limit 5 # Enable semantic search (optional) one init # re-run — prompts for OpenAI key # OR one mem config set embedding.apiKey sk-... # writes top-level openaiApiKey # OR export OPENAI_API_KEY=sk-... # no persistence # Status + diagnostics one mem status # backend, provider, _upgrade hint one mem doctor # 7-check health report
Key surfaces: add, get, update, archive, list, search (--deep forces semantic), context, link/linked/unlink, key, sources, find-by-source, find-by-key, export, import, migrate, vacuum, reindex. Run one guide memory for the full reference.
find-by-key is the cross-platform join: keys are matched case-insensitively (they are lowercased on write, and the query is lowercased to match), --type narrows to one record type, and --limit (default 10) caps how many records are shown per type while the reported per-type count and overall total stay full match counts. Above 2000 matches the response flags truncated and the counts become floors — re-run with --type for an exact answer. Populate identity_keys[] from a sync profile's identityKeys field (see one guide sync), which ships pre-wired for gmail/gmailThreads, google-calendar/events, and fathom/meetings.
Sync platform data into the unified memory store for instant queries, hybrid FTS + semantic search, scheduled refresh, and change-driven automation. Memory is always written; pass --no-memory to skip (rare).
# Discover → init (seeds from built-in, auto-tests) → declare searchable → preview → run one sync models stripe one sync init stripe balanceTransactions # Optional: declare clean fields for embedding, then preview one sync init stripe balanceTransactions --config '{ "memory": { "embed": true, "searchable": ["description","type","amount","currency"] } }' one sync test stripe/balanceTransactions --show-searchable # Run — every row lands in memory (SQLite also written for enrich-phase compat) one sync run stripe --since 90d one mem sync run stripe # identical (alias) # Profiles with `enrich` run a second detail pass. It enriches each record ONCE; # the list pass thereafter merges rather than replaces, so the enriched payload # survives (reported as `memPreserved`). `--full-refresh` reconciles deletions, # it does not refresh detail content. To refresh detail: one sync run gmail --re-enrich # re-fetch every detail endpoint # ...or set `enrich.invalidateOn` in the profile (e.g. "historyId", "updated_at") # so only records that actually changed upstream are re-enriched, automatically. # Query + search (reads from memory) one sync query stripe/balanceTransactions --where "status=available" --limit 20 one sync query stripe/customers --where 'address.city=SF' # dotted paths supported one sync search "refund" # hybrid, per-type # Schedule unattended syncs + change hooks one sync schedule add stripe --every 1h one sync init stripe balanceTransactions --config '{"onInsert":"one flow execute handle-new-txn"}' # Deletion detection one sync run stripe --full-refresh
Sync uses passthrough actions only. Profiles referencing a custom/composer action are rejected at runtime.
sync modelsalready filters to passthrough-only; if a model has no passthrough list endpoint, compose a flow instead of syncing.
Connections are late-bound. Profiles use
"connection": { "platform": "<name>", "tag"?: "..." }instead of literalconnectionKeystrings. The key is resolved at sync time, soone add <platform>(re-auth) doesn't break the profile.tagonly needed for multi-account platforms (e.g. two Gmail accounts).
memory.searchablepaths. Drives what gets embedded + FTS-indexed. Supports numeric indexes (values.name[0].full_name) and[]wildcards (messages[].snippet,messages[].payload.parts[].body.data). Without declared paths the default walker concatenates every string — correct but noisy for hierarchical APIs. Always declare whenembed: true.
| Subcommand | What it does |
|---|---|
profiles [platform] |
List built-in pre-validated profiles |
doctor |
Verify sync engine health |
models <platform> |
Discover available data models |
init <platform> <model> |
Create/patch profile (seeds from built-in, auto-tests) |
test <platform>/<model> |
Validate profile. --show-searchable previews embedded text |
run <platform> |
Sync data (--full-refresh, --since, --dry-run, --no-memory) |
query <platform>/<model> |
Query memory with --where (dotted paths), --after/before |
search <query> |
Hybrid FTS + semantic across all synced data |
list [platform] |
Show profiles, record counts, freshness |
schedule add/list/status/remove/repair |
Cron-backed scheduled syncs with drift detection |
remove <platform> |
Delete synced data (--dry-run to preview) |
Change hooks (onInsert, onUpdate, onChange) fire per-page during sync — pipe to a shell command, a flow, or an event log. Root-array responses (e.g. Hacker News /v0/topstories.json → [9129911, 9129199, ...]) are supported by setting resultsPath to "", "$", or "."; primitive elements are auto-wrapped as { [idField]: value }. Run one guide sync or one guide memory for the full reference.
Receive webhooks from platforms and forward them to any connected platform via passthrough actions.
one relay platforms # List relay-capable platforms + event type counts one relay event-types <platform> # List supported event types for a platform one relay create --connection-key <key> --create-webhook --event-filters '["event.type"]' one relay activate <id> --actions '<json>' # Attach passthrough forwarding actions one relay list # List existing relay endpoints one relay events --platform <p> # Inspect received events one relay deliveries --endpoint-id <id> # Check delivery status
Start with one relay platforms to discover which platforms support relay at all, then drill into event-types <platform> for the specific events. Run one guide relay for the full reference including --metadata requirements per platform.
Optional skills for your coding agent, installed on request. The One skill itself still comes from one init (and one config skills sync|status); these commands never touch it.
one skills list # the One skill and each optional skill, with where it is installed one skills add connect # the One Connect skill, into this project one skills add connect --global # ...or for every project on this machine one skills add connect --agents claude-code,windsurf # link exactly these agents one skills update [connect] # re-copy after upgrading @withone/connect one skills remove connect # remove it and the agent folders that point at it
What one skills add connect does:
@withone/connect package your project installed (node_modules/@withone/connect/skills/one-connect), so your agent reads the docs for the exact SDK version your code calls. If the package isn't installed yet, it downloads the latest published package from npm and checks it against the registry's integrity hash first..agents/skills/one-connect (read by Codex, Cursor, Amp and OpenCode) and links .claude/skills/one-connect for Claude Code. Windsurf, Kiro, Goose and Roo get a link when the project already has their folder (.windsurf, .kiro, .goose, .roo). Where links aren't allowed (Windows without developer mode), it uses a junction, then a copy..one-skill.json.one command run in the project re-copies the skill when the installed @withone/connect version changes. Local files only; no network.Commit .agents/skills/one-connect and the agent links so your team's agents share the skill. Agent mode (--agent) returns JSON for every subcommand.
Create and update a One Connect app from one.connect.ts, typed by defineConnectApp from @withone/connect/config (0.19.0 or later).
one connect types # writes one-connect-env.d.ts: tool slugs and action names autocomplete one connect create # approve the app in the browser; re-run to review and apply changes
one connect create opens a setup page in your signed-in browser, where you review the app (or, on a re-run, its new version) and approve it; nothing is created or changed with your API key. A new app's client secret and connect key are made there and shown once, so they never pass through the terminal. Once you approve, the CLI writes ONE_CLIENT_ID, ONE_PERMISSION_SET and ONE_REDIRECT_URI to .env.local (or .env). Each tool in ask takes access: read-only, read-write, full-access, or custom with actions, named as one connect types wrote them or by id from one actions find <platform> "<what it does>" --no-knowledge. Options: --config <file>, --env-file <file>, --no-open. In
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| | |
| |
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| | |
| | |
| ||
| | |
| |