---
name: skybits
description: Skybits is an agent focused, real-time, conflict-free collaborative editor that makes it possible for agents and humans to iterate on document, canvas and spreadsheet content in a safe and reliable manner. Skybits prevents silent AI edits because content updates are immediately visible to all collaborators and are auditable and revertible. Use this skill to read, search, create, and edit Skybits documents, canvases, and spreadsheets — whenever the user works with a Skybits URL, wants to author or extract content, or wants to leave or answer comments on it.
---

# Skybits

Skybits is a real-time collaborative workspace shared by humans and agents:
every edit is immediately visible to all collaborators, attributed, and
revertible. When you add, delete, or replace text, Skybits shows the user
what changed directly in the document for review. Documents are **live and
multi-user** — other collaborators may edit between any two of your calls. Mention concurrent
activity to the user only when it materially affects the task: content you
were asked to operate on no longer exists, another party's edit conflicts
with the user's stated intent, or a change you were about to make would no
longer mean what the user intended.

A Skybits document is one of four kinds:

- **Doc** — collaborative rich text as plain HTML (`<h1>`, `<p>`, `<strong>`,
  `<ul>`, `<a>`, `<table>`, …), no custom format to learn. Split into
  sections, each anchored by a stable `nodeId` (e.g. `XBbnTe7E6F`).
- **Canvas** — a raw source-file project with `index.html` as its entry
  point, rendered as a web page. Files are stored byte-for-byte and
  addressed by relative path.
- **Spreadsheet** — tabs of cells and formulas, addressed by A1 notation
  (`"B7"`, `"A1:H40"`) through its own tool surface, with an
  optimistic-concurrency `rev` handle on every read and write.
- **File** — raw uploaded bytes of any type, versioned. Upload a new version
  to replace it, download it with `get_file_url`; there is no in-place editing.

Any tool that reads or modifies an existing document takes a `doc_url` — the
full URL or just its id, the URL's last path segment (e.g.
`https://skybits.ai/documents/abc123` and `abc123` are both valid). Get one from
`create_doc`, `create_canvas`, `create_spreadsheet`, `create_file`, or
`find_documents`. Whatever the kind, **`outline_document` first**: it returns a
doc's section tree, a canvas's file inventory, a spreadsheet's per-tab
orientation, or a file's current version — the cheapest call, and the right
start on any unfamiliar document.

## Setup

The CLI is `bin/skybits` in this skill. If it is missing, outdated, or not
logged in, follow [references/install.md](references/install.md).

In PowerShell on Windows, invoke it as `& bin\skybits.exe`, quote `'@file'`
tokens, and write payload files as UTF-8 without BOM.

## Reading and editing (docs and canvases)

After orientation, **read selectively** — `read_document` a `scope` (a doc
section `nodeId` or canvas file path) rather than the whole object, or use
`find_in_document` to locate exact text, markup, or source. **Edit with
`edit_document`** — batch independent operations into one call. They run in
order, and the batch is atomic: any failure rejects the whole batch
and nothing is applied. Spreadsheets follow the same habit through their own
tools — see the Spreadsheets section.

## Docs

Create a doc with `create_doc(title, content)` — `content` is the full HTML
body (see HTML format below); the server parses it and assigns section
`nodeId`s. Image ids are per-document, so `create_doc` content can't include
`<img>` — create the doc, then upload and place images (see Images).

```bash
bin/skybits create_doc '{"title":"Meeting Notes","content":"<h1>Weekly Sync</h1><p>Attendees: …</p>"}'
```

### Section addressing

The document is a single linear sequence of HTML blocks; the section tree is
a convenience layered over it — a way to scope an operation to a contiguous
span of that sequence, not a container the content nests inside. A `scope`
value is a heading's `nodeId` from `outline_document`. It names the span from
that heading down to the next heading at the same or higher level,
subsections included, so naming a parent already covers its children:
`read_document` returns the whole span, `remove` deletes it whole, and
`insert_after` targets the point right after it. On a document titled by one
H1, that H1's section spans the entire document — to scope narrowly, name the
deepest heading above your target. Content between a heading and its first
subheading has no narrower section of its own; pin it with a more specific
`pattern` instead.

