@anynotate/protocol 0.3.0 → 0.4.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 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.3.0", "protocol": { "version": 2, "min": 1 } }`.
7
+ `POST /health` (or `GET`) → `{ "ok": true, "bridgeVersion": "0.5.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
 
@@ -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,9 @@ 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 screen at send time (`box` and crop are from when it was last seen); `README.md` adds `(off-screen when sent)` to its heading (additive, same releases).
115
+ - Optional `anchor.frames` on notes made inside same-origin iframes; `README.md` names the frames after the selector (additive, same releases).
91
116
 
92
117
  ## Threat model
93
118
 
@@ -42,6 +42,7 @@ export declare const responseSchemas: {
42
42
  pull: "pull";
43
43
  }>>;
44
44
  session: z.ZodOptional<z.ZodString>;
45
+ agent: z.ZodOptional<z.ZodString>;
45
46
  summary: z.ZodOptional<z.ZodString>;
46
47
  note: z.ZodOptional<z.ZodString>;
47
48
  }, z.core.$strip>;
@@ -88,6 +89,7 @@ export declare const responseSchemas: {
88
89
  path: z.ZodArray<z.ZodString>;
89
90
  near: z.ZodString;
90
91
  hosts: z.ZodOptional<z.ZodArray<z.ZodString>>;
92
+ frames: z.ZodOptional<z.ZodArray<z.ZodString>>;
91
93
  }, z.core.$strip>;
92
94
  element: z.ZodOptional<z.ZodObject<{
93
95
  tag: z.ZodString;
@@ -110,6 +112,13 @@ export declare const responseSchemas: {
110
112
  scrollY: z.ZodNumber;
111
113
  }, z.core.$strip>;
112
114
  crop: z.ZodString;
115
+ region: z.ZodOptional<z.ZodObject<{
116
+ x: z.ZodNumber;
117
+ y: z.ZodNumber;
118
+ w: z.ZodNumber;
119
+ h: z.ZodNumber;
120
+ }, z.core.$strip>>;
121
+ offscreen: z.ZodOptional<z.ZodBoolean>;
113
122
  }, z.core.$strip>>;
114
123
  id: z.ZodString;
115
124
  files: z.ZodObject<{
@@ -132,6 +141,7 @@ export declare const responseSchemas: {
132
141
  pull: "pull";
133
142
  }>>;
134
143
  session: z.ZodOptional<z.ZodString>;
144
+ agent: z.ZodOptional<z.ZodString>;
135
145
  summary: z.ZodOptional<z.ZodString>;
136
146
  note: z.ZodOptional<z.ZodString>;
137
147
  }, z.core.$strip>>;
package/dist/fixtures.js CHANGED
@@ -19,13 +19,21 @@ 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.3.0", protocol: { version: 2, min: 1 } };
36
+ const health = { ok: true, bridgeVersion: "0.5.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 = [
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>;
@@ -285,6 +318,7 @@ export declare const SendResponse: z.ZodObject<{
285
318
  pull: "pull";
286
319
  }>>;
287
320
  session: z.ZodOptional<z.ZodString>;
321
+ agent: z.ZodOptional<z.ZodString>;
288
322
  summary: z.ZodOptional<z.ZodString>;
289
323
  note: z.ZodOptional<z.ZodString>;
290
324
  }, z.core.$strip>;
@@ -332,6 +366,7 @@ export declare const BundleStatusResponse: z.ZodObject<{
332
366
  path: z.ZodArray<z.ZodString>;
333
367
  near: z.ZodString;
334
368
  hosts: z.ZodOptional<z.ZodArray<z.ZodString>>;
369
+ frames: z.ZodOptional<z.ZodArray<z.ZodString>>;
335
370
  }, z.core.$strip>;
336
371
  element: z.ZodOptional<z.ZodObject<{
337
372
  tag: z.ZodString;
@@ -354,6 +389,13 @@ export declare const BundleStatusResponse: z.ZodObject<{
354
389
  scrollY: z.ZodNumber;
355
390
  }, z.core.$strip>;
356
391
  crop: z.ZodString;
392
+ region: z.ZodOptional<z.ZodObject<{
393
+ x: z.ZodNumber;
394
+ y: z.ZodNumber;
395
+ w: z.ZodNumber;
396
+ h: z.ZodNumber;
397
+ }, z.core.$strip>>;
398
+ offscreen: z.ZodOptional<z.ZodBoolean>;
357
399
  }, z.core.$strip>>;
358
400
  id: z.ZodString;
359
401
  files: z.ZodObject<{
@@ -376,6 +418,7 @@ export declare const BundleStatusResponse: z.ZodObject<{
376
418
  pull: "pull";
377
419
  }>>;
378
420
  session: z.ZodOptional<z.ZodString>;
421
+ agent: z.ZodOptional<z.ZodString>;
379
422
  summary: z.ZodOptional<z.ZodString>;
380
423
  note: z.ZodOptional<z.ZodString>;
381
424
  }, 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
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@anynotate/protocol",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "description": "Wire types and zod schemas shared by Anynotate clients and the Anynotate bridge.",
5
5
  "license": "Apache-2.0",
6
6
  "author": "Kun Xu",
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,7 +57,7 @@ export type Exchange = {
49
57
  response: { schema: SchemaName; example: unknown };
50
58
  };
51
59
 
52
- const health = { ok: true, bridgeVersion: "0.3.0", protocol: { version: 2, min: 1 } };
60
+ const health = { ok: true, bridgeVersion: "0.5.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
 
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
  });