artarch-feedback-sdk 0.1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 tool-canvas contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,205 @@
1
+ # artarch-feedback-sdk
2
+
3
+ Headless browser SDK for collecting feedback with screenshots and screen recordings. The SDK owns browser capture and asset lifecycle; the host application owns authentication, CDN storage, and feedback APIs.
4
+
5
+ ## About ArtArch
6
+
7
+ [ArtArch](https://www.artarch.ai/) is an AI-native Creative OS for creators and
8
+ brands. It brings together AI-powered content creation, reusable production
9
+ workflows, and leading AI models to help users create, express, grow, and monetize
10
+ their work.
11
+
12
+ ## Purpose and capabilities
13
+
14
+ The package lets users send the visual context of a problem along with their
15
+ description. Capture and submission are separate operations: creating a screenshot
16
+ or recording returns a local `Blob`; calling `submit()` invokes the host's upload
17
+ and feedback adapters.
18
+
19
+ | Capability | Current behavior |
20
+ | --- | --- |
21
+ | Screenshot | Captures a frame from a user-selected browser tab, window, or screen as PNG. Optional maximum dimensions preserve the source aspect ratio. |
22
+ | Screen recording | Records the selected surface using `MediaRecorder`, with MIME detection, manual stop, a duration limit, and browser-ended capture handling. |
23
+ | Shared audio | Requests audio from the selected surface with `audio: true`, subject to browser and OS support. This does not request microphone access. |
24
+ | Capture lifecycle | Handles permission denial, cancellation, overlapping starts/stops, final media chunks, and release of tracks and listeners. |
25
+ | Feedback submission | Accepts text alone, text with a screenshot, text with a recording, or both attachments. Uploads finish before the feedback record is submitted. |
26
+ | Business context | Forwards host-provided `context`, such as platform, project ID, route, or node ID, without collecting it automatically. |
27
+
28
+ Capture requires a secure context (HTTPS or localhost), a user gesture, and the
29
+ browser's permission picker. The available surfaces and audio sources depend on
30
+ the browser and OS. This is real screen capture, not DOM session replay.
31
+
32
+ ## SDK and host responsibilities
33
+
34
+ The core is framework-independent and has no runtime dependencies on React,
35
+ Next.js, Axios, Sentry, or a cloud storage provider.
36
+
37
+ | Owner | Responsibilities |
38
+ | --- | --- |
39
+ | SDK | Browser capture, local media files, capture errors and cancellation, and sequencing upload/submission through `FeedbackTransport`. |
40
+ | Host `uploadAsset` adapter | Authentication, upload credentials, storage/CDN selection, and uploading the provided file. Returns `{ url, id? }`. |
41
+ | Host `submitFeedback` adapter | Sending the message, uploaded attachment URLs, and context to a business API. Returns `{ id? }`. |
42
+ | Host application | Feedback button and form, previews, validation, user-facing error messages, context selection, retention, and the feedback management backend. |
43
+
44
+ A host can build this user flow around the SDK:
45
+
46
+ ```text
47
+ Open feedback form -> Enter description -> Capture screenshot/recording
48
+ -> Preview in the host UI -> Submit -> Upload files -> Save feedback record
49
+ ```
50
+
51
+ `uploadAsset` can target OSS, S3, R2, or any other storage system; `submitFeedback`
52
+ can target an internal API or an adapter to another service. These are extension
53
+ interfaces, not bundled provider integrations. The host also owns cleanup of any
54
+ uploaded files left behind when another upload or the final submission fails.
55
+
56
+ ## Current scope
57
+
58
+ The current deliverable is the core SDK. It does not include a feedback widget,
59
+ preview UI, screenshot annotation/redaction, video editing, a feedback backend,
60
+ or an administration console. It does not automatically collect console logs,
61
+ network requests, page URLs, or historical session replays.
62
+
63
+ The npm package name is `artarch-feedback-sdk`; its directory in this repository
64
+ is `packages/feedback-sdk`. The SDK is not yet connected to the production
65
+ `tool-canvas` feedback UI. Update this status when integrating the package.
66
+
67
+ ## Install
68
+
69
+ ```bash
70
+ npm install artarch-feedback-sdk
71
+ # or
72
+ pnpm add artarch-feedback-sdk
73
+ ```
74
+
75
+ For local development, run `npm pack` in this package and install the
76
+ resulting `.tgz` in a consuming project. See the build and test commands below.
77
+
78
+ ## Host-provided transport
79
+
80
+ ```ts
81
+ import { FeedbackClient } from 'artarch-feedback-sdk';
82
+
83
+ const feedback = new FeedbackClient({
84
+ async uploadAsset({ asset, signal }) {
85
+ const ticket = await getUploadTicket({
86
+ filename: asset.filename,
87
+ contentType: asset.contentType,
88
+ signal,
89
+ });
90
+
91
+ const response = await fetch(ticket.uploadUrl, {
92
+ method: 'PUT',
93
+ body: asset.blob,
94
+ headers: { 'Content-Type': asset.contentType },
95
+ signal,
96
+ });
97
+
98
+ if (!response.ok) throw new Error('Asset upload failed');
99
+
100
+ return { url: ticket.cdnUrl, id: ticket.assetId };
101
+ },
102
+ submitFeedback(payload, options) {
103
+ return api.post('/feedback', payload, { signal: options?.signal });
104
+ },
105
+ });
106
+ ```
107
+
108
+ ## Capture and submit
109
+
110
+ `getDisplayMedia()` must be called from a user gesture. The browser will show its own permission picker every time.
111
+
112
+ ```ts
113
+ const screenshot = await feedback.captureScreenshot({ preferCurrentTab: true });
114
+
115
+ await feedback.submit({
116
+ message: '节点预览区域显示不完整',
117
+ screenshot,
118
+ context: {
119
+ platform: 'studio',
120
+ projectId,
121
+ route: window.location.href,
122
+ },
123
+ });
124
+ ```
125
+
126
+ For recording:
127
+
128
+ ```ts
129
+ await feedback.startRecording({ audio: true, maxDurationMs: 60_000 });
130
+ // User clicks stop later.
131
+ const recording = await feedback.stopRecording();
132
+ await feedback.submit({ message, recording });
133
+ ```
134
+
135
+ To handle automatic completion (duration limit or the browser ending sharing),
136
+ await `feedback.recorder.finished` after `startRecording()` resolves. `stop()` and
137
+ `finished` return the same result, retained until the next `start()`. Concurrent
138
+ stop calls include the final media chunk. Starting while permission is pending,
139
+ recording is active, or the result is being finalized rejects with
140
+ `recording-already-started`.
141
+
142
+ Passing an `AbortSignal` cancels capture and discards the recording; use `stop()`
143
+ to keep it. Cancelling a pending permission prompt cannot close the browser UI,
144
+ but the SDK rejects promptly and releases a stream if permission is granted later.
145
+ Screenshots use the selected source's pixels by default. Optional width/height
146
+ are maximum bounds and preserve its aspect ratio.
147
+
148
+ Upload and submit errors propagate to the host. The SDK never submits feedback
149
+ after an upload fails or the operation is cancelled. Transport adapters must
150
+ honor `signal` to abort their actual HTTP requests. Cancellation cannot roll back
151
+ an upload or feedback record already accepted by a server; the host owns cleanup.
152
+
153
+ The package intentionally has no React dependency and no CDN client. `FeedbackTransport` is the integration boundary for a presigned upload API, S3/R2/OSS, Sentry, or an internal feedback service.
154
+
155
+ ## Build and publish
156
+
157
+ Run inside this package using the npm account authorized to publish it:
158
+
159
+ ```bash
160
+ cd packages/feedback-sdk
161
+ npm run build
162
+ npm pack
163
+ npm publish --access public
164
+ ```
165
+
166
+ ## Tests
167
+
168
+ From the workspace root:
169
+
170
+ ```bash
171
+ pnpm install --frozen-lockfile
172
+ pnpm --filter artarch-feedback-sdk exec playwright install chromium
173
+ pnpm --filter artarch-feedback-sdk test:all
174
+ ```
175
+
176
+ Or, after installing this package's development dependencies, run inside this directory:
177
+
178
+ ```bash
179
+ npm test # Node tests plus an isolated tarball consumer
180
+ npm run test:browser # Real Chromium capture and playback
181
+ npm run test:all # Both suites
182
+ ```
183
+
184
+ - Node tests cover permission failures, cancellation before/during capture,
185
+ late permission results, first-frame readiness, dimensions, recorder errors,
186
+ concurrent starts/stops, automatic stops, cleanup, upload ordering and failure.
187
+ - The package test installs the actual npm tarball in a temporary consumer
188
+ outside the repository and checks native Node ESM imports and NodeNext types.
189
+ - Playwright serves a local fixture on a random port and closes it after testing.
190
+ It uses native `getDisplayMedia` and `MediaRecorder`, with Chromium test flags
191
+ selecting/rejecting the fixture tab. Tests decode PNG pixels at desktop and
192
+ narrow viewport sizes, play and sample changing video frames, and check cleanup.
193
+ No production accounts, CDN endpoints or feedback services are contacted.
194
+
195
+ Successful screenshots and recordings are attached under `test-results/`.
196
+ Failed browser tests retain screenshots and traces. Browser binaries are test
197
+ prerequisites; on Linux CI install them with `playwright install --with-deps chromium`.
198
+ `prepublishOnly` runs both suites and `prepack` rebuilds the package.
199
+
200
+ Automated tests cover Chromium tab capture. They do not physically click the
201
+ browser toolbar's Stop Sharing button: unit tests dispatch the track's `ended`
202
+ event and browser tests terminate the actual stream. Before release, manually
203
+ check that toolbar action, the native picker, window/screen capture, and audio
204
+ sharing on each supported browser/OS. A narrow Chromium viewport is not mobile
205
+ Safari coverage.
@@ -0,0 +1,4 @@
1
+ export declare function abortError(): DOMException;
2
+ export declare function throwIfAborted(signal?: AbortSignal): void;
3
+ export declare function abortable<T>(pending: Promise<T>, signal?: AbortSignal, releaseLateResult?: (value: T) => void): Promise<T>;
4
+ //# sourceMappingURL=abort.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"abort.d.ts","sourceRoot":"","sources":["../src/abort.ts"],"names":[],"mappings":"AAAA,wBAAgB,UAAU,IAAI,YAAY,CAEzC;AAED,wBAAgB,cAAc,CAAC,MAAM,CAAC,EAAE,WAAW,GAAG,IAAI,CAEzD;AAID,wBAAgB,SAAS,CAAC,CAAC,EACzB,OAAO,EAAE,OAAO,CAAC,CAAC,CAAC,EACnB,MAAM,CAAC,EAAE,WAAW,EACpB,iBAAiB,CAAC,EAAE,CAAC,KAAK,EAAE,CAAC,KAAK,IAAI,GACrC,OAAO,CAAC,CAAC,CAAC,CAoBZ"}
package/dist/abort.js ADDED
@@ -0,0 +1,35 @@
1
+ export function abortError() {
2
+ return new DOMException('The operation was aborted.', 'AbortError');
3
+ }
4
+ export function throwIfAborted(signal) {
5
+ if (signal?.aborted)
6
+ throw abortError();
7
+ }
8
+ // Native capture prompts cannot be dismissed programmatically. Release any
9
+ // stream granted after cancellation, even after the caller has moved on.
10
+ export function abortable(pending, signal, releaseLateResult) {
11
+ if (!signal)
12
+ return pending;
13
+ return new Promise((resolve, reject) => {
14
+ let cancelled = false;
15
+ const onAbort = () => {
16
+ cancelled = true;
17
+ signal.removeEventListener('abort', onAbort);
18
+ reject(abortError());
19
+ };
20
+ signal.addEventListener('abort', onAbort, { once: true });
21
+ if (signal.aborted)
22
+ onAbort();
23
+ pending.then(value => {
24
+ signal.removeEventListener('abort', onAbort);
25
+ if (cancelled)
26
+ releaseLateResult?.(value);
27
+ else
28
+ resolve(value);
29
+ }, error => {
30
+ signal.removeEventListener('abort', onAbort);
31
+ reject(error);
32
+ });
33
+ });
34
+ }
35
+ //# sourceMappingURL=abort.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"abort.js","sourceRoot":"","sources":["../src/abort.ts"],"names":[],"mappings":"AAAA,MAAM,UAAU,UAAU;IACxB,OAAO,IAAI,YAAY,CAAC,4BAA4B,EAAE,YAAY,CAAC,CAAC;AACtE,CAAC;AAED,MAAM,UAAU,cAAc,CAAC,MAAoB;IACjD,IAAI,MAAM,EAAE,OAAO;QAAE,MAAM,UAAU,EAAE,CAAC;AAC1C,CAAC;AAED,2EAA2E;AAC3E,yEAAyE;AACzE,MAAM,UAAU,SAAS,CACvB,OAAmB,EACnB,MAAoB,EACpB,iBAAsC;IAEtC,IAAI,CAAC,MAAM;QAAE,OAAO,OAAO,CAAC;IAC5B,OAAO,IAAI,OAAO,CAAI,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE;QACxC,IAAI,SAAS,GAAG,KAAK,CAAC;QACtB,MAAM,OAAO,GAAG,GAAG,EAAE;YACnB,SAAS,GAAG,IAAI,CAAC;YACjB,MAAM,CAAC,mBAAmB,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;YAC7C,MAAM,CAAC,UAAU,EAAE,CAAC,CAAC;QACvB,CAAC,CAAC;QACF,MAAM,CAAC,gBAAgB,CAAC,OAAO,EAAE,OAAO,EAAE,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC,CAAC;QAC1D,IAAI,MAAM,CAAC,OAAO;YAAE,OAAO,EAAE,CAAC;QAC9B,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE;YACnB,MAAM,CAAC,mBAAmB,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;YAC7C,IAAI,SAAS;gBAAE,iBAAiB,EAAE,CAAC,KAAK,CAAC,CAAC;;gBACrC,OAAO,CAAC,KAAK,CAAC,CAAC;QACtB,CAAC,EAAE,KAAK,CAAC,EAAE;YACT,MAAM,CAAC,mBAAmB,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;YAC7C,MAAM,CAAC,KAAK,CAAC,CAAC;QAChB,CAAC,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;AACL,CAAC"}
@@ -0,0 +1,14 @@
1
+ import type { CapturedAsset, RecordingOptions, ScreenshotOptions } from './types.js';
2
+ export declare function captureScreenshot(options?: ScreenshotOptions): Promise<CapturedAsset>;
3
+ export declare class ScreenRecorder {
4
+ private phase;
5
+ private recorder?;
6
+ private completion?;
7
+ private requestStop?;
8
+ start(options?: RecordingOptions): Promise<void>;
9
+ /** Resolves on manual, duration-limit, or browser stop; rejects on abort/error. */
10
+ get finished(): Promise<CapturedAsset>;
11
+ stop(): Promise<CapturedAsset>;
12
+ get isRecording(): boolean;
13
+ }
14
+ //# sourceMappingURL=capture.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"capture.d.ts","sourceRoot":"","sources":["../src/capture.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,aAAa,EAAE,gBAAgB,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAC;AAmDrF,wBAAsB,iBAAiB,CAAC,OAAO,GAAE,iBAAsB,GAAG,OAAO,CAAC,aAAa,CAAC,CAwC/F;AAED,qBAAa,cAAc;IACzB,OAAO,CAAC,KAAK,CAA4D;IACzE,OAAO,CAAC,QAAQ,CAAC,CAAgB;IACjC,OAAO,CAAC,UAAU,CAAC,CAAyB;IAC5C,OAAO,CAAC,WAAW,CAAC,CAAa;IAE3B,KAAK,CAAC,OAAO,GAAE,gBAAqB,GAAG,OAAO,CAAC,IAAI,CAAC;IA8G1D,mFAAmF;IACnF,IAAI,QAAQ,IAAI,OAAO,CAAC,aAAa,CAAC,CAErC;IAED,IAAI,IAAI,OAAO,CAAC,aAAa,CAAC;IAK9B,IAAI,WAAW,IAAI,OAAO,CAEzB;CACF"}
@@ -0,0 +1,256 @@
1
+ import { abortable, abortError, throwIfAborted } from './abort.js';
2
+ import { FeedbackCaptureError, normalizeCaptureError } from './errors.js';
3
+ const MIME_TYPES = [
4
+ 'video/mp4;codecs=avc1.424028,mp4a.40.2',
5
+ 'video/mp4',
6
+ 'video/webm;codecs=vp9,opus',
7
+ 'video/webm;codecs=vp8,opus',
8
+ 'video/webm',
9
+ ];
10
+ function ensureBrowserSupport() {
11
+ if (typeof navigator === 'undefined' || !navigator.mediaDevices?.getDisplayMedia) {
12
+ throw new FeedbackCaptureError('unsupported', 'Screen capture is not supported in this browser.');
13
+ }
14
+ }
15
+ function stopTracks(stream) {
16
+ stream.getTracks().forEach(track => track.stop());
17
+ }
18
+ function waitForFrame(video, stream, signal) {
19
+ return new Promise((resolve, reject) => {
20
+ const track = stream.getVideoTracks()[0];
21
+ let playing = false;
22
+ const cleanup = () => {
23
+ clearTimeout(timer);
24
+ video.removeEventListener('loadeddata', onFrame);
25
+ video.removeEventListener('error', onError);
26
+ track?.removeEventListener('ended', onError);
27
+ signal?.removeEventListener('abort', onAbort);
28
+ };
29
+ const fail = (error) => { cleanup(); reject(error); };
30
+ const onFrame = () => {
31
+ if (playing && video.readyState >= 2) {
32
+ cleanup();
33
+ resolve();
34
+ }
35
+ };
36
+ const onError = () => fail(new Error('Unable to read a frame from the captured screen.'));
37
+ const onAbort = () => fail(abortError());
38
+ const timer = setTimeout(onError, 10000);
39
+ video.addEventListener('loadeddata', onFrame);
40
+ video.addEventListener('error', onError);
41
+ track?.addEventListener('ended', onError);
42
+ signal?.addEventListener('abort', onAbort, { once: true });
43
+ if (signal?.aborted) {
44
+ onAbort();
45
+ return;
46
+ }
47
+ if (!track || track.readyState === 'ended') {
48
+ onError();
49
+ return;
50
+ }
51
+ try {
52
+ video.srcObject = stream;
53
+ video.play().then(() => { playing = true; onFrame(); }, fail);
54
+ }
55
+ catch (error) {
56
+ fail(error);
57
+ }
58
+ });
59
+ }
60
+ export async function captureScreenshot(options = {}) {
61
+ let stream;
62
+ let video;
63
+ try {
64
+ throwIfAborted(options.signal);
65
+ ensureBrowserSupport();
66
+ for (const size of [options.width, options.height]) {
67
+ if (size !== undefined && (!Number.isFinite(size) || size < 1))
68
+ throw new Error('Screenshot dimensions must be positive.');
69
+ }
70
+ stream = await abortable(navigator.mediaDevices.getDisplayMedia({
71
+ video: true,
72
+ audio: false,
73
+ ...{ preferCurrentTab: options.preferCurrentTab ?? true },
74
+ }), options.signal, stopTracks);
75
+ throwIfAborted(options.signal);
76
+ video = document.createElement('video');
77
+ video.muted = true;
78
+ video.playsInline = true;
79
+ await waitForFrame(video, stream, options.signal);
80
+ const scale = Math.min(1, (options.width ?? video.videoWidth) / video.videoWidth, (options.height ?? video.videoHeight) / video.videoHeight);
81
+ const width = Math.max(1, Math.round(video.videoWidth * scale));
82
+ const height = Math.max(1, Math.round(video.videoHeight * scale));
83
+ const canvas = document.createElement('canvas');
84
+ canvas.width = width;
85
+ canvas.height = height;
86
+ const context = canvas.getContext('2d');
87
+ if (!context)
88
+ throw new Error('Unable to create a screenshot canvas.');
89
+ context.drawImage(video, 0, 0, width, height);
90
+ const blob = await abortable(new Promise((resolve, reject) => {
91
+ canvas.toBlob(value => value ? resolve(value) : reject(new Error('Unable to encode screenshot.')), 'image/png');
92
+ }), options.signal);
93
+ throwIfAborted(options.signal);
94
+ return { kind: 'screenshot', blob, contentType: blob.type, filename: 'screenshot.png', width, height };
95
+ }
96
+ catch (error) {
97
+ throw normalizeCaptureError(error);
98
+ }
99
+ finally {
100
+ if (video) {
101
+ video.pause();
102
+ video.srcObject = null;
103
+ }
104
+ if (stream)
105
+ stopTracks(stream);
106
+ }
107
+ }
108
+ export class ScreenRecorder {
109
+ constructor() {
110
+ this.phase = 'idle';
111
+ }
112
+ async start(options = {}) {
113
+ if (this.phase !== 'idle') {
114
+ throw new FeedbackCaptureError('recording-already-started', 'A recording is already starting, running, or stopping.');
115
+ }
116
+ this.phase = 'requesting';
117
+ this.completion = undefined;
118
+ let stream;
119
+ try {
120
+ throwIfAborted(options.signal);
121
+ ensureBrowserSupport();
122
+ if (typeof MediaRecorder === 'undefined')
123
+ throw new FeedbackCaptureError('unsupported', 'Screen recording is not supported in this browser.');
124
+ if (options.maxDurationMs !== undefined && (!Number.isFinite(options.maxDurationMs) || options.maxDurationMs <= 0 || options.maxDurationMs > 2147483647)) {
125
+ throw new Error('Maximum duration must be a positive number within the browser timer limit.');
126
+ }
127
+ const mimeType = options.mimeType && MediaRecorder.isTypeSupported(options.mimeType)
128
+ ? options.mimeType : MIME_TYPES.find(type => MediaRecorder.isTypeSupported(type));
129
+ stream = await abortable(navigator.mediaDevices.getDisplayMedia({
130
+ video: options.video ?? true,
131
+ audio: options.audio ?? false,
132
+ }), options.signal, stopTracks);
133
+ throwIfAborted(options.signal);
134
+ const track = stream.getVideoTracks()[0];
135
+ if (!track || track.readyState === 'ended')
136
+ throw new Error('No live screen video track was returned.');
137
+ const recorder = new MediaRecorder(stream, {
138
+ ...(mimeType ? { mimeType } : {}),
139
+ ...(options.videoBitsPerSecond ? { videoBitsPerSecond: options.videoBitsPerSecond } : {}),
140
+ ...(options.audioBitsPerSecond ? { audioBitsPerSecond: options.audioBitsPerSecond } : {}),
141
+ });
142
+ this.recorder = recorder;
143
+ const capturedStream = stream;
144
+ const chunks = [];
145
+ const startedAt = Date.now();
146
+ let stoppedAt;
147
+ let settled = false;
148
+ let timer;
149
+ let fail;
150
+ this.completion = new Promise((resolve, reject) => {
151
+ const cleanup = () => {
152
+ clearTimeout(timer);
153
+ recorder.removeEventListener('dataavailable', onData);
154
+ recorder.removeEventListener('stop', onStop);
155
+ recorder.removeEventListener('error', onError);
156
+ track.removeEventListener('ended', stop);
157
+ options.signal?.removeEventListener('abort', onAbort);
158
+ stopTracks(capturedStream);
159
+ this.recorder = undefined;
160
+ this.requestStop = undefined;
161
+ this.phase = 'idle';
162
+ };
163
+ fail = error => {
164
+ if (settled)
165
+ return;
166
+ settled = true;
167
+ if (recorder.state !== 'inactive') {
168
+ try {
169
+ recorder.stop();
170
+ }
171
+ catch { /* Track cleanup still releases capture. */ }
172
+ }
173
+ cleanup();
174
+ chunks.length = 0;
175
+ reject(normalizeCaptureError(error));
176
+ };
177
+ const onData = (event) => { if (event.data.size > 0)
178
+ chunks.push(event.data); };
179
+ const onStop = () => {
180
+ if (settled)
181
+ return;
182
+ const contentType = recorder.mimeType || chunks[0]?.type || 'video/webm';
183
+ const blob = new Blob(chunks, { type: contentType });
184
+ if (!blob.size) {
185
+ fail(new Error('The recording produced no data.'));
186
+ return;
187
+ }
188
+ const { width, height } = track.getSettings();
189
+ settled = true;
190
+ cleanup();
191
+ chunks.length = 0;
192
+ resolve({ kind: 'recording', blob, contentType,
193
+ filename: contentType.includes('mp4') ? 'recording.mp4' : 'recording.webm',
194
+ durationMs: (stoppedAt ?? Date.now()) - startedAt, width, height });
195
+ };
196
+ const onError = (event) => fail(event.error ?? event);
197
+ const onAbort = () => fail(abortError());
198
+ const stop = () => {
199
+ if (settled || this.phase === 'stopping')
200
+ return;
201
+ this.phase = 'stopping';
202
+ stoppedAt = Date.now();
203
+ clearTimeout(timer);
204
+ try {
205
+ // An inactive recorder may still be dispatching its final data event.
206
+ // Only the stop event resolves the result, including for concurrent stops.
207
+ if (recorder.state !== 'inactive')
208
+ recorder.stop();
209
+ }
210
+ catch (error) {
211
+ fail(error);
212
+ }
213
+ };
214
+ this.requestStop = stop;
215
+ recorder.addEventListener('dataavailable', onData);
216
+ recorder.addEventListener('stop', onStop);
217
+ recorder.addEventListener('error', onError);
218
+ track.addEventListener('ended', stop);
219
+ options.signal?.addEventListener('abort', onAbort, { once: true });
220
+ if (options.maxDurationMs)
221
+ timer = setTimeout(stop, options.maxDurationMs);
222
+ });
223
+ // Keep automatic failures observable via finished/stop without an unhandled
224
+ // rejection when the host only requests the result after recording ends.
225
+ void this.completion.catch(() => { });
226
+ try {
227
+ recorder.start(1000);
228
+ this.phase = 'recording';
229
+ }
230
+ catch (error) {
231
+ fail(error);
232
+ throw error;
233
+ }
234
+ }
235
+ catch (error) {
236
+ if (stream)
237
+ stopTracks(stream);
238
+ this.recorder = undefined;
239
+ this.requestStop = undefined;
240
+ this.phase = 'idle';
241
+ throw normalizeCaptureError(error);
242
+ }
243
+ }
244
+ /** Resolves on manual, duration-limit, or browser stop; rejects on abort/error. */
245
+ get finished() {
246
+ return this.completion ?? Promise.reject(new FeedbackCaptureError('recording-not-started', 'There is no screen recording result.'));
247
+ }
248
+ stop() {
249
+ this.requestStop?.();
250
+ return this.finished;
251
+ }
252
+ get isRecording() {
253
+ return this.phase === 'recording' && this.recorder?.state === 'recording';
254
+ }
255
+ }
256
+ //# sourceMappingURL=capture.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"capture.js","sourceRoot":"","sources":["../src/capture.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,14 @@
1
+ import { ScreenRecorder } from './capture.js';
2
+ import type { CapturedAsset, FeedbackSubmission, FeedbackTransport, RecordingOptions, ScreenshotOptions } from './types.js';
3
+ export declare class FeedbackClient {
4
+ private readonly transport;
5
+ readonly recorder: ScreenRecorder;
6
+ constructor(transport: FeedbackTransport);
7
+ captureScreenshot(options?: ScreenshotOptions): Promise<CapturedAsset>;
8
+ startRecording(options?: RecordingOptions): Promise<void>;
9
+ stopRecording(): Promise<CapturedAsset>;
10
+ submit(input: FeedbackSubmission): Promise<{
11
+ id?: string;
12
+ }>;
13
+ }
14
+ //# sourceMappingURL=client.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AAAA,OAAO,EAAqB,cAAc,EAAE,MAAM,cAAc,CAAC;AAEjE,OAAO,KAAK,EACV,aAAa,EACb,kBAAkB,EAClB,iBAAiB,EACjB,gBAAgB,EAChB,iBAAiB,EAClB,MAAM,YAAY,CAAC;AAEpB,qBAAa,cAAc;IAGb,OAAO,CAAC,QAAQ,CAAC,SAAS;IAFtC,QAAQ,CAAC,QAAQ,iBAAwB;gBAEZ,SAAS,EAAE,iBAAiB;IAEzD,iBAAiB,CAAC,OAAO,CAAC,EAAE,iBAAiB,GAAG,OAAO,CAAC,aAAa,CAAC;IAItE,cAAc,CAAC,OAAO,CAAC,EAAE,gBAAgB,GAAG,OAAO,CAAC,IAAI,CAAC;IAIzD,aAAa,IAAI,OAAO,CAAC,aAAa,CAAC;IAIjC,MAAM,CAAC,KAAK,EAAE,kBAAkB,GAAG,OAAO,CAAC;QAAE,EAAE,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;CAiBlE"}
package/dist/client.js ADDED
@@ -0,0 +1,28 @@
1
+ import { captureScreenshot, ScreenRecorder } from './capture.js';
2
+ import { abortable, throwIfAborted } from './abort.js';
3
+ export class FeedbackClient {
4
+ constructor(transport) {
5
+ this.transport = transport;
6
+ this.recorder = new ScreenRecorder();
7
+ }
8
+ captureScreenshot(options) {
9
+ return captureScreenshot(options);
10
+ }
11
+ startRecording(options) {
12
+ return this.recorder.start(options);
13
+ }
14
+ stopRecording() {
15
+ return this.recorder.stop();
16
+ }
17
+ async submit(input) {
18
+ throwIfAborted(input.signal);
19
+ const assets = [input.screenshot, input.recording].filter((asset) => Boolean(asset));
20
+ const uploaded = await abortable(Promise.all(assets.map(async (asset) => ({
21
+ kind: asset.kind,
22
+ ...(await this.transport.uploadAsset({ asset, signal: input.signal })),
23
+ }))), input.signal);
24
+ throwIfAborted(input.signal);
25
+ return abortable(this.transport.submitFeedback({ message: input.message, assets: uploaded, context: input.context }, { signal: input.signal }), input.signal);
26
+ }
27
+ }
28
+ //# sourceMappingURL=client.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client.js","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AACjE,OAAO,EAAE,SAAS,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AASvD,MAAM,OAAO,cAAc;IAGzB,YAA6B,SAA4B;QAA5B,cAAS,GAAT,SAAS,CAAmB;QAFhD,aAAQ,GAAG,IAAI,cAAc,EAAE,CAAC;IAEmB,CAAC;IAE7D,iBAAiB,CAAC,OAA2B;QAC3C,OAAO,iBAAiB,CAAC,OAAO,CAAC,CAAC;IACpC,CAAC;IAED,cAAc,CAAC,OAA0B;QACvC,OAAO,IAAI,CAAC,QAAQ,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC;IACtC,CAAC;IAED,aAAa;QACX,OAAO,IAAI,CAAC,QAAQ,CAAC,IAAI,EAAE,CAAC;IAC9B,CAAC;IAED,KAAK,CAAC,MAAM,CAAC,KAAyB;QACpC,cAAc,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC;QAC7B,MAAM,MAAM,GAAG,CAAC,KAAK,CAAC,UAAU,EAAE,KAAK,CAAC,SAAS,CAAC,CAAC,MAAM,CACvD,CAAC,KAAK,EAA0B,EAAE,CAAC,OAAO,CAAC,KAAK,CAAC,CAClD,CAAC;QACF,MAAM,QAAQ,GAAG,MAAM,SAAS,CAAC,OAAO,CAAC,GAAG,CAC1C,MAAM,CAAC,GAAG,CAAC,KAAK,EAAC,KAAK,EAAC,EAAE,CAAC,CAAC;YACzB,IAAI,EAAE,KAAK,CAAC,IAAI;YAChB,GAAG,CAAC,MAAM,IAAI,CAAC,SAAS,CAAC,WAAW,CAAC,EAAE,KAAK,EAAE,MAAM,EAAE,KAAK,CAAC,MAAM,EAAE,CAAC,CAAC;SACvE,CAAC,CAAC,CACJ,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC;QACjB,cAAc,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC;QAC7B,OAAO,SAAS,CAAC,IAAI,CAAC,SAAS,CAAC,cAAc,CAC5C,EAAE,OAAO,EAAE,KAAK,CAAC,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,OAAO,EAAE,KAAK,CAAC,OAAO,EAAE,EACpE,EAAE,MAAM,EAAE,KAAK,CAAC,MAAM,EAAE,CACzB,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC;IACnB,CAAC;CACF"}
@@ -0,0 +1,8 @@
1
+ export type FeedbackCaptureErrorCode = 'unsupported' | 'permission-denied' | 'cancelled' | 'capture-failed' | 'recording-not-started' | 'recording-already-started';
2
+ export declare class FeedbackCaptureError extends Error {
3
+ readonly code: FeedbackCaptureErrorCode;
4
+ readonly cause?: unknown;
5
+ constructor(code: FeedbackCaptureErrorCode, message: string, cause?: unknown);
6
+ }
7
+ export declare function normalizeCaptureError(error: unknown): FeedbackCaptureError;
8
+ //# sourceMappingURL=errors.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,wBAAwB,GAChC,aAAa,GACb,mBAAmB,GACnB,WAAW,GACX,gBAAgB,GAChB,uBAAuB,GACvB,2BAA2B,CAAC;AAEhC,qBAAa,oBAAqB,SAAQ,KAAK;IAC7C,QAAQ,CAAC,IAAI,EAAE,wBAAwB,CAAC;IACxC,QAAQ,CAAC,KAAK,CAAC,EAAE,OAAO,CAAC;gBAEb,IAAI,EAAE,wBAAwB,EAAE,OAAO,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,OAAO;CAM7E;AAED,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,OAAO,GAAG,oBAAoB,CAa1E"}
package/dist/errors.js ADDED
@@ -0,0 +1,22 @@
1
+ export class FeedbackCaptureError extends Error {
2
+ constructor(code, message, cause) {
3
+ super(message);
4
+ this.name = 'FeedbackCaptureError';
5
+ this.code = code;
6
+ this.cause = cause;
7
+ }
8
+ }
9
+ export function normalizeCaptureError(error) {
10
+ if (error instanceof FeedbackCaptureError)
11
+ return error;
12
+ if (typeof DOMException !== 'undefined' && error instanceof DOMException) {
13
+ if (error.name === 'NotAllowedError' || error.name === 'PermissionDeniedError') {
14
+ return new FeedbackCaptureError('permission-denied', 'Screen capture permission was denied.', error);
15
+ }
16
+ if (error.name === 'AbortError') {
17
+ return new FeedbackCaptureError('cancelled', 'Screen capture was cancelled.', error);
18
+ }
19
+ }
20
+ return new FeedbackCaptureError('capture-failed', 'Screen capture failed.', error);
21
+ }
22
+ //# sourceMappingURL=errors.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"errors.js","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAQA,MAAM,OAAO,oBAAqB,SAAQ,KAAK;IAI7C,YAAY,IAA8B,EAAE,OAAe,EAAE,KAAe;QAC1E,KAAK,CAAC,OAAO,CAAC,CAAC;QACf,IAAI,CAAC,IAAI,GAAG,sBAAsB,CAAC;QACnC,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC;QACjB,IAAI,CAAC,KAAK,GAAG,KAAK,CAAC;IACrB,CAAC;CACF;AAED,MAAM,UAAU,qBAAqB,CAAC,KAAc;IAClD,IAAI,KAAK,YAAY,oBAAoB;QAAE,OAAO,KAAK,CAAC;IAExD,IAAI,OAAO,YAAY,KAAK,WAAW,IAAI,KAAK,YAAY,YAAY,EAAE,CAAC;QACzE,IAAI,KAAK,CAAC,IAAI,KAAK,iBAAiB,IAAI,KAAK,CAAC,IAAI,KAAK,uBAAuB,EAAE,CAAC;YAC/E,OAAO,IAAI,oBAAoB,CAAC,mBAAmB,EAAE,uCAAuC,EAAE,KAAK,CAAC,CAAC;QACvG,CAAC;QACD,IAAI,KAAK,CAAC,IAAI,KAAK,YAAY,EAAE,CAAC;YAChC,OAAO,IAAI,oBAAoB,CAAC,WAAW,EAAE,+BAA+B,EAAE,KAAK,CAAC,CAAC;QACvF,CAAC;IACH,CAAC;IAED,OAAO,IAAI,oBAAoB,CAAC,gBAAgB,EAAE,wBAAwB,EAAE,KAAK,CAAC,CAAC;AACrF,CAAC"}
@@ -0,0 +1,6 @@
1
+ export { FeedbackCaptureError } from './errors.js';
2
+ export type { FeedbackCaptureErrorCode } from './errors.js';
3
+ export { captureScreenshot, ScreenRecorder } from './capture.js';
4
+ export { FeedbackClient } from './client.js';
5
+ export type { CaptureKind, CapturedAsset, FeedbackPayload, FeedbackSubmission, FeedbackTransport, RecordingOptions, ScreenshotOptions, UploadAssetInput, UploadedAsset, } from './types.js';
6
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,oBAAoB,EAAE,MAAM,aAAa,CAAC;AACnD,YAAY,EAAE,wBAAwB,EAAE,MAAM,aAAa,CAAC;AAC5D,OAAO,EAAE,iBAAiB,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AACjE,OAAO,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAC7C,YAAY,EACV,WAAW,EACX,aAAa,EACb,eAAe,EACf,kBAAkB,EAClB,iBAAiB,EACjB,gBAAgB,EAChB,iBAAiB,EACjB,gBAAgB,EAChB,aAAa,GACd,MAAM,YAAY,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,4 @@
1
+ export { FeedbackCaptureError } from './errors.js';
2
+ export { captureScreenshot, ScreenRecorder } from './capture.js';
3
+ export { FeedbackClient } from './client.js';
4
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,oBAAoB,EAAE,MAAM,aAAa,CAAC;AAEnD,OAAO,EAAE,iBAAiB,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AACjE,OAAO,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC"}
@@ -0,0 +1,62 @@
1
+ export type CaptureKind = 'screenshot' | 'recording';
2
+ export interface CapturedAsset {
3
+ kind: CaptureKind;
4
+ blob: Blob;
5
+ contentType: string;
6
+ filename: string;
7
+ width?: number;
8
+ height?: number;
9
+ durationMs?: number;
10
+ }
11
+ export interface UploadedAsset {
12
+ url: string;
13
+ id?: string;
14
+ }
15
+ export interface UploadAssetInput {
16
+ asset: CapturedAsset;
17
+ signal?: AbortSignal;
18
+ }
19
+ export interface FeedbackPayload {
20
+ message: string;
21
+ assets: Array<{
22
+ kind: CaptureKind;
23
+ url: string;
24
+ id?: string;
25
+ }>;
26
+ context?: Record<string, unknown>;
27
+ }
28
+ export interface FeedbackTransport {
29
+ uploadAsset(input: UploadAssetInput): Promise<UploadedAsset>;
30
+ submitFeedback(payload: FeedbackPayload, options?: {
31
+ signal?: AbortSignal;
32
+ }): Promise<{
33
+ id?: string;
34
+ }>;
35
+ }
36
+ export interface ScreenshotOptions {
37
+ /** Maximum output size. Defaults to captured pixels; aspect ratio is preserved. */
38
+ width?: number;
39
+ height?: number;
40
+ /** Prefer the current browser tab in Chromium's picker when supported. */
41
+ preferCurrentTab?: boolean;
42
+ signal?: AbortSignal;
43
+ }
44
+ export interface RecordingOptions {
45
+ audio?: boolean;
46
+ video?: MediaTrackConstraints;
47
+ mimeType?: string;
48
+ videoBitsPerSecond?: number;
49
+ audioBitsPerSecond?: number;
50
+ /** Stops the recorder automatically after this duration. */
51
+ maxDurationMs?: number;
52
+ /** Cancels capture and discards the recording. Use stop() to keep the video. */
53
+ signal?: AbortSignal;
54
+ }
55
+ export interface FeedbackSubmission {
56
+ message: string;
57
+ screenshot?: CapturedAsset;
58
+ recording?: CapturedAsset;
59
+ context?: Record<string, unknown>;
60
+ signal?: AbortSignal;
61
+ }
62
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,WAAW,GAAG,YAAY,GAAG,WAAW,CAAC;AAErD,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,WAAW,CAAC;IAClB,IAAI,EAAE,IAAI,CAAC;IACX,WAAW,EAAE,MAAM,CAAC;IACpB,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,aAAa;IAC5B,GAAG,EAAE,MAAM,CAAC;IACZ,EAAE,CAAC,EAAE,MAAM,CAAC;CACb;AAED,MAAM,WAAW,gBAAgB;IAC/B,KAAK,EAAE,aAAa,CAAC;IACrB,MAAM,CAAC,EAAE,WAAW,CAAC;CACtB;AAED,MAAM,WAAW,eAAe;IAC9B,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,KAAK,CAAC;QACZ,IAAI,EAAE,WAAW,CAAC;QAClB,GAAG,EAAE,MAAM,CAAC;QACZ,EAAE,CAAC,EAAE,MAAM,CAAC;KACb,CAAC,CAAC;IACH,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACnC;AAED,MAAM,WAAW,iBAAiB;IAChC,WAAW,CAAC,KAAK,EAAE,gBAAgB,GAAG,OAAO,CAAC,aAAa,CAAC,CAAC;IAC7D,cAAc,CACZ,OAAO,EAAE,eAAe,EACxB,OAAO,CAAC,EAAE;QAAE,MAAM,CAAC,EAAE,WAAW,CAAA;KAAE,GACjC,OAAO,CAAC;QAAE,EAAE,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;CAC7B;AAED,MAAM,WAAW,iBAAiB;IAChC,mFAAmF;IACnF,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,0EAA0E;IAC1E,gBAAgB,CAAC,EAAE,OAAO,CAAC;IAC3B,MAAM,CAAC,EAAE,WAAW,CAAC;CACtB;AAED,MAAM,WAAW,gBAAgB;IAC/B,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,KAAK,CAAC,EAAE,qBAAqB,CAAC;IAC9B,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,4DAA4D;IAC5D,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,gFAAgF;IAChF,MAAM,CAAC,EAAE,WAAW,CAAC;CACtB;AAED,MAAM,WAAW,kBAAkB;IACjC,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,CAAC,EAAE,aAAa,CAAC;IAC3B,SAAS,CAAC,EAAE,aAAa,CAAC;IAC1B,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAClC,MAAM,CAAC,EAAE,WAAW,CAAC;CACtB"}
package/dist/types.js ADDED
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":""}
package/package.json ADDED
@@ -0,0 +1,41 @@
1
+ {
2
+ "name": "artarch-feedback-sdk",
3
+ "version": "0.1.0",
4
+ "description": "Headless browser feedback capture SDK for screenshots and screen recordings.",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "sideEffects": false,
8
+ "files": [
9
+ "dist",
10
+ "README.md",
11
+ "LICENSE"
12
+ ],
13
+ "main": "./dist/index.js",
14
+ "module": "./dist/index.js",
15
+ "types": "./dist/index.d.ts",
16
+ "exports": {
17
+ ".": {
18
+ "types": "./dist/index.d.ts",
19
+ "import": "./dist/index.js",
20
+ "default": "./dist/index.js"
21
+ }
22
+ },
23
+ "scripts": {
24
+ "build": "tsc -p tsconfig.build.json",
25
+ "test": "npm run build && node --test --test-timeout=15000 tests/*.test.mjs",
26
+ "test:browser": "npm run build && playwright test",
27
+ "test:all": "npm test && npm run test:browser",
28
+ "prepack": "npm run build",
29
+ "prepublishOnly": "npm run test:all"
30
+ },
31
+ "devDependencies": {
32
+ "@playwright/test": "1.63.0",
33
+ "typescript": "5.8.3"
34
+ },
35
+ "engines": {
36
+ "node": ">=22"
37
+ },
38
+ "publishConfig": {
39
+ "access": "public"
40
+ }
41
+ }