Tools that read or match *within* content (`read_document`, `find_in_document`,
`edit_document`'s `replace`) accept a single `nodeId` or a list (for non-contiguous
sections).
Tools that name a *position* (`insert_before`, `insert_after`, `remove`,
`move`) take a single `nodeId`. Omit `scope` on reads, searches, replacements,
and inserts to address the whole document or the corresponding document edge.
`remove` and `move` require specific section nodeIds; `move` also requires one
specific destination section in `before` or `after`.

### HTML format

`read_document` returns, and edits accept, this restricted tag set:

- Headings `<h1>`–`<h6>`; paragraphs `<p>`.
- Inline: `<strong>`, `<em>`, `<u>`, `<s>`, `<code>`, `<a href="…">`,
  `<span style="color:#hex">`, `<mark style="background-color:#hex">`.
- Alignment via `style="text-align:center"` on a block.
- Paragraph indents (set from the ruler) ride on a block's inline `style` as
  `margin-left`/`margin-right`/`text-indent` in px (e.g.
  `style="margin-left:48px;text-indent:-24px"` is a hanging indent). Preserve
  them through edits; they are optional and omitted when a block has no indent.
- Tab characters are the inline element `<tab/>` — never a literal tab, which
  is collapsible HTML whitespace. Each advances the text after it to the
  block's next ruler tab stop; stops ride on the block as
  `data-tab-stops='[{"posPx":288,"align":"right"}]'` — a JSON array of stop
  positions (px from the text-column edge), optional and omitted when a block
  has no custom stops. Preserve both through edits.
- Lists: `<ul>`, `<ol>`, `<li>`; task lists as `<ul data-type="tasklist">` with
  `<li data-checked="true|false">`.
- Tables: `<table>`/`<thead>`/`<tbody>`/`<tr>`/`<th>`/`<td>`. Header rows (all
  `<th>`) go in `<thead>`, data rows in `<tbody>`; cell content is wrapped in a
  block tag (`<td><p>…</p></td>`).
- Block: `<blockquote>`, `<pre><code lang="…">`, `<hr>`, `<br>`.
- Math: inline `<equation latex="\frac{a}{b}"/>` — write LaTeX and the server
  compiles it. It reads back with an authoritative `omml` attribute beside the
  `latex`, so change an equation by replacing the **whole element** with a new
  latex-only one, and leave an equation that has no `latex` (imported math
  outside the LaTeX subset) exactly as read.
- Images (block-level — siblings of `<p>`, never nested inside one):
  `<img src="images/<imageId>" alt="…" align="left|center|right" width="<px>">`,
  with captions as `<figure><img …><figcaption>…</figcaption></figure>`. Register
  the image first, then use its `images/<imageId>` reference (see Images).
  Size/align **only** via the `align`/`width` HTML attributes; CSS on images is
  dropped. Both are optional and omitted on read-back at their defaults (`align`
  center, `width` unset), so a center-aligned image reads back with no `align`.
  The `data-nodeId` attribute carries an image's stable id — preserve it through
  edits, omit it on a brand-new image.

Section boundaries appear as `<!-- @nodeId -->` comments. HTML comments are
read-only annotations: `find_in_document` and `replace` patterns never match
them — position an edit with the `scope`
parameter or by quoting document content, not by including comments in a pattern.

### Structure

Compose with **real block elements** — a separate `<p>`, `<ul>`/`<ol>`,
`<blockquote>`, or `<h*>` per logical unit. Packing items into one `<p>` with
`<br>` breaks or `-`/`*`/`1.` prefixes renders exactly as written — literal text,
not the list or paragraphs you almost certainly intend — so prefer the structural
tag unless the user explicitly asks for that literal output. Use `<br>` only for
genuine intra-paragraph breaks (addresses, signatures, lyrics).

