@simple-ai-lab/cli
v0.12.1
Published
Simple AI agents-as-code CLI
Readme
Simple AI CLI
simple synchronizes action-first Simple agents between production and a TypeScript repository.
pnpm add -g @simple-ai-lab/cli
simple login
simple init my-agent
cd my-agent
pnpm install
simple typecheck
simple diff
simple publish --label "Initial agent"Requirements
- A Simple AI account that belongs to at least one organization.
simple loginsigns in with the same account you use for the Simple AI dashboard. See the documentation to get started, or contact [email protected] for access. - Node.js 20 or later.
- pnpm, which the scaffolded project uses to install dependencies and typecheck.
simple typecheckalso runs npm, yarn, or bun when the project'spackage.jsonnames one of them as itspackageManager.
An agent's UUID, which simple pull takes, is shown on the agent's page in the dashboard.
Updating the CLI
Run simple update to upgrade to the latest release, or pass a version or tag, such as
simple update 0.11.0 or simple update next. The command supports pnpm, npm, and Yarn Classic global
installations on macOS, Linux, and Windows, plus Bun global installations on macOS and Linux. It verifies that
the selected package manager owns the running CLI, checks the requested release in that manager's configured
registry, and skips installation if that version is already installed. Otherwise, it updates that installation
and verifies the refreshed simple launcher and version. Manager commands use install-pkg/tinyexec
resolution from the global directory: local and ancestor node_modules/.bin directories and Node's bin
directory precede PATH.
If the selected manager cannot manage this installation, the command stops without installing.
Bun's Windows executable launchers are unsupported; use bun add -g @simple-ai-lab/cli@latest
with the Bun installation that installed the CLI instead.
Use simple update --dry-run to preview the detected installation and command without checking the registry
or installing.
For a source checkout or project dependency, use that project's package manager instead.
Only run the CLI in projects you trust
An agent project is code. simple typecheck, simple diff, simple publish, simple actions upgrade, and a simple pull into an existing project all load the project's agent.ts, and simple typecheck runs the project's package manager. Code in a project you cloned runs with your permissions and can read your saved sign-in, so treat a project from someone else the way you would any other code before running it.
A project's simple.lock.json names the API it was pulled from. The CLI signs in to Simple's own hosts and to your own machine. It refuses any other host unless you pass --trust-api-host to simple login, because signing in sends that host your session. Do not pass it for a host you were only told about by a project you cloned.
Pulling and publishing
Pull an existing branch into code:
simple pull <agent-uuid> --branch mainPull writes idiomatic defineAgent(...) source by default: Actions come back as action.http(...), action.code(...), and friends with real z.object(...) schemas; platform built-ins come back as action.sendEmail(...), action.sendTextMessage(...), and action.validateAddress(...) carrying only their sender configuration, since their contracts are fixed by the platform; prompt nodes become node.prompt(...); extraction schemas become Zod again.
Code Action handlers are written as their own TypeScript files under actions/, next to agent.ts, and referenced with source: sourceFile('./actions/<key>.ts'). Custom Data transforms follow the same pattern as actions/<key>.transform.ts. Edit those files directly; publish reads them back through the same references. A handler may import other files in the project by relative path, such as a lib/ file several handlers share. The platform compiles each handler on its own, so publish bundles such a handler with esbuild into one JavaScript file with the imported files inlined and its package imports left for the platform to resolve. Production stores that bundle, so edit these handlers in the project; pull leaves a handler alone while its bundle matches production. A handler that imports no project files is published exactly as written. The scaffolded tsconfig.json includes actions/**/*.ts so simple typecheck covers them. A tsconfig.json this CLI scaffolded before handlers existed is widened on the next pull; a hand-written one that does not reach actions/ is left alone and reported as a warning, so add the pattern to include yourself.
Prompt text is written to prompts/<node-key>.md and referenced with prompt: prompt.file('./prompts/<node-key>.md'). A prompt example is written to prompts/<node-key>-examples.md and passed as sourceFile('./prompts/<node-key>-examples.md'), the second argument. Both files are read back verbatim, so the text is exactly the file's bytes: an editor that appends a trailing newline on save changes the prompt, and simple diff will show it. A prompt with no text yet, or one carrying product-generated examples, keeps its text inline because prompt.file cannot reproduce those shapes.
The generated file is verified before it is written. The CLI compiles it and compares the result against the commit it was pulled from, field by field. If anything fails to round-trip — a legacy HTTP graph node, a legacy tool assignment, a schema shape Zod cannot express — the pull writes the lossless defineAgentSnapshot(...) form instead and reports the field that diverged. A pull therefore never writes a file that would publish something different from what was pulled.
Pass --format snapshot to always take the lossless form.
Pull refuses to overwrite locally changed source or referenced files, and refuses to replace a file under actions/ or prompts/ that it did not write itself (the lock records what the last pull wrote, and publish re-records those files so edits you have published stay replaceable). Use --force only when you intentionally want production to replace those changes. A file the previous pull wrote that the new graph no longer references is left in place, and stays replaceable by a later pull for as long as it is untouched. Without --branch, the CLI selects the product's most-recently-updated branch.
The generated simple.lock.json is committed with agent.ts. It records the selected Agent and branch, the observed branch head, stable graph identities, and immutable Action heads. Publishing validates the complete project on the server before mutation, creates or revises changed Actions with retry-safe identities, then creates one Agent v2 commit pinned to those exact Action versions. It refuses to overwrite Agent or branch metadata changed remotely since the last pull.
simple actions upgrade accepts newer remote Action heads. Pulled snapshots are rewritten with the remote Action values; ergonomic defineAgent(...) source must already match those values, so source and lock can never silently diverge.
simple login opens PropelAuth in your browser, uses Authorization Code + PKCE, asks you to select an organization
when needed, and refreshes the short-lived access token automatically. It asks the API which PropelAuth and public
OAuth client to use (/.well-known/oauth-protected-resource, RFC 9728), so signing in to any stage of Simple needs
only its API URL, and production is the default. No client secret is used or stored.
Each sign-in is saved as a named profile, like AWS CLI profiles, so the CLI can be signed in to several stages, or
as several users, at once. simple login --profile <name> --api-url <url> signs in a profile;
simple login alone signs in the default profile to production. Each command uses:
--profile <name>, orSIMPLE_PROFILE;- inside a project, the one profile signed in to the stage its
simple.lock.jsonnames; - otherwise the
defaultprofile.
simple profile list shows every profile, and simple logout --profile <name> (or --all) signs them out.
The CLI is OAuth-only; it does not accept organization API keys. Saved OAuth refresh tokens live in
~/.config/simple-ai/credentials.json, one per profile, with user-only permissions on macOS and Linux, and are replaced whenever
PropelAuth rotates them. A profile's session is only ever sent to its own API URL.
Run simple skill install to install the CLI-version-matched simple-agent authoring skill for Codex, Claude, and Cursor.
Choosing an organization
The saved session is on one organization at a time. Move it without signing in again:
simple whoami # who you are, and the organization ID you are on
simple org list
simple org use org_abc123 # or by name, when the name is unambiguousorg use re-scopes the session you already have, so it takes a round trip rather than a browser. Switching accounts
still needs simple logout and simple login; switching organizations does not.
To run a single command against another organization without moving the saved session, name it per command:
simple tags list --organization org_abc123
SIMPLE_ORGANIZATION=org_abc123 simple analyzers list--organization beats SIMPLE_ORGANIZATION, which beats the saved session. Prefer this form when more than one
command may run at once — several shells, worktrees, or agents — because org use changes one file that all of them
read, so a switch in one silently retargets the others. The per-command form never changes the saved organization, so it cannot.
Organization resources
Agents reference tags, analyzers, scorecards, and post-call workflows by UUID. Those resources are organization-level,
not part of an Agent commit, so they are created and listed with their own commands instead of appearing in agent.ts:
simple tags list
simple analyzers list
simple scorecards list
simple scorecards metrics list
simple post-call-workflows listEvery list prints <uuid> <name> <details>, and takes --json for the raw API payload — that is how you collect
the UUIDs an agent.ts, a scorecard section, or a workflow filter needs.
Each resource is created from a JSON definition file (--file <path>, or --file - to read stdin), from command
flags, or from both. Flags win over the file, so one checked-in definition can be reused with the differing fields
passed on the command line. Definitions are camelCase and reference other resources by UUID; the CLI validates them
locally, rejects fields it does not recognize, and reports every bad field at once before anything is sent.
simple tags create --name "Escalation" --description "Caller asked for a human" --color "#2563EB"
simple analyzers create --title "Sentiment" --prompt "Rate the caller sentiment" --output-enum positive,neutral,negative
simple scorecards metrics create --name "Greeted the caller" --criteria "The agent greets the caller by name"--output covers string, number, and boolean analyzers, and --output-enum covers enums. Anything nested is
authored in a file:
{
"title": "Resolution",
"prompt": "Summarize how the call was resolved",
"output": {
"type": "object",
"properties": [
{ "name": "resolved", "type": "boolean" },
{ "name": "reason", "type": "enum", "enum": ["fixed", "escalated", "abandoned"] }
]
},
"agentUuids": ["<agent-uuid>"]
}Scorecards reference metric library items by UUID, and sections own their own metrics. A scorecard and each of its
sections can be restricted to calls carrying certain tags — callTagUuids is the shorthand for "any of these tags":
{
"name": "Sales QA",
"agentUuids": ["<agent-uuid>"],
"callTagUuids": ["<tag-uuid>"],
"sections": [{ "name": "Opening", "metricUuids": ["<metric-uuid>"] }]
}Tag rules are stored in disjunctive normal form: groups are ORed, and the conditions inside a group are ANDed. Use
callTagFilter in place of callTagUuids for anything the shorthand cannot say — all_of, none_of, or alternatives.
This scores calls tagged both <a> and <b>, or calls tagged <c> that are not tagged <d>:
{
"name": "Sales QA",
"callTagFilter": [
[{ "operator": "all_of", "tagUuids": ["<a>", "<b>"] }],
[{ "operator": "any_of", "tagUuids": ["<c>"] }, { "operator": "none_of", "tagUuids": ["<d>"] }]
]
}Setting both forms on the same scorecard or section is rejected. Omitting both means no tag restriction.
A post-call workflow runs its actions in order against one Agent branch after a call ends, so it always needs a definition file. Filter actions gate the rest of the workflow on an analyzer result. Action identities are minted for you unless the definition already names them:
{
"name": "Email escalations to QA",
"enabled": true,
"actions": [
{ "type": "filter", "analyzerUuid": "<analyzer-uuid>", "resultPath": "escalated", "operator": "is_true" },
{ "type": "email", "to": "[email protected]", "subject": "Escalated call", "body": "Review this call." }
]
}simple post-call-workflows create --file workflow.json --agent <agent-uuid> --branch mainRun inside a pulled project and the branch in simple.lock.json is used, so --agent and --branch are only needed
elsewhere. agentBranchUuid in the definition file pins a branch outright and overrides both.
Updating and deleting these resources, and every procedure without a first-class command, stays available through
simple api.
Simulations
simple simulations covers the dashboard's Simulations area. Each command calls one procedure of the API's tests
router (simulations are test suites and test cases on the wire): its input is the procedure's JSON input, passed with
--input like simple api, plus any positional ids, and its output is the procedure's JSON response. The server owns
every rule. A response that reports errors, or a missing record, fails with exit code 3 and a JSON error on stderr,
so exit code 0 always means the work was done. --help on each command names its procedure and input.
Generate a test case from a production call, starting after a particular turn:
simple simulations transcript <call-uuid>
simple simulations cases from-call <call-uuid> --suite <suite-id> \
--goal "The agent confirms the appointment time" --up-to <row-id> --waittranscript prints the call's spoken turns and tool calls with their row ids. --up-to is inclusive: that row and
every row before it are replayed as history, and the simulation starts right after it. Without it the simulation
starts from the beginning of the call. The case's behavior and testing prompt are generated in the background;
--wait waits for both and fails if generation fails.
Edit and run:
simple simulations cases get <case-id>
simple simulations cases update <case-id> --input '{"input": {"testingAgentBehavior": "Asks to reschedule twice"}}' --wait
simple simulations scenarios edit-turn <scenario-row-id> --input '{"text": "I need to move it to Friday"}'
simple simulations runs start <suite-id> --input '{"agentV2BranchUuid": "<branch-uuid>"}' --waitGoals written without an id get one. A call-derived case replays a scenario, a copy of the call's transcript
shared by every case made from that call, so editing a scenario row regenerates the prompt of each of them. A
case's seedContextLastTranscriptUuid moves where its replayed history ends. A testingPrompt given to
cases create or cases update replaces the generated prompt and stays pinned: later edits don't regenerate it
until "testingPrompt": null unpins it and regenerates. Runs use the branch's live commit.
With --wait, runs start and runs get print every case run once the run settles and exit 1 unless all of them
passed.
Raw oRPC access
simple api is an authenticated escape hatch for any Simple API procedure, using the same OAuth session as the rest of the CLI. It can do nothing your account is not already allowed to do:
simple api agentV2.agents.list
simple api agentV2.agents.list --input '{"status":"active"}'
simple api agentV2.agents.get --input '{"uuid":"<agent-uuid>"}'
simple api agentV2.agents.get --input @agent.json
printf '%s' '{"status":"active"}' | simple api agentV2.agents.list --input -Procedure paths accept dotted form (agentV2.agents.list) or slash form (agentV2/agents/list). Input is always JSON, from one source: inline --input '{...}', a file with --input @file.json, or stdin with --input -. When --input is omitted, the input is {}. Success prints JSON on stdout. Every failure prints a JSON object on stderr with error, code, status, exitCode, and remediation, and uses exit codes 0 success, 1 general, 2 auth, 3 invalid request (bad path, arguments, input, or a missing resource).
