pk-annotator 0.7.0 → 0.8.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/dist/pka.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { B as Id, F as readJsonLines, I as resolveInside, M as isErrno, O as PkaError, P as readJson, S as wait, W as Timestamp, _ as prune, a as ErrorsInput, c as ReplyInput, h as list, i as TimelineEntry, j as findStore, l as SetStatusInput, m as get, n as RecordingManifest, o as GetInput, p as errors, s as ListInput, u as attach, v as reply, x as validateAttachTarget, y as setStatus, z as ID_PATTERN } from "./recording-82fajWWq.mjs";
2
+ import { B as Id, F as readJsonLines, I as resolveInside, M as isErrno, O as PkaError, P as readJson, S as wait, W as Timestamp, _ as prune, a as ErrorsInput, c as ReplyInput, h as list, i as TimelineEntry, j as findStore, l as SetStatusInput, m as get, n as RecordingManifest, o as GetInput, p as errors, s as ListInput, u as attach, v as reply, x as validateAttachTarget, y as setStatus, z as ID_PATTERN } from "./recording-D_Qkyq0N.mjs";
3
3
  import { createRequire } from "node:module";
4
4
  import { createWriteStream } from "node:fs";
5
5
  import { mkdir, readFile, stat, writeFile } from "node:fs/promises";