```html
<!-- bad: fake list crammed into one paragraph -->
<p>Findings:<br>- Throughput dropped 40%<br>- Error rate up 3x</p>

<!-- good: real heading + real list -->
<h2>Findings</h2>
<ul><li>Throughput dropped 40%.</li><li>Error rate up 3x.</li></ul>
```

### Editing

Edits go through `edit_document` as an ordered list of operations (at most 500
per call — split larger edits into batches):

- `replace` — find/replace within a scope's rendered HTML. Omit `scope` for the
  whole document. The pattern is
  plain text and case-sensitive by default (`flags: "i"` matches
  case-insensitively); `regex: true` makes it a JavaScript regex (flags
  `i`/`m`/`s`, `$1`/`$<name>` backreferences). Exactly one match unless
  `replace_all`. A pattern that misses byte-exactly retries with forgiving
  normalization (curly/straight quotes, dash variants,
  `&amp;`/`&lt;`/`&gt;` escaping, whitespace runs) and the result quotes the
  actual text replaced; `exact: true` disables that.
- `insert_before` / `insert_after` — add HTML before a section's heading or after
  its last block. Omit `scope` to insert at the document start/end respectively. To add
  several new sections at one spot, put them all in one operation's content rather
  than a separate op per section. A `pattern` (same matching rules as `replace`,
  exactly one match) anchors the position instead: the content lands before/after
  the block containing the match, `scope` becomes the search scope, and the
  anchor text is never modified — the safe way to position by quoting text.
- `remove` — delete a section and its subsections.
- `move` — relocate a section (with subsections) via `before`/`after`;
  the source and destination are specific sections. The content moves verbatim —
  heading levels are never adjusted to fit the destination.

Patterns match the HTML `read_document` returns minus its HTML comments (tags and
entities like `&amp;` match; `<!-- @nodeId -->` anchors and `<!-- comment-thread:… -->`
blocks never do). When an operation creates headings, the response returns their
new `nodeId`s.

The document **title** is not part of the HTML body — `edit_document` can't change
it. Rename a document with `update_document_title`.

Collaborators may keep or undo your edits. If content you wrote is gone on a later read, a collaborator removed it — do not reinstate it unless asked.

The CLI takes operation subcommands so you don't hand-build the array (the
`'{"operations": [...]}'` JSON form also works). Each op keyword starts a new
operation; its flags follow until the next keyword:

```bash
bin/skybits edit_document --doc-url abc123 \
  replace      --scope XBbnTe7E6F --pattern 'old text' --replace 'new text' \
  insert-after --content '@appendix.html' \
  remove --scope dFs3lJLutR
```

### Theme

`update_document_theme` sets document-wide typography (the `<body>` and
`<h1>`–`<h6>` font family, size, weight, line height, color). Partial updates
deep-merge. `get_document_theme` returns the current theme as a JSON object in
the same shape `update_document_theme` accepts — read it, tweak it, write it back.

The CLI builds the nested theme from flat `--<target>-<axis>` flags:

```bash
bin/skybits update_document_theme --doc-url abc123 \
  --body-font 'Inter, sans-serif' --body-size 11 --h1-size 28 --h1-weight 700
```

## Canvases

A canvas suits web layouts, interactive pages, visual designs, and other work
that needs unrestricted HTML, CSS, or JavaScript; a doc suits collaborative
prose with sections, comments, themes, and structured HTML editing.

The same `outline_document`, `read_document`, `find_in_document`,
`diff_document`, and `edit_document` surface serves docs and canvases. A doc
`scope` names section `nodeId`s; a canvas `scope` names relative file paths.

