@bitvea/feedback-cli 1.1.0 → 1.2.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.
@@ -0,0 +1,35 @@
1
+ /** The only address this receiver ever binds. */
2
+ export declare const DEV_HOST = "127.0.0.1";
3
+ /**
4
+ * The port the toolbar posts to when nothing says otherwise. Copy of
5
+ * `DEFAULT_LOOPBACK_PORT` in `packages/toolbar/src/gating.ts`.
6
+ */
7
+ export declare const DEV_DEFAULT_PORT = 4477;
8
+ export declare const ANNOTATIONS_PATH = "/v1/annotations";
9
+ export interface DevReceiverOptions {
10
+ /** The repository root; files go under `<root>/.bitvea/feedback`. */
11
+ root: string;
12
+ /** The port to listen on. 0 picks a free one, which is what tests use. */
13
+ port?: number;
14
+ /** Origins beyond this machine's that may send, already normalised. */
15
+ allowOrigins?: Iterable<string>;
16
+ /** The body limit in bytes. Defaults to the contract's. */
17
+ maxBytes?: number;
18
+ /** Where one line per event goes. Defaults to silence. */
19
+ log?: (line: string) => void;
20
+ /** Where refusals go. Defaults to silence. */
21
+ warn?: (line: string) => void;
22
+ }
23
+ export interface DevReceiver {
24
+ /** `http://127.0.0.1:<port>`. */
25
+ origin: string;
26
+ /** The collection URL, which is the toolbar's sink URL. */
27
+ sinkUrl: string;
28
+ port: number;
29
+ /** The absolute directory annotations are written to. */
30
+ dir: string;
31
+ close(): Promise<void>;
32
+ }
33
+ /** Start the receiver. Resolves once it is listening. */
34
+ export declare function startDevReceiver(options: DevReceiverOptions): Promise<DevReceiver>;
35
+ //# sourceMappingURL=server.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../src/dev/server.ts"],"names":[],"mappings":"AAgDA,iDAAiD;AACjD,eAAO,MAAM,QAAQ,cAAc,CAAC;AAEpC;;;GAGG;AACH,eAAO,MAAM,gBAAgB,OAAO,CAAC;AAErC,eAAO,MAAM,gBAAgB,oBAAoB,CAAC;AAIlD,MAAM,WAAW,kBAAkB;IACjC,qEAAqE;IACrE,IAAI,EAAE,MAAM,CAAC;IACb,0EAA0E;IAC1E,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,uEAAuE;IACvE,YAAY,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,CAAC;IAChC,2DAA2D;IAC3D,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,0DAA0D;IAC1D,GAAG,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IAC7B,8CAA8C;IAC9C,IAAI,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;CAC/B;AAED,MAAM,WAAW,WAAW;IAC1B,iCAAiC;IACjC,MAAM,EAAE,MAAM,CAAC;IACf,2DAA2D;IAC3D,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;IACb,yDAAyD;IACzD,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACxB;AAsED,yDAAyD;AACzD,wBAAsB,gBAAgB,CACpC,OAAO,EAAE,kBAAkB,GAC1B,OAAO,CAAC,WAAW,CAAC,CA8KtB"}
@@ -0,0 +1,270 @@
1
+ // The `dev` receiver: the loopback sink the toolbar's local mode POSTs to.
2
+ //
3
+ // Three routes and nothing else:
4
+ //
5
+ // POST /v1/annotations store one annotation (the toolbar's send)
6
+ // GET /v1/annotations?url=<p> list what is stored, for one page or all
7
+ // GET /v1/annotations/<id>.png one stored screenshot
8
+ //
9
+ // plus `GET /v1/health`, which answers `{"v":1}` so a script can wait for the
10
+ // port without guessing.
11
+ //
12
+ // What it promises, stated because "local" is a promise rather than a
13
+ // description of where the server happens to run:
14
+ //
15
+ // - It binds 127.0.0.1 and never anything wider. A receiver on 0.0.0.0 is a
16
+ // feedback endpoint for everybody on the same coffee-shop network.
17
+ // - It sends nothing anywhere. No telemetry, no update check, no request of
18
+ // its own at all: the only sockets it touches are the ones it accepts.
19
+ // - It answers only pages on this machine and origins the developer named,
20
+ // and addressed by a loopback name (see ./origins.ts for why both).
21
+ // - It never reads more body than `LOCAL_ANNOTATION_MAX_BYTES`, and it
22
+ // refuses a POST that is not `application/json`, which is what forces a
23
+ // browser to ask permission (a CORS preflight) before it can send one.
24
+ // - No cookie is read, none is set, and `Access-Control-Allow-Credentials`
25
+ // is never sent: the toolbar sends with `credentials: "omit"` and this
26
+ // receiver would not know what to do with an identity anyway.
27
+ import { createServer, } from "node:http";
28
+ import { LOCAL_ANNOTATION_MAX_BYTES, LOCAL_ANNOTATION_VERSION, replaceControlCharacters, validateAnnotation, } from "./contract.js";
29
+ import { isAllowedOrigin, isLoopbackHostHeader } from "./origins.js";
30
+ import { AnnotationExistsError, AnnotationFileError, createAnnotationStore, } from "./store.js";
31
+ /** The only address this receiver ever binds. */
32
+ export const DEV_HOST = "127.0.0.1";
33
+ /**
34
+ * The port the toolbar posts to when nothing says otherwise. Copy of
35
+ * `DEFAULT_LOOPBACK_PORT` in `packages/toolbar/src/gating.ts`.
36
+ */
37
+ export const DEV_DEFAULT_PORT = 4477;
38
+ export const ANNOTATIONS_PATH = "/v1/annotations";
39
+ const PNG_PATH = /^\/v1\/annotations\/([A-Za-z0-9_-]{1,64})\.png$/;
40
+ class HttpRefusal extends Error {
41
+ status;
42
+ code;
43
+ issues;
44
+ constructor(status, code, issues) {
45
+ super(code);
46
+ this.status = status;
47
+ this.code = code;
48
+ this.issues = issues;
49
+ }
50
+ }
51
+ function sendJson(res, status, body) {
52
+ const bytes = Buffer.from(JSON.stringify(body));
53
+ res.writeHead(status, {
54
+ "content-type": "application/json; charset=utf-8",
55
+ "content-length": bytes.length,
56
+ "cache-control": "no-store",
57
+ "x-content-type-options": "nosniff",
58
+ });
59
+ res.end(bytes);
60
+ }
61
+ /**
62
+ * The body, as text, or a refusal once it passes `maxBytes`.
63
+ *
64
+ * `Content-Length` is checked first so an honest oversized request is refused
65
+ * without reading it; the running count is what holds a chunked one to the
66
+ * same line.
67
+ */
68
+ function readBody(req, maxBytes) {
69
+ const declared = Number(req.headers["content-length"] ?? Number.NaN);
70
+ if (Number.isFinite(declared) && declared > maxBytes) {
71
+ return Promise.reject(new HttpRefusal(413, "payload_too_large"));
72
+ }
73
+ return new Promise((resolve, reject) => {
74
+ const chunks = [];
75
+ let size = 0;
76
+ let refused = false;
77
+ req.on("data", (chunk) => {
78
+ if (refused)
79
+ return;
80
+ size += chunk.length;
81
+ if (size > maxBytes) {
82
+ refused = true;
83
+ chunks.length = 0;
84
+ reject(new HttpRefusal(413, "payload_too_large"));
85
+ return;
86
+ }
87
+ chunks.push(chunk);
88
+ });
89
+ req.on("end", () => {
90
+ if (!refused)
91
+ resolve(Buffer.concat(chunks).toString("utf8"));
92
+ });
93
+ req.on("error", (err) => {
94
+ if (!refused)
95
+ reject(err);
96
+ });
97
+ });
98
+ }
99
+ function preview(text) {
100
+ // Control characters out before anything else: this line goes to the
101
+ // developer's terminal, and an ESC in somebody's text is an escape sequence
102
+ // there. `text` is the one field the contract lets keep them, because it is
103
+ // prose and may legitimately hold line breaks and tabs.
104
+ const line = replaceControlCharacters(text, " ")
105
+ .trim()
106
+ .replace(/\s+/g, " ");
107
+ return line.length > 60 ? `${line.slice(0, 59)}...` : line;
108
+ }
109
+ /** Start the receiver. Resolves once it is listening. */
110
+ export async function startDevReceiver(options) {
111
+ const store = createAnnotationStore(options.root);
112
+ const allow = new Set(options.allowOrigins ?? []);
113
+ const maxBytes = options.maxBytes ?? LOCAL_ANNOTATION_MAX_BYTES;
114
+ const log = options.log ?? (() => { });
115
+ const warn = options.warn ?? (() => { });
116
+ // One line per refused origin rather than one per request: a page that
117
+ // retries would otherwise bury the terminal.
118
+ const warnedOrigins = new Set();
119
+ const handle = async (req, res) => {
120
+ if (!isLoopbackHostHeader(req.headers.host)) {
121
+ throw new HttpRefusal(421, "host_not_loopback");
122
+ }
123
+ const origin = req.headers.origin;
124
+ if (origin !== undefined) {
125
+ if (!isAllowedOrigin(origin, allow)) {
126
+ if (!warnedOrigins.has(origin)) {
127
+ warnedOrigins.add(origin);
128
+ warn(`refused a request from ${origin}: not this machine and not named ` +
129
+ "with --allow-origin.");
130
+ }
131
+ throw new HttpRefusal(403, "origin_not_allowed");
132
+ }
133
+ // Set before anything else can answer, so a 413 or a 400 reaches the
134
+ // page as a status it can read rather than as an opaque network error.
135
+ res.setHeader("access-control-allow-origin", origin);
136
+ res.setHeader("vary", "Origin");
137
+ }
138
+ const url = new URL(req.url ?? "/", "http://127.0.0.1");
139
+ const method = req.method ?? "GET";
140
+ if (method === "OPTIONS") {
141
+ if (origin === undefined)
142
+ throw new HttpRefusal(400, "not_a_preflight");
143
+ res.setHeader("access-control-allow-methods", "GET, POST");
144
+ res.setHeader("access-control-allow-headers", "content-type");
145
+ res.setHeader("access-control-max-age", "600");
146
+ // Chrome's Private Network Access asks this of a loopback target; the
147
+ // origin has already been checked, so the answer can be yes.
148
+ if (req.headers["access-control-request-private-network"] === "true") {
149
+ res.setHeader("access-control-allow-private-network", "true");
150
+ }
151
+ res.writeHead(204);
152
+ res.end();
153
+ return;
154
+ }
155
+ if (url.pathname === "/v1/health") {
156
+ if (method !== "GET")
157
+ throw new HttpRefusal(405, "method_not_allowed");
158
+ sendJson(res, 200, { v: LOCAL_ANNOTATION_VERSION });
159
+ return;
160
+ }
161
+ if (url.pathname === ANNOTATIONS_PATH) {
162
+ if (method === "POST") {
163
+ const type = req.headers["content-type"] ?? "";
164
+ if (!/^application\/json\s*(;|$)/i.test(type)) {
165
+ throw new HttpRefusal(415, "content_type_not_json");
166
+ }
167
+ const text = await readBody(req, maxBytes);
168
+ let body;
169
+ try {
170
+ body = JSON.parse(text);
171
+ }
172
+ catch {
173
+ throw new HttpRefusal(400, "invalid_json");
174
+ }
175
+ const issues = validateAnnotation(body);
176
+ if (issues.length > 0) {
177
+ throw new HttpRefusal(400, "validation_failed", issues);
178
+ }
179
+ const annotation = body;
180
+ let saved;
181
+ try {
182
+ saved = store.save(annotation);
183
+ }
184
+ catch (err) {
185
+ if (err instanceof AnnotationFileError) {
186
+ throw new HttpRefusal(400, "validation_failed", [err.message]);
187
+ }
188
+ if (err instanceof AnnotationExistsError) {
189
+ throw new HttpRefusal(409, "annotation_exists");
190
+ }
191
+ throw err;
192
+ }
193
+ log(`+ ${annotation.kind} "${preview(annotation.text)}" on ${annotation.url}` +
194
+ ` -> ${saved.jsonPath}`);
195
+ sendJson(res, 201, { v: LOCAL_ANNOTATION_VERSION, id: annotation.id });
196
+ return;
197
+ }
198
+ if (method === "GET") {
199
+ const page = url.searchParams.get("url");
200
+ const annotations = store
201
+ .list()
202
+ .filter((a) => page === null || a.url === page)
203
+ .map((a) => ({
204
+ ...a,
205
+ screenshot: a.screenshot === null ? null : `${ANNOTATIONS_PATH}/${a.id}.png`,
206
+ }));
207
+ sendJson(res, 200, { v: LOCAL_ANNOTATION_VERSION, annotations });
208
+ return;
209
+ }
210
+ throw new HttpRefusal(405, "method_not_allowed");
211
+ }
212
+ const png = PNG_PATH.exec(url.pathname);
213
+ if (png?.[1]) {
214
+ if (method !== "GET")
215
+ throw new HttpRefusal(405, "method_not_allowed");
216
+ const bytes = store.png(png[1]);
217
+ if (bytes === null)
218
+ throw new HttpRefusal(404, "not_found");
219
+ res.writeHead(200, {
220
+ "content-type": "image/png",
221
+ "content-length": bytes.length,
222
+ "cache-control": "no-store",
223
+ "x-content-type-options": "nosniff",
224
+ });
225
+ res.end(bytes);
226
+ return;
227
+ }
228
+ throw new HttpRefusal(404, "not_found");
229
+ };
230
+ const server = createServer((req, res) => {
231
+ handle(req, res).catch((err) => {
232
+ if (res.headersSent) {
233
+ res.destroy();
234
+ return;
235
+ }
236
+ if (err instanceof HttpRefusal) {
237
+ if (err.status === 413) {
238
+ // The rest of an oversized body is not worth reading; close the
239
+ // connection once the answer is out rather than drain it.
240
+ res.setHeader("connection", "close");
241
+ res.on("finish", () => req.destroy());
242
+ }
243
+ sendJson(res, err.status, err.issues ? { error: err.code, issues: err.issues } : { error: err.code });
244
+ return;
245
+ }
246
+ warn(`internal error: ${err instanceof Error ? err.message : String(err)}`);
247
+ sendJson(res, 500, { error: "internal_error" });
248
+ });
249
+ });
250
+ await new Promise((resolve, reject) => {
251
+ server.once("error", reject);
252
+ server.listen(options.port ?? DEV_DEFAULT_PORT, DEV_HOST, () => {
253
+ server.off("error", reject);
254
+ resolve();
255
+ });
256
+ });
257
+ const { port } = server.address();
258
+ const origin = `http://${DEV_HOST}:${port}`;
259
+ return {
260
+ origin,
261
+ sinkUrl: `${origin}${ANNOTATIONS_PATH}`,
262
+ port,
263
+ dir: store.dir,
264
+ close: () => new Promise((resolve, reject) => {
265
+ server.close((err) => (err ? reject(err) : resolve()));
266
+ server.closeAllConnections();
267
+ }),
268
+ };
269
+ }
270
+ //# sourceMappingURL=server.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"server.js","sourceRoot":"","sources":["../../src/dev/server.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,43 @@
1
+ import { type LocalAnnotation } from "./contract.js";
2
+ /** Where the files go, under the repository root. */
3
+ export declare const FEEDBACK_DIR: string;
4
+ export declare const INBOX_FILE = "inbox.md";
5
+ export declare class AnnotationFileError extends Error {
6
+ }
7
+ /**
8
+ * An id that is already on disk.
9
+ *
10
+ * An annotation is written ONCE. The toolbar mints a fresh id for every send,
11
+ * its retry after a 413 included, so nothing it does ever re-sends one; and a
12
+ * receiver that replaced by id would let any other page on this machine's
13
+ * loopback overwrite a colleague's annotation, or delete its screenshot by
14
+ * re-sending the id without one.
15
+ */
16
+ export declare class AnnotationExistsError extends Error {
17
+ }
18
+ export interface AnnotationStore {
19
+ /** The absolute directory the files are in. */
20
+ dir: string;
21
+ /** Store one validated annotation. Re-sending an id replaces it. */
22
+ save(annotation: LocalAnnotation): {
23
+ jsonPath: string;
24
+ pngPath: string | null;
25
+ };
26
+ /**
27
+ * Every stored annotation, oldest first, with `screenshot` set to the PNG's
28
+ * file name (or null). A file that no longer validates is skipped rather
29
+ * than failing the list, because a person may have edited it by hand.
30
+ */
31
+ list(): LocalAnnotation[];
32
+ /** The bytes of `<id>.png`, or null when there is none. */
33
+ png(id: string): Buffer | null;
34
+ }
35
+ export declare function createAnnotationStore(root: string): AnnotationStore;
36
+ /**
37
+ * The inbox: every annotation, newest first, as a person or an agent reads it.
38
+ *
39
+ * Exported for the tests. Deterministic in its input, so regenerating it after
40
+ * a write that changed nothing produces the same bytes.
41
+ */
42
+ export declare function renderInbox(annotations: LocalAnnotation[]): string;
43
+ //# sourceMappingURL=store.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"store.d.ts","sourceRoot":"","sources":["../../src/dev/store.ts"],"names":[],"mappings":"AA0BA,OAAO,EAKL,KAAK,eAAe,EACrB,MAAM,YAAY,CAAC;AAEpB,qDAAqD;AACrD,eAAO,MAAM,YAAY,QAA8B,CAAC;AAExD,eAAO,MAAM,UAAU,aAAa,CAAC;AAarC,qBAAa,mBAAoB,SAAQ,KAAK;CAAG;AAEjD;;;;;;;;GAQG;AACH,qBAAa,qBAAsB,SAAQ,KAAK;CAAG;AASnD,MAAM,WAAW,eAAe;IAC9B,+CAA+C;IAC/C,GAAG,EAAE,MAAM,CAAC;IACZ,oEAAoE;IACpE,IAAI,CAAC,UAAU,EAAE,eAAe,GAAG;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAAA;KAAE,CAAC;IAChF;;;;OAIG;IACH,IAAI,IAAI,eAAe,EAAE,CAAC;IAC1B,2DAA2D;IAC3D,GAAG,CAAC,EAAE,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAAC;CAChC;AAED,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,MAAM,GAAG,eAAe,CAwEnE;AAoDD;;;;;GAKG;AACH,wBAAgB,WAAW,CAAC,WAAW,EAAE,eAAe,EAAE,GAAG,MAAM,CA2ClE"}
@@ -0,0 +1,215 @@
1
+ // The files: `.bitvea/feedback/<id>.json`, a sibling `<id>.png`, and a
2
+ // regenerated `inbox.md`.
3
+ //
4
+ // THE FILES ARE THE CONTRACT, and the HTTP receiver is only the way a browser
5
+ // reaches them. Every coding agent can already read a file, so "read
6
+ // `.bitvea/feedback/inbox.md`" is a complete integration with no MCP server,
7
+ // no port and no configuration - which is why the files are shaped for a
8
+ // reader rather than for this process. The JSON is the annotation exactly as
9
+ // the toolbar sent it, with the screenshot swapped for the sibling file's
10
+ // name; the inbox is the same facts, newest first, as prose.
11
+ //
12
+ // Nothing is ever read back from `inbox.md`. It is rewritten from the JSON
13
+ // files after every write, so a hand edit to it is overwritten and a hand edit
14
+ // to a JSON file is what the next inbox says.
15
+ import { randomUUID } from "node:crypto";
16
+ import { existsSync, mkdirSync, readdirSync, readFileSync, renameSync, rmSync, writeFileSync, } from "node:fs";
17
+ import { join } from "node:path";
18
+ import { ANNOTATION_ID_PATTERN, decodePngDataUrl, replaceControlCharacters, validateAnnotation, } from "./contract.js";
19
+ /** Where the files go, under the repository root. */
20
+ export const FEEDBACK_DIR = join(".bitvea", "feedback");
21
+ export const INBOX_FILE = "inbox.md";
22
+ /**
23
+ * Written once, the first time the directory is created, and never again.
24
+ *
25
+ * `*` ignores the directory's whole contents, itself included, so a screenshot
26
+ * of somebody's staging data cannot end up in a commit by default - a leak
27
+ * that would outlive the annotation. It is only ever created, never
28
+ * overwritten: a developer who deletes it to commit their annotations has
29
+ * made a decision this process must not undo.
30
+ */
31
+ const GITIGNORE = "# Written by `npx @bitvea/feedback-toolbar dev`.\n*\n";
32
+ export class AnnotationFileError extends Error {
33
+ }
34
+ /**
35
+ * An id that is already on disk.
36
+ *
37
+ * An annotation is written ONCE. The toolbar mints a fresh id for every send,
38
+ * its retry after a 413 included, so nothing it does ever re-sends one; and a
39
+ * receiver that replaced by id would let any other page on this machine's
40
+ * loopback overwrite a colleague's annotation, or delete its screenshot by
41
+ * re-sending the id without one.
42
+ */
43
+ export class AnnotationExistsError extends Error {
44
+ }
45
+ /** Write through a temp file and a rename, so a reader never sees half. */
46
+ function writeAtomically(path, contents) {
47
+ const temp = `${path}.${randomUUID()}.tmp`;
48
+ writeFileSync(temp, contents);
49
+ renameSync(temp, path);
50
+ }
51
+ export function createAnnotationStore(root) {
52
+ const dir = join(root, FEEDBACK_DIR);
53
+ const ensureDir = () => {
54
+ if (!existsSync(dir))
55
+ mkdirSync(dir, { recursive: true });
56
+ const gitignore = join(dir, ".gitignore");
57
+ if (!existsSync(gitignore))
58
+ writeFileSync(gitignore, GITIGNORE, "utf8");
59
+ };
60
+ const list = () => {
61
+ if (!existsSync(dir))
62
+ return [];
63
+ const rows = [];
64
+ for (const name of readdirSync(dir)) {
65
+ if (!name.endsWith(".json"))
66
+ continue;
67
+ const id = name.slice(0, -".json".length);
68
+ if (!ANNOTATION_ID_PATTERN.test(id))
69
+ continue;
70
+ let parsed;
71
+ try {
72
+ parsed = JSON.parse(readFileSync(join(dir, name), "utf8"));
73
+ }
74
+ catch {
75
+ continue;
76
+ }
77
+ const row = storedAnnotation(parsed, id);
78
+ if (row)
79
+ rows.push(row);
80
+ }
81
+ return rows.sort((a, b) => a.timestamp.localeCompare(b.timestamp) || a.id.localeCompare(b.id));
82
+ };
83
+ const writeInbox = () => {
84
+ writeAtomically(join(dir, INBOX_FILE), renderInbox(list()));
85
+ };
86
+ return {
87
+ dir,
88
+ save(annotation) {
89
+ ensureDir();
90
+ const jsonPath = join(dir, `${annotation.id}.json`);
91
+ if (existsSync(jsonPath)) {
92
+ throw new AnnotationExistsError(`${annotation.id} is already stored`);
93
+ }
94
+ const pngPath = join(dir, `${annotation.id}.png`);
95
+ let screenshot = null;
96
+ if (annotation.screenshot !== null) {
97
+ const bytes = decodePngDataUrl(annotation.screenshot);
98
+ if (bytes === null) {
99
+ throw new AnnotationFileError("screenshot: the data URL is not a PNG");
100
+ }
101
+ writeAtomically(pngPath, bytes);
102
+ screenshot = `${annotation.id}.png`;
103
+ }
104
+ else {
105
+ // A picture with no JSON beside it is left over from a write that
106
+ // died between the two files; it describes nothing this annotation
107
+ // says it has.
108
+ rmSync(pngPath, { force: true });
109
+ }
110
+ writeAtomically(jsonPath, `${JSON.stringify({ ...annotation, screenshot }, null, 2)}\n`);
111
+ writeInbox();
112
+ return { jsonPath, pngPath: screenshot === null ? null : pngPath };
113
+ },
114
+ list,
115
+ png(id) {
116
+ if (!ANNOTATION_ID_PATTERN.test(id))
117
+ return null;
118
+ const path = join(dir, `${id}.png`);
119
+ return existsSync(path) ? readFileSync(path) : null;
120
+ },
121
+ };
122
+ }
123
+ /**
124
+ * A stored JSON file as an annotation, or null when it is not one.
125
+ *
126
+ * On disk `screenshot` is the sibling file's name; the rest is the incoming
127
+ * contract, so it is checked by the same validator with the picture set aside.
128
+ */
129
+ function storedAnnotation(value, id) {
130
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
131
+ return null;
132
+ }
133
+ const record = value;
134
+ if (record["id"] !== id)
135
+ return null;
136
+ const screenshot = record["screenshot"];
137
+ if (screenshot !== null && screenshot !== `${id}.png`)
138
+ return null;
139
+ if (validateAnnotation({ ...record, screenshot: null }).length > 0) {
140
+ return null;
141
+ }
142
+ return record;
143
+ }
144
+ /** A code span that survives whatever backticks the value itself holds. */
145
+ function code(value) {
146
+ const longest = Math.max(0, ...(value.match(/`+/g) ?? []).map((m) => m.length));
147
+ const fence = "`".repeat(longest + 1);
148
+ const pad = value.startsWith("`") || value.endsWith("`") ? " " : "";
149
+ return `${fence}${pad}${value}${pad}${fence}`;
150
+ }
151
+ /**
152
+ * Every line ending CommonMark recognises, a lone `\r` included. Splitting on
153
+ * `\r?\n` alone left a bare carriage return inside a quoted line, where a
154
+ * markdown reader ends the line and the `>` with it - so the text of an
155
+ * annotation could step out of its quote and write the inbox's own structure.
156
+ */
157
+ const LINE_ENDING = /\r\n|\r|\n/;
158
+ /** The control characters left once the text is split into lines, tab kept. */
159
+ function withoutControl(line) {
160
+ return replaceControlCharacters(line, "", "\t");
161
+ }
162
+ /** The first line of the text, short enough for a heading. */
163
+ function headline(text) {
164
+ const first = withoutControl(text.trim().split(LINE_ENDING, 1)[0] ?? "");
165
+ const short = first.length > 80 ? `${first.slice(0, 79)}...` : first;
166
+ // A heading is one line of prose; markup characters at its start would make
167
+ // it something else.
168
+ return short.replace(/^[#>*\-+\d.\s]+/, "") || "(no text)";
169
+ }
170
+ /**
171
+ * The inbox: every annotation, newest first, as a person or an agent reads it.
172
+ *
173
+ * Exported for the tests. Deterministic in its input, so regenerating it after
174
+ * a write that changed nothing produces the same bytes.
175
+ */
176
+ export function renderInbox(annotations) {
177
+ const newestFirst = [...annotations].reverse();
178
+ const lines = [
179
+ "# BitVea feedback inbox",
180
+ "",
181
+ "Written by `npx @bitvea/feedback-toolbar dev` from the JSON files in this directory.",
182
+ "It is regenerated after every annotation, so edits made here are overwritten.",
183
+ "Each entry names its JSON file, which holds the same facts in full.",
184
+ "",
185
+ newestFirst.length === 1
186
+ ? "1 annotation, newest first."
187
+ : `${newestFirst.length} annotations, newest first.`,
188
+ ];
189
+ newestFirst.forEach((a, index) => {
190
+ lines.push("", `## ${index + 1}. ${headline(a.text)}`, "");
191
+ for (const raw of a.text.trim().split(LINE_ENDING)) {
192
+ const line = withoutControl(raw);
193
+ lines.push(line.length > 0 ? `> ${line}` : ">");
194
+ }
195
+ lines.push("");
196
+ lines.push(`- Page: ${a.url}`);
197
+ lines.push(a.selector === null
198
+ ? "- Element: none, this is a note about the whole page"
199
+ : `- Element: ${code(a.selector)}`);
200
+ if (a.component) {
201
+ const chain = a.component.owners.length > 1
202
+ ? ` (rendered by ${a.component.owners.slice(1).map(code).join(" < ")})`
203
+ : "";
204
+ lines.push(`- Component: ${code(a.component.name)}${chain}`);
205
+ }
206
+ lines.push(`- Filed: ${a.timestamp}`);
207
+ lines.push(`- Viewport: ${a.viewport.width} x ${a.viewport.height} at ${a.viewport.devicePixelRatio}x`);
208
+ if (a.screenshot !== null) {
209
+ lines.push(`- Screenshot: [${a.screenshot}](${a.screenshot})`);
210
+ }
211
+ lines.push(`- File: [${a.id}.json](${a.id}.json)`);
212
+ });
213
+ return `${lines.join("\n")}\n`;
214
+ }
215
+ //# sourceMappingURL=store.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"store.js","sourceRoot":"","sources":["../../src/dev/store.ts"],"names":[],"mappings":"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"}
package/dist/run.d.ts.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"run.d.ts","sourceRoot":"","sources":["../src/run.ts"],"names":[],"mappings":"AA4CA,MAAM,WAAW,UAAU;IACzB;;;;;;;;OAQG;IACH,cAAc,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;CACrC;AAoQD,wBAAsB,GAAG,CACvB,IAAI,EAAE,MAAM,EAAE,EACd,OAAO,GAAE,UAAe,GACvB,OAAO,CAAC,MAAM,CAAC,CAoBjB"}
1
+ {"version":3,"file":"run.d.ts","sourceRoot":"","sources":["../src/run.ts"],"names":[],"mappings":"AA8CA,MAAM,WAAW,UAAU;IACzB;;;;;;;;OAQG;IACH,cAAc,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;CACrC;AAgRD,wBAAsB,GAAG,CACvB,IAAI,EAAE,MAAM,EAAE,EACd,OAAO,GAAE,UAAe,GACvB,OAAO,CAAC,MAAM,CAAC,CAsBjB"}