Using Spark from the command line
Spark speaks the same protocol as most chat APIs, so anything that lets you set a base URL and an API key will work without a plugin. Point it here, paste a key, and go.
1. Create an API key
You need an account. Open Settings, find API keys, give the key a name and press Create.
The key is shown once. Copy it before you leave the page. If you lose it, delete the key and make another. You can hold five at a time.
Keys look like this:
sk-spark-4f19a2c7b0e83d5619ac2f8b47d0e3a1c6b95d72
A key can only talk to the model. It cannot read your conversations, change your password, or touch your account, so the worst a leaked key can do is spend your daily allowance. Delete it in Settings and it stops working immediately.
2. Point your client at Spark
| Base URL | https://spark.trysparkai.workers.dev/v1 |
|---|---|
| Auth | Authorization: Bearer sk-spark-… |
| Model | spark-1.4 |
Three endpoints exist:
GET /v1/models— list the models your key can use.POST /v1/chat/completions— chat, with or without streaming.GET /v1/quota— how many messages you have left today.
Many tools expect an environment variable named for a different vendor. That is fine. Set whatever variable your tool reads to the Spark base URL and key.
curl
curl https://spark.trysparkai.workers.dev/v1/chat/completions \
-H "Authorization: Bearer $SPARK_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "spark-1.4",
"messages": [{"role": "user", "content": "Explain a bloom filter in three sentences."}]
}'
Add "stream": true for token-by-token output as server-sent events, ending with data: [DONE].
Python
The standard OpenAI client works unchanged; only the base URL differs.
from openai import OpenAI
spark = OpenAI(
api_key="sk-spark-…",
base_url="https://spark.trysparkai.workers.dev/v1",
)
stream = spark.chat.completions.create(
model="spark-1.4",
messages=[{"role": "user", "content": "Write a haiku about race conditions."}],
stream=True,
)
for chunk in stream:
print(chunk.choices[0].delta.content or "", end="", flush=True)
Spark CLI
There is a purpose-built terminal client. It draws charts where you are reading rather than describing them, opens generated pages in your browser, and puts suggested shell commands into your history so you press Up and decide for yourself.
Windows
irm https://t4meri.github.io/install.ps1 | iex
That installs to %LOCALAPPDATA%\Programs\spark-cli, puts a spark
command on your PATH, and needs Node 20 or newer. Open a new terminal afterwards so the
PATH change is picked up. -Uninstall reverses it.
Piping a script from the internet straight into your shell runs whatever is at that URL. The script is short and readable: open install.ps1 first if you would rather see it before running it. Spark CLI itself warns about this pattern when a model suggests it.
macOS, Linux, or anywhere with npm
npm install -g github:T4meri/spark-cli
Then
spark login
spark chart "days in each month of 2026"
spark "explain this stack trace" < error.log
spark do "find files over 100MB" --to-history
The interactive session
spark chat opens a session. Press / and the commands appear as
you type, narrowed with every letter, so there is nothing to memorise. Tab completes the
highlighted one, Enter runs it, Esc puts the menu away.
› /s
› /skills [new <name>] Your own reusable briefs, listed or created
/save <file> Keep this conversation in a file you own
/serve Open the last generated page in a browser
/search [on|off] Whether answers may use the live web
Esc while an answer is streaming stops it and keeps what had already arrived. Sessions
are written to a thread file as you go, so /resume offers the recent ones
back.
Files go both ways
Attach a file with @path or pipe one in, and Spark can hand files back:
when an answer is a file it carries its path on the fence, and
spark apply writes them once you have seen the list.
spark "split this into a module and a test" @src/thing.js
spark apply
› src/thing.js overwrites 84 lines with 61
› test/thing.test.js new, 39 lines
Write 2, overwriting 1? [y/N]
A path that is absolute or climbs above the working directory is refused rather than written.
Skills
A skill is the brief you are tired of retyping, kept in a file.
spark skills new commit-message --project writes
.spark/skills/commit-message/SKILL.md next to your code, where it can be
checked in and shared; without --project it goes in your account and
follows you everywhere.
---
name: commit-message
description: Turn a diff into a commit subject and body
search: off
files: [style.md]
---
Read the diff below the divider and write a commit message for it.
1. Work out what changed and, more importantly, why.
2. Subject line in the imperative mood, under 60 characters.
It is a command from then on: it appears in the / menu tagged
skill, and anything you type after it is appended below a divider. From a
shell it is git diff | spark --skill commit-message. files:
attaches other files from the skill folder as reference material, and anything outside
that folder is refused.
Source and full documentation: github.com/T4meri/spark-cli. It has no runtime dependencies.
Other CLI tools
Anything that accepts a custom base URL will work. Two common shapes:
Environment variables
export OPENAI_API_KEY="sk-spark-…"
export OPENAI_BASE_URL="https://spark.trysparkai.workers.dev/v1"
Most tools built on the OpenAI client libraries read these two, so exporting them is often
the whole setup. Use spark-1.4 wherever the tool asks for a model name.
Config file
Tools that keep a config file usually want the same three values:
provider: openai-compatible
base_url: https://spark.trysparkai.workers.dev/v1
api_key: sk-spark-…
model: spark-1.4
If a tool refuses to start, check that it is not appending /v1 a second time.
The base URL already ends in /v1.
Limits and behaviour
- API requests draw on the same daily message allowance as the web app. Check it with
GET /v1/quota, or read theX-Spark-Quota-Remainingheader returned with every completion. - Five keys per account. Delete one to make room for another.
- Web search is on by default, so answers can reflect current information. Send
"web_search": falseto turn it off for a request. - Sampling parameters such as temperature are accepted and ignored. Responses are tuned centrally.
- Errors follow the usual shape: a JSON body with an
error.messagefield.