@panphora/clay-agent 0.0.0-stage → 0.1.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/LICENSE +21 -0
- package/README.md +81 -2
- package/bin/clay-agent.js +9 -0
- package/package.json +50 -4
- package/src/actions.js +288 -0
- package/src/cli.js +521 -0
- package/src/credentials.js +137 -0
- package/src/discovery.js +76 -0
- package/src/errors.js +90 -0
- package/src/hosts/hyperclay-adapter.js +85 -0
- package/src/hosts/hyperclay.js +210 -0
- package/src/hosts/whoami.js +56 -0
- package/src/html/edit.js +103 -0
- package/src/html/find.js +157 -0
- package/src/html/outline.js +154 -0
- package/src/html/regex-worker.js +17 -0
- package/src/html/select.js +54 -0
- package/src/html/source.js +73 -0
- package/src/mcp.js +296 -0
- package/src/runtime.js +46 -0
- package/src/session.js +69 -0
- package/src/target.js +209 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 David Miranda
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,82 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @panphora/clay-agent
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Let AI agents read and edit your [Hyperclay](https://hyperclay.com) documents, from the command line or as an MCP server for Claude Code, Claude Desktop and Cursor.
|
|
4
|
+
|
|
5
|
+
> **Early release.** Agent keys are not yet available on hyperclay.com, so this version cannot connect to anything yet. Support for the HTML Clay and Hyperclay Local desktop apps comes in a later version.
|
|
6
|
+
|
|
7
|
+
## How it works
|
|
8
|
+
|
|
9
|
+
You make an **agent key** in the Hyperclay dashboard: a named key that can be limited to one team or folder, and to reading, changing content, or full editing. You can revoke it at any time. The agent acts as you, with no more access than you have, and every change it makes is kept in the document's version history as "Recorded by you through <key name>", so any change can be restored.
|
|
10
|
+
|
|
11
|
+
Every write is based on the version the agent read. If the document changed in the meantime, the write is refused and the agent is told to read it again. Nothing is merged or overwritten silently.
|
|
12
|
+
|
|
13
|
+
## Sign in
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
npx -y @panphora/clay-agent login
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Paste the key when asked. It is saved in `~/.config/clay-agent/credentials.json`, readable only by you. The key is never accepted as a command-line argument (it would end up in your shell history), and it is never printed. `CLAY_AGENT_KEY` works instead of a saved key.
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
npx -y @panphora/clay-agent status
|
|
23
|
+
npx -y @panphora/clay-agent logout
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Use it as an MCP server
|
|
27
|
+
|
|
28
|
+
Claude Code:
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
claude mcp add clay -- npx -y @panphora/clay-agent mcp
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Claude Desktop and Cursor (`claude_desktop_config.json` or `.cursor/mcp.json`):
|
|
35
|
+
|
|
36
|
+
```json
|
|
37
|
+
{
|
|
38
|
+
"mcpServers": {
|
|
39
|
+
"clay": { "command": "npx", "args": ["-y", "@panphora/clay-agent", "mcp"] }
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
If Node is installed through nvm, use the absolute path to `npx` (`which npx`) as the command.
|
|
45
|
+
|
|
46
|
+
Options: `mcp --read-only` offers only the reading tools.
|
|
47
|
+
|
|
48
|
+
### Tools
|
|
49
|
+
|
|
50
|
+
| Tool | What it does |
|
|
51
|
+
|---|---|
|
|
52
|
+
| `list_documents` | Documents the key can reach |
|
|
53
|
+
| `read_document` | The whole document when small; otherwise an outline, or the parts you select (`select` by CSS selector, `range` by offset) |
|
|
54
|
+
| `outline_document` | The element tree with selector paths and sizes |
|
|
55
|
+
| `find_in_document` | Where some text appears, with the element around it |
|
|
56
|
+
| `read_data` | The page's structured data, through its `api` rules |
|
|
57
|
+
| `write_data` | Change that data |
|
|
58
|
+
| `edit_document` | Replace exact text in the version read; each match must be unique |
|
|
59
|
+
| `save_document` | Save a whole document |
|
|
60
|
+
| `list_versions`, `read_version`, `restore_version` | Version history |
|
|
61
|
+
|
|
62
|
+
Large documents (up to 5 MB) are read in pieces: an outline first, then the parts the agent needs, and small changes go through `edit_document`.
|
|
63
|
+
|
|
64
|
+
## Use it from the command line
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
clay-agent list
|
|
68
|
+
clay-agent read hyperclay:123 --outline
|
|
69
|
+
clay-agent read hyperclay:123 --select 'main > section'
|
|
70
|
+
clay-agent edit hyperclay:123 --find 'Draft' --replace 'Final' --if-match <etag>
|
|
71
|
+
clay-agent versions hyperclay:123
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
A document is `hyperclay:<id>` or its hyperclay.com URL. Writes need `--if-match` with the etag from a read. Add `--json` to any command for machine-readable output.
|
|
75
|
+
|
|
76
|
+
Exit codes: `0` done, `1` refused or failed, `2` the document changed since it was read (the current etag is printed), `3` the host is not reachable, `4` not signed in, or the key is malformed, revoked or expired, `5` the host may or may not have saved the write (read the document before trying again).
|
|
77
|
+
|
|
78
|
+
Needs Node 20.18.1 or later.
|
|
79
|
+
|
|
80
|
+
## License
|
|
81
|
+
|
|
82
|
+
MIT
|
package/package.json
CHANGED
|
@@ -1,6 +1,52 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@panphora/clay-agent",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Let AI agents read and edit malleable HTML documents on hyperclay.com, HTML Clay and Hyperclay Local, from the command line or as an MCP server",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"clay-agent": "bin/clay-agent.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"bin",
|
|
11
|
+
"src",
|
|
12
|
+
"README.md",
|
|
13
|
+
"LICENSE"
|
|
14
|
+
],
|
|
15
|
+
"scripts": {
|
|
16
|
+
"test": "node --test"
|
|
17
|
+
},
|
|
18
|
+
"engines": {
|
|
19
|
+
"node": ">=20.18.1"
|
|
20
|
+
},
|
|
21
|
+
"keywords": [
|
|
22
|
+
"mcp",
|
|
23
|
+
"model-context-protocol",
|
|
24
|
+
"hyperclay",
|
|
25
|
+
"html-clay",
|
|
26
|
+
"malleable-html",
|
|
27
|
+
"ai-agent"
|
|
28
|
+
],
|
|
29
|
+
"author": "David Miranda",
|
|
30
|
+
"license": "MIT",
|
|
31
|
+
"publishConfig": {
|
|
32
|
+
"access": "public"
|
|
33
|
+
},
|
|
34
|
+
"repository": {
|
|
35
|
+
"type": "git",
|
|
36
|
+
"url": "git+https://github.com/panphora/clay-agent.git"
|
|
37
|
+
},
|
|
38
|
+
"homepage": "https://github.com/panphora/clay-agent",
|
|
39
|
+
"bugs": {
|
|
40
|
+
"url": "https://github.com/panphora/clay-agent/issues"
|
|
41
|
+
},
|
|
42
|
+
"hyper": {
|
|
43
|
+
"kind": "cli",
|
|
44
|
+
"status": "active",
|
|
45
|
+
"url": "https://github.com/panphora/clay-agent"
|
|
46
|
+
},
|
|
47
|
+
"dependencies": {
|
|
48
|
+
"@modelcontextprotocol/sdk": "1.32.2",
|
|
49
|
+
"cheerio": "1.2.0",
|
|
50
|
+
"zod": "4.6.7"
|
|
51
|
+
}
|
|
52
|
+
}
|
package/src/actions.js
ADDED
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
import { ClayError } from './errors.js';
|
|
2
|
+
import { applyEdits } from './html/edit.js';
|
|
3
|
+
import { find } from './html/find.js';
|
|
4
|
+
import { outline } from './html/outline.js';
|
|
5
|
+
import { range as sliceRange, select as selectMatches } from './html/select.js';
|
|
6
|
+
import { createSession } from './session.js';
|
|
7
|
+
|
|
8
|
+
const KEY_PATTERN = /hcak_[0-9a-f]{64}/;
|
|
9
|
+
const TOO_LARGE_HINT =
|
|
10
|
+
'Use outline_document to see its structure, find_in_document to locate text, or read_document with select or range to read part of it.';
|
|
11
|
+
|
|
12
|
+
const CUT_HINT =
|
|
13
|
+
'The answer was cut to stay within maxChars. Read the rest with range, starting at the end offset shown.';
|
|
14
|
+
|
|
15
|
+
const WHOLE_READ_FIRST_HINT =
|
|
16
|
+
'Read the whole document with read_document first; save_document replaces all of it.';
|
|
17
|
+
|
|
18
|
+
function readFirstHint(explicitEtag) {
|
|
19
|
+
return explicitEtag
|
|
20
|
+
? 'Pass --if-match with the etag from a read.'
|
|
21
|
+
: 'Read the document first (read_document, outline_document, find_in_document or read_data), then write.';
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function shapeRead({ ref, host, etag, html, maxChars, input }) {
|
|
25
|
+
if (input.select !== undefined && input.select !== null) {
|
|
26
|
+
if (input.range !== undefined && input.range !== null) {
|
|
27
|
+
throw new ClayError('invalid-read', 'Pass either `select` or `range`, not both.', {
|
|
28
|
+
hint: 'Use `select` for elements by CSS selector, or `range` for a start and length.',
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
const { count, matches } = selectMatches(html, input.select, { maxChars });
|
|
32
|
+
const answer = { ref, host, etag, size: html.length, count, matches };
|
|
33
|
+
if (matches.some((match) => match.truncated)) answer.hint = CUT_HINT;
|
|
34
|
+
return answer;
|
|
35
|
+
}
|
|
36
|
+
if (input.range !== undefined && input.range !== null) {
|
|
37
|
+
const start = input.range.start;
|
|
38
|
+
const length = input.range.length;
|
|
39
|
+
const { start: from, end, html: slice, truncated } = sliceRange(html, start, length, { maxChars });
|
|
40
|
+
const answer = { ref, host, etag, size: html.length, start: from, end, html: slice };
|
|
41
|
+
if (truncated) {
|
|
42
|
+
answer.truncated = true;
|
|
43
|
+
answer.hint = CUT_HINT;
|
|
44
|
+
}
|
|
45
|
+
return answer;
|
|
46
|
+
}
|
|
47
|
+
if (html.length <= maxChars) {
|
|
48
|
+
return { ref, host, etag, size: html.length, html };
|
|
49
|
+
}
|
|
50
|
+
return {
|
|
51
|
+
ref,
|
|
52
|
+
host,
|
|
53
|
+
etag,
|
|
54
|
+
size: html.length,
|
|
55
|
+
tooLarge: true,
|
|
56
|
+
outline: outline(html, { depth: 3 }).outline,
|
|
57
|
+
hint: TOO_LARGE_HINT,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function createActions({ adapters, resolve, session = createSession(), readOnly = false, explicitEtag = false }) {
|
|
62
|
+
function refuseReadOnly() {
|
|
63
|
+
if (readOnly) throw new ClayError('read-only-mode', 'This server was started read-only.');
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
async function targetFor(input) {
|
|
67
|
+
const target = await resolve(input.document, { host: input.host });
|
|
68
|
+
const host = target.host;
|
|
69
|
+
const adapter = adapters[host];
|
|
70
|
+
if (!adapter) {
|
|
71
|
+
throw new ClayError('host-unavailable', `No adapter is available for ${host}.`, {
|
|
72
|
+
hint: 'This build talks to hyperclay.com; other hosts are not connected yet.',
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
const { id, ref } = await adapter.locate(target);
|
|
76
|
+
return { host, adapter, id, ref };
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async function load(input) {
|
|
80
|
+
const { host, adapter, id, ref } = await targetFor(input);
|
|
81
|
+
const stored = session.latest(ref);
|
|
82
|
+
const ifNoneMatch = stored && stored.html !== undefined ? stored.etag : undefined;
|
|
83
|
+
const result = await adapter.read(id, { ifNoneMatch });
|
|
84
|
+
if (result.notModified) {
|
|
85
|
+
return { ref, host, id, etag: stored.etag, html: stored.html, size: stored.html.length };
|
|
86
|
+
}
|
|
87
|
+
session.remember(ref, { etag: result.etag, html: result.html });
|
|
88
|
+
return {
|
|
89
|
+
ref,
|
|
90
|
+
host,
|
|
91
|
+
id,
|
|
92
|
+
etag: result.etag,
|
|
93
|
+
html: result.html,
|
|
94
|
+
size: result.size ?? result.html.length,
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function etagFor(input, ref, { whole = false } = {}) {
|
|
99
|
+
if (input.etag) return input.etag;
|
|
100
|
+
if (explicitEtag) {
|
|
101
|
+
throw new ClayError('read-first', 'This write needs the etag from a read.', {
|
|
102
|
+
hint: readFirstHint(true),
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
const known = whole ? session.wholeEtag(ref) : session.seenEtag(ref);
|
|
106
|
+
if (!known) {
|
|
107
|
+
throw new ClayError('read-first', 'This write needs the etag from a read.', {
|
|
108
|
+
hint: whole ? WHOLE_READ_FIRST_HINT : readFirstHint(false),
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
return known;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function afterWrite(ref, etag) {
|
|
115
|
+
session.remember(ref, { etag });
|
|
116
|
+
session.markSeen(ref, etag);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function refuseSecret(html) {
|
|
120
|
+
if (typeof html === 'string' && KEY_PATTERN.test(html)) {
|
|
121
|
+
throw new ClayError('secret-in-content', 'The document would contain an agent key. Remove it before saving.');
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return {
|
|
126
|
+
list_documents(input = {}) {
|
|
127
|
+
const host = input.host ?? 'hyperclay';
|
|
128
|
+
const adapter = adapters[host];
|
|
129
|
+
if (!adapter) {
|
|
130
|
+
throw new ClayError('host-unavailable', `No adapter is available for ${host}.`, {
|
|
131
|
+
hint: 'This build talks to hyperclay.com; other hosts are not connected yet.',
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
return adapter.list({ where: input.where, cursor: input.cursor });
|
|
135
|
+
},
|
|
136
|
+
|
|
137
|
+
async read_document(input = {}) {
|
|
138
|
+
const maxChars = input.maxChars ?? 40000;
|
|
139
|
+
const loaded = await load(input);
|
|
140
|
+
const shaped = shapeRead({
|
|
141
|
+
ref: loaded.ref,
|
|
142
|
+
host: loaded.host,
|
|
143
|
+
etag: loaded.etag,
|
|
144
|
+
html: loaded.html,
|
|
145
|
+
maxChars,
|
|
146
|
+
input,
|
|
147
|
+
});
|
|
148
|
+
session.markSeen(loaded.ref, loaded.etag);
|
|
149
|
+
if (!input.select && !input.range && !shaped.tooLarge) {
|
|
150
|
+
session.markWhole(loaded.ref, loaded.etag);
|
|
151
|
+
}
|
|
152
|
+
return shaped;
|
|
153
|
+
},
|
|
154
|
+
|
|
155
|
+
async outline_document(input = {}) {
|
|
156
|
+
const loaded = await load(input);
|
|
157
|
+
const result = outline(loaded.html, { depth: input.depth ?? 4, select: input.select ?? null });
|
|
158
|
+
session.markSeen(loaded.ref, loaded.etag);
|
|
159
|
+
const answer = {
|
|
160
|
+
ref: loaded.ref,
|
|
161
|
+
host: loaded.host,
|
|
162
|
+
etag: loaded.etag,
|
|
163
|
+
size: result.size,
|
|
164
|
+
hasApiRules: result.hasApiRules,
|
|
165
|
+
dataKeys: result.dataKeys,
|
|
166
|
+
depth: result.depth,
|
|
167
|
+
truncated: result.truncated,
|
|
168
|
+
outline: result.outline,
|
|
169
|
+
};
|
|
170
|
+
if (result.truncated) answer.hint = result.hint;
|
|
171
|
+
return answer;
|
|
172
|
+
},
|
|
173
|
+
|
|
174
|
+
async find_in_document(input = {}) {
|
|
175
|
+
const loaded = await load(input);
|
|
176
|
+
const { count, matches } = await find(loaded.html, {
|
|
177
|
+
text: input.text,
|
|
178
|
+
regex: input.regex,
|
|
179
|
+
limit: input.limit ?? 20,
|
|
180
|
+
});
|
|
181
|
+
session.markSeen(loaded.ref, loaded.etag);
|
|
182
|
+
return { ref: loaded.ref, host: loaded.host, etag: loaded.etag, count, matches };
|
|
183
|
+
},
|
|
184
|
+
|
|
185
|
+
async read_data(input = {}) {
|
|
186
|
+
const { host, adapter, id, ref } = await targetFor(input);
|
|
187
|
+
const result = await adapter.readData(id);
|
|
188
|
+
const stored = session.latest(ref);
|
|
189
|
+
const keep =
|
|
190
|
+
stored && stored.etag === result.etag && stored.html !== undefined ? { html: stored.html } : {};
|
|
191
|
+
session.remember(ref, { etag: result.etag, ...keep });
|
|
192
|
+
session.markSeen(ref, result.etag);
|
|
193
|
+
return { ref, host, etag: result.etag, data: result.data };
|
|
194
|
+
},
|
|
195
|
+
|
|
196
|
+
async write_data(input = {}) {
|
|
197
|
+
refuseReadOnly();
|
|
198
|
+
const { host, adapter, id, ref } = await targetFor(input);
|
|
199
|
+
refuseSecret(JSON.stringify(input.data));
|
|
200
|
+
const etag = etagFor(input, ref);
|
|
201
|
+
const result = await adapter.writeData(id, { data: input.data, etag });
|
|
202
|
+
afterWrite(ref, result.etag);
|
|
203
|
+
return { ref, host, etag: result.etag, changed: result.changed, data: result.data ?? input.data };
|
|
204
|
+
},
|
|
205
|
+
|
|
206
|
+
async edit_document(input = {}) {
|
|
207
|
+
refuseReadOnly();
|
|
208
|
+
const { host, adapter, id, ref } = await targetFor(input);
|
|
209
|
+
const etag = etagFor(input, ref);
|
|
210
|
+
let base = session.htmlFor(ref, etag);
|
|
211
|
+
if (base === null) {
|
|
212
|
+
const loaded = await load(input);
|
|
213
|
+
if (loaded.etag !== etag) {
|
|
214
|
+
throw new ClayError('conflict', 'The document changed after you read it.', {
|
|
215
|
+
etag: loaded.etag,
|
|
216
|
+
hint: 'Read it again and redo the edit.',
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
base = loaded.html;
|
|
220
|
+
}
|
|
221
|
+
const { html } = applyEdits(base, input.edits);
|
|
222
|
+
refuseSecret(html);
|
|
223
|
+
const result = await adapter.save(id, { html, etag });
|
|
224
|
+
afterWrite(ref, result.etag);
|
|
225
|
+
if (session.wholeEtag(ref) === etag) session.markWhole(ref, result.etag);
|
|
226
|
+
else session.clearWhole(ref);
|
|
227
|
+
return {
|
|
228
|
+
ref,
|
|
229
|
+
host,
|
|
230
|
+
etag: result.etag,
|
|
231
|
+
changed: result.changed,
|
|
232
|
+
chars: html.length,
|
|
233
|
+
undoVersionId: result.undoVersionId,
|
|
234
|
+
};
|
|
235
|
+
},
|
|
236
|
+
|
|
237
|
+
async save_document(input = {}) {
|
|
238
|
+
refuseReadOnly();
|
|
239
|
+
const { host, adapter, id, ref } = await targetFor(input);
|
|
240
|
+
refuseSecret(input.html);
|
|
241
|
+
const etag = etagFor(input, ref, { whole: true });
|
|
242
|
+
const result = await adapter.save(id, { html: input.html, etag });
|
|
243
|
+
afterWrite(ref, result.etag);
|
|
244
|
+
session.markWhole(ref, result.etag);
|
|
245
|
+
return {
|
|
246
|
+
ref,
|
|
247
|
+
host,
|
|
248
|
+
etag: result.etag,
|
|
249
|
+
changed: result.changed,
|
|
250
|
+
undoVersionId: result.undoVersionId,
|
|
251
|
+
};
|
|
252
|
+
},
|
|
253
|
+
|
|
254
|
+
async list_versions(input = {}) {
|
|
255
|
+
const { host, adapter, id, ref } = await targetFor(input);
|
|
256
|
+
const result = await adapter.versions(id, { before: input.before });
|
|
257
|
+
return { ref, host, versions: result.versions, nextBefore: result.nextBefore };
|
|
258
|
+
},
|
|
259
|
+
|
|
260
|
+
async read_version(input = {}) {
|
|
261
|
+
const { host, adapter, id, ref } = await targetFor(input);
|
|
262
|
+
const version = await adapter.version(id, input.version);
|
|
263
|
+
const maxChars = input.maxChars ?? 40000;
|
|
264
|
+
const shaped = shapeRead({
|
|
265
|
+
ref,
|
|
266
|
+
host,
|
|
267
|
+
etag: version.etag,
|
|
268
|
+
html: version.html,
|
|
269
|
+
maxChars,
|
|
270
|
+
input: { select: input.select, range: input.range },
|
|
271
|
+
});
|
|
272
|
+
const { ref: shapedRef, host: shapedHost, etag: shapedEtag, ...rest } = shaped;
|
|
273
|
+
const result = { ref: shapedRef, host: shapedHost };
|
|
274
|
+
if (shapedEtag !== undefined) result.etag = shapedEtag;
|
|
275
|
+
result.version = input.version;
|
|
276
|
+
return Object.assign(result, rest);
|
|
277
|
+
},
|
|
278
|
+
|
|
279
|
+
async restore_version(input = {}) {
|
|
280
|
+
refuseReadOnly();
|
|
281
|
+
const { host, adapter, id, ref } = await targetFor(input);
|
|
282
|
+
const etag = etagFor(input, ref);
|
|
283
|
+
const result = await adapter.restore(id, input.version, { etag });
|
|
284
|
+
afterWrite(ref, result.etag);
|
|
285
|
+
return { ref, host, etag: result.etag };
|
|
286
|
+
},
|
|
287
|
+
};
|
|
288
|
+
}
|