# @every-env/spiral-cli

A command-line interface for [Spiral](https://app.writewithspiral.com), the agent that writes in your voice.

## Quickstart

```bash
npx @every-env/spiral-cli@latest login
spiral write "a tweet about shipping fast" --instant
```

That's it. `login` opens a browser to authenticate, `write` produces a draft in your voice.

## Installation

```bash
# Run directly (no install needed)
npx @every-env/spiral-cli@latest --help

# Or install globally
npm install -g @every-env/spiral-cli
```

Requires Node.js >= 18.

## Authentication

### Browser sign-in (recommended)

```bash
# Print the sign-in link and open it in your browser
spiral login

# Or pass an API key directly
spiral login --token spiral_sk_...

# Check status
spiral auth status

# Logout
spiral logout
```

`spiral login` prints the sign-in URL and opens it in your browser, then waits
for you to approve. If the browser can't open (e.g. over SSH), just paste the
printed URL. No code to copy back — the token is delivered straight to the CLI
once you approve.

`spiral login` and `spiral logout` are shortcuts for `spiral auth login` and
`spiral auth logout` — use whichever reads better.

Get your API key at [app.writewithspiral.com/settings/api-keys](https://app.writewithspiral.com/settings/api-keys).

### Agent sign-in (no API key in the transcript)

When an AI coding agent runs the CLI, it can sign its human in **without ever
handling an API key** — so no `spiral_sk_...` key leaks into the chat
transcript, tool logs, or session history.

```bash
# 1. Agent starts a sign-in and gets a shareable link (non-blocking, exits 0).
spiral login --json
# → { "status": "pending",
#     "auth_url": "https://app.writewithspiral.com/connect?user_code=ABCD-2345",
#     "user_code": "ABCD-2345",
#     "expires_in": 900,
#     "poll_command": "spiral auth status --json" }

# 2. Agent hands `auth_url` to its human, who opens it and approves in a browser.

# 3. Agent polls status until the token lands (it claims the token for you).
spiral auth status --json
# → { "authenticated": false, "status": "pending", "auth_url": "..." }   (still waiting)
# → { "authenticated": true,  "status": "authenticated", "source": "stored" }  (done)
```

`spiral auth status` opportunistically completes a pending sign-in: once the
human approves, the next `status` call claims the token, stores it locally, and
reports `authenticated: true`. Subsequent `spiral write` calls then just work.

If a credential is already present (a prior `spiral login` or `SPIRAL_TOKEN`),
`spiral login --json` returns `{ "status": "already_authenticated",
"authenticated": true, "prefix": "..." }` and starts no new flow — there's
nothing to approve. To sign in as a different account, run `spiral logout`
first. So an agent should treat both `already_authenticated` and a later
`authenticated: true` from `auth status` as "done."

This path is auto-selected whenever the CLI isn't attached to an interactive
terminal (which is how agents run it). Use `--no-browser` to force it from a
real terminal too (prints the link as text instead of opening a browser), and
`--json` for machine-readable output. The existing `--token`, `SPIRAL_TOKEN`,
and `spiral setup --pairing-code` paths are unchanged.

### Environment variable

For CI/CD and agent environments:

```bash
export SPIRAL_TOKEN=spiral_sk_...
```

`SPIRAL_TOKEN` takes precedence over any token stored by `spiral login`, so
you can override locally-stored credentials without logging out.

## Usage

### Write content

```bash
spiral write "a blog post about shipping fast"         # Interactive (may ask questions)
spiral write "a tweet about AI" --instant               # Skip clarifying questions
spiral write "make it shorter" --session <id>           # Refine existing drafts
spiral write "..." --style <id> --workspace <id>        # With style and workspace
spiral write "..." --num-drafts 3                       # Multiple variations
spiral write "..." --instant --json                     # Machine-readable output
echo "prompt" | spiral write --instant --json           # Piped input
```

### Set up your voice

Spiral writes in *your* voice. Give it a few writing samples so drafts sound
like you from the first one — no per-request style wrangling.

```bash
spiral onboard                                        # Check voice setup and how to finish it
spiral samples                                        # List your writing samples
spiral samples add "a paragraph you actually wrote"   # Add a text sample
spiral samples add https://yourblog.com/a-post        # A URL → one sample
spiral samples add https://yourblog.com --site        # --site crawls the whole site into samples
cat draft.md | spiral samples add                     # Pipe text in
```

Samples added at the workspace root feed **Auto Style**, so every `spiral write`
leans on them automatically — pin a specific `--style` only when you want a
particular voice. Target a named style or team workspace with `--style <id>` /
`--workspace <id>`. `spiral onboard` reports whether you have enough to write in
voice yet and what to add next (add `--json` for agents).

Create a named style or a new workspace to organize voices, samples, and
prompts:

```bash
spiral styles create "Punchy LinkedIn" --channel linkedin   # New empty style
spiral samples add "a post you wrote" --style <id>          # then add samples to it
spiral workspaces create "Client A" --description "Work for A"
spiral workspaces create "Acme" --team <team-id>            # team workspace (needs a team plan)
```

A new style starts empty; add the user's writing with `spiral samples add
--style <id>`. A workspace groups styles, samples, and prompts (one per client
or project); pass its id to `--workspace` on later commands (e.g. `spiral
samples add "..." --workspace <id>`). A style id goes to `--style`, a workspace
id to `--workspace`; they are not interchangeable.

### Personalize and humanize existing text

Two single-shot commands for transforming text you already have. Both never ask
clarifying questions, take ~3-8 seconds, and accept positional input or piped
stdin. Use them when you don't need a full draft — just a voice swap or a clean-up.

```bash
# Rewrite text in your unique writing voice (uses your Spiral style guide).
spiral personalize "their text here"
spiral personalize "their text" --style <id>            # Use a specific style
spiral personalize "their text" --channel x             # Hint a channel-specific style
echo "their text" | spiral personalize                  # Or pipe from anywhere

# Apply your writing rules + remove AI tells (em-dashes, "delve",
# hype words). No voice rewrite — just mechanical cleanup.
spiral humanize "their text here"
pbpaste | spiral humanize | pbcopy                      # Clean what's on the clipboard
```

By default both print plain text to stdout — perfect for piping. Add `--json`
for the full response — `personalize` returns `{ text, style_used, quota_remaining }`,
`humanize` returns `{ text, quota_remaining }` (no `style_used`, since there's
no voice rewrite).

When in doubt: `personalize` includes the humanize cleanup as its final step,
so reach for `humanize` only when you specifically don't want a voice rewrite.

### Browse resources

```bash
spiral styles                  # List writing styles
spiral workspaces              # List workspaces
spiral prompts                 # List saved prompts (reusable templates)
spiral prompts get <command>   # Print a saved prompt's text
spiral sessions                # List past conversations
spiral drafts --session <id>   # View drafts from a session
spiral quota                   # Check session quota and plan
spiral prime                   # Full API documentation
```

Saved prompts are reusable templates you create in the Spiral app (they appear
there as /slash-commands). Write with one by passing its command to `--prompt`;
Spiral prepends the template server-side, and an unknown command fails fast
with the list of available commands:

```bash
spiral write "This week: our v2 launch." --prompt blog-intro
```

`spiral prompts get <command>` prints a template's raw text when you want to
pipe it somewhere else.

### Agent setup

For AI agents (Claude Code, Plus One, etc.), copy the install command from
[Settings → Agents](https://app.writewithspiral.com/settings/api-keys) and
paste it into your agent. The command looks like:

```bash
spiral setup --pairing-code <single-use-code>
```

The pairing code is single-use and expires in 15 minutes. The CLI exchanges
it for a fresh API key the first time it runs — no long-lived secret in the
install command itself.

The legacy `spiral setup --token spiral_sk_...` form still works for older
install commands that already embedded a raw key.

This validates the resulting key and prints a memory block for the agent to save. The
block tells the installing agent **when** to reach for each command — `write`
for new content, `personalize` for "rewrite in my voice" requests, `humanize`
for "remove AI tells" requests — so the agent picks the right tool without the
user having to spell it out every time.

### File attachments

Pass reference documents directly via `--file` (repeatable):

```bash
spiral write "Turn this into a blog post" --file quarterly-report.xlsx --instant
spiral write "Compare these two decks" --file deck-a.pdf --file deck-b.pdf --instant
```

Supported formats: PDF, DOCX, PPTX, XLSX, HTML, EPub, TXT, MD, CSV, JSON, XML.
Max 5 files, 10MB each. Spiral extracts text automatically — no need to paste
file contents into the prompt.

### Flags

| Flag | Description |
|------|-------------|
| `--json` | Machine-readable JSON output |
| `--session <id>` | Session ID for multi-turn / refinement |
| `--style <id>` | Writing style ID |
| `--workspace <id>` | Workspace ID |
| `--file <path>` | Attach a file (PDF, DOCX, XLSX, etc.) — repeatable |
| `--instant` | Skip clarifying questions |
| `--num-drafts <n>` | Number of drafts (1-5, default 1) |
| `--channel <c>` | Channel hint (`x`, `linkedin`, `blog`, ...) for `personalize` and `styles create` |
| `--description <t>` | Description for `styles create` / `workspaces create` |
| `--team <id>` | Team UUID for `workspaces create` (creates a team workspace) |
| `--token <key>` | API key for auth/setup |
| `--no-browser` | `login`: print the sign-in link and exit instead of opening a browser (agent path) |
| `--debug` | Show debug info on errors |
| `--help, -h` | Show help |
| `--version, -v` | Show version |

## Agent-native usage

spiral-cli is designed to work in any environment — terminals, Docker containers, subprocess shells, CI pipelines. In non-interactive environments (no TTY), it automatically:

- Skips spinners and terminal animations
- Outputs plain text without ANSI color codes
- Returns raw markdown instead of terminal-formatted text
- Times out stdin reads to avoid hanging

```bash
# Typical agent workflow
spiral prime                                            # Get API docs
spiral onboard --json                                   # Confirm the user's voice is set up first
spiral styles --json                                    # Discover styles
spiral write "a launch tweet" --instant --json           # Generate content
spiral write "make it punchier" --session <id> --json    # Refine
spiral personalize "$USER_TEXT" --json                   # Rewrite in user's voice
spiral humanize "$USER_TEXT" --json                      # Strip AI tells, no voice rewrite
```

### Exit codes

| Code | Meaning |
|------|---------|
| 0 | Success |
| 1 | General error |
| 2 | Authentication required |
| 3 | API error (quota, rate limit, timeout) |
| 4 | Network error |
| 5 | Invalid arguments |

## Subscription required

The Spiral API is paid-only. Each call to `spiral write`, `spiral personalize`, or `spiral humanize` requires:

1. An authenticated session (`spiral login`).
2. A paid Spiral plan (Personal, Team, or an active Every Bundle subscription).

If a request can't be served, the CLI prints a clear next step:

- **Invalid or expired API key (HTTP 401)**: `Invalid or expired API key. Run \`spiral auth login\` to re-authenticate.`
- **Free-tier account (HTTP 402)**: a Stripe Checkout link to subscribe — open it in your browser, complete payment, and re-run your command.
- **Paid plan over quota (HTTP 402, Personal only)**: a Stripe Checkout link for a one-time top-up. Team and Trial accounts get a clear quota message but no top-up link (those tiers don't support one-off top-ups; subscribe to a Personal plan, wait for the period to reset, or contact your team admin).

In `--json` mode the same information is returned in structured form so agents can parse it:

```json
{
  "error": "ApiError",
  "message": "A Spiral subscription is required to use the API. Visit app.writewithspiral.com and upgrade to a paid plan.",
  "statusCode": 402,
  "upgrade_url": "https://checkout.stripe.com/c/pay/cs_test_..."
}
```

When the URL points at Stripe Checkout, the post-payment redirect lands on a public confirmation page that tells the user to return to the CLI — useful when the browser isn't signed in to Spiral, or is signed in as a different account than the CLI's API key.

## Environment variables

| Variable | Description |
|----------|-------------|
| `SPIRAL_TOKEN` | Auth token (alternative to `spiral login`; takes precedence when set) |
| `SPIRAL_API_URL` | Override API endpoint (default: `https://api.writewithspiral.com`) |

## Writing styles

Create writing styles at [app.writewithspiral.com/writing-styles](https://app.writewithspiral.com/writing-styles) to customize Spiral's output to match your voice.

## License

MIT
