Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,21 @@ tot notes.md
| `tot remove <link>` | Remove the living page from its share link. |
| `tot login --key <key>` | Optional. Publish as an owned account instead of anonymous. |

## For AI agents

> [!NOTE]
> The skill works without the `tot` CLI: it publishes via curl against the HTTP API at
> `https://api.tot.page`. With `tot` on PATH it uses the CLI instead.

### Install

```bash
npx skills add plannotator/tot
```

Skill: [`skills/tot/SKILL.md`](skills/tot/SKILL.md) — publish, update, remove, HTML
assets, ownership. Plain API reference: [site/agents.md](site/agents.md).

## How it works

Files are served byte for byte. Markdown comes back as raw markdown. HTML comes back as raw HTML.
Expand Down
2 changes: 2 additions & 0 deletions docs/REPO_LAYOUT.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ Root is for the npm package and top-level project files:

- `src/` - CLI implementation.
- `test/` - CLI tests.
- `skills/tot` - distributable agent skill (`SKILL.md` + `references/`) that teaches
agents to publish via the `tot` CLI or the HTTP API directly, no install required.
- `package.json`, `pnpm-lock.yaml`, `tsconfig*.json`, `vitest.config.ts` - package tooling.
- `README.md`, `LICENSE` - public package metadata.

Expand Down
80 changes: 80 additions & 0 deletions skills/tot/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
---
name: tot
description: >-
Publish a markdown or HTML file to a live public tot.page URL — a living link
plus immutable @hash snapshots — then update or remove it, with curl alone or
via the tot CLI. Use when asked to publish, share, host, or put online a
document, report, or page, to update or delete an existing tot.page link, or
whenever tot or tot.page comes up.
---

# tot — publish a file to a live link

One local file becomes one public URL. No signup, no install — `curl` is enough
(the `tot` CLI, when present, makes the same calls for you). Files are served
raw: markdown comes back as raw `.md`, HTML as raw `.html`, byte for byte. No
config, no build step.

Every page has two URLs:

- **living** — `https://tot.page/{slug}`, always shows the latest version.
- **frozen** — `…/{path}@{hash}`, one immutable snapshot per publish, for
pointing at "exactly this version".

## The rules that bite

- **Checkpoint gate.** A fresh page answers `version: null` until its first git
checkpoint lands (~2–10s). Poll the read endpoint until `version` is
non-null — the link 404s until then. A publish is done when the poll passes,
not when the POST returns.
- **Open pages.** Without an API key every page is open: anyone holding the link
can read, update, or delete it. Publish content you mean to be public, and
treat each link as the credential it is.
- **1.5 MB body limit** (UTF-8) — over it the API answers `422`. Errors are
JSON: `{"error":{"code":"…","message":"…"}}`.

## Route: CLI or curl

```bash
command -v tot
```

- **found** → use the CLI; it scans HTML assets, waits out the checkpoint, and
tracks published pages in `~/.tot` across sessions:
`tot <file>` publish · `tot update <file|url>` · `tot remove <file|url>` · `tot list`
- **not found** → the curl flows below; identical capability, zero install.

## Publish (markdown, or HTML with no local refs)

```bash
BODY=$(jq -Rs '{kind: "markdown", body: .}' notes.md) # HTML → kind: "html"
RESP=$(curl -sS -X POST https://api.tot.page/v1/documents \
-H 'content-type: application/json' -d "$BODY")
WS=$(jq -r .workspace.id <<<"$RESP") # the page's identity — keep it
DOC=$(jq -r .document.id <<<"$RESP") # for update/remove — keep it
```

`jq -Rs` JSON-encodes the file safely; hand-writing `body` breaks on newlines
and quotes. HTML with local file references takes the asset flow instead — see
Branches below.

Pass the checkpoint gate, then report both URLs:

```bash
until PAGE=$(curl -sS "https://api.tot.page/v1/workspaces/$WS/documents/$DOC") \
&& [ "$(jq -r .version <<<"$PAGE")" != "null" ]; do sleep 2; done
jq -r '"living: \(.share_url)\nfrozen: \(.file_url)"' <<<"$PAGE"
```

Done when both URLs are in hand and given to the user. `$WS` and `$DOC` are the
page's identity for every later update or remove — keep them for the session;
across sessions, note them next to the source file, or switch to the CLI, which
records them in `~/.tot`.

## Branches

- **HTML referencing local files** (images, stylesheets, scripts, video) —
upload assets before the document:
[references/html-assets.md](references/html-assets.md)
- **Update · remove · key auth · full endpoint reference** —
[references/api.md](references/api.md)
85 changes: 85 additions & 0 deletions skills/tot/references/api.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
# tot.page API reference

Base URL `https://api.tot.page`; pages are served at `https://tot.page/{slug}`.
Auth is optional: send `Authorization: Bearer wsk_live_…` to publish as an owned
account. Without it pages are open — the link is the key (see SKILL.md).

Body limit 1.5 MB (UTF-8) → `422`. Errors are JSON:
`{"error":{"code":"…","message":"…"}}`.

## POST /v1/documents — publish (markdown, or HTML with no local refs)

