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/README.ko.md +9 -9
- package/README.md +2 -2
- package/dist/overlay.mjs +3 -3
- package/dist/{pka-mcp-server-DeBVJQYE.mjs → pka-mcp-server-BhcwUcgs.mjs} +20 -3
- package/dist/pka-mcp.mjs +1 -1
- package/dist/{pka-overlay-agent-theme-DfiOu97H.mjs → pka-overlay-agent-theme-BiWi6_IJ.mjs} +1 -1
- package/dist/pka-overlay-app-DjGKtS7t.mjs +274 -0
- package/dist/pka-overlay-channel-BZI-qbPH.mjs +113 -0
- package/dist/{pka-overlay-composer-CK1sTHAv.mjs → pka-overlay-composer-DCu_eGT0.mjs} +26 -26
- package/dist/{pka-overlay-copy-BTMmwtSZ.mjs → pka-overlay-copy-B-PxSL-y.mjs} +1 -1
- package/dist/{pka-overlay-perf-panel-xd8kgxtm.mjs → pka-overlay-perf-panel-DZY5Nwil.mjs} +2 -2
- package/dist/pka-overlay-send-Cu8DXY6B.mjs +139 -0
- package/dist/pka-overlay-thread-store-C55iLxND.mjs +1 -0
- package/dist/pka.mjs +23 -4
- package/dist/{recording-82fajWWq.mjs → recording-D_Qkyq0N.mjs} +32 -9
- package/dist/vite.mjs +64 -11
- package/package.json +1 -1
- package/dist/pka-overlay-app-D-PF-HmH.mjs +0 -267
- package/dist/pka-overlay-channel-C6qfFolf.mjs +0 -113
- package/dist/pka-overlay-send-UrgGuLhh.mjs +0 -139
- package/dist/pka-overlay-thread-store-X3FM2ev_.mjs +0 -1
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-
|
|
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)
|
|
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
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
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-
|
|
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)
|
|
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, () =>
|
|
722
|
-
|
|
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
|
-
|
|
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, () =>
|
|
741
|
-
|
|
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)
|
|
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.
|
|
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",
|