@digital-gravy/etch-connector 0.1.1 → 0.1.3
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 +51 -72
- package/dist/cli.js +74 -11
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
# etch-connector
|
|
2
2
|
|
|
3
|
+
> **AGENT: read this first.** Before interacting with Etch in any way, run
|
|
4
|
+
> `etch-connector --help` (or `npx @digital-gravy/etch-connector --help`) and
|
|
5
|
+
> read the **entire** output. It documents the backends, commands, flags, the
|
|
6
|
+
> stdout/stderr/exit-code contract, and how to pull the current Etch docs —
|
|
7
|
+
> everything you need to drive a tab correctly. Do not call any other command
|
|
8
|
+
> until you have read it.
|
|
9
|
+
|
|
3
10
|
A tiny local bridge that lets an AI harness — Claude Code, Cowork, or any tool
|
|
4
11
|
that can run a shell command — **drive a live Etch builder tab** by evaluating
|
|
5
12
|
scripts against the [`etch-public-api`](https://www.npmjs.com/package/@digital-gravy/etch-public-api)
|
|
@@ -28,25 +35,40 @@ it as an async function, and the return value + console output come back. It is
|
|
|
28
35
|
intentionally _not_ an MCP server yet, but it is built so that becoming one is a
|
|
29
36
|
thin add — see [Going to MCP](#going-to-mcp-later).
|
|
30
37
|
|
|
31
|
-
Scripts run in
|
|
38
|
+
Scripts run in a **page-enforced safe mode**: the script sees only `etch` and
|
|
39
|
+
the standard JS built-ins. `window`, `document`, `fetch`, storage, `eval`, … are
|
|
40
|
+
withheld.
|
|
41
|
+
|
|
42
|
+
## Install & run
|
|
43
|
+
|
|
44
|
+
No install needed — run it on demand with `npx` (npm) or `bunx` (Bun):
|
|
32
45
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
46
|
+
```bash
|
|
47
|
+
npx @digital-gravy/etch-connector serve
|
|
48
|
+
# or
|
|
49
|
+
bunx @digital-gravy/etch-connector serve
|
|
50
|
+
```
|
|
37
51
|
|
|
38
|
-
|
|
52
|
+
To get the short `etch-connector` command on your `PATH`, install it globally:
|
|
39
53
|
|
|
40
54
|
```bash
|
|
41
|
-
|
|
55
|
+
npm install -g @digital-gravy/etch-connector # or: bun install -g …
|
|
56
|
+
etch-connector serve
|
|
42
57
|
```
|
|
43
58
|
|
|
59
|
+
> Using the connector requires a valid, active Etch license — see
|
|
60
|
+
> [`LICENSE`](LICENSE).
|
|
61
|
+
|
|
62
|
+
The examples below use the bare `etch-connector` command; if you're running via
|
|
63
|
+
`npx`/`bunx` instead, prefix them with `npx @digital-gravy/etch-connector` (or
|
|
64
|
+
`bunx …`).
|
|
65
|
+
|
|
44
66
|
## Use
|
|
45
67
|
|
|
46
68
|
### 1. Start the daemon (once)
|
|
47
69
|
|
|
48
70
|
```bash
|
|
49
|
-
|
|
71
|
+
etch-connector serve
|
|
50
72
|
# bridge ws://127.0.0.1:7331
|
|
51
73
|
# control http://127.0.0.1:7332
|
|
52
74
|
```
|
|
@@ -70,20 +92,17 @@ started the daemon on a non-default WebSocket port.)
|
|
|
70
92
|
etch-connector tabs
|
|
71
93
|
|
|
72
94
|
# run a script (value → stdout, console output → stderr).
|
|
73
|
-
#
|
|
95
|
+
# reference the API as `etch.*`.
|
|
74
96
|
etch-connector eval -t homepage "return etch.apiVersion"
|
|
75
97
|
|
|
76
98
|
# multi-line / from a file / from stdin
|
|
77
99
|
etch-connector eval -t homepage -f ./script.js
|
|
78
100
|
echo "return await etch.blocks.getTree()" | etch-connector eval -t homepage
|
|
79
|
-
|
|
80
|
-
# full page access (opt in)
|
|
81
|
-
etch-connector eval -t homepage --unsafe "return document.title"
|
|
82
101
|
```
|
|
83
102
|
|
|
84
103
|
The script body runs as an **async function**, so it can `return` a value and
|
|
85
|
-
use top-level `await`.
|
|
86
|
-
|
|
104
|
+
use top-level `await`. Write scripts against the `etch-public-api` surface via
|
|
105
|
+
the `etch` binding (`window` is not available). Examples:
|
|
87
106
|
|
|
88
107
|
```js
|
|
89
108
|
return etch.apiVersion;
|
|
@@ -93,16 +112,6 @@ const groups = await etch.fields.listGroupsAsync();
|
|
|
93
112
|
return groups.length;
|
|
94
113
|
```
|
|
95
114
|
|
|
96
|
-
### Modes
|
|
97
|
-
|
|
98
|
-
| Mode | In scope | Use for |
|
|
99
|
-
| ---------------- | ------------------------------------ | ------------------------------- |
|
|
100
|
-
| `safe` (default) | `etch` + standard JS built-ins | Driving the builder via the API |
|
|
101
|
-
| `unsafe` | everything (`window`, `document`, …) | Debugging, page internals |
|
|
102
|
-
|
|
103
|
-
Pick per call with `--safe` / `--unsafe`, or change the daemon default with
|
|
104
|
-
`serve --unsafe`. A per-call flag overrides the daemon default.
|
|
105
|
-
|
|
106
115
|
### Output contract (for agents)
|
|
107
116
|
|
|
108
117
|
`eval` keeps stdout clean so a harness can capture it as the result:
|
|
@@ -112,40 +121,14 @@ Pick per call with `--safe` / `--unsafe`, or change the daemon default with
|
|
|
112
121
|
- **exit code** — `0` success · `2` the script threw · `1` operational failure
|
|
113
122
|
(no such tab, ambiguous target, timeout, daemon unreachable).
|
|
114
123
|
|
|
115
|
-
##
|
|
116
|
-
|
|
117
|
-
Before writing scripts against `etch.*`, **always pull the current Etch
|
|
118
|
-
documentation first** — the `etch-public-api` surface changes and training data
|
|
119
|
-
may be stale. Fetch the docs, _then_ write scripts.
|
|
120
|
-
|
|
121
|
-
### Preferred: Context7 MCP
|
|
122
|
-
|
|
123
|
-
If a Context7 MCP server is available, fetch the Etch docs through it:
|
|
124
|
-
|
|
125
|
-
1. Call `resolve-library-id` with the library name **`Etch`** (pass the user's
|
|
126
|
-
question alongside it).
|
|
127
|
-
2. From the matches, pick the entry for Etch / the Etch public API — prefer an
|
|
128
|
-
exact name match, and a version-specific id if the user named a version.
|
|
129
|
-
3. Call `query-docs` with the selected library id and the specific question
|
|
130
|
-
(e.g. _"how do I create a block with etch.blocks.create"_).
|
|
131
|
-
4. Write the script using the fetched docs, referencing `etch.*` exactly as the
|
|
132
|
-
docs show.
|
|
133
|
-
|
|
134
|
-
### Fallback: pull the docs manually
|
|
135
|
-
|
|
136
|
-
If no Context7 server is available, fetch the documentation directly from the
|
|
137
|
-
Etch docs site:
|
|
138
|
-
|
|
139
|
-
- **Public API reference:** <https://docs.etchwp.com/public-api> — the
|
|
140
|
-
authoritative surface for `window.etch.*` (`etch.blocks`, `etch.styles`,
|
|
141
|
-
`etch.components`, `etch.fields`, `etch.stylesheets`, …).
|
|
142
|
-
- **Main docs:** <https://docs.etchwp.com> — for everything else about Etch
|
|
143
|
-
(concepts, blocks, components, ACSS, units, …). Follow the links from there to
|
|
144
|
-
the specific page that answers the question.
|
|
124
|
+
## For AI agents
|
|
145
125
|
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
126
|
+
The authoritative agent instructions live in **`etch-connector --help`** — read
|
|
127
|
+
the entire output before interacting with Etch. It covers the backends, commands
|
|
128
|
+
and flags, the stdout/stderr/exit-code contract, how to pull the current Etch
|
|
129
|
+
docs (Context7 MCP or <https://docs.etchwp.com/public-api>), and the conventions
|
|
130
|
+
for structuring pages (e.g. the section/container layout pattern). Project
|
|
131
|
+
conventions for working on this repo live in [`AGENTS.md`](AGENTS.md).
|
|
149
132
|
|
|
150
133
|
## CLI
|
|
151
134
|
|
|
@@ -153,17 +136,16 @@ of the API docs, follow the project-specific conventions in
|
|
|
153
136
|
etch-connector — drive a live Etch tab from the command line
|
|
154
137
|
|
|
155
138
|
Usage:
|
|
156
|
-
etch-connector serve [--ws-port 7331] [--control-port 7332] [--ws-host 127.0.0.1]
|
|
139
|
+
etch-connector serve [--ws-port 7331] [--control-port 7332] [--ws-host 127.0.0.1]
|
|
157
140
|
etch-connector tabs [--json] [--control-port 7332] [--cdp]
|
|
158
|
-
etch-connector eval [code] [-t|--tab name] [-f|--file path] [--timeout ms] [--
|
|
141
|
+
etch-connector eval [code] [-t|--tab name] [-f|--file path] [--timeout ms] [--cdp]
|
|
159
142
|
etch-connector shot [-t name] [-s|--selector css] [--full] [--jpeg] [-o|--out file] [--freeze=false] (CDP)
|
|
160
143
|
etch-connector html <selector> [-t name] (CDP)
|
|
161
144
|
etch-connector computed <selector> [-t name] [--props a,b,c] (CDP)
|
|
162
145
|
|
|
163
146
|
Two backends:
|
|
164
147
|
bridge (default) the tab dials in via etch.connectTo(); page-sandboxed.
|
|
165
|
-
|
|
166
|
-
unsafe = full page access (--unsafe per eval, or serve --unsafe)
|
|
148
|
+
Scripts see only "etch" + the standard JS built-ins.
|
|
167
149
|
cdp connect OUT to a Chrome started with --remote-debugging-port=9222.
|
|
168
150
|
No daemon. Runs at DevTools privilege: full "window", plus screenshots,
|
|
169
151
|
rendered HTML, and computed styles. Opt in with --cdp (or --cdp-port /
|
|
@@ -234,17 +216,14 @@ Two layers, neither a substitute for the other:
|
|
|
234
216
|
the daemon. Run it only on a trusted machine and do not expose the ports beyond
|
|
235
217
|
loopback.
|
|
236
218
|
|
|
237
|
-
**Execution.**
|
|
238
|
-
|
|
239
|
-
`
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
`unsafe` mode runs the script with full, unconstrained access to the page — only
|
|
247
|
-
use it for trusted debugging.
|
|
219
|
+
**Execution.** Bridge mode runs the script inside a `with`-bound Proxy that
|
|
220
|
+
exposes only `etch` plus the standard JS built-ins and throws on `window`,
|
|
221
|
+
`document`, `fetch`, storage, `eval`, `Function`, … This is a **guardrail, not a
|
|
222
|
+
hardened sandbox**: because the script shares the page's JS realm, a _determined_
|
|
223
|
+
author can still reach globals through prototype / constructor chains (e.g.
|
|
224
|
+
`({}).constructor.constructor`). It reliably keeps casual and AI-authored
|
|
225
|
+
scripts on the `etch` surface and closes the obvious escape hatches; it does
|
|
226
|
+
**not** contain hostile code.
|
|
248
227
|
|
|
249
228
|
For a true boundary against adversarial scripts you would need to harden the
|
|
250
229
|
realm (SES `lockdown()`), which freezes all intrinsics app-wide and is out of
|
package/dist/cli.js
CHANGED
|
@@ -17,6 +17,14 @@ import { createControlServer, controlBaseUrl } from './control-server.js';
|
|
|
17
17
|
import { CdpClient, CdpError, CdpEvalError } from './cdp-client.js';
|
|
18
18
|
import { DEFAULT_CONTROL_PORT, DEFAULT_HOST, DEFAULT_WS_PORT } from './protocol.js';
|
|
19
19
|
const DEFAULT_CDP_PORT = 9222;
|
|
20
|
+
/**
|
|
21
|
+
* Unsafe mode (scripts get full, unconstrained access to the page) is
|
|
22
|
+
* intentionally DISABLED. The `--unsafe` flag and the branches that honor it
|
|
23
|
+
* are kept below as a deliberate dead path behind this constant — never
|
|
24
|
+
* reachable in shipped builds, and undocumented in --help and the README — so
|
|
25
|
+
* the machinery can be revived later without rebuilding it. Leave it `false`.
|
|
26
|
+
*/
|
|
27
|
+
const ALLOW_UNSAFE = false;
|
|
20
28
|
/** Long flags that never take a value, so they don't swallow the next token. */
|
|
21
29
|
const BOOLEAN_FLAGS = new Set([
|
|
22
30
|
'cdp',
|
|
@@ -110,7 +118,8 @@ async function runServe(flags) {
|
|
|
110
118
|
const wsPort = num(flags['ws-port'], DEFAULT_WS_PORT);
|
|
111
119
|
const controlHost = str(flags['control-host']) ?? DEFAULT_HOST;
|
|
112
120
|
const controlPort = num(flags['control-port'], DEFAULT_CONTROL_PORT);
|
|
113
|
-
|
|
121
|
+
// --unsafe is a dead path (see ALLOW_UNSAFE); defaultMode is always 'safe'.
|
|
122
|
+
const defaultMode = ALLOW_UNSAFE && flags['unsafe'] ? 'unsafe' : 'safe';
|
|
114
123
|
const bridge = new BridgeServer({ host: wsHost, port: wsPort });
|
|
115
124
|
bridge.on('connect', (tab) => console.error(`[etch-connector] + tab "${tab.name}" (${tab.url})`));
|
|
116
125
|
bridge.on('disconnect', (tab) => console.error(`[etch-connector] - tab "${tab.name}"`));
|
|
@@ -271,9 +280,10 @@ async function runEval(positional, flags) {
|
|
|
271
280
|
}
|
|
272
281
|
return;
|
|
273
282
|
}
|
|
274
|
-
// --
|
|
283
|
+
// --safe overrides the daemon default; omitting it defers to the daemon.
|
|
284
|
+
// --unsafe is a dead path (see ALLOW_UNSAFE): it never sets unsafe mode.
|
|
275
285
|
let mode;
|
|
276
|
-
if (flags['unsafe'])
|
|
286
|
+
if (ALLOW_UNSAFE && flags['unsafe'])
|
|
277
287
|
mode = 'unsafe';
|
|
278
288
|
else if (flags['safe'])
|
|
279
289
|
mode = 'safe';
|
|
@@ -376,26 +386,33 @@ async function runComputed(positional, flags) {
|
|
|
376
386
|
function printHelp() {
|
|
377
387
|
console.error(`etch-connector — drive a live Etch tab from the command line
|
|
378
388
|
|
|
389
|
+
Run without installing (requires a valid, active Etch license):
|
|
390
|
+
npx @digital-gravy/etch-connector serve
|
|
391
|
+
bunx @digital-gravy/etch-connector serve
|
|
392
|
+
Or install globally for the bare \`etch-connector\` command:
|
|
393
|
+
npm install -g @digital-gravy/etch-connector # or: bun install -g …
|
|
394
|
+
(When running via npx/bunx, prefix the commands below with
|
|
395
|
+
\`npx @digital-gravy/etch-connector\` / \`bunx …\`.)
|
|
396
|
+
|
|
379
397
|
Usage:
|
|
380
|
-
etch-connector serve [--ws-port ${DEFAULT_WS_PORT}] [--control-port ${DEFAULT_CONTROL_PORT}] [--ws-host ${DEFAULT_HOST}]
|
|
398
|
+
etch-connector serve [--ws-port ${DEFAULT_WS_PORT}] [--control-port ${DEFAULT_CONTROL_PORT}] [--ws-host ${DEFAULT_HOST}]
|
|
381
399
|
etch-connector tabs [--json] [--control-port ${DEFAULT_CONTROL_PORT}] [--cdp]
|
|
382
|
-
etch-connector eval [code] [-t|--tab name] [-f|--file path] [--timeout ms] [--
|
|
400
|
+
etch-connector eval [code] [-t|--tab name] [-f|--file path] [--timeout ms] [--cdp]
|
|
383
401
|
etch-connector shot [-t name] [-s|--selector css] [--full] [--jpeg] [-o|--out file] [--freeze=false] (CDP)
|
|
384
402
|
etch-connector html <selector> [-t name] (CDP)
|
|
385
403
|
etch-connector computed <selector> [-t name] [--props a,b,c] (CDP)
|
|
386
404
|
|
|
387
405
|
Two backends:
|
|
388
406
|
bridge (default) the tab dials in via etch.connectTo(); page-sandboxed.
|
|
389
|
-
|
|
390
|
-
unsafe = full page access (--unsafe per eval, or serve --unsafe)
|
|
407
|
+
Scripts see only "etch" + the standard JS built-ins.
|
|
391
408
|
cdp connect OUT to a Chrome started with --remote-debugging-port=${DEFAULT_CDP_PORT}.
|
|
392
409
|
No daemon. Runs at DevTools privilege: full "window", plus screenshots,
|
|
393
410
|
rendered HTML, and computed styles. Opt in with --cdp (or --cdp-port /
|
|
394
411
|
--remote-debugging-port / --cdp-host). Target a tab with -t <url|title substring>.
|
|
395
412
|
|
|
396
413
|
AGENT: before running anything, ask the user how they want to connect — bridge
|
|
397
|
-
|
|
398
|
-
|
|
414
|
+
(API only) or cdp (DevTools privilege: full page access). cdp grants more reach;
|
|
415
|
+
don't assume — let the user choose.
|
|
399
416
|
|
|
400
417
|
Examples:
|
|
401
418
|
etch-connector serve
|
|
@@ -410,8 +427,8 @@ Examples:
|
|
|
410
427
|
etch-connector computed --cdp -t etch -s .orbit__chip --props transform,animation-duration
|
|
411
428
|
|
|
412
429
|
The eval body runs as an async function, so it may 'return' a value and use
|
|
413
|
-
top-level 'await'. In bridge
|
|
414
|
-
|
|
430
|
+
top-level 'await'. In bridge mode reference the API as "etch.*" ("window" is
|
|
431
|
+
withheld); in cdp mode the full page is available.
|
|
415
432
|
|
|
416
433
|
Docs (AGENT: pull these BEFORE writing scripts against etch.* — the API surface
|
|
417
434
|
changes and training data may be stale):
|
|
@@ -419,6 +436,52 @@ changes and training data may be stale):
|
|
|
419
436
|
specific call (e.g. etch.blocks.create).
|
|
420
437
|
Manual fallback https://docs.etchwp.com/public-api (window.etch.*)
|
|
421
438
|
https://docs.etchwp.com (everything else)
|
|
439
|
+
|
|
440
|
+
Building Etch pages (AGENT: conventions — follow these or the builder won't
|
|
441
|
+
render what you author):
|
|
442
|
+
Images. ALWAYS use the Dynamic Image element (etch/dynamic-image, <etch:img>),
|
|
443
|
+
NEVER a plain etch/element with tag "img". Bind it by WP media ID, not a URL:
|
|
444
|
+
attributes: { mediaId: "{props.image}", useSrcSet: "true", alt: "{props.name}" }
|
|
445
|
+
The backing prop must be a Media prop, not a text prop:
|
|
446
|
+
type: { primitive: "string", specialized: "wpMediaId" }
|
|
447
|
+
(specialized "image" is the URL-based picker — NOT for mediaId.) Instances
|
|
448
|
+
then pass the numeric media ID, e.g. image: "10" — not the file URL.
|
|
449
|
+
|
|
450
|
+
Classes. A block's classes ARE its read-only \`styles\` array, not an HTML
|
|
451
|
+
attribute. Setting attributes.class in authored JSON is silently dropped, and
|
|
452
|
+
\`styles\` is rejected on authoring. Assign classes with etch.blocks.addClass()
|
|
453
|
+
— for component blocks, inside enterComponentEditMode() →
|
|
454
|
+
saveComponentEditModeAsync() → exitComponentEditMode(). An existing class
|
|
455
|
+
name re-links to its style record automatically (styling is preserved).
|
|
456
|
+
|
|
457
|
+
Sections. When you create a top-level section, always tag it
|
|
458
|
+
data-etch-element="section" and nest ONE container inside it as its inner —
|
|
459
|
+
a div block with data-etch-element="container" — and put the section's
|
|
460
|
+
content inside that container, not directly in the section:
|
|
461
|
+
|
|
462
|
+
section data-etch-element="section"
|
|
463
|
+
└─ div data-etch-element="container" ← content goes here
|
|
464
|
+
└─ … your blocks …
|
|
465
|
+
|
|
466
|
+
section + container carry Etch's default styles, so you get vertical spacing
|
|
467
|
+
/ gutter between sections, horizontal edge padding, and a constrained,
|
|
468
|
+
centered content width for free. Do NOT hand-roll wrapper widths, max-widths,
|
|
469
|
+
side padding, or inter-section margins — the pair already handles layout
|
|
470
|
+
structure, spacing, and content width.
|
|
471
|
+
|
|
472
|
+
Gotchas.
|
|
473
|
+
• NEVER call etch.*.saveAsync (or otherwise persist) unless the user
|
|
474
|
+
explicitly asks to save — authored changes stay in the builder until then.
|
|
475
|
+
• Component instance props are attributes: set them at create()/replace()
|
|
476
|
+
time, or update an existing instance in place with
|
|
477
|
+
etch.blocks.update(instanceId, { attributes: { image: "10" } })
|
|
478
|
+
• updateAsync({ blocks }) requires styles:[] on every element block
|
|
479
|
+
(validation differs from blocks.create(), which omits styles entirely).
|
|
480
|
+
• No etch.media API. Resolve a URL → media ID via the WP REST API:
|
|
481
|
+
curl 'http://<site>/?rest_route=/wp/v2/media&per_page=100&_fields=id,source_url'
|
|
482
|
+
(rest_route form works on plain permalinks; /wp-json/wp/v2/media on pretty
|
|
483
|
+
ones.)
|
|
484
|
+
|
|
422
485
|
See AGENTS.md for project conventions.`);
|
|
423
486
|
}
|
|
424
487
|
async function main() {
|