Request: `{"kind": "markdown" | "html", "body": "<string>", "title"?: "<string>"}`

Response `201` — note the nesting; the document lives under `.document`:

```json
{
"document": {
"id": "doc_…",
"workspace_id": "ws_…",
"doc_path": "index.md",
"kind": "markdown",
"title": null,
"version": null,
"share_url": "https://tot.page/{slug}",
"file_url": null,
"created_at": "…",
"updated_at": "…"
},
"workspace": { "id": "ws_…", "slug": "…", "share_url": "…", "visibility": "open" }
}
```

`version`/`file_url` are `null` until the first checkpoint lands (~2–10s); poll
the read endpoint below until `version` is non-null.

## GET /v1/workspaces/{wsId}/documents/{docId} — read

Returns the document object **bare** (no `.document` wrapper):

`id, workspace_id, doc_path, kind, title, version, share_url, file_url, body,
created_at, updated_at`

- `Accept: application/json` (default) → the object above.
- `Accept: text/markdown` → the raw body.

This is also the checkpoint poll.

## PUT /v1/workspaces/{wsId}/documents/{docId} — update, same living link

**Raw body, not JSON.**

```bash
curl -sS -X PUT "https://api.tot.page/v1/workspaces/$WS/documents/$DOC" \
-H 'content-type: text/markdown' --data-binary @notes.md
```

- Content-Type: `text/markdown` or `text/html`.
- Optional `If-Match: <version>` → `412` when the page changed under you.
- Returns the bare document with the new `version` and `file_url`. The living
`share_url` reflects the new version within ~60s.
- HTML with local refs: upload new/changed assets first — see
[html-assets.md](html-assets.md).

## DELETE /v1/workspaces/{wsId}/documents/{docId} — remove

`204`, hard delete, no undo. The living link 404s immediately; frozen `@hash`
snapshots stay up for the workspace's lifetime. Open pages: anyone with the
link can delete.

## Workspace endpoints — for HTML with assets

- `POST /v1/workspaces` → `201` with the workspace **nested**:
`{"workspace": {"id": "ws_…", "slug": "…", "share_url": "…", "visibility": "open"}}`
- `PUT /v1/workspaces/{wsId}/assets/{assetPath}` — raw asset bytes; 200.
- `POST /v1/workspaces/{wsId}/documents` — request
`{"doc_path": "index.html", "kind": "html", "body": "<string>"}`; `201` with the
bare document object.

Full asset flow and scanning rules: [html-assets.md](html-assets.md).

## GET /v1/me — identify a key

`Authorization: Bearer wsk_live_…` → `{ "user_id": "…", "email": "…",
"active_org_id": "…" }`. Use to verify a key before relying on it.
59 changes: 59 additions & 0 deletions skills/tot/references/html-assets.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
# HTML with local assets

For HTML that directly references local files. Bare HTML (only external URLs or
inline content) takes the simple flow in SKILL.md instead.

## Scan the HTML first

Direct refs that count as assets:

- `<img src>`, `<img srcset>` entries, `<video src>`, `<video poster>`,
`<audio src>`, `<source src>` / `<source srcset>`
- `<link href>` for `stylesheet`, `icon`, `preload`, `modulepreload`
- `<script src>`

Leave alone: external URLs (`http://`, `https://`, `//…`), `data:` URIs,
anchors (`#…`), and ordinary `<a href>` navigation links.

Paths must be relative to the HTML file. Root-relative refs (`/img/x.png`) and
`<base href>` are unsupported — rewrite them to file-relative paths before
publishing.

Asset content types: `image/png`, `image/jpeg`, `image/gif`, `image/webp`,
`image/svg+xml`, `text/css`, `application/javascript`, `video/mp4`.

## Publish flow

Assets first, document last — the page goes live referencing the assets, so a
missing one would ship broken. If any asset upload fails, stop before committing
the HTML: V1 never garbage-collects orphaned assets.

```bash
WS=$(curl -sS -X POST https://api.tot.page/v1/workspaces | jq -r .workspace.id)

# one PUT per referenced file — raw bytes, correct content-type,
# URL-encode each path segment
curl -sS -X PUT "https://api.tot.page/v1/workspaces/$WS/assets/logo.svg" \
-H 'content-type: image/svg+xml' --data-binary @logo.svg -o /dev/null

# document last
HTML=$(jq -Rs '{doc_path: "index.html", kind: "html", body: .}' index.html)
RESP=$(curl -sS -X POST "https://api.tot.page/v1/workspaces/$WS/documents" \
-H 'content-type: application/json' -d "$HTML")
DOC=$(jq -r .id <<<"$RESP")
```

Assets are served from the living link: `https://tot.page/{slug}/{assetPath}`.

Then pass the checkpoint gate (poll GET until `version` is non-null) and report
`share_url` + `file_url` exactly as in SKILL.md. Done when the page URL and every
asset URL answer `200`.

## Update with assets

Same workspace, same document:

1. Re-scan the new HTML.
2. `PUT` every referenced asset whose bytes changed (same `assets/{assetPath}`
paths — they replace in place).
3. `PUT` the document body last (`content-type: text/html`; raw body, not JSON).