rapier-html 1.1.1 → 1.1.2
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/BUILD.json +4 -4
- package/README.md +36 -6
- package/package.json +2 -1
- package/page.mjs +27 -5
- package/rapier.html +18 -18
- package/return-address.mjs +17 -0
package/BUILD.json
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"package": "rapier-html",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.2",
|
|
4
4
|
"path": "rapier.html",
|
|
5
|
-
"bytes":
|
|
6
|
-
"sha256": "
|
|
7
|
-
"builtAt": "2026-09-
|
|
5
|
+
"bytes": 1814968,
|
|
6
|
+
"sha256": "14f9723eb1d16fdeb39830aed5cbb0d4a71d304c7f859cbcc0662fbd7fafff3a",
|
|
7
|
+
"builtAt": "2026-09-27T17:48:31.488Z",
|
|
8
8
|
"node": "v22.22.2",
|
|
9
9
|
"canonical": true,
|
|
10
10
|
"packing": "zopfli",
|
package/README.md
CHANGED
|
@@ -9,6 +9,7 @@ npx -- rapier-html notes.md --view draw # opens on Draw: sketch and
|
|
|
9
9
|
npx -- rapier-html notes.md --view notes # opens on Notes: the document as cards
|
|
10
10
|
npx -- rapier-html notes.md --drawing sketch.svg # carries a drawing, opened on Draw over the document
|
|
11
11
|
npx -- rapier-html proposal.md --base original.md # opens on the diff of a proposed change
|
|
12
|
+
npx -- rapier-html notes.md --return "$RETURN_URL" --return-expires-at "$RETURN_EXPIRES_AT" # Send back
|
|
12
13
|
npx -- rapier-html notes.md out.html # a named output
|
|
13
14
|
npm install rapier-html # as a library: wrap, unwrap
|
|
14
15
|
```
|
|
@@ -46,6 +47,32 @@ The first `--` is for npm, so `--help` reaches the command. It never overwrites
|
|
|
46
47
|
**For agents**
|
|
47
48
|
- Hand a person a document, a sketch or a change as one file that opens with one click.
|
|
48
49
|
- What they save comes back byte for byte through `unwrap`.
|
|
50
|
+
- Add the return URL from `document.create_return`: the person edits the page and presses Send back;
|
|
51
|
+
`document.wait_for_user` wakes the agent, which reads the exact returned Markdown and continues.
|
|
52
|
+
|
|
53
|
+
## Send an edited document back
|
|
54
|
+
|
|
55
|
+
Call `document.create_return` on the Rapier agent door for the document's workspace. Pass its
|
|
56
|
+
`return_url` and `return_expires_at` as `--return "$RETURN_URL" --return-expires-at "$RETURN_EXPIRES_AT"`
|
|
57
|
+
or `wrap(..., {return: return_url, return_expires_at})`. Both values come from the same mint. Only the worker's
|
|
58
|
+
HTTPS return route at `https://mcp.rapier.website/return/…` is accepted. The page shows Send back
|
|
59
|
+
in Share; a page without an address has no such action.
|
|
60
|
+
|
|
61
|
+
The person reads and edits offline, then presses Send back when ready. That press sends the current
|
|
62
|
+
source and filename once, and shows acceptance or the worker's refusal. After acceptance, that page
|
|
63
|
+
sends nothing again. Save to the device stays local. The return URL lasts up to one day, accepts one
|
|
64
|
+
document up to 25 MiB, and grants no read or edit access to the agent's workspace.
|
|
65
|
+
|
|
66
|
+
Once the carried expiry passes, Share offers Save in place of Send back: the work is safe on the page.
|
|
67
|
+
A used or expired return refused by the worker offers the same Save. Saving keeps the current words,
|
|
68
|
+
filename, byte-order mark and line endings through the normal local save path. Bring that file back to the
|
|
69
|
+
assistant, which compares it against the workspace's current work and creates a fresh return for the next
|
|
70
|
+
handoff; the old capability stays spent.
|
|
71
|
+
|
|
72
|
+
The agent's `document.wait_for_user` returns a `returned` receipt with the `return_id`, name and time.
|
|
73
|
+
`document.get_context` lists receipts even when no agent was waiting. Read the source with
|
|
74
|
+
`document.read_context({return_id, start: 0})`, continuing at the returned `end` until `complete`.
|
|
75
|
+
The returned copy stays separate from the workspace's current document, so newer work survives too.
|
|
49
76
|
|
|
50
77
|
## As a library
|
|
51
78
|
|
|
@@ -62,15 +89,16 @@ console.log('words back exactly:', unwrap(page).text === words);
|
|
|
62
89
|
await writeFile('hello.rapier.html', page, {flag: 'wx'}); // 'wx': never over an existing file
|
|
63
90
|
```
|
|
64
91
|
|
|
65
|
-
`wrap(rapierHtml, text, name, {view, drawing, drawingName, base, baseName})` and `unwrap(pageHtml)`,
|
|
66
|
-
which returns `{html, text, name, view, drawing, drawingName, base, baseName}` with `null` for what a
|
|
92
|
+
`wrap(rapierHtml, text, name, {view, drawing, drawingName, base, baseName, return: returnURL, return_expires_at})` and `unwrap(pageHtml)`,
|
|
93
|
+
which returns `{html, text, name, view, drawing, drawingName, base, baseName, return: returnURL, return_expires_at}` with `null` for what a
|
|
67
94
|
page does not carry. Also exported: `encodeCarried`, `decodeCarried`, `main`.
|
|
68
95
|
|
|
69
96
|
## How the page carries the document
|
|
70
97
|
|
|
71
98
|
One `<script type="text/markdown" id="rapier-document">` block at the top of the body, never executed,
|
|
72
99
|
read once at boot. A drawing rides in `id="rapier-drawing"`, the text a change was proposed against in
|
|
73
|
-
`id="rapier-base"`, the view to open on in the document block's `data-view
|
|
100
|
+
`id="rapier-base"`, the view to open on in the document block's `data-view`, the optional return URL
|
|
101
|
+
in `data-return` and its exact expiry in `data-return-expires-at`. The block holds no `<`
|
|
74
102
|
before `/` or `!`, no carriage return and no NUL: `\\`, `\/`, `\!`, `\r` and `\0` stand for them, one
|
|
75
103
|
reversible pass each way, so a note about HTML is as safe as any other. A byte-order mark is kept as a
|
|
76
104
|
fact of the file and written back on Save.
|
|
@@ -78,10 +106,12 @@ fact of the file and written back on Save.
|
|
|
78
106
|
## Safety
|
|
79
107
|
|
|
80
108
|
- It never overwrites: an output that exists, the document itself included, is refused. Unknown options,
|
|
81
|
-
a `--view` that is not `draw` or `notes`, a `--drawing` or `--base` with no file
|
|
82
|
-
refused too.
|
|
109
|
+
a `--view` that is not `draw` or `notes`, a `--drawing` or `--base` with no file, a missing or repeated
|
|
110
|
+
`--return` or `--return-expires-at`, a return without its expiry, an invalid expiry, an unsafe return URL and a third filename are refused too.
|
|
83
111
|
- Nothing reports on the page's use. It reaches the network only when the document or the person asks:
|
|
84
|
-
a picture or video the document links to,
|
|
112
|
+
a picture or video the document links to, the math, diagram and PDF-import helpers on first use,
|
|
113
|
+
or the person's Send back press. A return URL names only the worker's exact origin and return route;
|
|
114
|
+
credentials, query strings and fragments are refused by both the writer and carried-page boot.
|
|
85
115
|
|
|
86
116
|
## Licence
|
|
87
117
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "rapier-html",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.2",
|
|
4
4
|
"description": "Rapier, the offline Markdown editor, as one HTML page with your document inside it: write, draw and paint, notes, the diff of a change; no account, nothing fetched.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"markdown",
|
|
@@ -33,6 +33,7 @@
|
|
|
33
33
|
"files": [
|
|
34
34
|
"cli.mjs",
|
|
35
35
|
"page.mjs",
|
|
36
|
+
"return-address.mjs",
|
|
36
37
|
"rapier.html",
|
|
37
38
|
"README.md",
|
|
38
39
|
"LICENSE",
|
package/page.mjs
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import {readFile, writeFile} from 'node:fs/promises';
|
|
2
2
|
import {basename, dirname, join} from 'node:path';
|
|
3
|
+
import {returnAddress, returnExpiresAt} from './return-address.mjs';
|
|
3
4
|
|
|
4
5
|
const START = '<script type="text/markdown" id="rapier-document"';
|
|
5
6
|
const DRAWING_START = '<script type="text/plain" id="rapier-drawing"';
|
|
@@ -30,7 +31,11 @@ export function wrap(pageHtml, text, name, options) {
|
|
|
30
31
|
const docName = safeName(name, 'document.md');
|
|
31
32
|
// `view`: the view the page opens on with the document behind it, `draw` or `notes` (docs/page-door.md).
|
|
32
33
|
if (opts.view != null && !['draw', 'notes'].includes(opts.view)) throw new Error('the view is draw or notes');
|
|
33
|
-
const
|
|
34
|
+
const address = opts.return == null ? null : returnAddress(opts.return);
|
|
35
|
+
const expiry = opts.return_expires_at == null ? null : returnExpiresAt(opts.return_expires_at);
|
|
36
|
+
if (!!address !== !!expiry) throw new Error('a return needs its URL and return_expires_at from document.create_return');
|
|
37
|
+
const block = START + ' data-name="' + docName + '"' + (opts.view ? ' data-view="' + opts.view + '"' : '') +
|
|
38
|
+
(address ? ' data-return="' + address + '" data-return-expires-at="' + expiry + '"' : '') + '>' + encodeCarried(text) + END;
|
|
34
39
|
let carriedBlocks = '';
|
|
35
40
|
if (opts.drawing != null) {
|
|
36
41
|
if (typeof opts.drawing !== 'string') throw new Error('the drawing must be SVG text');
|
|
@@ -52,13 +57,18 @@ export function wrap(pageHtml, text, name, options) {
|
|
|
52
57
|
return titled.slice(0, at + shift) + '\n' + block + carriedBlocks + titled.slice(at + shift);
|
|
53
58
|
}
|
|
54
59
|
export function unwrap(pageHtml) {
|
|
55
|
-
let html = pageHtml, text = null, name = null, view = null;
|
|
60
|
+
let html = pageHtml, text = null, name = null, view = null, address = null, expiry = null;
|
|
56
61
|
const a = pageHtml.indexOf(START);
|
|
57
62
|
if (a >= 0) {
|
|
58
63
|
const open = pageHtml.indexOf('>', a), b = pageHtml.indexOf(END, open);
|
|
59
64
|
if (open < 0 || b < 0) throw new Error('a carried document without its end');
|
|
60
65
|
name = /data-name="([^"]*)"/.exec(pageHtml.slice(a, open))?.[1] ?? null;
|
|
61
66
|
view = /data-view="(draw|notes)"/.exec(pageHtml.slice(a, open))?.[1] ?? null;
|
|
67
|
+
const carriedReturn = /data-return="([^"]*)"/.exec(pageHtml.slice(a, open));
|
|
68
|
+
if (carriedReturn) address = returnAddress(carriedReturn[1]);
|
|
69
|
+
const carriedExpiry = /data-return-expires-at="([^"]*)"/.exec(pageHtml.slice(a, open));
|
|
70
|
+
if (carriedExpiry) expiry = returnExpiresAt(carriedExpiry[1]);
|
|
71
|
+
if (!!address !== !!expiry) throw new Error('a carried return needs its URL and return_expires_at');
|
|
62
72
|
text = decodeCarried(pageHtml.slice(open + 1, b));
|
|
63
73
|
html = pageHtml.slice(0, a) + pageHtml.slice(b + END.length);
|
|
64
74
|
if (a > 0 && html[a - 1] === '\n') html = html.slice(0, a - 1) + html.slice(a);
|
|
@@ -83,20 +93,22 @@ export function unwrap(pageHtml) {
|
|
|
83
93
|
html = html.slice(0, e) + html.slice(b + END.length);
|
|
84
94
|
if (e > 0 && html[e - 1] === '\n') html = html.slice(0, e - 1) + html.slice(e);
|
|
85
95
|
}
|
|
86
|
-
return {html, text, name, view, drawing, drawingName, base, baseName};
|
|
96
|
+
return {html, text, name, view, drawing, drawingName, base, baseName, return: address, return_expires_at: expiry};
|
|
87
97
|
}
|
|
88
98
|
// Unknown options, a bare --drawing or --base and a third name are refused, never dropped.
|
|
89
|
-
const USAGE = 'usage: rapier-html <document.md> [out.html] [--view draw|notes] [--drawing sketch.svg] [--base original.md]\n' +
|
|
99
|
+
const USAGE = 'usage: rapier-html <document.md> [out.html] [--view draw|notes] [--drawing sketch.svg] [--base original.md] [--return <url> --return-expires-at <timestamp>]\n' +
|
|
90
100
|
' writes <document>.rapier.html beside the file: Rapier with the document inside it, one file, offline.\n' +
|
|
91
101
|
' It never overwrites: an output that already exists, the document itself included, is refused.\n' +
|
|
92
102
|
' --view opens the page on Draw (sketch and paint, the document behind it) or on Notes\n' +
|
|
93
103
|
' --drawing carries an SVG drawing alongside the document, opened on Draw as the page boots\n' +
|
|
94
104
|
' --base carries the text the document was proposed against; the page opens on their diff\n' +
|
|
105
|
+
' --return carries a one-use return URL from document.create_return; Share offers Send back\n' +
|
|
106
|
+
' --return-expires-at carries the same mint\'s expiry; Share offers Save once the return expires\n' +
|
|
95
107
|
' -- everything after it is a filename, even one that starts with a dash\n' +
|
|
96
108
|
' --help this text';
|
|
97
109
|
export async function main(argv, pagesDir, {log = console.log} = {}) {
|
|
98
110
|
const names = [];
|
|
99
|
-
let drawingPath = null, basePath = null, view = null, onlyNames = false;
|
|
111
|
+
let drawingPath = null, basePath = null, view = null, address = null, expiry = null, onlyNames = false;
|
|
100
112
|
for (let i = 0; i < argv.length; i++) {
|
|
101
113
|
const arg = argv[i];
|
|
102
114
|
if (onlyNames || !arg.startsWith('-')) names.push(arg);
|
|
@@ -110,6 +122,14 @@ export async function main(argv, pagesDir, {log = console.log} = {}) {
|
|
|
110
122
|
if (basePath !== null || !argv[i + 1] || argv[i + 1].startsWith('-')) throw new Error('--base takes one text file (write a name that starts with a dash as ./-original.md)');
|
|
111
123
|
basePath = argv[++i];
|
|
112
124
|
}
|
|
125
|
+
else if (arg === '--return') {
|
|
126
|
+
if (address !== null || !argv[i + 1] || argv[i + 1].startsWith('-')) throw new Error('--return takes one HTTPS return URL');
|
|
127
|
+
address = returnAddress(argv[++i]);
|
|
128
|
+
}
|
|
129
|
+
else if (arg === '--return-expires-at') {
|
|
130
|
+
if (expiry !== null || !argv[i + 1] || argv[i + 1].startsWith('-')) throw new Error('--return-expires-at takes one ISO timestamp from document.create_return');
|
|
131
|
+
expiry = returnExpiresAt(argv[++i]);
|
|
132
|
+
}
|
|
113
133
|
else if (arg !== '--drawing') throw new Error('unknown option ' + arg + '\n' + USAGE);
|
|
114
134
|
else if (drawingPath !== null || !argv[i + 1] || argv[i + 1].startsWith('-')) throw new Error('--drawing takes one SVG file (write a name that starts with a dash as ./-sketch.svg)');
|
|
115
135
|
else drawingPath = argv[++i];
|
|
@@ -124,6 +144,8 @@ export async function main(argv, pagesDir, {log = console.log} = {}) {
|
|
|
124
144
|
if (drawingPath) { wrapOptions.drawing = await readFile(drawingPath, 'utf8'); wrapOptions.drawingName = basename(drawingPath); }
|
|
125
145
|
if (basePath) { wrapOptions.base = await readFile(basePath, 'utf8'); wrapOptions.baseName = basename(basePath); }
|
|
126
146
|
if (view) wrapOptions.view = view;
|
|
147
|
+
if (address) wrapOptions.return = address;
|
|
148
|
+
if (expiry) wrapOptions.return_expires_at = expiry;
|
|
127
149
|
// R85b: always a new file; 'wx' refuses any existing path, links included.
|
|
128
150
|
try { await writeFile(out, wrap(page, text, name, wrapOptions), {flag: 'wx'}); }
|
|
129
151
|
catch (error) { throw error.code === 'EEXIST' ? new Error(out + ' already exists, and rapier-html never overwrites a file: name a new one') : error; }
|