wield-mcp 0.0.0-stage → 0.1.1

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 ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Wield (wield.run)
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,86 @@
1
- # Temporary Holding Version
1
+ <p align="center"><img src="https://cdn.jsdelivr.net/npm/wield-mcp/assets/wield-mark.svg" width="96" height="96" alt="Wield"></p>
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ # wield-mcp
4
+
5
+ **Websites as tools for your AI agent. Your logins never leave your computer.**
6
+
7
+ Wield gives Claude, Cursor, Codex or any MCP client dozens of websites as fast, reliable tools: train connections, second-hand listings, doctor appointments, food delivery, shopping, housing. Each task is a *recipe*: a reviewed, signed description of the requests a site's own pages make. Wield runs it in a tab of that site **in your own browser, with your own session**, and returns structured data, usually in under a second.
8
+
9
+ - **Private:** requests go from your browser to the website, nowhere else. Passwords, cookies and results never reach Wield's servers; in the site's tab Wield reads only the tokens a recipe names. Every request is logged on your computer.
10
+ - **Safe:** recipes are Ed25519-signed and can only reach the hosts they list. Anything that changes data (booking, sending a message) waits for your **Allow** in the browser, showing exactly what will be sent.
11
+ - **Fast:** no screenshots, no clicking through pages. One recipe, a few requests, structured output.
12
+
13
+ ## Install
14
+
15
+ You need Node.js 20+ and a Chromium browser: Chrome, Edge, Brave, Arc, Vivaldi, Opera or Chromium. Works on macOS, Windows and Linux.
16
+
17
+ ```bash
18
+ npx -y wield-mcp init
19
+ ```
20
+
21
+ `init` does the following:
22
+ - installs Wield into `~/.wield`;
23
+ - registers its bridge with your browsers;
24
+ - asks, app by app, whether to add Wield to Claude Desktop, Claude Code, Cursor and Codex, making a backup of each config file first;
25
+ - shows you how to add the Wield browser extension;
26
+ - runs one live sample, ending in "Wield is ready".
27
+
28
+ **Claude Desktop** also has a one-click bundle at [wield.run/install](https://wield.run/install).
29
+
30
+ ```bash
31
+ npx -y wield-mcp doctor # check every link and get a fix for each problem
32
+ npx -y wield-mcp check # one live sample through your browser
33
+ npx -y wield-mcp demo # three sites, then every host your computer contacted
34
+ ```
35
+
36
+ ### Adding it to an MCP client by hand
37
+
38
+ Run `init` once first (it installs the bridge your browser needs). Then:
39
+
40
+ ```json
41
+ { "mcpServers": { "wield": { "command": "npx", "args": ["-y", "wield-mcp"] } } }
42
+ ```
43
+
44
+ ## Tools
45
+
46
+ | Tool | What it does |
47
+ |---|---|
48
+ | `find_recipe` | Find a ready-made task for what the user wants, in German or English |
49
+ | `run` | Run a recipe in the user's browser and return its structured output. Writes need the user's Allow |
50
+ | `list_sites` | The sites Wield can use, with their tasks and the user's login state |
51
+ | `status` | Is the browser connected; registry, signatures, account and policy |
52
+ | `request_recipe` | Ask for a recipe for a new site, or record the task once to build a private one (Pro/Team) |
53
+
54
+ ## Configuration
55
+
56
+ `~/.wield/config.json`:
57
+
58
+ ```json
59
+ { "registry_url": "https://wield.run/api/v1", "telemetry": false, "country": "DE", "api_key": "wld_…" }
60
+ ```
61
+
62
+ | Setting | Default | Notes |
63
+ |---|---|---|
64
+ | `telemetry` | `false` | When on, each run sends only `{recipe_id, version, ok, duration_ms, error_code}` |
65
+ | `country` | from your time zone | Search ranking prefers sites in this country |
66
+ | `api_key` | none | For private recipes, your organization's policy and the recipe builder (Pro and Team plans) |
67
+
68
+ The same settings can come from environment variables: `WIELD_TELEMETRY`, `WIELD_COUNTRY`, `WIELD_API_KEY`, `WIELD_REGISTRY_URL`, `WIELD_HOME`.
69
+
70
+ ## What leaves your computer
71
+
72
+ | Destination | What |
73
+ |---|---|
74
+ | Your browser → the website | Each recipe's requests, with your session, to the hosts the recipe lists |
75
+ | wield-mcp → wield.run | Downloads of the public recipe index and recipes (signed). With an API key: your organization's private recipes and policy |
76
+ | Nothing else | Telemetry is off unless you turn it on |
77
+
78
+ Logs of both are kept on your computer: `~/.wield/logs/executor-*.ndjson` and `mcp-*.ndjson`. They hold method, host, path, status and timing, never bodies or query values.
79
+
80
+ ## Links
81
+
82
+ - [Documentation](https://wield.run/docs): quick start, how it works, security model, tools, recipe format.
83
+ - [Troubleshooting](https://wield.run/docs/troubleshooting)
84
+ - [Privacy](https://wield.run/privacy)
85
+
86
+ MIT licensed.
@@ -0,0 +1,25 @@
1
+ # Wield setup prompt ("paste into Claude Code")
2
+
3
+ For C6 / the site: render the text between the markers verbatim, with a copy button. It assumes `wield-mcp` is
4
+ published on npm (it is not yet: `private: true` in apps/mcp/package.json until Victor publishes). The terminal
5
+ alternative for people who prefer it is the one-liner at the bottom.
6
+
7
+ <!-- prompt:start -->
8
+ ```text
9
+ Set up Wield (wield.run) on this computer. Wield lets you use websites for me through my own Chrome, with my own logins; my passwords and cookies never leave this machine, and you never need them.
10
+
11
+ 1. Run `node -v`. If Node is missing or older than v20, tell me how to install Node 20+ for my OS and stop.
12
+ 2. Run `npx -y wield-mcp@latest init --no-wait` and show me its output. Then list which AI apps it found (Claude Desktop, Claude Code, Cursor, Codex) and ask me which ones to connect. When I answer, run `npx -y wield-mcp@latest init --yes --no-wait` only if I said all of them; otherwise for each app I chose, add the "Manual config" it printed to that app yourself, backing up the file first. Never touch an app I didn't choose. If it reports that Claude Desktop is running, tell me to quit Claude Desktop (Cmd+Q) and then run the same command again, because Claude Desktop overwrites config edits made while it is open.
13
+ 3. Tell me to load the Wield extension in Chrome: open chrome://extensions, switch on Developer mode, click "Load unpacked", press Cmd+Shift+G (Linux: Ctrl+L), paste ~/.wield/extension, press Return, then Select. The card must show the ID mkgnkapppehppdfjgohaoclabmjgfkgn. Wait until I say it's done.
14
+ 4. Run `~/.wield/bin/wield doctor`. If it reports problems, fix the ones you can and tell me exactly what to click for the rest, then run it again until it says "All good".
15
+ 5. Tell me which apps to restart, and that in a new Claude Code session I can ask things like "find me a train from Berlin to Munich tomorrow" or "Kleinanzeigen bikes in Berlin under 300 €".
16
+
17
+ Do not ask me for any password, and do not log in to any website for me.
18
+ ```
19
+ <!-- prompt:end -->
20
+
21
+ Terminal alternative (asks before changing each app, then waits for the extension):
22
+
23
+ ```bash
24
+ npx -y wield-mcp@latest init
25
+ ```
@@ -0,0 +1,2 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="512" height="512"><defs><linearGradient id="hd" x1="0" y1="0" x2="0" y2="1"><stop offset="0" stop-color="#ffd85a"/><stop offset=".5" stop-color="#ff9a24"/><stop offset="1" stop-color="#ff5200"/></linearGradient>
2
+ <linearGradient id="ht" x1="0" y1="0" x2="0" y2="1"><stop offset="0" stop-color="#ff6a0a"/><stop offset="1" stop-color="#f6ece0"/></linearGradient></defs><rect width="24" height="24" rx="5.4" fill="#16110c"/><g transform="translate(3.4 3.4) scale(0.7166666666666667)"><g transform="translate(0 -2.45)"><path d="M7.0 11.6 2.4 16.6 7.0 21.6M17.0 11.6 21.6 16.6 17.0 21.6" fill="none" stroke="#f6ece0" stroke-width="2.7" stroke-linecap="round" stroke-linejoin="round"/><path d="M11.0 21.6 12.6 10.28" stroke="#f6ece0" stroke-width="2.3" stroke-linecap="round"/><g transform="translate(12.6 10.28) rotate(8.045051779050041)"><rect x="-1.15" y="-.2" width="2.3" height="2.4" fill="url(#ht)"/></g><g transform="translate(12.908 8.102) rotate(8.05) scale(0.84)"><path d="M-4.6 -1.9 -2.9 -1.9 -2.4 -1.5 1.3 -1.5 4.4 -0.55 4.6 -0.2 4.6 0.2 4.4 0.55 1.3 1.5 -2.4 1.5 -2.9 1.9 -4.6 1.9Z" fill="url(#hd)" stroke="url(#hd)" stroke-width=".35" stroke-linejoin="round"/><path d="M-4.2 -1.25H-2.9M-1.9 -.95H1.0" stroke="#fff1b8" stroke-width=".55" stroke-linecap="round" opacity=".85"/><rect x="-1.4" y="1.35" width="2.8" height=".8" rx=".3" fill="#f6ece0"/></g><rect x="7.01" y="5.50" width="1.3" height="1.3" fill="#ffc23d"/><rect x="5.41" y="7.20" width=".9" height=".9" fill="#ff8a1f"/></g></g></svg>
@@ -0,0 +1,2 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="512" height="512"><defs><linearGradient id="hd" x1="0" y1="0" x2="0" y2="1"><stop offset="0" stop-color="#ffd85a"/><stop offset=".5" stop-color="#ff9a24"/><stop offset="1" stop-color="#ff5200"/></linearGradient>
2
+ <linearGradient id="ht" x1="0" y1="0" x2="0" y2="1"><stop offset="0" stop-color="#ff6a0a"/><stop offset="1" stop-color="#16110c"/></linearGradient></defs><g transform="translate(0 -2.45)"><path d="M7.0 11.6 2.4 16.6 7.0 21.6M17.0 11.6 21.6 16.6 17.0 21.6" fill="none" stroke="#16110c" stroke-width="2.7" stroke-linecap="round" stroke-linejoin="round"/><path d="M11.0 21.6 12.6 10.28" stroke="#16110c" stroke-width="2.3" stroke-linecap="round"/><g transform="translate(12.6 10.28) rotate(8.045051779050041)"><rect x="-1.15" y="-.2" width="2.3" height="2.4" fill="url(#ht)"/></g><g transform="translate(12.908 8.102) rotate(8.05) scale(0.84)"><path d="M-4.6 -1.9 -2.9 -1.9 -2.4 -1.5 1.3 -1.5 4.4 -0.55 4.6 -0.2 4.6 0.2 4.4 0.55 1.3 1.5 -2.4 1.5 -2.9 1.9 -4.6 1.9Z" fill="url(#hd)" stroke="url(#hd)" stroke-width=".35" stroke-linejoin="round"/><path d="M-4.2 -1.25H-2.9M-1.9 -.95H1.0" stroke="#fff1b8" stroke-width=".55" stroke-linecap="round" opacity=".85"/><rect x="-1.4" y="1.35" width="2.8" height=".8" rx=".3" fill="#16110c"/></g><rect x="7.01" y="5.50" width="1.3" height="1.3" fill="#ffc23d"/><rect x="5.41" y="7.20" width=".9" height=".9" fill="#ff8a1f"/></g></svg>
package/dist/bridge.js ADDED
@@ -0,0 +1,433 @@
1
+ // src/bridge.ts
2
+ import { appendFileSync, chmodSync, existsSync, lstatSync, mkdirSync, unlinkSync, writeFileSync } from "fs";
3
+ import { createConnection, createServer } from "net";
4
+ import { basename, dirname, join as join2 } from "path";
5
+
6
+ // src/native.ts
7
+ var MAX_TO_EXTENSION = 1024 * 1024;
8
+ var MAX_FROM_EXTENSION = 64 * 1024 * 1024;
9
+ function encodeNative(msg) {
10
+ const json = Buffer.from(JSON.stringify(msg), "utf8");
11
+ const header = Buffer.alloc(4);
12
+ header.writeUInt32LE(json.length, 0);
13
+ return Buffer.concat([header, json]);
14
+ }
15
+ function readNative(input, onMessage, onError) {
16
+ let buf = Buffer.alloc(0);
17
+ input.on("data", (chunk) => {
18
+ buf = buf.length ? Buffer.concat([buf, chunk]) : chunk;
19
+ for (; ; ) {
20
+ if (buf.length < 4) return;
21
+ const len = buf.readUInt32LE(0);
22
+ if (len > MAX_FROM_EXTENSION) {
23
+ onError(new Error(`native message too large (${len} bytes)`));
24
+ buf = Buffer.alloc(0);
25
+ return;
26
+ }
27
+ if (buf.length < 4 + len) return;
28
+ const body = buf.subarray(4, 4 + len);
29
+ buf = buf.subarray(4 + len);
30
+ let msg;
31
+ try {
32
+ msg = JSON.parse(body.toString("utf8"));
33
+ } catch (e) {
34
+ onError(new Error(`invalid JSON from extension: ${e.message}`));
35
+ continue;
36
+ }
37
+ onMessage(msg);
38
+ }
39
+ });
40
+ }
41
+ function makeNativeWriter(output) {
42
+ return (msg) => {
43
+ const frame = encodeNative(msg);
44
+ if (frame.length - 4 > MAX_TO_EXTENSION) return { ok: false, size: frame.length - 4 };
45
+ output.write(frame);
46
+ return { ok: true };
47
+ };
48
+ }
49
+
50
+ // src/paths.ts
51
+ import { createHash } from "crypto";
52
+ import { homedir, userInfo } from "os";
53
+ import { join } from "path";
54
+ function wieldHome() {
55
+ return process.env.WIELD_HOME || join(homedir(), ".wield");
56
+ }
57
+ function expandHome(p) {
58
+ return p.startsWith("~/") ? join(homedir(), p.slice(2)) : p;
59
+ }
60
+ function defaultSocketPath(home, platform = process.platform) {
61
+ if (platform === "win32") {
62
+ let user = "user";
63
+ try {
64
+ user = userInfo().username.replace(/[^A-Za-z0-9_.-]/g, "_").slice(0, 40) || "user";
65
+ } catch {
66
+ }
67
+ return `\\\\.\\pipe\\wield-${user}-${createHash("sha256").update(home.toLowerCase()).digest("hex").slice(0, 12)}`;
68
+ }
69
+ const p = join(home, "bridge.sock");
70
+ if (Buffer.byteLength(p) <= MAX_SOCKET_PATH) return p;
71
+ const uid = typeof process.getuid === "function" ? process.getuid() : 0;
72
+ const dir = process.env.XDG_RUNTIME_DIR && process.env.XDG_RUNTIME_DIR.length < 40 ? process.env.XDG_RUNTIME_DIR : "/tmp";
73
+ return join(dir, `wield-${uid}-${createHash("sha256").update(home).digest("hex").slice(0, 12)}.sock`);
74
+ }
75
+ var MAX_SOCKET_PATH = 100;
76
+ var isPipe = (p) => p.startsWith("\\\\.\\pipe\\");
77
+ function socketPath() {
78
+ return expandHome(process.env.WIELD_SOCKET || defaultSocketPath(wieldHome()));
79
+ }
80
+ function logDir() {
81
+ return join(wieldHome(), "logs");
82
+ }
83
+ function day(d = /* @__PURE__ */ new Date()) {
84
+ const p = (n) => String(n).padStart(2, "0");
85
+ return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())}`;
86
+ }
87
+
88
+ // src/bridge.ts
89
+ var EVENT_DATA_KEYS = ["method", "origin", "path", "status", "ms", "bytes"];
90
+ var REQUESTS = { status: "status_result", policy: "policy_ack", index: "index_ack", record_start: "recording" };
91
+ var REPLIES = new Set(Object.values(REQUESTS));
92
+ var NOTIFY = /* @__PURE__ */ new Set(["warm"]);
93
+ var REC_ID = /^[A-Za-z0-9_-]{1,64}$/;
94
+ var Bridge = class {
95
+ constructor(o) {
96
+ this.o = o;
97
+ this.toExtension = makeNativeWriter(o.output);
98
+ }
99
+ o;
100
+ server = null;
101
+ clients = /* @__PURE__ */ new Set();
102
+ runOwner = /* @__PURE__ */ new Map();
103
+ reqOwner = /* @__PURE__ */ new Map();
104
+ helloQueue = [];
105
+ lastHelloAck = null;
106
+ nextId = 1;
107
+ closed = false;
108
+ toExtension;
109
+ async start() {
110
+ mkdirSync(this.o.logDir, { recursive: true, mode: 448 });
111
+ const pipe = isPipe(this.o.socketPath);
112
+ if (!pipe) {
113
+ const dir = dirname(this.o.socketPath);
114
+ const created = mkdirSync(dir, { recursive: true, mode: 448 });
115
+ if (created || basename(dir) === ".wield" || dir === dirname(this.o.logDir)) {
116
+ try {
117
+ chmodSync(dir, 448);
118
+ } catch {
119
+ }
120
+ }
121
+ await this.clearStaleSocket();
122
+ }
123
+ readNative(
124
+ this.o.input,
125
+ (m) => this.fromExtension(m),
126
+ (err) => this.debug(`native: ${err.message}`)
127
+ );
128
+ this.o.input.on("end", () => void this.close("Chrome disconnected"));
129
+ this.o.input.on("close", () => void this.close("Chrome disconnected"));
130
+ this.server = createServer((sock) => this.onClient(sock));
131
+ const oldMask = process.umask(127);
132
+ try {
133
+ await new Promise((resolve, reject) => {
134
+ this.server.once("error", reject);
135
+ this.server.listen(this.o.socketPath, () => resolve());
136
+ });
137
+ } finally {
138
+ process.umask(oldMask);
139
+ }
140
+ if (!pipe) chmodSync(this.o.socketPath, 384);
141
+ this.log({ kind: "bridge", msg: `bridge started (pid ${process.pid})` });
142
+ this.debug(`listening on ${this.o.socketPath}`);
143
+ }
144
+ async close(reason = "bridge stopped") {
145
+ if (this.closed) return;
146
+ this.closed = true;
147
+ for (const h of this.helloQueue) clearTimeout(h.timer);
148
+ this.helloQueue = [];
149
+ for (const c of this.clients) {
150
+ this.send(c, { type: "error", code: "executor_offline", message: reason });
151
+ c.sock.end();
152
+ }
153
+ this.clients.clear();
154
+ await new Promise((r) => this.server ? this.server.close(() => r()) : r());
155
+ try {
156
+ if (!isPipe(this.o.socketPath) && existsSync(this.o.socketPath) && lstatSync(this.o.socketPath).isSocket()) unlinkSync(this.o.socketPath);
157
+ } catch {
158
+ }
159
+ this.log({ kind: "bridge", msg: `bridge stopped: ${reason}` });
160
+ }
161
+ // ---------------------------------------------------------------------------
162
+ async clearStaleSocket() {
163
+ const p = this.o.socketPath;
164
+ let st;
165
+ try {
166
+ st = lstatSync(p);
167
+ } catch {
168
+ return;
169
+ }
170
+ if (!st.isSocket()) throw new Error(`${p} exists and is not a socket; refusing to remove it`);
171
+ const live = await new Promise((resolve) => {
172
+ const s = createConnection(p);
173
+ s.once("connect", () => {
174
+ s.destroy();
175
+ resolve(true);
176
+ });
177
+ s.once("error", () => resolve(false));
178
+ });
179
+ this.debug(live ? `taking over live socket ${p}` : `removing stale socket ${p}`);
180
+ unlinkSync(p);
181
+ }
182
+ onClient(sock) {
183
+ const client = { id: this.nextId++, sock, runs: /* @__PURE__ */ new Set(), reqs: /* @__PURE__ */ new Set() };
184
+ this.clients.add(client);
185
+ sock.setEncoding("utf8");
186
+ let buf = "";
187
+ sock.on("data", (chunk) => {
188
+ buf += chunk;
189
+ if (buf.length > 4 * 1024 * 1024) {
190
+ this.send(client, { type: "error", code: "bad_message", message: "line too long" });
191
+ sock.destroy();
192
+ return;
193
+ }
194
+ let nl;
195
+ while ((nl = buf.indexOf("\n")) >= 0) {
196
+ const line = buf.slice(0, nl).trim();
197
+ buf = buf.slice(nl + 1);
198
+ if (!line) continue;
199
+ let msg;
200
+ try {
201
+ msg = JSON.parse(line);
202
+ } catch {
203
+ this.send(client, { type: "error", code: "bad_message", message: "invalid JSON" });
204
+ continue;
205
+ }
206
+ this.fromClient(client, msg);
207
+ }
208
+ });
209
+ const gone = () => this.dropClient(client);
210
+ sock.on("close", gone);
211
+ sock.on("error", gone);
212
+ }
213
+ dropClient(c) {
214
+ if (!this.clients.delete(c)) return;
215
+ for (const runId of c.runs) {
216
+ this.runOwner.delete(runId);
217
+ if (!this.closed) this.toExtension({ type: "cancel", run_id: runId });
218
+ }
219
+ for (const reqId of c.reqs) this.reqOwner.delete(reqId);
220
+ this.helloQueue = this.helloQueue.filter((h) => {
221
+ if (h.client === c) clearTimeout(h.timer);
222
+ return h.client !== c;
223
+ });
224
+ }
225
+ send(c, msg) {
226
+ if (c.sock.destroyed || !c.sock.writable) return;
227
+ c.sock.write(JSON.stringify(msg) + "\n");
228
+ }
229
+ // -- client → extension -------------------------------------------------------
230
+ fromClient(c, m) {
231
+ switch (m.type) {
232
+ case "hello": {
233
+ const timer = setTimeout(() => {
234
+ const i = this.helloQueue.findIndex((h) => h.timer === timer);
235
+ if (i >= 0) this.helloQueue.splice(i, 1);
236
+ this.send(c, { type: "error", code: "executor_offline", message: "the extension did not answer hello" });
237
+ }, this.o.helloTimeoutMs ?? 5e3);
238
+ this.helloQueue.push({ client: c, timer });
239
+ this.forward(c, m);
240
+ return;
241
+ }
242
+ case "run": {
243
+ const runId = typeof m.run_id === "string" ? m.run_id : "";
244
+ if (!runId) return this.send(c, { type: "error", code: "bad_message", message: "run needs run_id" });
245
+ if (this.runOwner.has(runId)) return this.send(c, { type: "error", code: "bad_message", run_id: runId, message: "duplicate run_id" });
246
+ const recipe = m.recipe ?? {};
247
+ const res = this.toExtension(m);
248
+ if (!res.ok) {
249
+ this.log({ kind: "run", run_id: runId, msg: `refused ${String(recipe.id)}: message too large (${res.size} bytes)` });
250
+ return this.send(c, { type: "error", code: "recipe_too_large", run_id: runId, message: `run message is ${res.size} bytes; Chrome allows 1 MB` });
251
+ }
252
+ this.runOwner.set(runId, c);
253
+ c.runs.add(runId);
254
+ this.log({ kind: "run", run_id: runId, msg: `run ${String(recipe.id)} v${String(recipe.version)}`, recipe_id: recipe.id, version: recipe.version, recipe_sha256: m.recipe_sha256 });
255
+ return;
256
+ }
257
+ case "cancel": {
258
+ const runId = typeof m.run_id === "string" ? m.run_id : "";
259
+ if (this.runOwner.get(runId) === c) this.forward(c, { type: "cancel", run_id: runId });
260
+ return;
261
+ }
262
+ default: {
263
+ const type = String(m.type);
264
+ if (type in REQUESTS) {
265
+ const reqId = typeof m.req_id === "string" ? m.req_id : "";
266
+ if (!reqId) return this.send(c, { type: "error", code: "bad_message", message: `${type} needs req_id` });
267
+ const owner = this.reqOwner.get(reqId);
268
+ if (owner && owner !== c) return this.send(c, { type: "error", code: "bad_message", req_id: reqId, message: "req_id already in use" });
269
+ this.reqOwner.set(reqId, c);
270
+ c.reqs.add(reqId);
271
+ if (type === "record_start") this.log({ kind: "record", run_id: reqId, msg: `record_start ${String(m.origin ?? "")}`.trim() });
272
+ this.forward(c, m);
273
+ return;
274
+ }
275
+ if (NOTIFY.has(type)) return this.forward(c, m);
276
+ this.send(c, { type: "error", code: "bad_message", message: `unknown message type ${JSON.stringify(m.type)}` });
277
+ }
278
+ }
279
+ }
280
+ forward(c, m) {
281
+ const res = this.toExtension(m);
282
+ if (!res.ok) this.send(c, { type: "error", code: "recipe_too_large", message: `message is ${res.size} bytes; Chrome allows 1 MB` });
283
+ }
284
+ // -- extension → client -------------------------------------------------------
285
+ fromExtension(m) {
286
+ if (!m || typeof m !== "object") return;
287
+ const runId = typeof m.run_id === "string" ? m.run_id : void 0;
288
+ const reqId = typeof m.req_id === "string" ? m.req_id : void 0;
289
+ switch (m.type) {
290
+ case "hello_ack": {
291
+ this.lastHelloAck = m;
292
+ const h = this.helloQueue.shift();
293
+ if (h) {
294
+ clearTimeout(h.timer);
295
+ this.send(h.client, m);
296
+ }
297
+ return;
298
+ }
299
+ case "event": {
300
+ this.logEvent(m);
301
+ const c = runId ? this.runOwner.get(runId) ?? this.reqOwner.get(runId) : void 0;
302
+ if (c) this.send(c, m);
303
+ return;
304
+ }
305
+ case "result": {
306
+ this.log({
307
+ kind: "result",
308
+ run_id: runId,
309
+ level: m.ok ? "info" : "error",
310
+ msg: m.ok ? `ok in ${String(m.duration_ms)} ms (${String(m.requests)} requests)` : `failed: ${String(m.error?.code)}`,
311
+ ok: m.ok === true,
312
+ duration_ms: m.duration_ms,
313
+ requests: m.requests,
314
+ ...m.ok ? {} : { error_code: m.error?.code }
315
+ });
316
+ const c = runId ? this.runOwner.get(runId) : void 0;
317
+ if (runId) {
318
+ this.runOwner.delete(runId);
319
+ c?.runs.delete(runId);
320
+ }
321
+ if (c) this.send(c, m);
322
+ return;
323
+ }
324
+ case "recording": {
325
+ const c = reqId ? this.reqOwner.get(reqId) : void 0;
326
+ if (reqId) {
327
+ this.reqOwner.delete(reqId);
328
+ c?.reqs.delete(reqId);
329
+ }
330
+ const trace = m.trace ?? {};
331
+ const entries = Array.isArray(trace.entries) ? trace.entries.length : Array.isArray(trace.requests) ? trace.requests.length : void 0;
332
+ const origin = typeof trace.origin === "string" ? trace.origin : typeof trace.site?.origin === "string" ? String(trace.site.origin) : void 0;
333
+ const recId = typeof m.rec_id === "string" && REC_ID.test(m.rec_id) ? m.rec_id : void 0;
334
+ let saved;
335
+ if (!c && recId && !m.discarded) saved = this.saveRecording(recId, m);
336
+ this.log({
337
+ kind: "record",
338
+ run_id: reqId,
339
+ msg: m.discarded ? `recording ${recId ?? "?"} discarded` : `recording ${recId ?? "?"}${origin ? ` on ${origin}` : ""}${entries !== void 0 ? `, ${entries} entries` : ""}${saved ? ", saved" : ""}`
340
+ });
341
+ if (c) this.send(c, m);
342
+ return;
343
+ }
344
+ default: {
345
+ if (REPLIES.has(String(m.type))) {
346
+ const c = reqId ? this.reqOwner.get(reqId) : void 0;
347
+ if (reqId) {
348
+ this.reqOwner.delete(reqId);
349
+ c?.reqs.delete(reqId);
350
+ }
351
+ if (c) this.send(c, m);
352
+ return;
353
+ }
354
+ this.debug(`ignoring extension message type ${JSON.stringify(m.type)}`);
355
+ return;
356
+ }
357
+ case "error": {
358
+ const c = runId && this.runOwner.get(runId) || reqId && this.reqOwner.get(reqId);
359
+ if (c) this.send(c, m);
360
+ else this.log({ kind: "error", level: "warn", msg: `extension error for no client: ${String(m.code ?? "")}`.trim() });
361
+ return;
362
+ }
363
+ }
364
+ }
365
+ saveRecording(recId, m) {
366
+ const dir = this.o.recordingsDir ?? join2(dirname(this.o.logDir), "recordings");
367
+ try {
368
+ mkdirSync(dir, { recursive: true, mode: 448 });
369
+ const file = join2(dir, `${recId}.json`);
370
+ writeFileSync(file, JSON.stringify({ rec_id: recId, saved_at: (/* @__PURE__ */ new Date()).toISOString(), trace: m.trace }), { mode: 384 });
371
+ return file;
372
+ } catch (e) {
373
+ this.debug(`could not save recording ${recId}: ${e.message}`);
374
+ return void 0;
375
+ }
376
+ }
377
+ // -- logs -----------------------------------------------------------------------
378
+ /** Events are logged with only the protocol-allowed fields, whatever the extension sent. */
379
+ logEvent(m) {
380
+ const data = m.data ?? {};
381
+ const clean = {};
382
+ for (const k of EVENT_DATA_KEYS) if (data[k] !== void 0) clean[k] = data[k];
383
+ this.log({
384
+ ts: typeof m.ts === "string" ? m.ts : void 0,
385
+ run_id: m.run_id,
386
+ level: m.level,
387
+ kind: m.kind,
388
+ msg: typeof m.msg === "string" ? m.msg.slice(0, 500) : void 0,
389
+ ...Object.keys(clean).length ? { data: clean } : {}
390
+ });
391
+ }
392
+ log(entry) {
393
+ const line = JSON.stringify({ ts: (/* @__PURE__ */ new Date()).toISOString(), ...stripUndefined(entry) }) + "\n";
394
+ try {
395
+ appendFileSync(join2(this.o.logDir, `executor-${day()}.ndjson`), line, { mode: 384 });
396
+ } catch (e) {
397
+ this.debug(`log write failed: ${e.message}`);
398
+ }
399
+ }
400
+ debug(s) {
401
+ this.o.debug?.(s);
402
+ }
403
+ /** For tests. */
404
+ get helloAck() {
405
+ return this.lastHelloAck;
406
+ }
407
+ };
408
+ function stripUndefined(o) {
409
+ const out = {};
410
+ for (const [k, v] of Object.entries(o)) if (v !== void 0) out[k] = v;
411
+ return out;
412
+ }
413
+
414
+ // src/index.ts
415
+ var debug = process.env.WIELD_BRIDGE_DEBUG ? (s) => process.stderr.write(`[wield-bridge] ${s}
416
+ `) : void 0;
417
+ var bridge = new Bridge({ input: process.stdin, output: process.stdout, socketPath: socketPath(), logDir: logDir(), debug });
418
+ var stop = (reason) => {
419
+ void bridge.close(reason).finally(() => process.exit(0));
420
+ };
421
+ process.stdin.on("end", () => stop("Chrome disconnected"));
422
+ process.on("SIGTERM", () => stop("SIGTERM"));
423
+ process.on("SIGINT", () => stop("SIGINT"));
424
+ process.on("uncaughtException", (e) => {
425
+ process.stderr.write(`[wield-bridge] fatal: ${e.stack ?? e}
426
+ `);
427
+ stop("crash");
428
+ });
429
+ bridge.start().catch((e) => {
430
+ process.stderr.write(`[wield-bridge] cannot start: ${e.message}
431
+ `);
432
+ process.exit(1);
433
+ });
@@ -0,0 +1 @@
1
+ {"build":"0.1.13+20261107213428.da3a"}
@@ -0,0 +1,56 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1">
6
+ <title>Wield · Allow?</title>
7
+ <link rel="stylesheet" href="tokens.css">
8
+ <link rel="stylesheet" href="ui.css">
9
+ </head>
10
+ <!-- Classes are semantic (ask-*), so the look can change in ui.css alone. -->
11
+ <body class="ask" data-kind="write">
12
+ <header class="top ask-head">
13
+ <span class="brand" aria-label="Wield">
14
+ <svg viewBox="0 0 24 24" aria-hidden="true"><defs><linearGradient id="wm-hd" x1="0" y1="0" x2="0" y2="1"><stop offset="0" stop-color="#ffd85a"/><stop offset=".5" stop-color="#ff9a24"/><stop offset="1" stop-color="#ff5200"/></linearGradient><linearGradient id="wm-ht" x1="0" y1="0" x2="0" y2="1"><stop offset="0" stop-color="#ff6a0a"/><stop offset="1" stop-color="currentColor"/></linearGradient></defs><g transform="translate(0 -2.45)"><path d="M7.0 11.6 2.4 16.6 7.0 21.6M17.0 11.6 21.6 16.6 17.0 21.6" fill="none" stroke="currentColor" stroke-width="2.7" stroke-linecap="round" stroke-linejoin="round"/><path d="M11.0 21.6 12.6 10.28" stroke="currentColor" stroke-width="2.3" stroke-linecap="round"/><g transform="translate(12.6 10.28) rotate(8.045051779050041)"><rect x="-1.15" y="-.2" width="2.3" height="2.4" fill="url(#wm-ht)"/></g><g transform="translate(12.908 8.102) rotate(8.05) scale(0.84)"><path d="M-4.6 -1.9 -2.9 -1.9 -2.4 -1.5 1.3 -1.5 4.4 -0.55 4.6 -0.2 4.6 0.2 4.4 0.55 1.3 1.5 -2.4 1.5 -2.9 1.9 -4.6 1.9Z" fill="url(#wm-hd)" stroke="url(#wm-hd)" stroke-width=".35" stroke-linejoin="round"/><path d="M-4.2 -1.25H-2.9M-1.9 -.95H1.0" stroke="#fff1b8" stroke-width=".55" stroke-linecap="round" opacity=".85"/><rect x="-1.4" y="1.35" width="2.8" height=".8" rx=".3" fill="currentColor"/></g><rect x="7.01" y="5.50" width="1.3" height="1.3" fill="#ffc23d"/><rect x="5.41" y="7.20" width=".9" height=".9" fill="#ff8a1f"/></g></svg>
15
+ <b>wield</b>
16
+ </span>
17
+ <span class="badge plain ask-kind" id="kind">Needs your OK</span>
18
+ </header>
19
+
20
+ <main class="ask-body" id="main">
21
+ <p class="label ask-eyebrow" id="eyebrow">Your agent wants to</p>
22
+ <h1 class="ask-title" id="title"></h1>
23
+ <p class="ask-site" id="site"></p>
24
+
25
+ <section class="ask-section" id="write" hidden aria-labelledby="h-sent">
26
+ <h2 class="label" id="h-sent">What gets sent</h2>
27
+ <ul class="ask-writes" id="writes"></ul>
28
+ </section>
29
+
30
+ <section class="ask-section" id="perm" hidden>
31
+ <p class="ask-explain" id="perm-text"></p>
32
+ <p class="ask-explain" id="perm-chrome">Chrome asks you to confirm next. You can remove access any time in chrome://extensions.</p>
33
+ </section>
34
+
35
+ <section class="ask-section" id="draft" hidden aria-labelledby="h-draft">
36
+ <p class="ask-warning" id="draft-warning" role="alert" hidden></p>
37
+ <h2 class="label" id="h-draft">Sites it may contact</h2>
38
+ <ul class="ask-fields" id="draft-origins"></ul>
39
+ <h2 class="label">Requests</h2>
40
+ <ul class="ask-writes" id="draft-steps"></ul>
41
+ </section>
42
+
43
+ <p class="ask-note" id="note"></p>
44
+ </main>
45
+
46
+ <footer class="ask-foot">
47
+ <div class="ask-timer" aria-hidden="true"><span id="timer-bar"></span></div>
48
+ <p class="ask-countdown num" id="countdown" aria-live="polite"></p>
49
+ <div class="ask-actions">
50
+ <button class="btn btn-secondary ask-deny" id="deny" type="button" disabled>Deny</button>
51
+ <button class="btn btn-primary ask-allow" id="allow" type="button" disabled>Allow</button>
52
+ </div>
53
+ </footer>
54
+ <script src="confirm.js"></script>
55
+ </body>
56
+ </html>