<p align="center">
  <a href="https://taleseal.com"><img src="https://raw.githubusercontent.com/Taleseal/taleseal/main/assets/readme-banner.png" alt="taleseal: your AI writes the page, you send the link" width="1200"></a>
</p>

<p align="center">
  <a href="https://www.npmjs.com/package/taleseal"><img src="https://img.shields.io/npm/v/taleseal?label=taleseal&color=c13521" alt="npm: taleseal"></a>
  <a href="https://github.com/Taleseal/taleseal/blob/main/LICENSE"><img src="https://img.shields.io/badge/licence-MIT-1c1b18" alt="licence: MIT"></a>
</p>

<p align="center">
  <a href="https://taleseal.com/integrate/tales/gallery">block gallery</a> ·
  <a href="https://taleseal.com/start">get started</a> ·
  <a href="https://github.com/Taleseal/taleseal">plugins</a> ·
  <a href="https://taleseal.com/pricing">pricing (free)</a> ·
  <a href="https://taleseal.com/security">security &amp; trust</a>
</p>

# taleseal

Your AI writes the page. You send the link.

A **tale** is a clean page of real blocks (words, tables, charts, diagrams, code, images) that
your AI drafts at a private, unguessable link. Not a transcript, not a wall of chat: a page a human
wants to read. You review it, ask for changes in plain words, and the same link updates. Publish,
and the reader needs no account.

This CLI is how an AI (or you) drafts, edits and publishes one.

## One command to a live page

```sh
npx -y taleseal@latest init claude   # or: init codex · init cursor · bare init
```

`init` sets up your tool's taleseal plugin (marketplace install where the tool has a CLI,
printed steps where it does not) and publishes a live welcome page, no account needed. It
prints the page's link and a once-shown claim link that keeps the page beyond 24 hours. With
a stored key the page publishes straight to your account instead. Whatever happens to the
tool setup, the page still publishes: a failed plugin install prints the manual steps and
carries on.

## The rest of the surface

```sh
npx taleseal draft tale.json       # works with no account: publishes LIVE, anonymously,
                                   #   and prints a claim link (deleted in 24 h unclaimed)
npx taleseal login                 # sign in, once, to draft privately and keep pages
npx taleseal draft tale.json       # with a key: create a DRAFT; get a private link to review
npx taleseal publish <tale-id>     # publish it; send the same link on
```

`draft` scrubs the content and shows you an exposure report before anything leaves the machine.
With an API key the draft URL carries a DRAFT banner and renders exactly as the published page
will, so review happens in the browser, and publishing keeps the link stable: the recipient
always sees the latest published revision.

With no key at all (or `--anon`) there is no draft step: the page is live the moment it is
created, immutable, rendered with an unclaimed treatment (no branding, an "Unverified" sender,
outbound links as plain text), and hard-deleted after 24 hours unless the printed claim link is
used to keep it. Claiming is free, transfers the page to the claiming account, and restores
links and branding. A key that is set but empty is refused as a misconfiguration; it never
silently goes anonymous.

## The flow

```
compose tale.json  →  validate  →  draft (gate + link)  →  review in the browser
                                        →  publish  →  send  →  (revise → publish)*  →  retract

no account:  compose  →  validate  →  draft (gate)  →  LIVE at once  →  claim within 24 h to keep
```

Editing an existing draft is id-addressed and atomic: read the `outline` for the block ids and the
`draftSeq`, then send ops echoing that `draftSeq` as `--base`. A stale base is a conflict: nothing
applies and you re-base on the outline it hands back; a batch applies whole or not at all.

## Login

Bare `taleseal login` opens the browser: approve there (signing up on the way if needed) and an
API key is created and stored in `~/.config/taleseal/config.json` (`$XDG_CONFIG_HOME` respected,
file mode `0600`). It works from SSH boxes and containers too: open the printed URL on any device
and match the confirmation code. Codes last ten minutes and work once.