Create a canvas with `create_canvas`. It accepts either a `files` object or
`content` as shorthand for `{"index.html": content}`; omitting both creates the
empty canvas seed. `read_document` returns one
file byte-for-byte or multiple/all files with delimiter lines;
`outline_document` inventories files and `find_in_document` searches raw source.
`edit_document` runs ordered atomic canvas batches; each operation's `op` is
`replace`, `write`, `insert_after`, `remove`, or `rename`, and its file path
goes in `scope`. `write` creates a file when absent;
`replace` accepts one path, a path list, or omitted scope for every file and
counts matches across that combined scope. `insert_after` appends source to one
file. The `rename` and `remove` operations reject `index.html`.

Canvases render in a sandboxed frame without Skybits cookies or session access.
For each viewer, the first paint is display-only: HTML and CSS render while
scripts, forms, and dialogs are inactive. A human can enable interactivity for
that browser session, and any canvas file edit turns it off again. Make the static
first paint meaningful. Treat JavaScript behavior as progressive enhancement.
The frame has open network access, so public external libraries and CDNs work;
platform-protected image URLs do not load there. Canvas CSS owns page layout.
Use fixed-size page containers with `-webkit-print-color-adjust: exact` and
`print-color-adjust: exact`, plus `@page` and `@media print`, for paginated or
print designs; full-bleed app layouts can fill the frame normally.

A doc can embed a canvas as
`<canvas-embed doc="CANVAS_ID" align="left|center|right" width="<px>"/>`.
The content is width-fit scaled and the frame height tracks the scaled content.
`align` and `width` behave exactly as on `<img>` and are omitted on read-back at
their defaults. The embed remains a reference on the read surface; inserting it
creates the required document link. `data-nodeId` carries the embed's stable id.

After building or filling a canvas, check your work with `render_report` —
a headless render returning page count, per-page ink coverage, overflow and
page-break metrics, and optional screenshots. A slow render returns `pending`
with a `reportId`; poll `get_render_report`.

Plain JSON arguments work for every canvas tool:

```bash
bin/skybits create_canvas '{"title":"Demo","files":{"index.html":"<link rel=\"stylesheet\" href=\"styles.css\"><main>Hello</main>","styles.css":"main { padding: 2rem; }"}}'
bin/skybits read_document '{"doc_url":"abc123","scope":"index.html"}'
bin/skybits edit_document '{"doc_url":"abc123","operations":[{"op":"replace","scope":"index.html","pattern":"<main>Hello</main>","replace":"<main>Hello, world</main>"},{"op":"write","scope":"js/app.js","content":"console.log(1)\n"}]}'
```

For multi-KB source, put the JSON payload in a file and pass `'@canvas.json'`
rather than shell-escaping it inline.

See `references/canvas.md` for the full tool spec — operation semantics,
response shapes, and the error catalog (`references/render-report.md` for the
render self-check). Worked layout patterns (e.g. paged print documents) live
under `references/recipes/`.

## Spreadsheets

A spreadsheet has its own tool surface — `read_document`, `edit_document`,
`find_in_document`, and `diff_document` do not apply, and spreadsheets carry
no comment threads. Orient with `outline_document` (defined names, then
per-tab dims, populated regions, merges, frozen panes), then read
selectively with `read_sheet`
(**never a whole large sheet in one call** — page with `start_row`), or search
values/formulas/notes with `find_in_spreadsheet` when you know what you're
looking for but not where. Writes are atomic batches: `edit_cells` for
values, formats, notes, and links; `edit_conditional_formats` for workbook
rules; `edit_named_ranges` for defined names; `restructure_spreadsheet` for
rows, columns, merges, and tabs.

Every spreadsheet read returns `rev` and every write requires it — copy the
read's `rev` straight into the write. This is optimistic concurrency, not a
lock: the head advances whenever anyone edits, so rev movement between your
calls is normal, and a `spreadsheet_changed_since_rev` rejection is routine
rather than a failure — the error names the fresh rev; re-read the affected
range, re-derive your addresses from the fresh read, and reapply in the same
turn rather than stopping to ask. `create_spreadsheet` returns the starting
`rev`, so your first `edit_cells` needs no read. After any
`restructure_spreadsheet` — yours, or one revealed by a rev mismatch — treat
previously read A1 addresses on the restructured tab as stale and re-read.
See `references/sheets.md` for the full tool spec — response shapes, the
per-tool rev rules, and the error catalog.

