@anynotate/protocol 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/PROTOCOL.md +28 -2
- package/dist/fixtures.d.ts +11 -0
- package/dist/fixtures.js +10 -2
- package/dist/index.d.ts +45 -0
- package/dist/index.js +11 -1
- package/package.json +1 -1
- package/src/fixtures.ts +10 -2
- package/src/index.ts +13 -1
package/PROTOCOL.md
CHANGED
|
@@ -4,7 +4,7 @@ The bridge is an HTTP server on `127.0.0.1`, port `47291` by default (`ANYNOTATE
|
|
|
4
4
|
|
|
5
5
|
## Handshake
|
|
6
6
|
|
|
7
|
-
`POST /health` (or `GET`) → `{ "ok": true, "bridgeVersion": "0.
|
|
7
|
+
`POST /health` (or `GET`) → `{ "ok": true, "bridgeVersion": "0.6.1", "protocol": { "version": 2, "min": 1 } }`.
|
|
8
8
|
|
|
9
9
|
A client supports a range of protocol versions (the extension: `min 2, max 2`) and calls `compatibility(clientRange, response.protocol)`:
|
|
10
10
|
|
|
@@ -40,7 +40,7 @@ The allow-list is the valid `chrome-extension://` entries from `$ANYNOTATE_HOME/
|
|
|
40
40
|
| Method and path | Body | Response |
|
|
41
41
|
|---|---|---|
|
|
42
42
|
| `GET` / `POST /health` | — | `HealthResponse` |
|
|
43
|
-
| `GET` / `POST /sessions` | — | `Session[]`: live push sessions, herdr panes (`method: "herdr"`), then sessions seen by the prompt hook outside herdr (`method: "next-prompt"
|
|
43
|
+
| `GET` / `POST /sessions` | — | `Session[]`: live push sessions, herdr panes (`method: "herdr"`), then sessions seen by the prompt hook outside herdr (`method: "next-prompt"`, with the agent's own conversation title when the bridge can read it) |
|
|
44
44
|
| `POST /bundles` | `multipart/form-data`: field `bundle` = JSON `BundleInput`; files `page.md`, `screenshot.png`, optional `snapshot.html`, `crops/A<n>.png` | `201 SendResponse` (`{ id, status }`); `400 ErrorResponse` for an invalid bundle or file name |
|
|
45
45
|
| `POST /bundles/<id>/status`, `GET /bundles/<id>` | — | `BundleStatusResponse` (`{ bundle, status }`; `status` is `null` if the status file is missing or unreadable); `404` if unknown |
|
|
46
46
|
| `POST /bundles/<id>/ack` | `AckRequest` (`{ "summary"?: string }`) | `OkResponse`; `404` if unknown or busy |
|
|
@@ -78,6 +78,28 @@ Each annotation may carry an `intent` telling the agent what the user wants. `IN
|
|
|
78
78
|
|
|
79
79
|
The v1 values `question`, `bug` and `note` are still accepted and appear in the heading as before, with no directive line. `change` was also a v1 value; it keeps its name and now gets the directive line. No `intent` means no directive either.
|
|
80
80
|
|
|
81
|
+
### Region notes
|
|
82
|
+
|
|
83
|
+
On pages that draw into a `<canvas>` (online documents, design tools, maps), there is no text or element under the user's pointer worth selecting, so the user draws a rectangle instead. Such a note is an `element` annotation whose `element` is the node under the rectangle (usually the `<canvas>`) and which carries an optional `region`:
|
|
84
|
+
|
|
85
|
+
```ts
|
|
86
|
+
region?: { x: number; y: number; w: number; h: number }
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
The rectangle is in CSS pixels, relative to the viewport at capture time; add `viewport.scrollY` for the page offset. `w` and `h` are non-negative. `box` still describes the whole element, and `crops/A<n>.png` shows just the rectangle. `README.md` adds a line under the note's heading, such as `Region: 420×180 at (310, 96) on <canvas#board> · see crops/A3.png`.
|
|
90
|
+
|
|
91
|
+
`region` is only valid on `kind: "element"`; a `text` annotation with a `region` is rejected. It is an additive field: a bridge that predates it parses the annotation as a plain element note and drops `region`, so the agent still gets the comment, element and crop.
|
|
92
|
+
|
|
93
|
+
### Notes inside frames
|
|
94
|
+
|
|
95
|
+
A note made on text or an element inside a same-origin `<iframe>` carries the frames on its anchor:
|
|
96
|
+
|
|
97
|
+
```ts
|
|
98
|
+
anchor.frames?: string[]
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
Each entry is a CSS selector for an `<iframe>`, outermost first, resolved in the document of the frame before it (the first in the top page). `css`, `hosts`, `path` and `quote` then refer to the innermost frame's document, while `box` and `region` stay in the top page's viewport. `README.md` names the frames after the note's selector, such as ``selector: `p#inner` inside iframe `iframe#card` ``. It is additive: a bridge that predates it drops `frames`, and the agent gets a selector without the frame it lives in.
|
|
102
|
+
|
|
81
103
|
## Evolution
|
|
82
104
|
|
|
83
105
|
- Additive changes — optional fields, new endpoints — keep `PROTOCOL_VERSION`. Both sides ignore fields they don't know.
|
|
@@ -88,6 +110,10 @@ The v1 values `question`, `bug` and `note` are still accepted and appear in the
|
|
|
88
110
|
|
|
89
111
|
- **v1** — handshake, token-only access, native-host token delivery.
|
|
90
112
|
- **v2** — intents `explain`, `change`, `approve` (legacy `question`, `bug`, `note` still accepted). `PROTOCOL_MIN` stays `1`: a v2 bridge accepts v1 clients.
|
|
113
|
+
- Optional `region` on element annotations (additive, `@anynotate/protocol` 0.4.0, bridge 0.5.1).
|
|
114
|
+
- Optional `offscreen: true` on any annotation whose target was not on the page at send time (`box` and crop are from when the note was made); `README.md` puts "Not on the page when sent — crop is from when the note was made." under its heading (without the crop clause when no crop was sent; additive, same releases; wording since bridge 0.6.1).
|
|
115
|
+
- Optional `anchor.frames` on notes made inside same-origin iframes; `README.md` names the frames after the selector (additive, same releases).
|
|
116
|
+
- Optional `workspace` on `Session`: the display name of the herdr workspace holding a herdr pane (additive, `@anynotate/protocol` 0.5.0, bridge 0.6.1).
|
|
91
117
|
|
|
92
118
|
## Threat model
|
|
93
119
|
|
package/dist/fixtures.d.ts
CHANGED
|
@@ -25,6 +25,7 @@ export declare const responseSchemas: {
|
|
|
25
25
|
}>;
|
|
26
26
|
pane: z.ZodOptional<z.ZodString>;
|
|
27
27
|
sessionIds: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
28
|
+
workspace: z.ZodOptional<z.ZodString>;
|
|
28
29
|
}, z.core.$strip>>;
|
|
29
30
|
SendResponse: z.ZodObject<{
|
|
30
31
|
id: z.ZodString;
|
|
@@ -42,6 +43,7 @@ export declare const responseSchemas: {
|
|
|
42
43
|
pull: "pull";
|
|
43
44
|
}>>;
|
|
44
45
|
session: z.ZodOptional<z.ZodString>;
|
|
46
|
+
agent: z.ZodOptional<z.ZodString>;
|
|
45
47
|
summary: z.ZodOptional<z.ZodString>;
|
|
46
48
|
note: z.ZodOptional<z.ZodString>;
|
|
47
49
|
}, z.core.$strip>;
|
|
@@ -88,6 +90,7 @@ export declare const responseSchemas: {
|
|
|
88
90
|
path: z.ZodArray<z.ZodString>;
|
|
89
91
|
near: z.ZodString;
|
|
90
92
|
hosts: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
93
|
+
frames: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
91
94
|
}, z.core.$strip>;
|
|
92
95
|
element: z.ZodOptional<z.ZodObject<{
|
|
93
96
|
tag: z.ZodString;
|
|
@@ -110,6 +113,13 @@ export declare const responseSchemas: {
|
|
|
110
113
|
scrollY: z.ZodNumber;
|
|
111
114
|
}, z.core.$strip>;
|
|
112
115
|
crop: z.ZodString;
|
|
116
|
+
region: z.ZodOptional<z.ZodObject<{
|
|
117
|
+
x: z.ZodNumber;
|
|
118
|
+
y: z.ZodNumber;
|
|
119
|
+
w: z.ZodNumber;
|
|
120
|
+
h: z.ZodNumber;
|
|
121
|
+
}, z.core.$strip>>;
|
|
122
|
+
offscreen: z.ZodOptional<z.ZodBoolean>;
|
|
113
123
|
}, z.core.$strip>>;
|
|
114
124
|
id: z.ZodString;
|
|
115
125
|
files: z.ZodObject<{
|
|
@@ -132,6 +142,7 @@ export declare const responseSchemas: {
|
|
|
132
142
|
pull: "pull";
|
|
133
143
|
}>>;
|
|
134
144
|
session: z.ZodOptional<z.ZodString>;
|
|
145
|
+
agent: z.ZodOptional<z.ZodString>;
|
|
135
146
|
summary: z.ZodOptional<z.ZodString>;
|
|
136
147
|
note: z.ZodOptional<z.ZodString>;
|
|
137
148
|
}, z.core.$strip>>;
|
package/dist/fixtures.js
CHANGED
|
@@ -19,19 +19,27 @@ export const exampleBundleInput = {
|
|
|
19
19
|
box: { x: 10, y: 20, w: 100, h: 18 },
|
|
20
20
|
viewport: { w: 1280, h: 800, dpr: 2, scrollY: 0 },
|
|
21
21
|
crop: "crops/A1.png",
|
|
22
|
+
}, {
|
|
23
|
+
id: "A2", kind: "element", comment: "make this bowl bigger", intent: "change",
|
|
24
|
+
anchor: { css: "canvas#plating", path: ["main", "figure"], near: "Serving" },
|
|
25
|
+
element: { tag: "canvas", text: "", html: '<canvas id="plating" width="800" height="600"></canvas>', attrs: { id: "plating" } },
|
|
26
|
+
box: { x: 40, y: 300, w: 800, h: 600 },
|
|
27
|
+
viewport: { w: 1280, h: 800, dpr: 2, scrollY: 0 },
|
|
28
|
+
crop: "crops/A2.png",
|
|
29
|
+
region: { x: 310, y: 420, w: 220, h: 140 },
|
|
22
30
|
}],
|
|
23
31
|
};
|
|
24
32
|
export const examplePage = "# Tomato soup\n\nstir in ⟦A1⟧200 ml cream before serving\n";
|
|
25
33
|
export const responseSchemas = {
|
|
26
34
|
HealthResponse, SessionsResponse, SendResponse, BundleStatusResponse, OkResponse, ErrorResponse,
|
|
27
35
|
};
|
|
28
|
-
const health = { ok: true, bridgeVersion: "0.
|
|
36
|
+
const health = { ok: true, bridgeVersion: "0.6.1", protocol: { version: 2, min: 1 } };
|
|
29
37
|
const queued = { state: "queued", at: "2026-09-24T15:32:01.000Z" };
|
|
30
38
|
const exampleBundle = { ...exampleBundleInput, id: EXAMPLE_BUNDLE_ID, files: { page: "page.md", screenshot: "screenshot.png" } };
|
|
31
39
|
export const conformance = [
|
|
32
40
|
{ name: "health-extension", request: { method: "POST", path: "/health", auth: "extension" }, status: 200, response: { schema: "HealthResponse", example: health } },
|
|
33
41
|
{ name: "health-probe", request: { method: "GET", path: "/health", auth: "none" }, status: 200, response: { schema: "HealthResponse", example: health } },
|
|
34
|
-
{ name: "sessions-extension", request: { method: "POST", path: "/sessions", auth: "extension" }, status: 200, response: { schema: "SessionsResponse", example: [{ id: "w1:p2", agent: "claude", cwd: "/home/me/project", title: "shell", method: "herdr", pane: "w1:p2" }] } },
|
|
42
|
+
{ name: "sessions-extension", request: { method: "POST", path: "/sessions", auth: "extension" }, status: 200, response: { schema: "SessionsResponse", example: [{ id: "w1:p2", agent: "claude", cwd: "/home/me/project", title: "shell", method: "herdr", pane: "w1:p2", workspace: "project" }] } },
|
|
35
43
|
{ name: "sessions-token", request: { method: "GET", path: "/sessions", auth: "token" }, status: 200, response: { schema: "SessionsResponse", example: [] } },
|
|
36
44
|
{ name: "sessions-extension-no-token", request: { method: "POST", path: "/sessions", auth: "extension-no-token" }, status: 401, response: { schema: "ErrorResponse", example: { error: "bad token" } } },
|
|
37
45
|
{ name: "sessions-unauthenticated", request: { method: "POST", path: "/sessions", auth: "none" }, status: 401, response: { schema: "ErrorResponse", example: { error: "bad token" } } },
|
package/dist/index.d.ts
CHANGED
|
@@ -6,6 +6,14 @@ export declare const INTENTS: readonly ["explain", "change", "approve"];
|
|
|
6
6
|
export declare const LEGACY_INTENTS: readonly ["question", "bug", "note"];
|
|
7
7
|
/** The intent clients should send; parsed annotations may also hold a LEGACY_INTENTS value. */
|
|
8
8
|
export type Intent = (typeof INTENTS)[number];
|
|
9
|
+
/** A rectangle the user drew, in CSS px relative to the viewport at capture time (add `viewport.scrollY` for page y). */
|
|
10
|
+
export declare const Region: z.ZodObject<{
|
|
11
|
+
x: z.ZodNumber;
|
|
12
|
+
y: z.ZodNumber;
|
|
13
|
+
w: z.ZodNumber;
|
|
14
|
+
h: z.ZodNumber;
|
|
15
|
+
}, z.core.$strip>;
|
|
16
|
+
export type Region = z.infer<typeof Region>;
|
|
9
17
|
export declare const Annotation: z.ZodObject<{
|
|
10
18
|
id: z.ZodString;
|
|
11
19
|
kind: z.ZodEnum<{
|
|
@@ -35,6 +43,7 @@ export declare const Annotation: z.ZodObject<{
|
|
|
35
43
|
path: z.ZodArray<z.ZodString>;
|
|
36
44
|
near: z.ZodString;
|
|
37
45
|
hosts: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
46
|
+
frames: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
38
47
|
}, z.core.$strip>;
|
|
39
48
|
element: z.ZodOptional<z.ZodObject<{
|
|
40
49
|
tag: z.ZodString;
|
|
@@ -57,6 +66,13 @@ export declare const Annotation: z.ZodObject<{
|
|
|
57
66
|
scrollY: z.ZodNumber;
|
|
58
67
|
}, z.core.$strip>;
|
|
59
68
|
crop: z.ZodString;
|
|
69
|
+
region: z.ZodOptional<z.ZodObject<{
|
|
70
|
+
x: z.ZodNumber;
|
|
71
|
+
y: z.ZodNumber;
|
|
72
|
+
w: z.ZodNumber;
|
|
73
|
+
h: z.ZodNumber;
|
|
74
|
+
}, z.core.$strip>>;
|
|
75
|
+
offscreen: z.ZodOptional<z.ZodBoolean>;
|
|
60
76
|
}, z.core.$strip>;
|
|
61
77
|
export type Annotation = z.infer<typeof Annotation>;
|
|
62
78
|
export declare const Target: z.ZodObject<{
|
|
@@ -107,6 +123,7 @@ export declare const BundleInput: z.ZodObject<{
|
|
|
107
123
|
path: z.ZodArray<z.ZodString>;
|
|
108
124
|
near: z.ZodString;
|
|
109
125
|
hosts: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
126
|
+
frames: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
110
127
|
}, z.core.$strip>;
|
|
111
128
|
element: z.ZodOptional<z.ZodObject<{
|
|
112
129
|
tag: z.ZodString;
|
|
@@ -129,6 +146,13 @@ export declare const BundleInput: z.ZodObject<{
|
|
|
129
146
|
scrollY: z.ZodNumber;
|
|
130
147
|
}, z.core.$strip>;
|
|
131
148
|
crop: z.ZodString;
|
|
149
|
+
region: z.ZodOptional<z.ZodObject<{
|
|
150
|
+
x: z.ZodNumber;
|
|
151
|
+
y: z.ZodNumber;
|
|
152
|
+
w: z.ZodNumber;
|
|
153
|
+
h: z.ZodNumber;
|
|
154
|
+
}, z.core.$strip>>;
|
|
155
|
+
offscreen: z.ZodOptional<z.ZodBoolean>;
|
|
132
156
|
}, z.core.$strip>>;
|
|
133
157
|
}, z.core.$strip>;
|
|
134
158
|
export type BundleInput = z.infer<typeof BundleInput>;
|
|
@@ -174,6 +198,7 @@ export declare const Bundle: z.ZodObject<{
|
|
|
174
198
|
path: z.ZodArray<z.ZodString>;
|
|
175
199
|
near: z.ZodString;
|
|
176
200
|
hosts: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
201
|
+
frames: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
177
202
|
}, z.core.$strip>;
|
|
178
203
|
element: z.ZodOptional<z.ZodObject<{
|
|
179
204
|
tag: z.ZodString;
|
|
@@ -196,6 +221,13 @@ export declare const Bundle: z.ZodObject<{
|
|
|
196
221
|
scrollY: z.ZodNumber;
|
|
197
222
|
}, z.core.$strip>;
|
|
198
223
|
crop: z.ZodString;
|
|
224
|
+
region: z.ZodOptional<z.ZodObject<{
|
|
225
|
+
x: z.ZodNumber;
|
|
226
|
+
y: z.ZodNumber;
|
|
227
|
+
w: z.ZodNumber;
|
|
228
|
+
h: z.ZodNumber;
|
|
229
|
+
}, z.core.$strip>>;
|
|
230
|
+
offscreen: z.ZodOptional<z.ZodBoolean>;
|
|
199
231
|
}, z.core.$strip>>;
|
|
200
232
|
id: z.ZodString;
|
|
201
233
|
files: z.ZodObject<{
|
|
@@ -226,6 +258,7 @@ export declare const Status: z.ZodObject<{
|
|
|
226
258
|
pull: "pull";
|
|
227
259
|
}>>;
|
|
228
260
|
session: z.ZodOptional<z.ZodString>;
|
|
261
|
+
agent: z.ZodOptional<z.ZodString>;
|
|
229
262
|
summary: z.ZodOptional<z.ZodString>;
|
|
230
263
|
note: z.ZodOptional<z.ZodString>;
|
|
231
264
|
}, z.core.$strip>;
|
|
@@ -242,6 +275,7 @@ export declare const Session: z.ZodObject<{
|
|
|
242
275
|
}>;
|
|
243
276
|
pane: z.ZodOptional<z.ZodString>;
|
|
244
277
|
sessionIds: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
278
|
+
workspace: z.ZodOptional<z.ZodString>;
|
|
245
279
|
}, z.core.$strip>;
|
|
246
280
|
export type Session = z.infer<typeof Session>;
|
|
247
281
|
export declare const PROTOCOL_VERSION = 2;
|
|
@@ -267,6 +301,7 @@ export declare const SessionsResponse: z.ZodArray<z.ZodObject<{
|
|
|
267
301
|
}>;
|
|
268
302
|
pane: z.ZodOptional<z.ZodString>;
|
|
269
303
|
sessionIds: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
304
|
+
workspace: z.ZodOptional<z.ZodString>;
|
|
270
305
|
}, z.core.$strip>>;
|
|
271
306
|
export type SessionsResponse = z.infer<typeof SessionsResponse>;
|
|
272
307
|
export declare const SendResponse: z.ZodObject<{
|
|
@@ -285,6 +320,7 @@ export declare const SendResponse: z.ZodObject<{
|
|
|
285
320
|
pull: "pull";
|
|
286
321
|
}>>;
|
|
287
322
|
session: z.ZodOptional<z.ZodString>;
|
|
323
|
+
agent: z.ZodOptional<z.ZodString>;
|
|
288
324
|
summary: z.ZodOptional<z.ZodString>;
|
|
289
325
|
note: z.ZodOptional<z.ZodString>;
|
|
290
326
|
}, z.core.$strip>;
|
|
@@ -332,6 +368,7 @@ export declare const BundleStatusResponse: z.ZodObject<{
|
|
|
332
368
|
path: z.ZodArray<z.ZodString>;
|
|
333
369
|
near: z.ZodString;
|
|
334
370
|
hosts: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
371
|
+
frames: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
335
372
|
}, z.core.$strip>;
|
|
336
373
|
element: z.ZodOptional<z.ZodObject<{
|
|
337
374
|
tag: z.ZodString;
|
|
@@ -354,6 +391,13 @@ export declare const BundleStatusResponse: z.ZodObject<{
|
|
|
354
391
|
scrollY: z.ZodNumber;
|
|
355
392
|
}, z.core.$strip>;
|
|
356
393
|
crop: z.ZodString;
|
|
394
|
+
region: z.ZodOptional<z.ZodObject<{
|
|
395
|
+
x: z.ZodNumber;
|
|
396
|
+
y: z.ZodNumber;
|
|
397
|
+
w: z.ZodNumber;
|
|
398
|
+
h: z.ZodNumber;
|
|
399
|
+
}, z.core.$strip>>;
|
|
400
|
+
offscreen: z.ZodOptional<z.ZodBoolean>;
|
|
357
401
|
}, z.core.$strip>>;
|
|
358
402
|
id: z.ZodString;
|
|
359
403
|
files: z.ZodObject<{
|
|
@@ -376,6 +420,7 @@ export declare const BundleStatusResponse: z.ZodObject<{
|
|
|
376
420
|
pull: "pull";
|
|
377
421
|
}>>;
|
|
378
422
|
session: z.ZodOptional<z.ZodString>;
|
|
423
|
+
agent: z.ZodOptional<z.ZodString>;
|
|
379
424
|
summary: z.ZodOptional<z.ZodString>;
|
|
380
425
|
note: z.ZodOptional<z.ZodString>;
|
|
381
426
|
}, z.core.$strip>>;
|
package/dist/index.js
CHANGED
|
@@ -4,6 +4,8 @@ export const Agent = z.string().regex(AGENT_NAME);
|
|
|
4
4
|
export const INTENTS = ["explain", "change", "approve"];
|
|
5
5
|
export const LEGACY_INTENTS = ["question", "bug", "note"];
|
|
6
6
|
const Box = z.object({ x: z.number(), y: z.number(), w: z.number(), h: z.number() });
|
|
7
|
+
/** A rectangle the user drew, in CSS px relative to the viewport at capture time (add `viewport.scrollY` for page y). */
|
|
8
|
+
export const Region = z.object({ x: z.number(), y: z.number(), w: z.number().nonnegative(), h: z.number().nonnegative() });
|
|
7
9
|
export const Annotation = z.object({
|
|
8
10
|
id: z.string().regex(/^A\d+$/),
|
|
9
11
|
// v1 kinds only; "region" | "draw" are reserved for later tools.
|
|
@@ -17,6 +19,8 @@ export const Annotation = z.object({
|
|
|
17
19
|
path: z.array(z.string()),
|
|
18
20
|
near: z.string(),
|
|
19
21
|
hosts: z.array(z.string()).optional(),
|
|
22
|
+
/** CSS selectors of the iframes holding the note, outermost first, each resolved in the previous frame's document; absent for the top document. `css`, `hosts` and `path` are then inside the innermost frame, while `box` and `region` stay in the top page's viewport. */
|
|
23
|
+
frames: z.array(z.string()).optional(),
|
|
20
24
|
}),
|
|
21
25
|
element: z.object({
|
|
22
26
|
tag: z.string(),
|
|
@@ -29,7 +33,10 @@ export const Annotation = z.object({
|
|
|
29
33
|
box: Box,
|
|
30
34
|
viewport: z.object({ w: z.number(), h: z.number(), dpr: z.number(), scrollY: z.number() }),
|
|
31
35
|
crop: z.string().regex(/^crops\/A\d+\.png$/),
|
|
32
|
-
|
|
36
|
+
region: Region.optional(),
|
|
37
|
+
/** True when the note's target was not on screen at send time (scrolled out of a virtual list, another tab); `box` and the crop are from when it was last seen. */
|
|
38
|
+
offscreen: z.boolean().optional(),
|
|
39
|
+
}).refine((a) => !a.region || a.kind === "element", { message: "region is only allowed on element annotations", path: ["region"] });
|
|
33
40
|
export const Target = z.object({
|
|
34
41
|
agent: Agent,
|
|
35
42
|
sessionId: z.string().optional(),
|
|
@@ -58,6 +65,7 @@ export const Status = z.object({
|
|
|
58
65
|
at: z.string(),
|
|
59
66
|
via: Via.optional(),
|
|
60
67
|
session: z.string().optional(),
|
|
68
|
+
agent: Agent.optional(),
|
|
61
69
|
summary: z.string().optional(),
|
|
62
70
|
note: z.string().optional(),
|
|
63
71
|
});
|
|
@@ -69,6 +77,8 @@ export const Session = z.object({
|
|
|
69
77
|
method: z.enum(["push", "herdr", "next-prompt"]),
|
|
70
78
|
pane: z.string().optional(),
|
|
71
79
|
sessionIds: z.array(z.string()).optional(),
|
|
80
|
+
/** Display name of the herdr workspace a herdr pane is in. */
|
|
81
|
+
workspace: z.string().optional(),
|
|
72
82
|
});
|
|
73
83
|
// Protocol v2. Additive changes (optional fields, new endpoints) keep the version; removals, renames and
|
|
74
84
|
// semantic changes bump PROTOCOL_VERSION, and the bridge keeps accepting the previous version for a release.
|
package/package.json
CHANGED
package/src/fixtures.ts
CHANGED
|
@@ -24,6 +24,14 @@ export const exampleBundleInput: BundleInput = {
|
|
|
24
24
|
box: { x: 10, y: 20, w: 100, h: 18 },
|
|
25
25
|
viewport: { w: 1280, h: 800, dpr: 2, scrollY: 0 },
|
|
26
26
|
crop: "crops/A1.png",
|
|
27
|
+
}, {
|
|
28
|
+
id: "A2", kind: "element", comment: "make this bowl bigger", intent: "change",
|
|
29
|
+
anchor: { css: "canvas#plating", path: ["main", "figure"], near: "Serving" },
|
|
30
|
+
element: { tag: "canvas", text: "", html: '<canvas id="plating" width="800" height="600"></canvas>', attrs: { id: "plating" } },
|
|
31
|
+
box: { x: 40, y: 300, w: 800, h: 600 },
|
|
32
|
+
viewport: { w: 1280, h: 800, dpr: 2, scrollY: 0 },
|
|
33
|
+
crop: "crops/A2.png",
|
|
34
|
+
region: { x: 310, y: 420, w: 220, h: 140 },
|
|
27
35
|
}],
|
|
28
36
|
};
|
|
29
37
|
export const examplePage = "# Tomato soup\n\nstir in ⟦A1⟧200 ml cream before serving\n";
|
|
@@ -49,14 +57,14 @@ export type Exchange = {
|
|
|
49
57
|
response: { schema: SchemaName; example: unknown };
|
|
50
58
|
};
|
|
51
59
|
|
|
52
|
-
const health = { ok: true, bridgeVersion: "0.
|
|
60
|
+
const health = { ok: true, bridgeVersion: "0.6.1", protocol: { version: 2, min: 1 } };
|
|
53
61
|
const queued = { state: "queued", at: "2026-09-24T15:32:01.000Z" };
|
|
54
62
|
const exampleBundle = { ...exampleBundleInput, id: EXAMPLE_BUNDLE_ID, files: { page: "page.md", screenshot: "screenshot.png" } };
|
|
55
63
|
|
|
56
64
|
export const conformance: Exchange[] = [
|
|
57
65
|
{ name: "health-extension", request: { method: "POST", path: "/health", auth: "extension" }, status: 200, response: { schema: "HealthResponse", example: health } },
|
|
58
66
|
{ name: "health-probe", request: { method: "GET", path: "/health", auth: "none" }, status: 200, response: { schema: "HealthResponse", example: health } },
|
|
59
|
-
{ name: "sessions-extension", request: { method: "POST", path: "/sessions", auth: "extension" }, status: 200, response: { schema: "SessionsResponse", example: [{ id: "w1:p2", agent: "claude", cwd: "/home/me/project", title: "shell", method: "herdr", pane: "w1:p2" }] } },
|
|
67
|
+
{ name: "sessions-extension", request: { method: "POST", path: "/sessions", auth: "extension" }, status: 200, response: { schema: "SessionsResponse", example: [{ id: "w1:p2", agent: "claude", cwd: "/home/me/project", title: "shell", method: "herdr", pane: "w1:p2", workspace: "project" }] } },
|
|
60
68
|
{ name: "sessions-token", request: { method: "GET", path: "/sessions", auth: "token" }, status: 200, response: { schema: "SessionsResponse", example: [] } },
|
|
61
69
|
{ name: "sessions-extension-no-token", request: { method: "POST", path: "/sessions", auth: "extension-no-token" }, status: 401, response: { schema: "ErrorResponse", example: { error: "bad token" } } },
|
|
62
70
|
{ name: "sessions-unauthenticated", request: { method: "POST", path: "/sessions", auth: "none" }, status: 401, response: { schema: "ErrorResponse", example: { error: "bad token" } } },
|
package/src/index.ts
CHANGED
|
@@ -11,6 +11,10 @@ export type Intent = (typeof INTENTS)[number];
|
|
|
11
11
|
|
|
12
12
|
const Box = z.object({ x: z.number(), y: z.number(), w: z.number(), h: z.number() });
|
|
13
13
|
|
|
14
|
+
/** A rectangle the user drew, in CSS px relative to the viewport at capture time (add `viewport.scrollY` for page y). */
|
|
15
|
+
export const Region = z.object({ x: z.number(), y: z.number(), w: z.number().nonnegative(), h: z.number().nonnegative() });
|
|
16
|
+
export type Region = z.infer<typeof Region>;
|
|
17
|
+
|
|
14
18
|
export const Annotation = z.object({
|
|
15
19
|
id: z.string().regex(/^A\d+$/),
|
|
16
20
|
// v1 kinds only; "region" | "draw" are reserved for later tools.
|
|
@@ -24,6 +28,8 @@ export const Annotation = z.object({
|
|
|
24
28
|
path: z.array(z.string()),
|
|
25
29
|
near: z.string(),
|
|
26
30
|
hosts: z.array(z.string()).optional(),
|
|
31
|
+
/** CSS selectors of the iframes holding the note, outermost first, each resolved in the previous frame's document; absent for the top document. `css`, `hosts` and `path` are then inside the innermost frame, while `box` and `region` stay in the top page's viewport. */
|
|
32
|
+
frames: z.array(z.string()).optional(),
|
|
27
33
|
}),
|
|
28
34
|
element: z.object({
|
|
29
35
|
tag: z.string(),
|
|
@@ -36,7 +42,10 @@ export const Annotation = z.object({
|
|
|
36
42
|
box: Box,
|
|
37
43
|
viewport: z.object({ w: z.number(), h: z.number(), dpr: z.number(), scrollY: z.number() }),
|
|
38
44
|
crop: z.string().regex(/^crops\/A\d+\.png$/),
|
|
39
|
-
|
|
45
|
+
region: Region.optional(),
|
|
46
|
+
/** True when the note's target was not on screen at send time (scrolled out of a virtual list, another tab); `box` and the crop are from when it was last seen. */
|
|
47
|
+
offscreen: z.boolean().optional(),
|
|
48
|
+
}).refine((a) => !a.region || a.kind === "element", { message: "region is only allowed on element annotations", path: ["region"] });
|
|
40
49
|
export type Annotation = z.infer<typeof Annotation>;
|
|
41
50
|
|
|
42
51
|
export const Target = z.object({
|
|
@@ -76,6 +85,7 @@ export const Status = z.object({
|
|
|
76
85
|
at: z.string(),
|
|
77
86
|
via: Via.optional(),
|
|
78
87
|
session: z.string().optional(),
|
|
88
|
+
agent: Agent.optional(),
|
|
79
89
|
summary: z.string().optional(),
|
|
80
90
|
note: z.string().optional(),
|
|
81
91
|
});
|
|
@@ -89,6 +99,8 @@ export const Session = z.object({
|
|
|
89
99
|
method: z.enum(["push", "herdr", "next-prompt"]),
|
|
90
100
|
pane: z.string().optional(),
|
|
91
101
|
sessionIds: z.array(z.string()).optional(),
|
|
102
|
+
/** Display name of the herdr workspace a herdr pane is in. */
|
|
103
|
+
workspace: z.string().optional(),
|
|
92
104
|
});
|
|
93
105
|
export type Session = z.infer<typeof Session>;
|
|
94
106
|
|