rewritable 0.3.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +261 -5
- package/bin/rwa.mjs +1000 -9
- package/package.json +2 -2
- package/seeds/rewritable.html +4065 -207
- package/src/agent-loop.mjs +155 -0
- package/src/apply-edits.mjs +664 -0
- package/src/atomic-write.mjs +38 -0
- package/src/backend.mjs +43 -0
- package/src/clone-extract.mjs +249 -0
- package/src/clone.mjs +161 -0
- package/src/commands.mjs +90 -10
- package/src/create.mjs +256 -0
- package/src/doc.mjs +69 -0
- package/src/dsl-compiler.mjs +357 -0
- package/src/edit.mjs +300 -0
- package/src/fetch-page.mjs +346 -0
- package/src/host.mjs +126 -0
- package/src/identity.mjs +257 -0
- package/src/import-claude.mjs +28 -4
- package/src/import-vision.mjs +1 -1
- package/src/import.mjs +76 -10
- package/src/ls.mjs +105 -0
- package/src/publish-site.mjs +85 -0
- package/src/publish.mjs +98 -0
- package/src/seed-extract.mjs +40 -0
- package/src/seed.mjs +1387 -5
- package/src/self-contained.mjs +115 -0
- package/src/skill-manifest.mjs +227 -0
- package/src/skin.mjs +350 -0
- package/src/skins.mjs +274 -0
- package/src/template.mjs +109 -0
package/README.md
CHANGED
|
@@ -21,37 +21,293 @@ rwa new my-notes.html # → ./my-notes.html
|
|
|
21
21
|
|
|
22
22
|
rwa import notes.md # → ./notes.html
|
|
23
23
|
rwa import page.html out.html
|
|
24
|
+
|
|
25
|
+
rwa clone https://example.com/post # → ./post.html (fetches; SSRF-guarded)
|
|
26
|
+
rwa clone https://example.com/post --localize-images # also inline remote images as data: URIs (self-contained)
|
|
27
|
+
|
|
28
|
+
rwa edit notes.html "Add a section on testing" # instruction → agent loop
|
|
29
|
+
echo '{"version":"rwa-edit/1","edits":[...]}' | rwa edit notes.html
|
|
30
|
+
rwa edit notes.html --plan plan.json # envelope from a file
|
|
31
|
+
|
|
32
|
+
rwa doc notes.html # print the editable body
|
|
33
|
+
rwa doc notes.html --json # read + edit-contract, one call
|
|
34
|
+
|
|
35
|
+
rwa publish notes.html # → a hosted 24h share URL
|
|
36
|
+
rwa host notes.html --url https://host.example # → {id, token, url} (round-trip editing)
|
|
37
|
+
|
|
38
|
+
rwa skin notes.html notion-clean # apply a named style preset (offline)
|
|
39
|
+
rwa skin notes.html stripe-docs --l1 # + content-aware restyle (needs a backend)
|
|
40
|
+
rwa skin notes.html reset # remove the skin
|
|
24
41
|
```
|
|
25
42
|
|
|
26
43
|
### `rwa new`
|
|
27
44
|
|
|
28
|
-
Writes a fresh rwa container with a unique per-file `DOC_UUID`, a filename-derived `<title>`, and the seed's "
|
|
45
|
+
Writes a fresh rwa container with a unique per-file `DOC_UUID`, a filename-derived `<title>`, and the seed's "Untitled" starter content. Press `⌘K` in the browser to make it become anything.
|
|
46
|
+
|
|
47
|
+
Pass `--kind <name>` to scaffold a different primary stance at first paint:
|
|
48
|
+
|
|
49
|
+
- `--kind document` (default) — prose container; lens placeholder *"Write, or describe what you want."*
|
|
50
|
+
- `--kind workflow` — three-stage scaffold (Inbox / In progress / Done); lens placeholder *"Add an item, or describe a stage move."*
|
|
51
|
+
- `--kind presentation` — prose slide deck (split on `h1`/`h2`); the *Present* toggle renders it as slides at view time without changing the stored text (spec §5.10); lens placeholder *"Add a slide, or describe a change."*
|
|
52
|
+
- `--kind skill-host` — hosts permission-gated skills installed from `.rwa-skill.json` files; ships an empty runtime-owned frozen `#rwa-skills` zone the runtime (never the agent) rewrites on install/uninstall; installed skills are reported via `rwa doc`/`ls` as `tool`/`compute` affordances (`provenance:'installed'`). See `docs/specs/re-write-able-actions-spec-v0.8.md` §2.
|
|
53
|
+
|
|
54
|
+
The product-kind taxonomy is documented at `docs/specs/rwa-product-types.md` in the main repo. The substrate runtime is unchanged across kinds — only the `INLINE_DOC` body and lens placeholder vary at emit time.
|
|
55
|
+
|
|
56
|
+
**Your own templates.** Label any rwa file as a reusable template by adding `data-rwa-template="<name>"` to its root element (the body's first child, typically `<article>`). Then `rwa new <name>` scans the current folder, finds the labeled file, and clones it — pristine seed + the template's content, a fresh `DOC_UUID`, and the label stripped (the clone is an instance, not the template):
|
|
57
|
+
|
|
58
|
+
```sh
|
|
59
|
+
# label invoice.html: <article data-rwa-template="invoice"> … </article>
|
|
60
|
+
rwa new invoice # → ./invoice-2026-05-30.html (cloned from invoice.html)
|
|
61
|
+
rwa new invoice april.html # → ./april.html
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
No registry, no shipped starters: the file you made yesterday is the template for the file you make tomorrow. A bare-word first argument is a template name; a `.html`/path argument is the output path (so `rwa new notes.html` still writes a blank doc). If no file in the folder carries the label, it exits `2` with a hint. Multiple matches: most-recent wins (printed). The clone always pulls the *latest* bootstrap from the seed, so an old template doesn't lock you into an old runtime. CLI-only for v1 — cross-folder discovery (`~/.rwa/templates/`, `--from`) is deferred.
|
|
29
65
|
|
|
30
66
|
### `rwa import <input> [path]`
|
|
31
67
|
|
|
32
68
|
Embeds the input file's content as the document's initial state. Supported formats:
|
|
33
69
|
|
|
34
|
-
- `.md`, `.markdown` — converted via [`marked`](https://marked.js.org/) (GFM enabled)
|
|
70
|
+
- `.md`, `.markdown` — converted via [`marked`](https://marked.js.org/) (GFM enabled). Inline HTML in markdown is **sanitized**: `<script>`/`<iframe>`/`<object>`/`<embed>`/`<svg>`/`<math>`/`<link>`/`<meta>`/`<base>` elements are dropped, `on*=` event-handler attributes are stripped, and any `href`/`src` outside the safe scheme allow-list (`http`, `https`, `mailto`, `tel`, plus `data:image/*` for `src`) is neutralised to `#`. Removals are reported as warnings on stderr.
|
|
35
71
|
- `.html`, `.htm` — `<!DOCTYPE>`/`<html>`/`<head>`/`<body>` shells stripped, `<style>` tags retained from `<head>`, body content kept as-is. **`<script>` tags are preserved** (rwa documents support inline JS); a stderr warning is printed when scripts are detected.
|
|
36
72
|
- `.csv` — parsed via [`papaparse`](https://www.papaparse.com/) (RFC 4180; handles quoted commas, embedded newlines, escaped quotes, BOM). First row becomes `<thead>`, remaining rows `<tbody>`; every cell is HTML-escaped. Parse warnings print to stderr but don't abort the import.
|
|
37
73
|
- `.txt` — paragraph-split on blank lines, HTML chars escaped
|
|
38
74
|
|
|
39
75
|
Output defaults to `<input-basename>.html` in the input's directory. Conversion is deterministic and offline — no API key, no network.
|
|
40
76
|
|
|
77
|
+
### `rwa clone <url> [path]`
|
|
78
|
+
|
|
79
|
+
Clone a public webpage into a self-contained rewritable: fetch the page, extract its main article and title, and bake the content into a fresh container. First-class for **WordPress / ikangai posts** — a blog post becomes an editable, shareable single-file `.html` you can rewrite with `⌘K`.
|
|
80
|
+
|
|
81
|
+
```sh
|
|
82
|
+
rwa clone https://www.ikangai.com/some-post/ # → ./some-post.html
|
|
83
|
+
rwa clone https://www.ikangai.com/some-post/ out.html
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Unlike `rwa import`, which is **offline**, `rwa clone` **requires the network** (it fetches the URL). The fetch is **SSRF-guarded**: only `http`/`https` schemes, private/loopback/link-local/metadata addresses are blocked (including via DNS rebinding and per-hop redirect re-validation), responses are capped in size and must be HTML. A blocked or failed fetch exits `2` (`file_error`, e.g. `blocked_host`, `bad_scheme`, `not_html`, `http_error`) and writes no file. The destination is checked first — an existing file exits `2` (`exists`) unless you pass `--force`.
|
|
87
|
+
|
|
88
|
+
Cloning is **content-only** in v1: the extracted article text/markup plus the page title (prepended as an `<h1>`) and a provenance footer linking back to the source. The source page's styles are **not** cloned — the new rewritable renders with the seed's baseline typography (re-style it later with `rwa skin` or `⌘K`).
|
|
89
|
+
|
|
90
|
+
### `rwa create <task...>` (alias `rwa draft`)
|
|
91
|
+
|
|
92
|
+
Scaffold **and** agent-fill a new rewritable in one shot, from a natural-language task. The CLI bootstraps the container, hands the brief to the model, and bakes the generated content into the file — which is then an ordinary, self-contained rewritable (edit it in-browser with `⌘K`, or re-run `rwa create` for a fresh one). Unlike `new`/`import`, this verb calls the model, so it is **not** offline; but its **output** is always self-contained.
|
|
93
|
+
|
|
94
|
+
```sh
|
|
95
|
+
rwa create a presentation about the rewritable architecture
|
|
96
|
+
rwa create an interactive document that visualizes token usage --data tokens.json
|
|
97
|
+
rwa draft presentation --from ./q2-deck.html --data q3.csv --out q3-deck.html
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
The leading word resolves a **frame** by the same template-first precedence as `rwa new` (a cwd `data-rwa-template` match, else a built-in kind); the rest is the brief. Flags: `--kind <name>` forces the kind (and disables leading-word detection); `--from <file>` bases the artifact on an existing rewritable's body; `--data <file>` (or `-` for stdin) bakes a dataset inline; `--out <path>` sets the output (default `./<kind>-YYYY-MM-DD.html`); `--force`/`--open`; and the backend flags (`--backend`/`--model`/`--base-url`/`--api-key`) as in `rwa edit`.
|
|
101
|
+
|
|
102
|
+
Created output is held to a **stricter, code-enforced self-containment bar** than `new`/`import`: no runtime CDN/remote references (`<script src>`, `<link href>`, `@import`, `url()`, `srcset`, …) — visualizations are hand-rolled SVG/Canvas, data is embedded. A violation fails loud (exit 4, `not_self_contained`) and writes no file. The write is atomic: a failed run (agent, envelope, or self-containment) leaves nothing at `--out`. Exit codes match `rwa edit`: 0 ok · 1 usage · 2 file · 3 envelope · 4 agent. The API key is used only for the model call and never written into the artifact.
|
|
103
|
+
|
|
104
|
+
### `rwa edit <path> [instruction]`
|
|
105
|
+
|
|
106
|
+
Programmatic edit entry point. Applies an `rwa-edit/1` tool envelope (`apply_edits`, `apply_dsl_plan`, or `replace_document`) to an existing rwa container in place. Three invocation forms:
|
|
107
|
+
|
|
108
|
+
```sh
|
|
109
|
+
# 1. Instruction path — run the agent loop, apply the resulting envelope.
|
|
110
|
+
rwa edit notes.html "Add a section on testing"
|
|
111
|
+
|
|
112
|
+
# 2. Piped envelope — read a tool envelope as JSON from stdin.
|
|
113
|
+
echo '{"version":"rwa-edit/1","edits":[{"find":"old","replace":"new"}]}' \
|
|
114
|
+
| rwa edit notes.html
|
|
115
|
+
|
|
116
|
+
# 3. --plan <file> — read the envelope from a file. Use `--plan -` to force stdin.
|
|
117
|
+
rwa edit notes.html --plan plan.json
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
All three paths funnel through the same `applyPlan` splice/write code path: extract `INLINE_DOC`, apply the edit (with frozen-zone + reserved-marker + structural-shape checks), and atomic-rename the file in place.
|
|
121
|
+
|
|
122
|
+
The agent loop retries up to 3 times when the model emits plain text instead of a tool call (`no_tool_call`) or when the tool arguments aren't valid JSON (`invalid_json`). Apply-time failures (`frozen_zone_violation`, `find_not_found`, `find_not_unique`, `structural_shape_changed`, `reserved_substring`, `dsl_compile_error`) surface immediately as `envelope_error` (exit 3) without retrying through the model. This differs from the browser runtime, which feeds apply failures back as `tool_result` for the model to recover from — bringing that behavior to the CLI is tracked as a v2 follow-up in `cli/TODO.md`. After 3 exhausted retries the failure surfaces as `agent_error/no_envelope_after_retries` (exit 4).
|
|
123
|
+
|
|
124
|
+
#### Backend flags (instruction path only)
|
|
125
|
+
|
|
126
|
+
| Flag | Effect |
|
|
127
|
+
|---|---|
|
|
128
|
+
| `--backend <name>` | `openrouter` (default), `ollama`, `lmstudio`. Falls back to `$RWA_BACKEND`. `bridge` is browser-only by design. |
|
|
129
|
+
| `--model <id>` | model id passed to the backend. Falls back to `$RWA_MODEL`, then `google/gemini-3.5-flash`. |
|
|
130
|
+
| `--base-url <url>` | OpenAI-compatible base URL override. Defaults: `https://openrouter.ai/api/v1`, `http://localhost:11434/v1` (or `$RWA_OLLAMA_URL`), `http://localhost:1234/v1` (or `$RWA_LMSTUDIO_URL`). |
|
|
131
|
+
| `--api-key <key>` | openrouter only; falls back to `$RWA_OPENROUTER_KEY`. ollama / lmstudio run locally without auth. |
|
|
132
|
+
|
|
133
|
+
#### Other edit flags
|
|
134
|
+
|
|
135
|
+
| Flag | Effect |
|
|
136
|
+
|---|---|
|
|
137
|
+
| `--plan <file>` | read the tool envelope from a file (or `--plan -` for explicit stdin). |
|
|
138
|
+
| `--json` | emit one JSON object per line on stderr for structured failure / retry reporting. Each line is `{code, subcode, details}` (or `{phase:"retry", attempt, reason}` during agent retries). |
|
|
139
|
+
|
|
140
|
+
### `rwa doc <path>`
|
|
141
|
+
|
|
142
|
+
The **read** counterpart to `rwa edit`. `rwa edit` writes the editable body; `rwa doc` reads it. An agent handed a rewritable `.html` shouldn't have to parse the ~4000-line bootstrap to find the document it's allowed to touch — `rwa doc` prints exactly the LF-canonical text the edit contract operates on, so anchors computed against it round-trip through `rwa edit`.
|
|
143
|
+
|
|
144
|
+
```sh
|
|
145
|
+
# Plain mode — the editable body, pipe/terminal friendly (one trailing newline).
|
|
146
|
+
rwa doc notes.html
|
|
147
|
+
rwa doc notes.html | grep -n '<h2'
|
|
148
|
+
|
|
149
|
+
# --json — the full editing contract + self-description in a single call.
|
|
150
|
+
rwa doc notes.html --json
|
|
151
|
+
# → {"rwa":"self-description/1","source":"static","uuid":"…","kind":"document",
|
|
152
|
+
# "title":"Status report","blocks":3,"affordances":[],"frozenZones":["sig"],
|
|
153
|
+
# "baseline":{"edit":["lens"],"tools":[…],"export":["html","print"],"history":["undo"]},
|
|
154
|
+
# "rewritable":true,"length":465,"doc":"…"}
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
`--json` gives an agent everything it needs to edit safely in one read: `doc` (the byte-exact body), `frozenZones` (author-declared invariants it must preserve, or `apply_edits` rejects the change with `frozen_zone_violation`), `kind` (which framing applies), and `uuid` (to correlate). `rewritable:true` is an explicit parsed-field marker.
|
|
158
|
+
|
|
159
|
+
The payload is also a `self-description/1` object — the answer to *"what is this, and what can be done with it?"* ([`docs/specs/rwa-self-description-spec.md`](../docs/specs/rwa-self-description-spec.md)): `affordances` (the type's registered provider kinds — `[]` for a base document, `["view"]` for a presentation), `title`, `blocks` (addressable-block count), and `baseline` (the substrate-universal ops every container has — lens-edit, the three edit tools, html/print export, undo). `source:"static"` marks this as computed from the file bytes (no JS executed); the in-browser `runtime.describe()` emits the same shape live. The CLI projection is pinned to the reference oracle (`tools/self-description.mjs`) by test, so it cannot drift from the contract.
|
|
160
|
+
|
|
161
|
+
A custom-affordance file (e.g. a datatable) whose real affordances the kind-template can only *guess* at may carry its own answer: an inert `<script id="rwa-affordances">` block declaring its affordances. When that declaration is **trustworthy** — *edit-unreachable*, i.e. outside the editable body or carrying `data-rwa-frozen` (which `rwa edit` now enforces) so it can't be silently drifted — `rwa doc` prefers it and reports `source:"declared"` with the file's *real* affordances. `uuid`/`frozenZones` are always filled from the bytes (container facts the author can't fake); an edit-reachable or malformed declaration is ignored and the answer falls back to `source:"static"`. So `rwa ls`/`rwa doc` tell the truth about a multi-affordance file the moment it declares itself honestly.
|
|
162
|
+
|
|
163
|
+
`rwa doc` never reads stdin and never writes the file. On a non-rewritable target it exits `2` with `not_a_rewritable` and an empty stdout — a clean "is this a rewritable?" probe. Errors always go to stderr (plain `rwa doc: file_error/not_found {…}`, or `--json` `{code, subcode, details}`), so stdout stays clean for piping.
|
|
164
|
+
|
|
165
|
+
### `rwa ls [paths...]`
|
|
166
|
+
|
|
167
|
+
Where `rwa doc` answers *"what is this file?"*, `rwa ls` answers *"what are all these?"* — the inventory of a folder of rewritables, one line each. Hand it a directory (or a list of files; default is `./`) and it prints each rewritable's identity; non-rewritables and bad paths are counted, never hidden.
|
|
168
|
+
|
|
169
|
+
```sh
|
|
170
|
+
rwa ls # the rewritables in the current directory
|
|
171
|
+
rwa ls demo/ # …in a folder
|
|
172
|
+
rwa ls a.html b.html # …an explicit list
|
|
173
|
+
# KIND TITLE AFFORDANCES FILE
|
|
174
|
+
# document Invoice tracker — demo/invoice-tracker.html
|
|
175
|
+
# presentation Q1 Architecture view demo/q1.html
|
|
176
|
+
# datatable Sales 2026 view,edit-surface,compute demo/sales.html
|
|
177
|
+
#
|
|
178
|
+
# 3 rewritables
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
`--json` emits an array of rows for an agent — `{file, status, self}` where `status` is `rewritable` (with the full `self-description/1` object), `not_a_rewritable`, or `error` (with a `reason`). The scan is lenient like its namesake: one bad path among many is a row, not a fatal exit, so a completed scan exits `0`. This is how an agent handed a project learns its whole rewritable inventory — and every container's affordances — in a single call.
|
|
182
|
+
|
|
183
|
+
### `rwa publish <path>`
|
|
184
|
+
|
|
185
|
+
Publish a local rewritable to the hosted share service and get back a URL. A rewritable is already shareable as a file — it's a self-contained `.html` you can email or host anywhere — but `rwa publish` is the one-command path to an *anonymous, hosted* snapshot: create with `rwa new`, edit locally, publish.
|
|
186
|
+
|
|
187
|
+
```
|
|
188
|
+
rwa publish notes.html
|
|
189
|
+
# ✓ Published!
|
|
190
|
+
# URL: https://ab12cd34.rewritable.ikangai.com/
|
|
191
|
+
# Expires: in 24 hours (anonymous share)
|
|
192
|
+
# Note: the hosted copy gets a fresh DOC_UUID (distinct container)
|
|
193
|
+
|
|
194
|
+
rwa publish notes.html --json # {"short":"…","url":"…","expiresAt":…} on stdout
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
It POSTs **your edited bytes** (the current `INLINE_DOC`), unlike the browser `/new` and `/import` pages, which publish a fresh or freshly-converted container. The hosted snapshot gets its own fresh `DOC_UUID` (a distinct container at its own origin) and is **anonymous, ephemeral (24h), and rate-limited** — it's a share link, not durable storage. The file on your disk remains the durable artifact.
|
|
198
|
+
|
|
199
|
+
**Target** resolves `--url <base>` › `$RWA_PUBLISH_URL` › `https://rewritable.ikangai.com` (point it at a self-hosted service or local dev with either). The file is checked locally first — a non-rewritable exits `2` (`not_a_rewritable`) **before any network call**. Remote/network failures exit `4` with an honest reason on stderr (`publish_error/network_error`, `/rate_limited`, `/body_too_large`, `/validation_failed`, …); `--json` emits those as `{code, subcode, details}`. stdout stays clean for the URL/JSON.
|
|
200
|
+
|
|
201
|
+
### `rwa publish-site <path>`
|
|
202
|
+
|
|
203
|
+
The **durable** counterpart to `rwa publish`. Where `rwa publish` POSTs to the hosted service for an *anonymous, ephemeral (24h)* share, `rwa publish-site` copies the file **verbatim** onto a static site you control via `scp` and prints the live URL. Same bytes, your own host, no expiry.
|
|
204
|
+
|
|
205
|
+
```
|
|
206
|
+
RWA_SITE_HOST=user@host RWA_SITE_PATH=/var/www/r RWA_SITE_URL=https://example.com/r \
|
|
207
|
+
rwa publish-site my-doc.html
|
|
208
|
+
# → ✓ Published to https://example.com/r/my-doc.html
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
**Config** is flags-over-env — three vars, each overridable by a flag:
|
|
212
|
+
|
|
213
|
+
| Var | Flag | Meaning |
|
|
214
|
+
|---|---|---|
|
|
215
|
+
| `RWA_SITE_HOST` | `--host` | the scp target, e.g. `user@host` |
|
|
216
|
+
| `RWA_SITE_PATH` | `--path` | the remote directory the file lands in |
|
|
217
|
+
| `RWA_SITE_URL` | `--url` | the public base URL that directory is served at |
|
|
218
|
+
|
|
219
|
+
It needs the system `scp` binary and **ssh access already configured** on this machine (key/agent) — there is no auth flow inside `rwa`. The **filename is kept 1:1** (the basename of your local file), so the live URL is predictable and a re-publish **overwrites** the previous copy. The file is checked locally first — a non-rewritable exits `2` before any transport.
|
|
220
|
+
|
|
221
|
+
This command is **network-bearing** (like `rwa clone`), so the offline-first rule does not apply to it.
|
|
222
|
+
|
|
223
|
+
### `rwa host <path>`
|
|
224
|
+
|
|
225
|
+
Ingest a local rewritable into a **hosted runtime** and get back the keys to keep editing it there. Where `rwa publish` makes an *anonymous, read-only* snapshot, `rwa host` POSTs the file's bytes to a hosted runtime's `POST /r`, which mints an `id` and a per-rwa **capability token** and returns `{id, token, url}`. The `url` is `<base>/r/<id>#k=<token>` — the token rides the `#k=` fragment (so it never reaches the server on a navigation), which is how you keep editing the hosted copy. It is the round-trip-editing foundation, the network-bearing counterpart of `publish`.
|
|
226
|
+
|
|
227
|
+
```
|
|
228
|
+
rwa host notes.html --url https://host.example
|
|
229
|
+
# ✓ Hosted!
|
|
230
|
+
# id: abc12345
|
|
231
|
+
# token: cap-tok-…
|
|
232
|
+
# url: https://host.example/r/abc12345#k=cap-tok-…
|
|
233
|
+
# Note: the url carries your capability token in its #k= fragment — keep it to keep editing.
|
|
234
|
+
|
|
235
|
+
rwa host notes.html --url https://host.example --json # {"id":"…","token":"…","url":"…"} on stdout
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
**Target** resolves `--url <base>` › `$RWA_HOST_URL` (no baked-in default — a hosted runtime is your own service). The file is checked locally first — a non-rewritable exits `2` (`not_a_rewritable`) **before any network call**, and a missing target exits `1` (`config_error`). Transport/HTTP failures exit `4` with an honest reason on stderr (`host_error/network_error`, `/server_error`, `/body_too_large`); `--json` emits those as `{code, subcode, details}`. stdout stays clean for the result. **Only the file bytes are sent** — a rewritable carries no secret (the API key lives in sessionStorage, never in the file).
|
|
239
|
+
|
|
240
|
+
This command is **network-bearing** (like `rwa clone` / `rwa publish-site`), so the offline-first rule does not apply to it.
|
|
241
|
+
|
|
242
|
+
### `rwa skin <path> <name>`
|
|
243
|
+
|
|
244
|
+
Pick a **named look** for a rewritable instead of hand-styling it from the blank lens. A skin is one self-contained `<style data-rwa-skin="NAME">` block — system fonts only, no web fonts or remote assets — that the command splices into the **document body**. So it commits with the document, ships inside the exported `.html`, survives sharing, and one in-browser undo (`⌘Z`) reverts it. Five presets ship today: `notion-clean`, `linear-dark`, `editorial-serif`, `stripe-docs`, `terminal-mono` (clean · dark · editorial · docs · terminal).
|
|
245
|
+
|
|
246
|
+
```
|
|
247
|
+
rwa skin notes.html notion-clean # apply (an unknown name lists every preset)
|
|
248
|
+
rwa skin notes.html editorial-serif # re-skin — replaces the current skin, never stacks
|
|
249
|
+
rwa skin notes.html reset # remove the skin (and any --l1 sk-* wrappers)
|
|
250
|
+
rwa skin notes.html linear-dark --json # {"exitCode":0,"mode":"insert","skin":"linear-dark"}
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
This is **deterministic and offline** — no model, no key. The block is scoped to `#rwa-doc-mount`, so it overrides the seed's baseline typography while leaving the runtime chrome's palette untouched (a dark skin re-tints the document, not the lens). Applying the first skin inserts the block (a `replace_document`); re-skinning swaps it in place (an `apply_edits`); `reset` removes it (plus any `sk-*` wrappers a prior `--l1` restyle left) — each one commit. Routed through the same write path as `rwa edit`, so frozen zones and `data-rwa-id`s are preserved and a non-rewritable target exits `2` (`not_a_rewritable`). `--theme-only` is the explicit name for this deterministic swap.
|
|
254
|
+
|
|
255
|
+
#### `--l1` — content-aware restyle (opt-in, model-driven)
|
|
256
|
+
|
|
257
|
+
The theme block tints the document, but some looks only land once the markup carries hook elements (an eyebrow line, a stat row, a hero). `--l1` opts into the **always-on content-aware restyle** the browser runtime ships in its ✦ gallery: the CLI de-skins the doc, drives the model with the preset's recipe to add **additive** `sk-*` class hooks and wrapper `<div>`/`<span>`s (no content is deleted, moved, or re-tagged; `data-rwa-id`s and frozen zones are untouched), then splices the theme block onto the model's output and commits **once** — theme + wrappers land together (one undo in the browser).
|
|
258
|
+
|
|
259
|
+
```
|
|
260
|
+
rwa skin notes.html stripe-docs --l1 # uses $RWA_BACKEND / openrouter
|
|
261
|
+
rwa skin notes.html linear-dark --l1 --backend ollama # local model, no key
|
|
262
|
+
rwa skin notes.html notion-clean --l1 --json # {"exitCode":0,"mode":"l1","skin":"notion-clean","degraded":false}
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
Unlike the rest of `rwa skin`, `--l1` needs a backend — it reuses the **same `--backend` / `--model` / `--base-url` / `--api-key` flags (and env chain)** as `rwa edit`'s instruction path. A re-skin first **deterministically** strips the previous skin's `sk-*` wrappers (so they never accumulate, regardless of what the model does). If the model declines or produces nothing usable, the skin still lands **theme-only** (one write) and a note is printed — `--json` reports `"degraded":true`. A **missing or unreachable backend fails loud** (`exit 4`), the same as `rwa edit` — `--l1` never silently downgrades just because the model couldn't be reached. Without `--l1`, `rwa skin` is byte-for-byte the deterministic, offline theme swap above. (`docs/plans/2026-06-03-skinning-design.md`.)
|
|
266
|
+
|
|
267
|
+
### Driving a rewritable from an agent — no embedded LLM, no API key
|
|
268
|
+
|
|
269
|
+
`rwa doc` + `rwa edit --plan` close a fully **deterministic** edit loop. An agent that can already reason (Claude Code, a script, a CI job) doesn't need the in-file `⌘K` model or an OpenRouter key: it reads the body, computes its own `apply_edits` envelope against anchors it can see, and applies it. Read → decide → write → confirm, all offline:
|
|
270
|
+
|
|
271
|
+
```sh
|
|
272
|
+
# 1. READ — get the exact body the edit contract sees (and what it must preserve).
|
|
273
|
+
rwa doc report.html --json > /tmp/state.json
|
|
274
|
+
# state.json: { "doc": "<article><h1>Untitled</h1>…", "frozenZones": [...], ... }
|
|
275
|
+
|
|
276
|
+
# 2. DECIDE — the agent picks a unique anchor from state.json.doc and forms an
|
|
277
|
+
# rwa-edit/1 envelope. (Each `find` must appear exactly once; avoid frozenZones.)
|
|
278
|
+
echo '{"version":"rwa-edit/1","edits":[{"find":"Untitled","replace":"Q2 Revenue Review"}]}' \
|
|
279
|
+
> /tmp/plan.json
|
|
280
|
+
|
|
281
|
+
# 3. WRITE — apply deterministically, in place, atomically. No model in the loop.
|
|
282
|
+
rwa edit report.html --plan /tmp/plan.json
|
|
283
|
+
|
|
284
|
+
# 4. CONFIRM — read back; the anchor round-trips, the bootstrap/uuid are untouched.
|
|
285
|
+
rwa doc report.html | grep '<h1>'
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
Because the anchors in step 1 are the *same* text step 3 splices against, what the agent reads is exactly what it can edit — no HTML-parsing guesswork, no drift. The browser runtime's agent loop (multi-turn tool-use against a model) and this CLI loop apply through the identical `apply_edits` core, so an envelope that works here behaves identically in the file's own `⌘K`.
|
|
289
|
+
|
|
41
290
|
### Flags
|
|
42
291
|
|
|
43
292
|
| Flag | Effect |
|
|
44
293
|
|---|---|
|
|
45
294
|
| `--force`, `-f` | overwrite the destination if it exists |
|
|
46
295
|
| `--open`, `-o` | open the resulting file in the default app |
|
|
296
|
+
| `--kind <name>` | (`rwa new` only) starter kind: `document` (default), `workflow`, `presentation`, `skill-host` |
|
|
47
297
|
| `--version` | print version |
|
|
48
298
|
| `--help`, `-h` | usage |
|
|
49
299
|
|
|
50
300
|
### Exit codes
|
|
51
301
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
302
|
+
| Code | Name | Meaning |
|
|
303
|
+
|---|---|---|
|
|
304
|
+
| `0` | success | edit applied / file written |
|
|
305
|
+
| `1` | usage_error | bad arguments, missing input, unknown backend, conflicting input sources |
|
|
306
|
+
| `2` | file_error | target not found, read/write failure, not a rewritable container |
|
|
307
|
+
| `3` | envelope_error | malformed JSON, ambiguous/unknown shape, version mismatch, missing required fields, apply-time failures (`frozen_zone_violation`, `find_not_found`, `find_not_unique`, `structural_shape_changed`, `reserved_substring`, `dsl_compile_error`) |
|
|
308
|
+
| `4` | agent_error | agent loop exhausted retries (`no_envelope_after_retries`), backend HTTP/network error (`backend_error`), or missing API key (`no_api_key`) |
|
|
309
|
+
|
|
310
|
+
Exit codes 1–4 are emitted by `rwa edit` and are stable. `rwa doc` reuses the same `file_error` (exit `2`) surface — `not_found`, `read_error`, `not_a_rewritable` — and exits `1`/`missing_file_arg` when no path is given. Other verbs (`new`, `import`) use `0`/`1`/`2` only — `2` for argument or format issues, `1` for everything else. The `--json` flag turns each `rwa edit` stderr line into a single-line JSON object; on `rwa doc` it switches stdout to the editing-contract object (failures still emit the `{code, subcode, details}` object on stderr).
|
|
55
311
|
|
|
56
312
|
## Design
|
|
57
313
|
|