## Files

`create_file(title)` makes a file resource, empty until something is uploaded —
which is what a workflow whose output is a file starts from. `upload_file_version`
appends the next version: the previous one keeps its number and stays
downloadable, so replacing a file never loses the old bytes. `get_file_url`
returns a short-lived download URL for a version (`version` defaults to the
latest); `read_document` returns a text-like version's contents directly, up to
1 MiB. `update_file_settings` pins the content types a resource accepts, as a
contract for whatever writes to it.

Uploading and downloading local files is the CLI's job — it never puts the
signed URL in your context:

```bash
bin/skybits upload_file ./report.pdf --title "Q3 report"
bin/skybits upload_file ./report.pdf --doc abc123
bin/skybits get_file_url abc123 --out ./report.pdf
bin/skybits file_versions abc123
```

See `references/files.md` for the version model, the accept-list, and what a
sync target's writer does.

## Templates and copying

`copy_document` duplicates a document into a fresh one you own (read access on
the source suffices; content, images, theme, page setup, and comment threads
carry over, shares/tags do not). It copies the current state; `include_history`
additionally copies every version of a file resource, and is refused for the
other kinds.

Templates are read-only master documents — a doc or a canvas. Listings flag
them `isTemplate`; `find_documents location:"templates"` filters to just them.
To make something from a template, `copy_document` it and edit the copy — the
template itself always refuses edits. `set_template` marks a doc or
canvas you can edit as a template, or converts one back to editable.

## Pointing the user at a location

