wdio-agent-service 0.0.1

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 Vince Graics
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,137 @@
1
+ # WebdriverIO Agent Service
2
+
3
+ A WebdriverIO service that adds LLM-powered browser automation through a simple `browser.agent(prompt)` command.
4
+
5
+ ## Why?
6
+
7
+ Modern web UIs change frequently—cookie banners appear, button labels shift, modals pop up unexpectedly. Traditional selectors break. LLMs can handle this ambiguity, but running every action through an LLM is slow and expensive.
8
+
9
+ **The solution: use both.**
10
+
11
+ ```ts
12
+ // Stable actions → use regular WebdriverIO (fast, free, reliable)
13
+ await browser.url('https://shop.example.com');
14
+ await browser.$('input#search').setValue('mechanical keyboard');
15
+ await browser.$('button[type="submit"]').click();
16
+
17
+ // Unpredictable UI → let the LLM handle it (flexible, resilient)
18
+ await browser.agent('accept the cookie consent banner');
19
+ await browser.agent('close any promotional popup');
20
+
21
+ // Back to stable actions
22
+ await browser.$('.product-card').click();
23
+ await browser.$('#add-to-cart').click();
24
+ ```
25
+
26
+ This hybrid approach lets you:
27
+ - **Save tokens** on predictable interactions
28
+ - **Stay resilient** against UI changes where they matter most
29
+ - **Keep tests fast** by only invoking the LLM when needed
30
+ - **Reduce flakiness** in areas prone to A/B tests or dynamic content
31
+
32
+ ## Installation
33
+
34
+ ```bash
35
+ npm install wdio-agent-service
36
+ ```
37
+
38
+ ## Configuration
39
+
40
+ Add the service to your `wdio.conf.ts`:
41
+
42
+ ```ts
43
+ export const config: WebdriverIO.Config = {
44
+ // ...
45
+ services: [
46
+ ['agent', {
47
+ provider: 'ollama', // LLM provider (currently supports 'ollama')
48
+ providerUrl: 'http://localhost:11434', // Provider API endpoint
49
+ model: 'qwen2.5-coder:7b', // Model to use
50
+ maxActions: 1, // Max actions per prompt (default: 1)
51
+ timeout: 30000, // Request timeout in ms (default: 30000)
52
+ debug: false, // Enable debug logging (default: false)
53
+ }]
54
+ ],
55
+ };
56
+ ```
57
+
58
+ ## Usage
59
+
60
+ The service adds a single command to the browser object:
61
+
62
+ ```ts
63
+ await browser.agent('your natural language instruction');
64
+ ```
65
+
66
+ ### Supported Actions
67
+
68
+ The LLM can return three types of actions:
69
+
70
+ | Action | Description | Example Prompt |
71
+ |--------|-------------|----------------|
72
+ | `CLICK` | Click on an element | `"click the login button"` |
73
+ | `SET_VALUE` | Type into an input field | `"type hello@example.com in the email field"` |
74
+ | `NAVIGATE` | Go to a URL | `"navigate to https://example.com"` |
75
+
76
+ ### Examples
77
+
78
+ ```ts
79
+ // Handle cookie consent (varies wildly across sites)
80
+ await browser.agent('accept all cookies');
81
+
82
+ // Dismiss popups and modals
83
+ await browser.agent('close the newsletter signup modal');
84
+
85
+ // Navigate dynamic menus
86
+ await browser.agent('click on Settings in the user menu');
87
+
88
+ // Fill forms with context
89
+ await browser.agent('enter john.doe@test.com in the email input');
90
+ ```
91
+
92
+ ### Return Value
93
+
94
+ `browser.agent()` returns an array of executed actions:
95
+
96
+ ```ts
97
+ const actions = await browser.agent('click the submit button');
98
+ // [{ type: 'CLICK', target: 'button[type="submit"]' }]
99
+ ```
100
+
101
+ ## When to Use `agent()` vs Regular Commands
102
+
103
+ | Scenario | Recommendation |
104
+ |----------|----------------|
105
+ | Static selectors that rarely change | Regular WebdriverIO |
106
+ | Login forms with stable IDs | Regular WebdriverIO |
107
+ | Cookie consent banners | `agent()` |
108
+ | Promotional popups/modals | `agent()` |
109
+ | Third-party widgets | `agent()` |
110
+ | Elements with dynamic/generated IDs | `agent()` |
111
+ | A/B tested UI components | `agent()` |
112
+
113
+ ## Local LLM Setup (Ollama)
114
+
115
+ 1. Install [Ollama](https://ollama.ai)
116
+ 2. Pull a model: `ollama pull qwen2.5-coder:7b`
117
+ 3. Run `ollama serve` in the terminal
118
+ 4. Ollama runs on `http://localhost:11434` by default
119
+
120
+ ### Recommended Models
121
+
122
+ | Model | Size | Speed | Accuracy |
123
+ |-------|------|-------|----------|
124
+ | `qwen2.5-coder:7b` | 4.7GB | Fast | Good |
125
+ | `qwen2.5-coder:14b` | 9GB | Medium | Better |
126
+ | `llama3.1:8b` | 4.7GB | Fast | Good |
127
+
128
+ ### Agent Flow
129
+
130
+ 1. `browser.agent(prompt)` extracts visible, interactable elements from the page
131
+ 2. Elements + prompt are sent to the LLM
132
+ 3. LLM returns structured actions (CLICK, SET_VALUE, NAVIGATE)
133
+ 4. Actions are executed via WebdriverIO
134
+
135
+ ## License
136
+
137
+ MIT
@@ -0,0 +1,61 @@
1
+ import { Services } from '@wdio/types';
2
+
3
+ type Providers = 'ollama';
4
+
5
+ /**
6
+ * WebdriverIO Agent Service
7
+ * Adds browser.agent(prompt) command for LLM-powered automation
8
+ */
9
+ declare class AgentService implements Services.ServiceInstance {
10
+ private readonly resolvedConfig;
11
+ private provider;
12
+ private maxActions;
13
+ private debug;
14
+ constructor(serviceOptions?: AgentServiceConfig);
15
+ before(_capabilities: WebdriverIO.Capabilities, _specs: string[], browser: WebdriverIO.Browser): void;
16
+ private executeAgent;
17
+ }
18
+
19
+ type ActionType = 'CLICK' | 'SET_VALUE' | 'NAVIGATE';
20
+ declare const VALID_ACTIONS: ActionType[];
21
+ interface AgentServiceConfig {
22
+ /** LLM Provider. Default: ollama */
23
+ provider?: Providers;
24
+ /** LLM Provider API endpoint. Default (ollama): http://localhost:11434 */
25
+ providerUrl?: string;
26
+ /** LLM Provider API token. Default: '' */
27
+ token?: string;
28
+ /** LLM model name. Default: qwen2.5-coder:7b */
29
+ model?: string;
30
+ /** Maximum actions per prompt. Default: 1 */
31
+ maxActions?: number;
32
+ /** LLM Request timeout in ms. Default: 30000 */
33
+ timeout?: number;
34
+ /** Enable debug logging. Default: false */
35
+ debug?: boolean;
36
+ }
37
+ interface AgentAction {
38
+ type: ActionType;
39
+ target: string;
40
+ value?: string;
41
+ }
42
+ interface PromptInput {
43
+ system: string;
44
+ user: string;
45
+ }
46
+
47
+ declare const launcher: typeof AgentService;
48
+ declare global {
49
+ namespace WebdriverIO {
50
+ interface Browser {
51
+ /**
52
+ * Execute natural language browser automation using LLM
53
+ * @param prompt - Natural language instruction (e.g., "accept all cookies")
54
+ * @returns Result containing executed actions
55
+ */
56
+ agent: (prompt: string) => Promise<AgentAction[]>;
57
+ }
58
+ }
59
+ }
60
+
61
+ export { type ActionType, type AgentAction, type AgentServiceConfig, type PromptInput, VALID_ACTIONS, AgentService as default, launcher };
package/build/index.js ADDED
@@ -0,0 +1,449 @@
1
+ // services/agent.service.ts
2
+ import "webdriverio";
3
+
4
+ // providers/ollama.provider.ts
5
+ var OllamaProvider = class {
6
+ constructor(config) {
7
+ this.config = config;
8
+ }
9
+ async send(prompt) {
10
+ const url = `${this.config.providerUrl}/api/generate`;
11
+ const body = {
12
+ model: this.config.model,
13
+ prompt: typeof prompt === "string" ? prompt : `${prompt.system}
14
+
15
+ ${prompt.user}`,
16
+ stream: false,
17
+ options: {
18
+ temperature: 0.1,
19
+ // Low temperature for consistent output
20
+ num_predict: 500
21
+ // Limit response length
22
+ }
23
+ };
24
+ if (this.config.debug) {
25
+ console.log("[Agent] LLM Request:", JSON.stringify(body, null, 2));
26
+ }
27
+ const controller = new AbortController();
28
+ const timeoutId = setTimeout(() => controller.abort(), this.config.timeout);
29
+ try {
30
+ const response = await fetch(url, {
31
+ method: "POST",
32
+ headers: { "Content-Type": "application/json" },
33
+ body: JSON.stringify(body),
34
+ signal: controller.signal
35
+ });
36
+ if (!response.ok) {
37
+ const errorText = await response.text();
38
+ throw new Error(`Ollama request failed: ${response.status} ${response.statusText} - ${errorText}`);
39
+ }
40
+ const data = await response.json();
41
+ if (this.config.debug) {
42
+ console.debug("[Agent] LLM Response:", JSON.stringify({
43
+ model: data.model,
44
+ response: data.response,
45
+ done: data.done
46
+ }, null, 2));
47
+ }
48
+ return data.response ?? "";
49
+ } catch (error) {
50
+ if (error instanceof Error && error.name === "AbortError") {
51
+ throw new Error(`Ollama request timed out after ${this.config.timeout}ms`);
52
+ }
53
+ throw error;
54
+ } finally {
55
+ clearTimeout(timeoutId);
56
+ }
57
+ }
58
+ };
59
+
60
+ // providers/index.ts
61
+ var initializeProvider = (config) => {
62
+ switch (config.provider) {
63
+ case "ollama":
64
+ return new OllamaProvider(config);
65
+ default:
66
+ throw new Error(`Unsupported provider: ${config.provider}`);
67
+ }
68
+ };
69
+
70
+ // scripts/get-elements.ts
71
+ import "webdriverio";
72
+
73
+ // scripts/get-interactable-browser-elements.ts
74
+ var interactableBrowserElementsScript = () => (function() {
75
+ const interactableSelectors = [
76
+ "a[href]",
77
+ // Links with href
78
+ "button",
79
+ // Buttons
80
+ 'input:not([type="hidden"])',
81
+ // Input fields (except hidden)
82
+ "select",
83
+ // Select dropdowns
84
+ "textarea",
85
+ // Text areas
86
+ '[role="button"]',
87
+ // Elements with button role
88
+ '[role="link"]',
89
+ // Elements with link role
90
+ '[role="checkbox"]',
91
+ // Elements with checkbox role
92
+ '[role="radio"]',
93
+ // Elements with radio role
94
+ '[role="tab"]',
95
+ // Elements with tab role
96
+ '[role="menuitem"]',
97
+ // Elements with menuitem role
98
+ '[role="combobox"]',
99
+ // Elements with combobox role
100
+ '[role="option"]',
101
+ // Elements with option role
102
+ '[role="switch"]',
103
+ // Elements with switch role
104
+ '[role="slider"]',
105
+ // Elements with slider role
106
+ '[role="textbox"]',
107
+ // Elements with textbox role
108
+ '[role="searchbox"]',
109
+ // Elements with searchbox role
110
+ '[contenteditable="true"]',
111
+ // Editable content
112
+ '[tabindex]:not([tabindex="-1"])',
113
+ // Elements with tabindex
114
+ "img",
115
+ // Images
116
+ "picture",
117
+ // Picture elements
118
+ "svg",
119
+ // SVG graphics
120
+ "video",
121
+ // Video elements
122
+ "canvas",
123
+ // Canvas elements
124
+ '[style*="background-image"]'
125
+ // Elements with background images
126
+ ];
127
+ function isVisible(element) {
128
+ if (typeof element.checkVisibility === "function") {
129
+ return element.checkVisibility({
130
+ opacityProperty: true,
131
+ visibilityProperty: true,
132
+ contentVisibilityAuto: true
133
+ });
134
+ }
135
+ const style = window.getComputedStyle(element);
136
+ return style.display !== "none" && style.visibility !== "hidden" && style.opacity !== "0" && element.offsetWidth > 0 && element.offsetHeight > 0;
137
+ }
138
+ function getCssSelector(element) {
139
+ if (element.id) {
140
+ return `#${CSS.escape(element.id)}`;
141
+ }
142
+ if (element.className && typeof element.className === "string") {
143
+ const classes = element.className.trim().split(/\s+/).filter(Boolean);
144
+ if (classes.length > 0) {
145
+ const classSelector = classes.slice(0, 2).map((c) => `.${CSS.escape(c)}`).join("");
146
+ const tagWithClass = `${element.tagName.toLowerCase()}${classSelector}`;
147
+ if (document.querySelectorAll(tagWithClass).length === 1) {
148
+ return tagWithClass;
149
+ }
150
+ }
151
+ }
152
+ let current = element;
153
+ const path = [];
154
+ while (current && current !== document.documentElement) {
155
+ let selector = current.tagName.toLowerCase();
156
+ if (current.id) {
157
+ selector = `#${CSS.escape(current.id)}`;
158
+ path.unshift(selector);
159
+ break;
160
+ }
161
+ const parent = current.parentElement;
162
+ if (parent) {
163
+ const siblings = Array.from(parent.children).filter(
164
+ (child) => child.tagName === current.tagName
165
+ );
166
+ if (siblings.length > 1) {
167
+ const index = siblings.indexOf(current) + 1;
168
+ selector += `:nth-child(${index})`;
169
+ }
170
+ }
171
+ path.unshift(selector);
172
+ current = current.parentElement;
173
+ if (path.length >= 4) {
174
+ break;
175
+ }
176
+ }
177
+ return path.join(" > ");
178
+ }
179
+ function getElements2() {
180
+ const allElements = [];
181
+ interactableSelectors.forEach((selector) => {
182
+ const elements = document.querySelectorAll(selector);
183
+ elements.forEach((element) => {
184
+ if (!allElements.includes(element)) {
185
+ allElements.push(element);
186
+ }
187
+ });
188
+ });
189
+ return allElements.filter((element) => isVisible(element) && !element.disabled).map((element) => {
190
+ const el = element;
191
+ const inputEl = element;
192
+ const rect = el.getBoundingClientRect();
193
+ const isInViewport = rect.top >= 0 && rect.left >= 0 && rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) && rect.right <= (window.innerWidth || document.documentElement.clientWidth);
194
+ const elementProperties = {
195
+ tagName: el.tagName.toLowerCase(),
196
+ type: el.getAttribute("type") || "",
197
+ id: el.id || "",
198
+ className: (typeof el.className === "string" ? el.className : "") || "",
199
+ textContent: el.textContent?.trim() || "",
200
+ value: inputEl.value || "",
201
+ placeholder: inputEl.placeholder || "",
202
+ href: el.getAttribute("href") || "",
203
+ ariaLabel: el.getAttribute("aria-label") || "",
204
+ role: el.getAttribute("role") || "",
205
+ src: el.getAttribute("src") || "",
206
+ alt: el.getAttribute("alt") || "",
207
+ cssSelector: getCssSelector(el),
208
+ isInViewport
209
+ };
210
+ return elementProperties;
211
+ });
212
+ }
213
+ return getElements2();
214
+ })();
215
+ var get_interactable_browser_elements_default = interactableBrowserElementsScript;
216
+
217
+ // scripts/get-elements.ts
218
+ import { encode } from "@toon-format/toon";
219
+ var getElements = async (browser) => {
220
+ const elements = await browser.execute(get_interactable_browser_elements_default);
221
+ return encode(elements).replace(/,""/g, ",").replace(/"",/g, ",");
222
+ };
223
+
224
+ // prompts/index.ts
225
+ var userPrompt = (elements, userRequest, maxActions = 1) => `
226
+ You are a browser automation assistant. Your task is to analyze a user's natural language request and a list of webpage elements,
227
+ then return the minimum necessary browser actions as a JSON array.
228
+
229
+ # Input Data
230
+
231
+ Here are the webpage elements available for interaction:
232
+ <elements>
233
+ ${elements}
234
+ </elements>
235
+
236
+ Here is the user's request:
237
+ <user_request>
238
+ ${userRequest}
239
+ </user_request>
240
+
241
+ The maximum number of actions you can return is:
242
+ <max_actions>
243
+ ${maxActions}
244
+ </max_actions>
245
+
246
+ # Action Types
247
+
248
+ You can return three types of actions:
249
+
250
+ 1. **CLICK**: Click on an element
251
+ - Prefer elements with these tags/attributes: button, a, [role="button"]
252
+ - Format: \`{"action":"CLICK","target":"<cssSelector>"}\`
253
+
254
+ 2. **SET_VALUE**: Type text into an input field
255
+ - Prefer elements with these tags: input, textarea
256
+ - Must include a "value" field with the text to type
257
+ - Format: \`{"action":"SET_VALUE","target":"<cssSelector>","value":"<text>"}\`
258
+ - NEVER use SET_VALUE on button or a (link) elements
259
+
260
+ 3. **NAVIGATE**: Navigate to a URL
261
+ - Only use when the user explicitly mentions a URL to visit
262
+ - Format: \`{"action":"NAVIGATE","target":"<url>"}\`
263
+
264
+ # Important Rules
265
+
266
+ ## Minimizing Actions
267
+ - Return the MINIMUM number of actions needed (between 1 and max_actions)
268
+ - If the user request contains words like "type", "click", "navigate", or "open" for a single thing, you only need 1 action
269
+ - Only return multiple actions if the request cannot be accomplished with a single action
270
+ - Do not be overeager in creating multiple actions
271
+
272
+ ## Element Matching
273
+ - Use the EXACT cssSelector from the elements data
274
+ - Match the MOST SPECIFIC element - prefer EXACT text matches over partial matches
275
+ - Compare the FULL request phrase against element text and attributes, not just individual keywords
276
+ - For example, if the request is "click the submit button", look for elements with text exactly matching "submit" rather than just any button
277
+
278
+ ## Action-Specific Rules
279
+ - If you CLICK on an \`a\` element with an href attribute, it will automatically navigate - do NOT add a separate NAVIGATE action
280
+ - For SET_VALUE actions, extract the value from the user's request
281
+ - Example: "search for cats" \u2192 value should be "cats"
282
+ - Example: "type hello@example.com in the email field" \u2192 value should be "hello@example.com"
283
+ - NAVIGATE actions should be rare - only use them when the user explicitly asks to go to a specific URL
284
+
285
+ # Output Format
286
+
287
+ Your final output must be ONLY a JSON array with no other text, as a simple string. Each action in the array should be a JSON object with the following structure:
288
+
289
+ For CLICK:
290
+ [{"action":"CLICK","target":"cssSelector"}]
291
+
292
+ For SET_VALUE:
293
+ [{"action":"SET_VALUE","target":"cssSelector","value":"text to type"}]
294
+
295
+ For NAVIGATE:
296
+ [{"action":"NAVIGATE","target":"https://example.com"}]
297
+
298
+ Example output for multiple actions:
299
+ [{"action":"SET_VALUE","target":"input#search","value":"mystery books"},{"action":"CLICK","target":"button.search-btn"}]
300
+
301
+ # Instructions
302
+
303
+ Before generating your JSON output, analyze the request systematically:
304
+
305
+ 1. **Interpret the request**: What is the user trying to accomplish? Break down the request into its core intent.
306
+
307
+ 2. **Determine action count**: Does this map to a single action or multiple actions?
308
+ Remember to minimize - if a single action can accomplish the goal, use only one.
309
+
310
+ 3. **List candidate elements**: Look through the elements list and write out ALL elements that could potentially match the request. For each candidate, include:
311
+ - The cssSelector
312
+ - The element type/tag
313
+ - Relevant text content or attributes (placeholder, aria-label, href, etc.)
314
+ - Why it might match the request
315
+
316
+ It's OK for this section to be quite long if there are many potential matches.
317
+
318
+ 4. **Select the best match**: From your list of candidates, choose the most specific element that exactly matches the user's intent.
319
+ Explain why this element is the best choice over the other candidates.
320
+
321
+ 5. **Determine action type(s)**: Based on the request and selected element(s), what action type is appropriate?
322
+ - For typing/entering text: Use SET_VALUE on input/textarea elements
323
+ - For clicking: Use CLICK on buttons/links
324
+ - For navigation: Only if URL explicitly mentioned and not just clicking a link
325
+
326
+ If you need a SET_VALUE action, explicitly extract and write out the value text from the user's request here.
327
+
328
+ 6. **Validate**: Check that your selected actions follow all the rules (correct element types, no redundant actions, within max_actions limit, etc.)
329
+
330
+ After your analysis, output ONLY the JSON array with no additional text or explanation.
331
+ `.trim();
332
+
333
+ // commands/parse-llm-response.ts
334
+ var parseLlmResponse = (response, maxActions) => {
335
+ const stripped = response.replace(/```(?:json)?\s*/gi, "").replace(/```\s*$/g, "").trim();
336
+ const jsonMatch = stripped.match(/\[[\s\S]*]/);
337
+ if (!jsonMatch) {
338
+ throw new Error(`No JSON array found in LLM response.
339
+ Response: ${response}`);
340
+ }
341
+ let parsed;
342
+ try {
343
+ parsed = JSON.parse(jsonMatch[0]);
344
+ } catch (e) {
345
+ throw new Error(`Failed to parse JSON from LLM response: ${e}
346
+ Response: ${response}`);
347
+ }
348
+ if (!Array.isArray(parsed) || parsed.length === 0) {
349
+ throw new Error(`LLM returned empty or invalid actions array.
350
+ Response: ${response}`);
351
+ }
352
+ const limited = parsed.slice(0, maxActions);
353
+ return limited.map((action, index) => {
354
+ const actionType = action.action.toUpperCase();
355
+ if (!VALID_ACTIONS.includes(actionType)) {
356
+ throw new Error(`Invalid action type "${actionType}" at index ${index}.Valid: ${VALID_ACTIONS.join(", ")}`);
357
+ }
358
+ if (!action.target) {
359
+ throw new Error(`Missing target at index ${index}`);
360
+ }
361
+ if (actionType === "SET_VALUE" && !action.value) {
362
+ throw new Error(`SET_VALUE at index ${index} requires value field`);
363
+ }
364
+ return {
365
+ type: actionType,
366
+ target: action.target,
367
+ value: action.value
368
+ };
369
+ });
370
+ };
371
+
372
+ // commands/execute-agent-action.ts
373
+ import "webdriverio";
374
+ var actionsByType = {
375
+ CLICK: async (_browser, action) => {
376
+ await _browser.$(action.target).click();
377
+ },
378
+ SET_VALUE: async (_browser, action) => {
379
+ await _browser.$(action.target).setValue(action.value);
380
+ },
381
+ NAVIGATE: async (_browser, action) => {
382
+ const url = action.target;
383
+ const targetUrl = url.match(/^https?:\/\//) ? url : `https://${url}`;
384
+ await _browser.url(targetUrl);
385
+ }
386
+ };
387
+ var executeAgentAction = async (_browser, debug, action) => {
388
+ if (debug) {
389
+ console.log(`[Agent] Executing: ${action.type} on "${action.target}"${action.value ? ` with value "${action.value}"` : ""}`);
390
+ }
391
+ const agentAction = actionsByType[action.type];
392
+ if (!agentAction) {
393
+ throw new Error(`Unknown action type: ${action.type}`);
394
+ }
395
+ return agentAction(_browser, action);
396
+ };
397
+
398
+ // services/agent.service.ts
399
+ var AgentService = class {
400
+ constructor(serviceOptions = {}) {
401
+ this.resolvedConfig = {
402
+ provider: serviceOptions.provider ?? "ollama",
403
+ providerUrl: serviceOptions.providerUrl ?? "http://localhost:11434",
404
+ model: serviceOptions.model ?? "qwen2.5-coder:7b",
405
+ maxActions: serviceOptions.maxActions ?? 1,
406
+ timeout: serviceOptions.timeout ?? 3e4,
407
+ debug: serviceOptions.debug ?? false
408
+ };
409
+ }
410
+ before(_capabilities, _specs, browser) {
411
+ this.debug = this.resolvedConfig.debug;
412
+ this.maxActions = this.resolvedConfig.maxActions;
413
+ this.provider = initializeProvider(this.resolvedConfig);
414
+ browser.addCommand("agent", async (prompt) => this.executeAgent(browser, prompt), {});
415
+ if (this.resolvedConfig.debug) {
416
+ console.log("[Agent] Service initialized with config:", this.resolvedConfig);
417
+ }
418
+ }
419
+ async executeAgent(_browser, prompt) {
420
+ if (this.debug) {
421
+ console.log(`[Agent] Processing prompt: "${prompt}"`);
422
+ }
423
+ const elements = await getElements(_browser);
424
+ if (this.debug) {
425
+ console.log(`[Agent] Found ${elements.slice(1, elements.indexOf("]"))} visible elements`);
426
+ }
427
+ const llmPrompt = userPrompt(elements, prompt, this.maxActions);
428
+ const response = await this.provider.send(llmPrompt);
429
+ const actions = parseLlmResponse(response, this.maxActions);
430
+ if (this.debug) {
431
+ console.log(`[Agent] Parsed ${actions.length} action(s)`);
432
+ }
433
+ for (const action of actions) {
434
+ await executeAgentAction(_browser, this.debug, action);
435
+ }
436
+ return actions;
437
+ }
438
+ };
439
+
440
+ // types/index.ts
441
+ var VALID_ACTIONS = ["CLICK", "SET_VALUE", "NAVIGATE"];
442
+ var index_default = AgentService;
443
+ var launcher = AgentService;
444
+ export {
445
+ VALID_ACTIONS,
446
+ index_default as default,
447
+ launcher
448
+ };
449
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../services/agent.service.ts","../providers/ollama.provider.ts","../providers/index.ts","../scripts/get-elements.ts","../scripts/get-interactable-browser-elements.ts","../prompts/index.ts","../commands/parse-llm-response.ts","../commands/execute-agent-action.ts","../types/index.ts"],"sourcesContent":["import type { Services } from '@wdio/types';\nimport type { AgentAction, AgentServiceConfig } from '../types';\nimport type { LLMProvider } from '../providers';\nimport 'webdriverio';\nimport { initializeProvider } from '../providers';\nimport { getElements } from '../scripts/get-elements';\nimport { userPrompt } from '../prompts';\nimport { parseLlmResponse } from '../commands/parse-llm-response';\nimport { executeAgentAction } from '../commands/execute-agent-action';\n\n/**\n * WebdriverIO Agent Service\n * Adds browser.agent(prompt) command for LLM-powered automation\n */\nexport default class AgentService implements Services.ServiceInstance {\n private readonly resolvedConfig: AgentServiceConfig;\n private provider!: LLMProvider;\n\n private maxActions!: number;\n private debug!: boolean;\n\n constructor(serviceOptions: AgentServiceConfig = {}) {\n this.resolvedConfig = {\n provider: serviceOptions.provider ?? 'ollama',\n providerUrl: serviceOptions.providerUrl ?? 'http://localhost:11434',\n model: serviceOptions.model ?? 'qwen2.5-coder:7b',\n maxActions: serviceOptions.maxActions ?? 1,\n timeout: serviceOptions.timeout ?? 30000,\n debug: serviceOptions.debug ?? false,\n };\n }\n\n before(\n _capabilities: WebdriverIO.Capabilities,\n _specs: string[],\n browser: WebdriverIO.Browser,\n ): void {\n this.debug = this.resolvedConfig.debug!;\n this.maxActions = this.resolvedConfig.maxActions!;\n\n this.provider = initializeProvider(this.resolvedConfig);\n\n browser.addCommand('agent', async (prompt: string): Promise<AgentAction[]> => this.executeAgent(browser, prompt), {\n\n });\n\n if (this.resolvedConfig.debug) {\n console.log('[Agent] Service initialized with config:', this.resolvedConfig);\n }\n }\n\n private async executeAgent(_browser: WebdriverIO.Browser, prompt: string): Promise<AgentAction[]> {\n if (this.debug) {\n console.log(`[Agent] Processing prompt: \"${prompt}\"`);\n }\n\n const elements = await getElements(_browser);\n\n if (this.debug) {\n console.log(`[Agent] Found ${elements.slice(1, elements.indexOf(']'))} visible elements`);\n }\n\n const llmPrompt = userPrompt(elements, prompt, this.maxActions);\n\n const response = await this.provider.send(llmPrompt);\n\n const actions = parseLlmResponse(response, this.maxActions);\n\n if (this.debug) {\n console.log(`[Agent] Parsed ${actions.length} action(s)`);\n }\n\n for (const action of actions) {\n await executeAgentAction(_browser, this.debug, action);\n }\n\n return actions;\n }\n}\n","import type { AgentServiceConfig, PromptInput } from '../types';\nimport type { LLMProvider } from './index';\n\nexport class OllamaProvider implements LLMProvider {\n\n constructor(private config: AgentServiceConfig) {\n }\n\n async send(prompt: PromptInput | string): Promise<string> {\n const url = `${this.config.providerUrl}/api/generate`;\n\n const body = {\n model: this.config.model,\n prompt: typeof prompt === 'string' ? prompt : `${prompt.system}\\n\\n${prompt.user}`,\n stream: false,\n options: {\n temperature: 0.1, // Low temperature for consistent output\n num_predict: 500, // Limit response length\n },\n };\n\n if (this.config.debug) {\n console.log('[Agent] LLM Request:', JSON.stringify(body, null, 2));\n }\n\n const controller = new AbortController();\n const timeoutId = setTimeout(() => controller.abort(), this.config.timeout);\n\n try {\n const response = await fetch(url, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify(body),\n signal: controller.signal,\n });\n\n if (!response.ok) {\n const errorText = await response.text();\n throw new Error(`Ollama request failed: ${response.status} ${response.statusText} - ${errorText}`);\n }\n\n const data = (await response.json()) as {\n model: string;\n response: string;\n done: boolean;\n };\n\n if (this.config.debug) {\n console.debug('[Agent] LLM Response:', JSON.stringify({\n model: data.model,\n response: data.response,\n done: data.done,\n }, null, 2));\n }\n\n return data.response ?? '';\n\n } catch (error) {\n if (error instanceof Error && error.name === 'AbortError') {\n throw new Error(`Ollama request timed out after ${this.config.timeout}ms`);\n }\n throw error;\n } finally {\n clearTimeout(timeoutId);\n }\n }\n}\n","import type { AgentServiceConfig, PromptInput } from '../types';\nimport { OllamaProvider } from './ollama.provider';\n\nexport type Providers = 'ollama';\n\n/**\n * LLM Provider Interface\n * Extensible pattern for future providers (OpenAI, Anthropic, etc.)\n */\nexport interface LLMProvider {\n send(prompt: PromptInput | string): Promise<string>;\n}\n\nexport const initializeProvider = (config: AgentServiceConfig): LLMProvider => {\n switch (config.provider) {\n case 'ollama':\n return new OllamaProvider(config);\n default:\n throw new Error(`Unsupported provider: ${config.provider}`);\n }\n};\n\n","import 'webdriverio';\nimport interactableBrowserElementsScript from './get-interactable-browser-elements';\nimport { encode } from '@toon-format/toon';\n\nexport const getElements = async (browser: WebdriverIO.Browser): Promise<string> => {\n // The driver instance is a Browser\n const elements = await browser.execute(interactableBrowserElementsScript);\n\n return encode(elements)\n .replace(/,\"\"/g, ',')\n .replace(/\"\",/g, ',');\n};","/**\n * Browser script to get elements on the page\n */\nconst interactableBrowserElementsScript = () => (function () {\n const interactableSelectors = [\n 'a[href]', // Links with href\n 'button', // Buttons\n 'input:not([type=\"hidden\"])', // Input fields (except hidden)\n 'select', // Select dropdowns\n 'textarea', // Text areas\n '[role=\"button\"]', // Elements with button role\n '[role=\"link\"]', // Elements with link role\n '[role=\"checkbox\"]', // Elements with checkbox role\n '[role=\"radio\"]', // Elements with radio role\n '[role=\"tab\"]', // Elements with tab role\n '[role=\"menuitem\"]', // Elements with menuitem role\n '[role=\"combobox\"]', // Elements with combobox role\n '[role=\"option\"]', // Elements with option role\n '[role=\"switch\"]', // Elements with switch role\n '[role=\"slider\"]', // Elements with slider role\n '[role=\"textbox\"]', // Elements with textbox role\n '[role=\"searchbox\"]', // Elements with searchbox role\n '[contenteditable=\"true\"]', // Editable content\n '[tabindex]:not([tabindex=\"-1\"])', // Elements with tabindex\n 'img', // Images\n 'picture', // Picture elements\n 'svg', // SVG graphics\n 'video', // Video elements\n 'canvas', // Canvas elements\n '[style*=\"background-image\"]', // Elements with background images\n ];\n\n /**\n * Check if an element is visible\n * @param {HTMLElement} element - The element to check\n * @returns {boolean} - Whether the element is visible\n */\n function isVisible(element: HTMLElement): boolean {\n // Use checkVisibility if available (modern browsers)\n if (typeof element.checkVisibility === 'function') {\n return element.checkVisibility({\n opacityProperty: true,\n visibilityProperty: true,\n contentVisibilityAuto: true,\n });\n }\n\n // Fallback for browsers that don't support checkVisibility\n const style = window.getComputedStyle(element);\n return style.display !== 'none' &&\n style.visibility !== 'hidden' &&\n style.opacity !== '0' &&\n element.offsetWidth > 0 &&\n element.offsetHeight > 0;\n }\n\n /**\n * Get a CSS selector for an element\n * @param {HTMLElement} element - The element to get a selector for\n * @returns {string} - The CSS selector\n */\n function getCssSelector(element: HTMLElement): string {\n if (element.id) {\n return `#${CSS.escape(element.id)}`;\n }\n\n // Try to build a selector with classes if available\n if (element.className && typeof element.className === 'string') {\n const classes = element.className.trim().split(/\\s+/).filter(Boolean);\n if (classes.length > 0) {\n // Use up to 2 classes to avoid overly complex selectors\n const classSelector = classes.slice(0, 2).map(c => `.${CSS.escape(c)}`).join('');\n const tagWithClass = `${element.tagName.toLowerCase()}${classSelector}`;\n\n // Check if this selector uniquely identifies the element\n if (document.querySelectorAll(tagWithClass).length === 1) {\n return tagWithClass;\n }\n }\n }\n\n // Build a path-based selector\n let current: HTMLElement | null = element;\n const path = [];\n\n while (current && current !== document.documentElement) {\n let selector = current.tagName.toLowerCase();\n\n // Add ID if available\n if (current.id) {\n selector = `#${CSS.escape(current.id)}`;\n path.unshift(selector);\n break;\n }\n\n // Add position among siblings\n const parent = current.parentElement;\n if (parent) {\n const siblings = Array.from(parent.children).filter(child =>\n child.tagName === current!.tagName,\n );\n\n if (siblings.length > 1) {\n const index = siblings.indexOf(current) + 1;\n selector += `:nth-child(${index})`;\n }\n }\n\n path.unshift(selector);\n current = current.parentElement;\n\n // Limit path length to avoid overly complex selectors\n if (path.length >= 4) {\n break;\n }\n }\n\n return path.join(' > ');\n }\n\n /**\n * Get all visible elements on the page based on elementType\n * @returns {Record<string, unknown>[]} - Array of element information objects\n */\n function getElements(): Record<string, unknown>[] {\n // Get all potentially matching elements\n const allElements: Element[] = [];\n interactableSelectors.forEach(selector => {\n const elements = document.querySelectorAll(selector);\n elements.forEach(element => {\n if (!allElements.includes(element)) {\n allElements.push(element);\n }\n });\n });\n\n // Filter for visible elements and collect information\n return allElements\n .filter(element => isVisible(element as HTMLElement) && !(element as HTMLInputElement).disabled)\n .map(element => {\n const el = element as HTMLElement;\n const inputEl = element as HTMLInputElement;\n\n // Get element information\n const rect = el.getBoundingClientRect();\n const isInViewport = (\n rect.top >= 0 &&\n rect.left >= 0 &&\n rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) &&\n rect.right <= (window.innerWidth || document.documentElement.clientWidth)\n );\n\n // Build object with ALL fields for uniform schema (enables TOON tabular format)\n // Empty string '' used for missing values, post-processed to bare commas\n const elementProperties: Record<string, unknown> = {\n tagName: el.tagName.toLowerCase(),\n type: el.getAttribute('type') || '',\n id: el.id || '',\n className: (typeof el.className === 'string' ? el.className : '') || '',\n textContent: el.textContent?.trim() || '',\n value: inputEl.value || '',\n placeholder: inputEl.placeholder || '',\n href: el.getAttribute('href') || '',\n ariaLabel: el.getAttribute('aria-label') || '',\n role: el.getAttribute('role') || '',\n src: el.getAttribute('src') || '',\n alt: el.getAttribute('alt') || '',\n cssSelector: getCssSelector(el),\n isInViewport: isInViewport,\n };\n\n return elementProperties;\n });\n }\n\n return getElements();\n})();\n\nexport default interactableBrowserElementsScript;","export const userPrompt = (elements: string, userRequest: string, maxActions = 1) => `\nYou are a browser automation assistant. Your task is to analyze a user's natural language request and a list of webpage elements,\nthen return the minimum necessary browser actions as a JSON array.\n\n# Input Data\n\nHere are the webpage elements available for interaction:\n<elements>\n${elements}\n</elements>\n\nHere is the user's request:\n<user_request>\n${userRequest}\n</user_request>\n\nThe maximum number of actions you can return is:\n<max_actions>\n${maxActions}\n</max_actions>\n\n# Action Types\n\nYou can return three types of actions:\n\n1. **CLICK**: Click on an element\n - Prefer elements with these tags/attributes: button, a, [role=\"button\"]\n - Format: \\`{\"action\":\"CLICK\",\"target\":\"<cssSelector>\"}\\`\n \n2. **SET_VALUE**: Type text into an input field\n - Prefer elements with these tags: input, textarea\n - Must include a \"value\" field with the text to type\n - Format: \\`{\"action\":\"SET_VALUE\",\"target\":\"<cssSelector>\",\"value\":\"<text>\"}\\`\n - NEVER use SET_VALUE on button or a (link) elements\n\n3. **NAVIGATE**: Navigate to a URL\n - Only use when the user explicitly mentions a URL to visit\n - Format: \\`{\"action\":\"NAVIGATE\",\"target\":\"<url>\"}\\`\n\n# Important Rules\n\n## Minimizing Actions\n- Return the MINIMUM number of actions needed (between 1 and max_actions)\n- If the user request contains words like \"type\", \"click\", \"navigate\", or \"open\" for a single thing, you only need 1 action\n- Only return multiple actions if the request cannot be accomplished with a single action\n- Do not be overeager in creating multiple actions\n\n## Element Matching\n- Use the EXACT cssSelector from the elements data\n- Match the MOST SPECIFIC element - prefer EXACT text matches over partial matches\n- Compare the FULL request phrase against element text and attributes, not just individual keywords\n- For example, if the request is \"click the submit button\", look for elements with text exactly matching \"submit\" rather than just any button\n\n## Action-Specific Rules\n- If you CLICK on an \\`a\\` element with an href attribute, it will automatically navigate - do NOT add a separate NAVIGATE action\n- For SET_VALUE actions, extract the value from the user's request\n - Example: \"search for cats\" → value should be \"cats\"\n - Example: \"type hello@example.com in the email field\" → value should be \"hello@example.com\"\n- NAVIGATE actions should be rare - only use them when the user explicitly asks to go to a specific URL\n\n# Output Format\n\nYour final output must be ONLY a JSON array with no other text, as a simple string. Each action in the array should be a JSON object with the following structure:\n\nFor CLICK:\n[{\"action\":\"CLICK\",\"target\":\"cssSelector\"}]\n\nFor SET_VALUE:\n[{\"action\":\"SET_VALUE\",\"target\":\"cssSelector\",\"value\":\"text to type\"}]\n\nFor NAVIGATE:\n[{\"action\":\"NAVIGATE\",\"target\":\"https://example.com\"}]\n\nExample output for multiple actions:\n[{\"action\":\"SET_VALUE\",\"target\":\"input#search\",\"value\":\"mystery books\"},{\"action\":\"CLICK\",\"target\":\"button.search-btn\"}]\n\n# Instructions\n\nBefore generating your JSON output, analyze the request systematically:\n\n1. **Interpret the request**: What is the user trying to accomplish? Break down the request into its core intent.\n\n2. **Determine action count**: Does this map to a single action or multiple actions?\nRemember to minimize - if a single action can accomplish the goal, use only one.\n\n3. **List candidate elements**: Look through the elements list and write out ALL elements that could potentially match the request. For each candidate, include:\n - The cssSelector\n - The element type/tag\n - Relevant text content or attributes (placeholder, aria-label, href, etc.)\n - Why it might match the request\n \n It's OK for this section to be quite long if there are many potential matches.\n\n4. **Select the best match**: From your list of candidates, choose the most specific element that exactly matches the user's intent.\nExplain why this element is the best choice over the other candidates.\n\n5. **Determine action type(s)**: Based on the request and selected element(s), what action type is appropriate? \n - For typing/entering text: Use SET_VALUE on input/textarea elements\n - For clicking: Use CLICK on buttons/links\n - For navigation: Only if URL explicitly mentioned and not just clicking a link\n \n If you need a SET_VALUE action, explicitly extract and write out the value text from the user's request here.\n\n6. **Validate**: Check that your selected actions follow all the rules (correct element types, no redundant actions, within max_actions limit, etc.)\n\nAfter your analysis, output ONLY the JSON array with no additional text or explanation.\n`.trim();","import type { ActionType, AgentAction } from '../types';\nimport { VALID_ACTIONS } from '../types';\n\ninterface LlmProposedAction {\n action: string;\n target: string;\n value?: string;\n}\n\nexport const parseLlmResponse = (response: string, maxActions: number): AgentAction[] => {\n // Strip markdown code blocks if present\n const stripped = response.replace(/```(?:json)?\\s*/gi, '').replace(/```\\s*$/g, '').trim();\n\n // Extract JSON from response (LLM might include extra text)\n const jsonMatch = stripped.match(/\\[[\\s\\S]*]/);\n if (!jsonMatch) {\n throw new Error(`No JSON array found in LLM response.\\nResponse: ${response}`);\n }\n\n let parsed: LlmProposedAction[];\n try {\n parsed = JSON.parse(jsonMatch[0]) as LlmProposedAction[];\n } catch (e) {\n throw new Error(`Failed to parse JSON from LLM response: ${e}\\nResponse: ${response}`);\n }\n\n if (!Array.isArray(parsed) || parsed.length === 0) {\n throw new Error(`LLM returned empty or invalid actions array.\\nResponse: ${response}`);\n }\n\n // Limit to maxActions\n const limited = parsed.slice(0, maxActions);\n\n return limited.map((action, index) => {\n // Accept both \"action\" and \"type\" field names from LLM\n const actionType = action.action.toUpperCase() as ActionType;\n if (!VALID_ACTIONS.includes(actionType)) {\n throw new Error(`Invalid action type \"${actionType}\" at index ${index}.Valid: ${VALID_ACTIONS.join(', ')}`);\n }\n\n if (!action.target) {\n throw new Error(`Missing target at index ${index}`);\n }\n\n if (actionType === 'SET_VALUE' && !action.value) {\n throw new Error(`SET_VALUE at index ${index} requires value field`);\n }\n\n return {\n type: actionType,\n target: action.target,\n value: action.value,\n };\n });\n};","import 'webdriverio';\nimport type { ActionType, AgentAction } from '../types';\n\nconst actionsByType: Record<ActionType, (_browser: WebdriverIO.Browser, action: AgentAction) => Promise<void>> = {\n CLICK: async (_browser: WebdriverIO.Browser, action: AgentAction) => {\n await _browser.$(action.target).click();\n },\n SET_VALUE: async (_browser: WebdriverIO.Browser, action: AgentAction) => {\n await _browser.$(action.target).setValue(action.value!);\n },\n NAVIGATE: async (_browser: WebdriverIO.Browser, action: AgentAction) => {\n const url = action.target;\n const targetUrl = url.match(/^https?:\\/\\//) ? url : `https://${url}`;\n await _browser.url(targetUrl);\n },\n};\n\nexport const executeAgentAction = async (_browser: WebdriverIO.Browser, debug: boolean, action: AgentAction): Promise<void> => {\n if (debug) {\n console.log(`[Agent] Executing: ${action.type} on \"${action.target}\"${action.value ? ` with value \"${action.value}\"` : ''}`);\n }\n\n const agentAction = actionsByType[action.type];\n\n if (!agentAction) {\n throw new Error(`Unknown action type: ${action.type}`);\n }\n\n return agentAction(_browser, action);\n};\n","import type { Providers } from '../providers';\nimport AgentService from '../services/agent.service';\n\nexport type ActionType = 'CLICK' | 'SET_VALUE' | 'NAVIGATE';\nexport const VALID_ACTIONS: ActionType[] = ['CLICK', 'SET_VALUE', 'NAVIGATE'];\n\nexport interface AgentServiceConfig {\n /** LLM Provider. Default: ollama */\n provider?: Providers;\n\n /** LLM Provider API endpoint. Default (ollama): http://localhost:11434 */\n providerUrl?: string;\n\n /** LLM Provider API token. Default: '' */\n token?: string;\n\n /** LLM model name. Default: qwen2.5-coder:7b */\n model?: string;\n\n /** Maximum actions per prompt. Default: 1 */\n maxActions?: number;\n\n /** LLM Request timeout in ms. Default: 30000 */\n timeout?: number;\n\n /** Enable debug logging. Default: false */\n debug?: boolean;\n}\n\nexport interface AgentAction {\n type: ActionType;\n target: string;\n value?: string;\n}\n\nexport interface PromptInput {\n system: string;\n user: string;\n}\n\nexport default AgentService;\nexport const launcher = AgentService;\n\ndeclare global {\n namespace WebdriverIO {\n interface Browser {\n /**\n * Execute natural language browser automation using LLM\n * @param prompt - Natural language instruction (e.g., \"accept all cookies\")\n * @returns Result containing executed actions\n */\n agent: (prompt: string) => Promise<AgentAction[]>;\n }\n }\n}"],"mappings":"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","names":["getElements"]}
package/package.json ADDED
@@ -0,0 +1,56 @@
1
+ {
2
+ "name": "wdio-agent-service",
3
+ "repository": {
4
+ "type": "git",
5
+ "url": "git://github.com/Winify/wdio-agent-service.git"
6
+ },
7
+ "version": "0.0.1",
8
+ "author": "Vince Graics",
9
+ "description": "WebdriverIO plugin for agentic browser actions. Integrates into WebdrverIO codebase/ecosystem.",
10
+ "keywords": [
11
+ "wdio-plugin",
12
+ "wdio-service",
13
+ "webdriverio",
14
+ "agent",
15
+ "testing",
16
+ "ai",
17
+ "ollama"
18
+ ],
19
+ "type": "module",
20
+ "main": "./build/index.js",
21
+ "module": "./build/index.js",
22
+ "types": "./build/index.d.ts",
23
+ "files": [
24
+ "build",
25
+ "README.md"
26
+ ],
27
+ "scripts": {
28
+ "lint": "eslint --fix && tsc --noEmit",
29
+ "build": "rimraf build && tsup",
30
+ "postbuild": "npm pack",
31
+ "prepare": "husky",
32
+ "test:examples": "pnpm run build && cd examples && pnpm i && pnpm test"
33
+ },
34
+ "license": "MIT",
35
+ "dependencies": {
36
+ "@types/node": "^24.0.0"
37
+ },
38
+ "devDependencies": {
39
+ "@toon-format/toon": "^2.1.0",
40
+ "@wdio/eslint": "^0.1.3",
41
+ "@wdio/globals": "^9.23.0",
42
+ "@wdio/types": "^9.23.2",
43
+ "eslint": "^9.39.2",
44
+ "husky": "^9.1.7",
45
+ "rimraf": "^6.1.2",
46
+ "tsup": "^8.5.1",
47
+ "typescript": "^5.9.3",
48
+ "webdriverio": "^9.23.2"
49
+ },
50
+ "peerDependencies": {
51
+ "@toon-format/toon": ">=2.0.0",
52
+ "@wdio/globals": ">=9.0.0",
53
+ "@wdio/types": ">=9.0.0",
54
+ "webdriverio": ">=9.0.0"
55
+ }
56
+ }