@@ -798,6 +798,7 @@ async function locate(page, target, step) {
798
798
  let notes = [];
799
799
  for (;;) {
800
800
  notes = [];
801
+ let ambiguous = false;
801
802
  for (const [strategy, locator] of strategies) {
802
803
  let count;
803
804
  try {
@@ -808,7 +809,25 @@ async function locate(page, target, step) {
808
809
  continue;
809
810
  }
810
811
  if (count === 1) return locator;
811
- if (count > 1) notes.push(`${strategy} matched ${count}`);
812
+ if (count > 1) {
813
+ ambiguous = true;
814
+ notes.push(`${strategy} matched ${count}`);
815
+ }
816
+ }
817
+ if (!ambiguous && target.label !== void 0) {
818
+ const fields = page.locator(target.tag);
819
+ const matches = await fields.evaluateAll((elements, { label, cap }) => elements.flatMap((element, index) => {
820
+ if (!(element instanceof HTMLInputElement) && !(element instanceof HTMLTextAreaElement) && !(element instanceof HTMLSelectElement) && !(element instanceof HTMLFormElement) && !(element instanceof HTMLElement && element.isContentEditable)) return [];
821
+ const labels = element instanceof HTMLInputElement || element instanceof HTMLTextAreaElement || element instanceof HTMLSelectElement ? element.labels : null;
822
+ if (element.hasAttribute("aria-label") || element.hasAttribute("aria-labelledby") || labels !== null && labels.length > 0) return [];
823
+ return (element.getAttribute("name") ?? element.getAttribute("placeholder"))?.replace(/\s+/g, " ").trim().slice(0, cap) === label ? [index] : [];
824
+ }), {
825
+ label: target.label,
826
+ cap: TEXT_CAP
827
+ });
828
+ const index = matches[0];
829
+ if (matches.length === 1 && index !== void 0) return fields.nth(index);
830
+ if (matches.length > 1) notes.push(`name or placeholder matched ${matches.length}`);
812
831
  }
813
832
  if (Date.now() >= deadline) break;
814
833
  await setTimeout(100);
@@ -850,14 +869,14 @@ async function settle(page, tracker) {
850
869
  function samePage(current, target) {
851
870
  const a = new URL(current);
852
871
  const b = new URL(target);
853
- return a.origin === b.origin && a.pathname === b.pathname && a.search === b.search;
872
+ return a.origin === b.origin && a.pathname === b.pathname && a.search === b.search && a.hash === b.hash;
854
873
  }
855
874
  function producedByReplay(step, target, state) {
856
875
  const want = new URL(target);
857
876
  return state.changes.some(({ type, url, entered }) => {
858
877
  if (type === "reload" || type === "traverse" !== (step.type === "traverse")) return false;
859
878
  const got = new URL(url);
860
- if (got.origin !== want.origin || got.pathname !== want.pathname) return false;
879
+ if (got.origin !== want.origin || got.pathname !== want.pathname || got.hash !== want.hash) return false;
861
880
  return entered || got.search === want.search;
862
881
  });
863
882
  }
@@ -28,6 +28,11 @@ const Selector = z.strictObject({
28
28
  testId: z.string().optional(),
29
29
  css: z.string()
30
30
  });
31
+ const Quote = z.strictObject({
32
+ exact: z.string().min(1),
33
+ prefix: z.string(),
34
+ suffix: z.string()
35
+ });
31
36
  const ElementRef = z.strictObject({
32
37
  n: z.number().int().positive(),
33
38
  url: z.string().optional(),
@@ -39,7 +44,8 @@ const ElementRef = z.strictObject({
39
44
  box: Box,
40
45
  crop: RelativePath.optional(),
41
46
  text: z.string().optional(),
42
- nearbyText: z.string().optional()
47
+ nearbyText: z.string().optional(),
48
+ quote: Quote.optional()
43
49
  });
44
50
  const AttachmentKind = z.enum([
45
51
  "recording",
@@ -528,6 +534,11 @@ async function withAnnotationLock(store, id, act) {
528
534
  const files = await requireAnnotation(store, id);
529
535
  return withLock(store, resolveInside(files.dir, "state.lock"), Date.now() + LOCK_WAIT_MS, () => act(files));
530
536
  }
537
+ async function withLiveErrorsLock(store, act) {
538
+ const lock = resolveInside(store, "live", "errors.lock");
539
+ await refuseSymlinks(store, lock);
540
+ return withLock(store, lock, Date.now() + LOCK_WAIT_MS, act);
541
+ }
531
542
  async function readClaim(store, id) {
532
543
  const files = annotationFiles(store, id);
533
544
  try {
@@ -753,6 +764,11 @@ const ElementView = z.strictObject({
753
764
  }),
754
765
  crop: z.string().optional(),
755
766
  text: z.string().optional(),
767
+ quote: z.strictObject({
768
+ exact: z.string(),
769
+ prefix: z.string(),
770
+ suffix: z.string()
771
+ }).optional().describe("Text the human selected inside the element, with the text just before and after it"),
756
772
  nearbyText: z.string().optional(),
757
773
  html: z.string().optional(),
758
774
  box: Box.optional()
@@ -829,6 +845,11 @@ function elementView(element, detail) {
829
845
  crop: element.crop,
830
846
  text: optionalPageText(element.text, full ? CAP.text : CAP.short)
831
847
  };
848
+ if (element.quote !== void 0) view.quote = {
849
+ exact: pageText(element.quote.exact, full ? CAP.html : CAP.text),
850
+ prefix: pageText(element.quote.prefix, CAP.short),
851
+ suffix: pageText(element.quote.suffix, CAP.short)
852
+ };
832
853
  if (full) {
833
854
  view.nearbyText = optionalPageText(element.nearbyText, CAP.text);
834
855
  view.html = pageText(element.html, CAP.html);
@@ -1324,14 +1345,16 @@ async function errors(store, input) {
1324
1345
  }
1325
1346
  /** Merges new or changed groups into live/errors.json by fingerprint. Used by the Vite plugin. */
1326
1347
  async function upsertErrorGroups(store, groups) {
1327
- const merged = new Map(((await readErrors(store))?.groups ?? []).map((group) => [group.fingerprint, group]));
1328
- for (const group of groups) merged.set(group.fingerprint, group);
1329
- const snapshot = {
1330
- updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
1331
- groups: [...merged.values()].sort((a, b) => Date.parse(b.lastSeen) - Date.parse(a.lastSeen)).slice(0, MAX_ERROR_GROUPS)
1332
- };
1333
- await writeJsonAtomic(store, liveErrorsFile(store), snapshot);
1334
- return snapshot;
1348
+ return withLiveErrorsLock(store, async () => {
1349
+ const merged = new Map(((await readErrors(store))?.groups ?? []).map((group) => [group.fingerprint, group]));
1350
+ for (const group of groups) merged.set(group.fingerprint, group);
1351
+ const snapshot = {
1352
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
1353
+ groups: [...merged.values()].sort((a, b) => Date.parse(b.lastSeen) - Date.parse(a.lastSeen)).slice(0, MAX_ERROR_GROUPS)
1354
+ };
1355
+ await writeJsonAtomic(store, liveErrorsFile(store), snapshot);
1356
+ return snapshot;
1357
+ });
1335
1358
  }
1336
1359
  /**
1337
1360
  * Checks the capture files an annotation declares: each under capture/, listed
package/dist/vite.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import { A as createStore, B as Id, C as agentsDir, D as MissingAnnotationError, H as State, I as resolveInside, L as AnnotationDraft, M as isErrno, N as listIds, O as PkaError, R as ErrorGroup, T as AgentPresence, U as ThreadEntry, V as RelativePath, b as upsertErrorGroups, d as checkCaptureFiles, f as create, g as loadAnnotationUpdates, k as checkStoreSize, r as RecordingManifestDraft, t as RecordingErrors, w as liveAgents, z as ID_PATTERN } from "./recording-82fajWWq.mjs";
1
+ import { A as createStore, B as Id, C as agentsDir, D as MissingAnnotationError, H as State, I as resolveInside, L as AnnotationDraft, M as isErrno, N as listIds, O as PkaError, R as ErrorGroup, T as AgentPresence, U as ThreadEntry, V as RelativePath, b as upsertErrorGroups, d as checkCaptureFiles, f as create, g as loadAnnotationUpdates, k as checkStoreSize, r as RecordingManifestDraft, t as RecordingErrors, w as liveAgents, z as ID_PATTERN } from "./recording-D_Qkyq0N.mjs";
2
2
  import { createRequire } from "node:module";
3
3
  import { execFile } from "node:child_process";
4
4
  import { randomBytes } from "node:crypto";
@@ -15,6 +15,9 @@ import { TraceMap, originalPositionFor } from "@jridgewell/trace-mapping";
15
15
  const CHANNEL = {
16
16
  create: "pka:create",
17
17
  file: "pka:file",
18
+ uploadReady: "pka:upload-ready",
19
+ fileWritten: "pka:file-written",
20
+ cancelUpload: "pka:cancel-upload",
18
21
  errors: "pka:errors",
19
22
  created: "pka:created",
20
23
  createFailed: "pka:create-failed",
@@ -48,6 +51,7 @@ const FileChunkMessage = z.strictObject({
48
51
  offset: z.number().int().nonnegative(),
49
52
  data: z.base64().max(Math.ceil(MAX_CHUNK_BYTES / 3) * 4)
50
53
  });
54
+ const CancelUploadMessage = z.strictObject({ requestId: Id });
51
55
  const ErrorsMessage = z.strictObject({ groups: z.array(ErrorGroup).min(1).max(50) });
52
56
  const SyncMessage = z.strictObject({ ids: z.array(Id).max(200) });
53
57
  const SymbolicateMessage = z.strictObject({
@@ -61,6 +65,12 @@ const InstallUpdateMessage = z.strictObject({
61
65
  requestId: Id,
62
66
  version: Version
63
67
  });
68
+ z.strictObject({ requestId: Id });
69
+ z.strictObject({
70
+ requestId: Id,
71
+ path: RelativePath,
72
+ offset: z.number().int().nonnegative()
73
+ });
64
74
  z.strictObject({
65
75
  requestId: Id,
66
76
  id: Id,
@@ -468,6 +478,7 @@ const BODIES_GLOBAL = "__PKA_BODIES__";
468
478
  const STAGING_DIR = ".staging";
469
479
  const MB = 1048576;
470
480
  const MAX_RECORDING_JSON_BYTES = 4 * MB;
481
+ const MAX_UPLOADS = 8;
471
482
  const execFileAsync = promisify(execFile);
472
483
  /** A staging directory named <pid>-<hex> belongs to a running process unless that pid is gone. */
473
484
  function ownerAlive(name) {
@@ -597,10 +608,17 @@ async function serve(server, maxStoreBytes, storeRoot) {
597
608
  try {
598
609
  await step();
599
610
  } catch (thrown) {
600
- await abort(upload, thrown);
611
+ if (!upload.done) await abort(upload, thrown);
601
612
  }
602
613
  }).catch((cause) => error(`cleaning up ${upload.requestId} failed: ${describeError(cause)}`));
603
614
  }
615
+ function cancel(upload, reason) {
616
+ if (upload.done) return;
617
+ upload.done = true;
618
+ upload.queue = upload.queue.then(async () => {
619
+ if (uploads.get(upload.requestId) === upload) await abort(upload, reason);
620
+ }).catch((cause) => error(`upload cleanup failed: ${describeError(cause)}`));
621
+ }
604
622
  /** Replaces the page's raw stack and top frame with source positions. */
605
623
  async function symbolicateGroup(group) {
606
624
  const { topFrame: _pageTopFrame, ...rest } = group;
@@ -647,7 +665,11 @@ async function serve(server, maxStoreBytes, storeRoot) {
647
665
  }
648
666
  async function finish(upload) {
649
667
  const recordingDirectories = new Set(upload.draft.attachments.filter((attachment) => attachment.kind === "recording").map((attachment) => path.posix.dirname(attachment.path)));
650
- for (const directory of recordingDirectories) await completeRecording(upload, directory);
668
+ for (const directory of recordingDirectories) {
669
+ await completeRecording(upload, directory);
670
+ if (upload.done) return;
671
+ }
672
+ if (upload.done) return;
651
673
  const { id } = await create(store, upload.draft, {
652
674
  dir: upload.dir,
653
675
  paths: [...upload.files.keys()]
@@ -696,6 +718,10 @@ async function serve(server, maxStoreBytes, storeRoot) {
696
718
  refuse(`Request ${requestId} is already in progress`);
697
719
  return;
698
720
  }
721
+ if (uploads.size >= MAX_UPLOADS || [...uploads.values()].some((upload) => upload.client === client)) {
722
+ refuse("Another upload is in progress; wait for it to finish and retry");
723
+ return;
724
+ }
699
725
  try {
700
726
  checkCaptureFiles(message.draft, message.files.map((file) => file.path));
701
727
  } catch (thrown) {
@@ -715,11 +741,19 @@ async function serve(server, maxStoreBytes, storeRoot) {
715
741
  }])),
716
742
  remaining: declaredBytes,
717
743
  done: false,
744
+ busy: true,
718
745
  queue: Promise.resolve()
719
746
  };
720
747
  uploads.set(requestId, upload);
721
- enqueue(upload, () => prepare(upload, declaredBytes));
722
- if (declaredBytes === 0) enqueue(upload, () => finish(upload));
748
+ enqueue(upload, async () => {
749
+ await prepare(upload, declaredBytes);
750
+ if (upload.done) return;
751
+ if (declaredBytes === 0) await finish(upload);
752
+ else {
753
+ upload.busy = false;
754
+ client.send(CHANNEL.uploadReady, { requestId });
755
+ }
756
+ });
723
757
  });
724
758
  listen(CHANNEL.file, FileChunkMessage, async (message, client) => {
725
759
  const upload = uploads.get(message.requestId);
@@ -727,18 +761,36 @@ async function serve(server, maxStoreBytes, storeRoot) {
727
761
  warn(`dropped ${CHANNEL.file}: no upload ${message.requestId} in progress for this page`);
728
762
  return;
729
763
  }
764
+ if (upload.done) return;
765
+ if (upload.busy) {
766
+ cancel(upload, new PkaError("Wait for the upload acknowledgement before sending another chunk; reload the page and retry"));
767
+ return;
768
+ }
730
769
  const file = upload.files.get(message.path);
731
770
  const data = Buffer.from(message.data, "base64");
732
- if (file === void 0 || message.offset !== file.received || file.received + data.length > file.bytes) {
733
- const reason = new PkaError(file === void 0 ? `Chunk for ${message.path}, which the request did not declare` : `Chunk for ${message.path} at offset ${message.offset} does not continue the ${file.received} of ${file.bytes} bytes received`);
734
- enqueue(upload, () => Promise.reject(reason));
771
+ if (file === void 0 || data.length === 0 || message.offset !== file.received || file.received + data.length > file.bytes) {
772
+ cancel(upload, new PkaError(file === void 0 ? `Chunk for ${message.path}, which the request did not declare` : `Chunk for ${message.path} at offset ${message.offset} does not continue the ${file.received} of ${file.bytes} bytes received`));
735
773
  return;
736
774
  }
737
775
  file.received += data.length;
738
776
  upload.remaining -= data.length;
777
+ upload.busy = true;
739
778
  const target = resolveInside(upload.dir, message.path);
740
- enqueue(upload, () => writeChunk(target, message.offset, data));
741
- if (upload.remaining === 0) enqueue(upload, () => finish(upload));
779
+ enqueue(upload, async () => {
780
+ await writeChunk(target, message.offset, data);
781
+ if (upload.done) return;
782
+ if (upload.remaining !== 0) upload.busy = false;
783
+ client.send(CHANNEL.fileWritten, {
784
+ requestId: upload.requestId,
785
+ path: message.path,
786
+ offset: file.received
787
+ });
788
+ if (upload.remaining === 0) await finish(upload);
789
+ });
790
+ });
791
+ listen(CHANNEL.cancelUpload, CancelUploadMessage, async (message, client) => {
792
+ const upload = uploads.get(message.requestId);
793
+ if (upload?.client === client) cancel(upload, new PkaError("The page cancelled the upload"));
742
794
  });
743
795
  listen(CHANNEL.errors, ErrorsMessage, async (message, client) => {
744
796
  const groups = await Promise.all(message.groups.map(symbolicateGroup));
@@ -808,7 +860,7 @@ async function serve(server, maxStoreBytes, storeRoot) {
808
860
  client.send(CHANNEL.synced, synced);
809
861
  });
810
862
  const onDisconnect = (_data, client) => {
811
- for (const upload of uploads.values()) if (upload.client === client) enqueue(upload, () => Promise.reject(new PkaError("The page disconnected before the upload finished")));
863
+ for (const upload of uploads.values()) if (upload.client === client) cancel(upload, new PkaError("The page disconnected before the upload finished"));
812
864
  };
813
865
  hot.on("vite:client:disconnect", onDisconnect);
814
866
  listeners.push(["vite:client:disconnect", onDisconnect]);
@@ -946,6 +998,7 @@ async function serve(server, maxStoreBytes, storeRoot) {
946
998
  annotationWatchers.clear();
947
999
  for (const [event, listener] of listeners) hot.off(event, listener);
948
1000
  for (const upload of uploads.values()) upload.done = true;
1001
+ await Promise.all([...uploads.values()].map((upload) => upload.queue));
949
1002
  uploads.clear();
950
1003
  await rm(serverStaging, {
951
1004
  recursive: true,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pk-annotator",
3
- "version": "0.7.0",
3
+ "version": "0.8.0",
4
4
  "description": "Dev-only browser annotation overlay for Vite apps that sends picked elements, captures, and prompts to Claude Code and Codex through an MCP server.",
5
5
  "keywords": [
6
6
  "annotation",