Document ids, node ids, and thread ids mean nothing to the user — never cite
them when talking to them. Point at places with links instead: the document's
`url` plus
`#section-<nodeId>` (a heading's nodeId) or `#comment-<threadId>` opens the
document scrolled to that section or comment thread. Format the link however
presents best in your interface — e.g.
`[Budget review](https://skybits.ai/documents/abc123#section-h3f9a2c1)` where
Markdown renders — with the heading text or comment topic as the label; a
bare URL works everywhere.

## Search

`find_in_document` searches the content `read_document` returns: comment-free
rendered HTML for a doc or raw source for a canvas. Scope to doc sections or
canvas files. Matching is plain text by default and a JavaScript regex with
`regex: true`. Doc matches name their section; canvas matches name their path
and source line. Regex matches also report capture groups. Default `limit` is
25; `limit: 0` returns all, but the response is hard-capped at 100 matches (the
reported total still reflects the true count beyond that).
A pattern that misses byte-exactly retries with the same forgiving
normalization `replace` uses, and each match quotes the document's actual text.
On a spreadsheet, `find_in_spreadsheet` is the equivalent search over cell
values, formulas, and notes.

## History

`document_history` lists the timeline newest-first, including the created state
as revision `0`. A revision number is the handle you
pass as a `version`: `read_document` and `read_sheet` can read a past revision, and
`diff_document(from_version, to_version)` shows what changed between two
revisions. The revision an `edit_document` lands at is returned in that tool's
result, so you can diff or re-read your own edit without first listing history.
Non-content actions — renames, theme changes, comment activity — record
revisions too, so revision numbers can advance without any content edit.

## Comments

Docs and canvases both carry comment threads: a doc anchors one
to text, a canvas to an element. In a doc, threads
also appear inline in
`read_document` output as `<span data-comment-thread="…">` wrappers and
`<!-- comment-thread:… -->` annotations. Tools: `list_comment_threads`,
`create_comment_thread`, `reply_to_comment_thread`, `set_comment_thread_status`.

Creating a thread is two-step: `create_comment_thread(comment, quote)` mints
the thread (the `quote` is the content it's about, kept as the thread's
excerpt) and returns its threadId; you then anchor it with an `edit_document`
replace op — in a doc by wrapping the target text in
`<span data-comment-thread="THREADID">…</span>` (the same spans you preserve
when editing around existing comments), in a canvas by adding
`data-comment-thread="THREADID"` to the target element's start tag.

A typical pass: `list_comment_threads` → `read_document` the section for context →
`edit_document` if a change is needed (not every thread is meant for you) →
`reply_to_comment_thread`
→ `set_comment_thread_status` when addressed.

The comment text is the `--comment` flag; `--quote` is the text it's about:

```bash
bin/skybits create_comment_thread --doc-url abc123 \
  --quote 'Open Questions' \
  --comment 'Who owns the go/no-go decision?'
```

Details in `references/comments.md`.

## Sharing and collaborators

`find_people` resolves a name or email fragment against people the user has
collaborated with or directly invited. Results are ordered by relevance and never
search the global user directory. Use the returned email with `share`; when more
than one person plausibly matches, ask the user which one they mean.

`list_collaborators` shows who a document is shared with — each person's role
(owner/editor/commenter/viewer), the "anyone with the link" setting, and any pending email
invites. It needs only read access to the document.

`share` grants, changes, or revokes access for people (by email) or via link
("anyone").

`link_documents` gives everyone who can access the source viewer access to the
target — one hop only; links never chain. `list_document_links` shows outgoing and incoming
links; `canViewOther` says whether you can view the other document, and a false
value means its title is withheld. `unlink_documents` removes a link.

## Sync graph

The sync graph keeps documents in sync; it is sync state, not a share. An **edge**
is one-way (one source informs one target); a **set** is a group of documents
whose changes one agent manages, resolved together in one pass. Resolve either
the same way: `list_sync_jobs` → `describe_sync_job` → `claim_sync_job` → do the
work carrying the claim id → `finalize_sync_nodes` → `close_sync_claim`. Finalize
each node at the revision you incorporated; anything above it stays stale and wakes
the job again. A node of any kind can be in a job, including a file resource,
whose new version is written with `upload_file_version`; `describe_sync_job`
states per kind how to see what changed and which tools write it, so read it
before you start. `list_sync_graph` shows a document's connections;
`update_sync_edges` wires edges, and `create_sync_set` / `update_sync_set` /
`delete_sync_set` manage sets. See
`references/sync-graph.md` for the full tool spec.
## Trash and restore

`trash_documents` soft-deletes documents (recoverable until purged) and
`untrash_documents` is the inverse. To list trashed documents,
`find_documents` takes a `location` (`documents` by default, or `trash`).

`restore_document` (singular) is unrelated to the trash: it restores one
document's content to a past revision.

## Tags

Tags are hierarchical slash-paths (`Engineering/Frontend/React`); every tag
tool addresses a tag by its full path `name`. Assigning a leaf is enough —
ancestors are created on the fly and a `find_documents` `tags` filter of
`Engineering` matches it and every nested tag. `list_tags` (filter with
`query`), `create_tag` (set description/color), `update_tag` (renaming a
prefix moves nested tags), `delete_tag` (cascades to nested tags only —
ancestors stay),
`assign_tag`, `unassign_tag`. Users may call tags folders; the app shows them
with folder icons.

## Images

`upload_image` registers an image and returns its `imageId`; it does **not** place
the image. In a document, insert `<img src="images/<imageId>">` via
`edit_document`. In a canvas, write `images/<imageId>` directly in its source
files.

The CLI unifies both upload sources — `--url` for a remote URL the server
fetches, `--path` for a local file uploaded directly from disk — and
`get_image_url --out` downloads an image's bytes straight to a local file
(the URL is doc-scoped; the `imageId` comes from `upload_image` or an
`<img>` src):

```bash
bin/skybits upload_image --doc-url abc123 --path ./diagram.png
bin/skybits upload_image --doc-url abc123 --url https://example.com/chart.png
bin/skybits get_image_url --doc-url abc123 --image-id 4f12d6c8 --out ./photo.png
```

## Import and export

Export is async: `start_export` enqueues a job over the cross-product of docs ×
formats; poll `get_job` until terminal, then hand the download URL to the user
(it's short-lived — re-call `get_job` for a fresh one).
`cancel_job` cancels a queued/running job; `list_jobs` shows recent jobs — active ones plus the last 24h of finished ones — and the per-user concurrency budget.

```bash
bin/skybits export <doc-id>... --export-formats pdf --out ./export.pdf
bin/skybits job list
```

Use the CLI to import local files and folders. It walks the paths, validates
against the live config, uploads, and tags by directory structure. `--as doc`
(the default) converts each file into the doc HTML model; `--as canvas` stores
`.html`/`.zip` source verbatim as canvas files (`.zip` requires it). (A file at
`reports/forecasts/jan.md` under `--base-tag Q1` goes into the tag
`Q1/reports/forecasts`):

```bash
bin/skybits import ./reports --base-tag "Q1"
```

## Output format

`read_document`, document listings, people, collaborator, tag, comment-thread
and document-link lists, `document_history`, `share`, the render reports,
the sync views, and the import/export job tools accept a `format` argument:
`text` (default) is the readable rendering, `json` returns the same fields as
a structured object for programmatic use (filtering, counting, piping
onward). See each tool's schema for the JSON shape.

## If a tool misbehaves

When a tool errors in a way its message doesn't explain, behaves contrary to
its description, or these instructions steered you wrong, file a short note
with `report_problem` (a `summary`, plus optional `tool` and `category`) once
you've finished or worked around it. Recovering does not cancel the report —
a workaround that cost you extra calls is exactly what the people improving
these tools need to hear about. The report goes to the Skybits team; include
only the diagnostic note, never document content, conversation text, or
private information.

## CLI usage

Call a tool by name. Put the optional base JSON object first (`{…}`,
`'@file.json'`, or `-` for stdin), followed by `--key value` or `--key=value`
flags; flags override the base object's fields:

```bash
bin/skybits outline_document --doc-url abc123
bin/skybits find_documents --query roadmap --limit=5
bin/skybits find_in_document --doc-url abc123 --pattern '(\w+) issue' --flags i
```

- Keys may use hyphens (`--doc-url` → `doc_url`). A bare flag is `true`
  (`--regex`); the *same* flag repeated is a list (`--scope A --scope B`).
  Single-quote shell values — inside double quotes the shell expands `$…`,
  silently corrupting content like `$40`.
- `'@file'` reads UTF-8 JSON as the base object or text as a flag value;
  `-` reads stdin once per call. `--key value` supplies a string, except
  `null` supplies JSON null. `--key=value` converts numbers and `true`/`false`;
  other values, including `null`, `@file`, objects, and arrays, stay literal.
  Use the equals form for values beginning with `--`.
- Nested object/array params (`edit_document`'s `operations`, the theme) come from the
  JSON object or the subcommand builders shown above, e.g.
  `bin/skybits edit_document '@ops.json' --doc-url abc123`.

The command prints the tool's `result` on stdout; on failure it prints
`HTTP <status>: <body>` on stderr and exits non-zero. Any HTTP client works too —
`POST {host}/v1/tools/{tool}` with `Authorization: Bearer <key>`.

## Discovering tools

- `bin/skybits help` — one-line catalog of every tool and bundled
  command.
- `bin/skybits help <tool>` — that tool's parameters, plus a pointer
  to its custom CLI form when it has one. Custom commands (`edit_document`,
  `update_document_theme`, `upload_image`, `get_image_url`, `export`,
  `import`, `job`, `login`) also
  respond to `-h`.
- `bin/skybits schemas [tool ...]` — raw JSON Schema for one, several,
  or all tools (the source of truth for argument names and types).
- `references/` holds deeper per-topic specs — response shapes, error formats, and
  edge cases. The `/skill/` index lists every file; fetch the one you need.