For CI, scripts and browserless machines, `taleseal login --key tk_…` stores a key minted by hand
in the [dashboard](https://taleseal.com/dashboard) instead. It is validated against the server
first: a key rejected with 401 is never stored; if the server is unreachable the key is stored
anyway, with a warning. `taleseal logout` removes the stored key.

## Usage

```sh
Compose and publish:
  validate <tale.json>            Check against the schema: each problem with its exact JSON path.
  draft <tale.json>               Redact, show the exposure gate, then create the tale as a DRAFT
                                    and print its URL. With no key at all (or --anon): publish
                                    LIVE anonymously: no draft step; deleted in 24 h unless claimed.
  publish <tale-id>               Promote the current draft to the revision the recipient sees.
  revise <tale-id> <tale.json>    Store a new whole-tale draft body; the recipient keeps seeing
                                    the published revision until the next publish.
  retract <tale-id>               Destroy the tale and every revision. The URL answers 410 forever.

Read a draft:
  pull <tale-id> [file]           Write the current draft body to <file> (or stdout).
  outline <tale-id>               The draftSeq you echo as --base, plus one line per block.
  get <tale-id> [blockId...]      Full JSON of the named blocks (or all).

Edit a draft (id-addressed, atomic, optimistic concurrency):
  ops <tale-id> <ops.json>        Apply a JSON array of ops in one atomic batch.
  insert <tale-id> <blocks.json>  --after <id> | --before <id> | --start | --end
  replace <tale-id> <blockId> <block.json>
  remove <tale-id> <blockId...>
  move <tale-id> <blockId>        --after <id> | --before <id> | --start | --end
  set-envelope <tale-id>          --title … | --standfirst … | --recipient … | --stationery … |
                                    --sender-name … | --sender-org … | --cta-label … | --cta-url … |
                                    --expires-at … | --clear <fields>

Other:
  upload <path>                   Upload an image and print the reference an "image" block carries.
  login [--key tk_…]              Sign in and store an API key.  logout  Remove it.

Options:
  --base <n>    The draftSeq from `outline`, echoed as an edit's concurrency token. Omit it and the
                CLI reads the current draftSeq itself: convenient, but a concurrent edit would be
                clobbered; pass --base to be safe.
  --idem <key>  Idempotency key: re-sending a batch with the same key never re-applies.
  --anon        Draft only: publish anonymously even when a key is available; live at once,
                immutable, deleted after 24 hours unless claimed.
  --yes         Skip the pre-draft / pre-publish prompt (hooks, CI).
  --json        Print machine-readable results.
  -h, --help    Show this help.
```

On success the tale URL is printed to stdout. Errors go to stderr with exit code 1.

## The exposure gate

Secrets travel nowhere without a human seeing what would ship. When `draft` (or `revise`) is about
to send a body, it first scrubs it client-side (AWS keys, GitHub/Slack tokens, `sk-` API keys,
JWTs, `Bearer` headers, PEM private-key blocks and `password=`/`secret:`-style assignments become
`[REDACTED:<kind>]` markers) and prints an exposure report counting what it found. Then:

- **Interactive terminal**: the report is shown and you are asked to confirm. Anything but yes
  cancels; nothing leaves the machine.
- **Not a TTY** (git hooks, CI, piped output): sending is refused with exit code 1 unless `--yes`
  is passed. Automation must opt in explicitly.

## Configuration

Both values resolve in the same order everywhere: env var first, then the config file written by
`taleseal login`, then the default:

| Value | Resolution order |
| --- | --- |
| API key | `TALESEAL_API_KEY` env var → config `"apiKey"` |
| Base URL | `TALESEAL_URL` env var → config `"url"` → `https://taleseal.com` |

The config file is `~/.config/taleseal/config.json` (`$XDG_CONFIG_HOME/taleseal/config.json` when
set), shape `{"apiKey": "tk_…", "url"?: "https://…"}`, mode `0600`. Publish, revise and retract
require a key. `draft` with no key at all publishes anonymously (`--anon` forces it even with
one); a key that is set but empty is refused as a misconfiguration, never treated as anonymous.
`validate` needs none.

## Branding

Colours and a logo are not a tale or CLI concern. Branding is a paid add-on the account owner
configures once at [taleseal.com/dashboard/brand](https://taleseal.com/dashboard/brand), and every
tale the account publishes picks it up automatically. Anonymous pages carry no branding while
unclaimed; claiming one applies the claiming account's brand from then on.

## Programmatic API

The same surface as the CLI, from the SDK:

```ts
import { applyTaleOps, composeTale, draftTale, getTaleOutline, publishTale } from "@taleseal/sdk";

// Compose a whole tale and create it as a draft (reads TALESEAL_API_KEY;
// TALESEAL_URL defaults to https://taleseal.com).
const tale = composeTale({ title: "Brightloop upgraded to Growth", blocks: [/* … */] });
const { id } = await draftTale(tale);

// Or edit an existing draft, block by block, atomically.
const { draftSeq } = await getTaleOutline(id);
await applyTaleOps(id, { baseRevision: draftSeq, ops: [/* … */] });

await publishTale(id); // promote the draft to the revision the recipient sees

// Or with no account at all: published live at once, deleted in 24 h unless claimed.
const anon = await draftTale(tale, { anonymous: true }); // anon.claimUrl, anon.claimExpiresAt
```

Requires Node ≥ 22.
