explorbot 0.1.20 → 0.1.21

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,415 @@
1
+ import { parseResearchSections } from "../../../../src/ai/researcher/parser.js";
2
+ const MAX_PRIMARY_CANDIDATES = 3;
3
+ const MAX_INTERACTIONS = 5;
4
+ const MAX_LINKS = 15;
5
+ const DEFAULT_WAIT_MS = 700;
6
+ const TAB_WAIT_MS = 500;
7
+ export async function collectDocInteractions(explorer, state, research) {
8
+ const sections = parseResearchSections(research);
9
+ const transitions = [];
10
+ const tabGroup = findTabGroup(sections);
11
+ if (tabGroup) {
12
+ transitions.push(...(await exploreTabGroup(explorer, tabGroup, state.url)));
13
+ }
14
+ for (const candidate of findActionCandidates(sections)) {
15
+ if (transitions.length >= MAX_INTERACTIONS) {
16
+ break;
17
+ }
18
+ const transition = await executeInteraction(explorer, candidate, state.url, DEFAULT_WAIT_MS);
19
+ if (!transition) {
20
+ continue;
21
+ }
22
+ transitions.push(transition);
23
+ }
24
+ return transitions;
25
+ }
26
+ export function pickDocActionCandidates(research) {
27
+ return findActionCandidates(parseResearchSections(research)).map((candidate) => ({
28
+ label: candidate.element.name.trim(),
29
+ role: candidate.role,
30
+ section: candidate.sectionName,
31
+ }));
32
+ }
33
+ async function exploreTabGroup(explorer, tabGroup, restoreUrl) {
34
+ const transitions = [];
35
+ for (const element of tabGroup.elements) {
36
+ const transition = await executeInteraction(explorer, {
37
+ element,
38
+ container: tabGroup.container,
39
+ role: 'tab',
40
+ sectionName: tabGroup.sectionName,
41
+ }, restoreUrl, TAB_WAIT_MS);
42
+ if (!transition) {
43
+ continue;
44
+ }
45
+ transitions.push(transition);
46
+ }
47
+ await restoreInteractionState(explorer, restoreUrl, buildPrimaryCommand(tabGroup.elements[0], tabGroup.container));
48
+ return transitions;
49
+ }
50
+ async function executeInteraction(explorer, candidate, restoreUrl, waitMs) {
51
+ const beforeState = explorer.getStateManager().getCurrentState();
52
+ if (!beforeState) {
53
+ return null;
54
+ }
55
+ const executed = await attemptInteraction(explorer, candidate);
56
+ if (!executed) {
57
+ return null;
58
+ }
59
+ await wait(waitMs);
60
+ const afterState = explorer.getStateManager().getCurrentState();
61
+ if (!afterState) {
62
+ return null;
63
+ }
64
+ const ariaChanges = countAriaChanges(beforeState.ariaSnapshot || '', afterState.ariaSnapshot || '');
65
+ const urlChanged = beforeState.url !== afterState.url;
66
+ const transition = buildTransition(candidate, beforeState, afterState, {
67
+ urlChanged,
68
+ newElements: ariaChanges.newCount,
69
+ removedElements: ariaChanges.removedCount,
70
+ });
71
+ if (urlChanged) {
72
+ await restoreInteractionState(explorer, restoreUrl);
73
+ }
74
+ return transition;
75
+ }
76
+ async function attemptInteraction(explorer, candidate) {
77
+ const action = explorer.createAction();
78
+ for (const command of buildClickCommands(candidate.element, candidate.container)) {
79
+ const success = await action.attempt(command, buildPurpose(candidate), false);
80
+ if (success) {
81
+ return true;
82
+ }
83
+ }
84
+ return false;
85
+ }
86
+ async function restoreInteractionState(explorer, restoreUrl, primaryCommand) {
87
+ if (primaryCommand) {
88
+ const action = explorer.createAction();
89
+ const restored = await action.attempt(primaryCommand, `Restore initial state on ${restoreUrl}`, false);
90
+ if (restored) {
91
+ await wait(TAB_WAIT_MS);
92
+ return;
93
+ }
94
+ }
95
+ const action = explorer.createAction();
96
+ await action.attempt(`I.amOnPage(${JSON.stringify(restoreUrl)})`, `Restore page ${restoreUrl}`, false);
97
+ }
98
+ function buildTransition(candidate, beforeState, afterState, changes) {
99
+ const transition = {
100
+ action: describeAction(candidate),
101
+ before: summarizeInteractiveState(beforeState),
102
+ after: summarizeInteractiveState(afterState),
103
+ discoveredUrls: collectLinks(afterState).map((link) => link.url),
104
+ newCapabilities: collectDiscoveryNotes(afterState, changes),
105
+ element: buildInteractionElement(candidate),
106
+ changes,
107
+ };
108
+ if (changes.urlChanged) {
109
+ transition.targetUrl = afterState.url;
110
+ }
111
+ return transition;
112
+ }
113
+ function buildInteractionElement(candidate) {
114
+ const element = {
115
+ role: candidate.role,
116
+ name: candidate.element.name.trim(),
117
+ section: candidate.sectionName,
118
+ };
119
+ if (candidate.container) {
120
+ element.container = candidate.container;
121
+ }
122
+ if (candidate.element.css || candidate.element.xpath) {
123
+ element.locator = candidate.element.css || candidate.element.xpath || undefined;
124
+ }
125
+ return element;
126
+ }
127
+ function collectDiscoveryNotes(state, changes) {
128
+ const notes = [];
129
+ const headings = collectHeadings(state);
130
+ const links = collectLinks(state);
131
+ if (changes.urlChanged) {
132
+ notes.push('URL changed after interaction');
133
+ }
134
+ if (changes.newElements > 0) {
135
+ notes.push(`ARIA snapshot gained ${changes.newElements} elements`);
136
+ }
137
+ if (changes.removedElements > 0) {
138
+ notes.push(`ARIA snapshot removed ${changes.removedElements} elements`);
139
+ }
140
+ if (headings.length > 0) {
141
+ notes.push(`Visible headings after interaction: ${headings.slice(0, 3).join(' | ')}`);
142
+ }
143
+ if (links.length > 0) {
144
+ notes.push(`Visible links after interaction: ${Math.min(links.length, MAX_LINKS)}`);
145
+ }
146
+ return notes;
147
+ }
148
+ function findTabGroup(sections) {
149
+ for (const section of sections) {
150
+ const sectionName = section.name.toLowerCase();
151
+ const container = section.containerCss?.toLowerCase() || '';
152
+ if (isOverlaySection(sectionName, container)) {
153
+ continue;
154
+ }
155
+ const elements = section.elements.filter((element) => getElementRole(element) === 'tab');
156
+ if (elements.length < 2 || elements.length > 6) {
157
+ continue;
158
+ }
159
+ return {
160
+ elements,
161
+ container: section.containerCss || undefined,
162
+ sectionName: section.name,
163
+ };
164
+ }
165
+ return null;
166
+ }
167
+ function findActionCandidates(sections) {
168
+ const candidates = [];
169
+ const seen = new Set();
170
+ const navigationLabels = collectNavigationLabels(sections);
171
+ for (const section of sections) {
172
+ const sectionName = section.name.toLowerCase();
173
+ const container = section.containerCss?.toLowerCase() || '';
174
+ if (isOverlaySection(sectionName, container)) {
175
+ continue;
176
+ }
177
+ if (isNavigationSection(sectionName)) {
178
+ continue;
179
+ }
180
+ for (const element of section.elements) {
181
+ const candidate = toInteractionCandidate(element, section.name, section.containerCss, navigationLabels);
182
+ if (!candidate) {
183
+ continue;
184
+ }
185
+ const key = `${candidate.role}:${normalizeCandidateLabel(candidate.element.name)}`;
186
+ if (seen.has(key)) {
187
+ continue;
188
+ }
189
+ seen.add(key);
190
+ candidates.push(candidate);
191
+ }
192
+ }
193
+ return candidates.sort((a, b) => scoreCandidate(b) - scoreCandidate(a)).slice(0, MAX_PRIMARY_CANDIDATES);
194
+ }
195
+ function toInteractionCandidate(element, sectionName, container, navigationLabels) {
196
+ const role = getElementRole(element);
197
+ if (role !== 'link' && role !== 'button' && role !== 'tab') {
198
+ return null;
199
+ }
200
+ if (!hasUsableName(element)) {
201
+ return null;
202
+ }
203
+ if (isShellLocator(element.css) || isShellLocator(element.xpath) || isShellLocator(container)) {
204
+ return null;
205
+ }
206
+ if (role === 'link' && navigationLabels.has(normalizeCandidateLabel(element.name))) {
207
+ return null;
208
+ }
209
+ return {
210
+ element,
211
+ container: container || undefined,
212
+ role,
213
+ sectionName,
214
+ };
215
+ }
216
+ function buildClickCommands(element, container) {
217
+ const commands = [];
218
+ if (element.css) {
219
+ if (container && !element.css.startsWith(container)) {
220
+ commands.push(`I.click(${JSON.stringify(element.css)}, ${JSON.stringify(container)})`);
221
+ }
222
+ commands.push(`I.click(${JSON.stringify(element.css)})`);
223
+ }
224
+ if (element.aria) {
225
+ if (container) {
226
+ commands.push(`I.click(${JSON.stringify(element.aria)}, ${JSON.stringify(container)})`);
227
+ }
228
+ commands.push(`I.click(${JSON.stringify(element.aria)})`);
229
+ }
230
+ const xpath = buildXPathLocator(element);
231
+ if (xpath) {
232
+ commands.push(`I.click(${JSON.stringify(xpath)})`);
233
+ }
234
+ return [...new Set(commands)];
235
+ }
236
+ function buildPrimaryCommand(element, container) {
237
+ return buildClickCommands(element, container)[0] || null;
238
+ }
239
+ function buildXPathLocator(element) {
240
+ if (!element.name) {
241
+ return null;
242
+ }
243
+ const text = xpathStringLiteral(element.name.trim());
244
+ const role = getElementRole(element);
245
+ if (role === 'link') {
246
+ return `//a[normalize-space()=${text}]`;
247
+ }
248
+ if (role === 'button' || role === 'tab') {
249
+ return `//*[self::button or @role="button" or @role="tab"][normalize-space()=${text}]`;
250
+ }
251
+ return `//*[normalize-space()=${text}]`;
252
+ }
253
+ function buildPurpose(candidate) {
254
+ return `Explore ${candidate.role} ${candidate.element.name.trim()}`;
255
+ }
256
+ function summarizeAria(aria) {
257
+ const lines = aria.split('\n').filter((line) => line.trim());
258
+ if (lines.length === 0) {
259
+ return 'No elements';
260
+ }
261
+ const roleCounts = {};
262
+ for (const role of lines.map(extractAriaRole).filter((role) => Boolean(role))) {
263
+ roleCounts[role] = (roleCounts[role] || 0) + 1;
264
+ }
265
+ const topRoles = Object.entries(roleCounts)
266
+ .sort((a, b) => b[1] - a[1])
267
+ .slice(0, 5)
268
+ .map(([role, count]) => `${role}:${count}`)
269
+ .join(', ');
270
+ return `${lines.length} elements (${topRoles})`;
271
+ }
272
+ function extractAriaRole(line) {
273
+ const roleMatch = line.match(/\[role: ([\w-]+)\]/);
274
+ if (roleMatch) {
275
+ return roleMatch[1];
276
+ }
277
+ const yamlMatch = line.trim().match(/^- ([\w-]+)(?:\s|$|:)/);
278
+ if (yamlMatch) {
279
+ return yamlMatch[1];
280
+ }
281
+ return null;
282
+ }
283
+ function countAriaChanges(before, after) {
284
+ const beforeLines = new Set(before.split('\n').filter((line) => line.trim()));
285
+ const afterLines = new Set(after.split('\n').filter((line) => line.trim()));
286
+ let newCount = 0;
287
+ let removedCount = 0;
288
+ for (const line of afterLines) {
289
+ if (!beforeLines.has(line)) {
290
+ newCount++;
291
+ }
292
+ }
293
+ for (const line of beforeLines) {
294
+ if (!afterLines.has(line)) {
295
+ removedCount++;
296
+ }
297
+ }
298
+ return { newCount, removedCount };
299
+ }
300
+ function summarizeInteractiveState(state) {
301
+ const parts = [summarizeAria(state.ariaSnapshot || '')];
302
+ const headings = collectHeadings(state).slice(0, 3);
303
+ const links = collectLinks(state).slice(0, 3);
304
+ if (state.url) {
305
+ parts.push(`URL ${state.url}`);
306
+ }
307
+ if (headings.length > 0) {
308
+ parts.push(`Headings: ${headings.map((heading) => limitInlineText(heading, 90)).join(' | ')}`);
309
+ }
310
+ if (links.length > 0) {
311
+ parts.push(`Links: ${links.map((link) => `${link.title} -> ${link.url}`).join('; ')}`);
312
+ }
313
+ return parts.join('. ');
314
+ }
315
+ function collectHeadings(state) {
316
+ return [state.h1, state.h2, state.h3, state.h4].filter((heading) => Boolean(heading)).map((heading) => heading.trim());
317
+ }
318
+ function collectLinks(state) {
319
+ return (state.links || [])
320
+ .filter((link) => link.url)
321
+ .slice(0, MAX_LINKS)
322
+ .map((link) => ({
323
+ title: link.title || link.url,
324
+ url: link.url,
325
+ }));
326
+ }
327
+ function describeAction(candidate) {
328
+ return `Clicked ${candidate.role}: ${candidate.element.name.trim()}`;
329
+ }
330
+ function hasUsableName(element) {
331
+ const name = element.name.trim();
332
+ if (!name) {
333
+ return false;
334
+ }
335
+ if (name.length < 2) {
336
+ return false;
337
+ }
338
+ return true;
339
+ }
340
+ function isNavigationSection(sectionName) {
341
+ return /(navigation|menu|header|footer|breadcrumb)/i.test(sectionName);
342
+ }
343
+ function isOverlaySection(sectionName, container) {
344
+ return /(overlay|modal|popup|dialog)/i.test(sectionName) || /(overlay|modal|popup|dialog)/i.test(container);
345
+ }
346
+ function scoreCandidate(candidate) {
347
+ let score = 0;
348
+ if (candidate.role === 'link') {
349
+ score += 50;
350
+ }
351
+ if (candidate.role === 'button') {
352
+ score += 40;
353
+ }
354
+ if (candidate.role === 'tab') {
355
+ score += 30;
356
+ }
357
+ if (candidate.container) {
358
+ score += 10;
359
+ }
360
+ if (candidate.element.css) {
361
+ score += 5;
362
+ }
363
+ if (candidate.element.name.trim().length > 8) {
364
+ score += 5;
365
+ }
366
+ return score;
367
+ }
368
+ function isShellLocator(locator) {
369
+ if (!locator) {
370
+ return false;
371
+ }
372
+ return /(nav\[role="navigation"\]|header|menu|breadcrumb|footer)/i.test(locator);
373
+ }
374
+ function collectNavigationLabels(sections) {
375
+ const labels = new Set();
376
+ for (const section of sections) {
377
+ if (!isNavigationSection(section.name.toLowerCase())) {
378
+ continue;
379
+ }
380
+ for (const element of section.elements) {
381
+ const label = normalizeCandidateLabel(element.name);
382
+ if (!label) {
383
+ continue;
384
+ }
385
+ labels.add(label);
386
+ }
387
+ }
388
+ return labels;
389
+ }
390
+ function normalizeCandidateLabel(label) {
391
+ return label.trim().toLowerCase();
392
+ }
393
+ function limitInlineText(text, maxLength) {
394
+ const normalized = text.replace(/\s+/g, ' ').trim();
395
+ if (normalized.length <= maxLength) {
396
+ return normalized;
397
+ }
398
+ return `${normalized.slice(0, maxLength - 3).trimEnd()}...`;
399
+ }
400
+ function getElementRole(element) {
401
+ return (element.aria?.role || element.type || '').toLowerCase();
402
+ }
403
+ function xpathStringLiteral(value) {
404
+ if (!value.includes("'")) {
405
+ return `'${value}'`;
406
+ }
407
+ if (!value.includes('"')) {
408
+ return `"${value}"`;
409
+ }
410
+ const parts = value.split("'").map((part) => `'${part}'`);
411
+ return `concat(${parts.join(`, "'", `)})`;
412
+ }
413
+ async function wait(timeout) {
414
+ await new Promise((resolve) => setTimeout(resolve, timeout));
415
+ }
@@ -83,6 +83,7 @@ export function createDocsCommands(name = 'docs') {
83
83
  maxPages: 100,
84
84
  output: 'docs',
85
85
  screenshot: true,
86
+ interactive: false,
86
87
  collapseDynamicPages: true,
87
88
  scope: 'site',
88
89
  includePaths: [],
@@ -104,6 +104,7 @@ class DocbotConfigParser {
104
104
  maxPages: 100,
105
105
  output: 'docs',
106
106
  screenshot: true,
107
+ interactive: false,
107
108
  collapseDynamicPages: true,
108
109
  scope: 'site',
109
110
  includePaths: [],
@@ -37,7 +37,7 @@ class DocBot {
37
37
  config: this.options.docsConfig,
38
38
  path: this.options.path,
39
39
  });
40
- this.documentarian = new Documentarian(this.explorBot.getProvider(), this.config);
40
+ this.documentarian = new Documentarian(this.explorBot.getProvider(), this.config, this.explorBot.getExplorer());
41
41
  this.ensureDirectory(this.configParser.getOutputDir());
42
42
  this.ensureDirectory(this.getPagesDir());
43
43
  }
@@ -112,17 +112,21 @@ class DocBot {
112
112
  summary: documentation.summary,
113
113
  canCount: documentation.can.length,
114
114
  mightCount: documentation.might.length,
115
+ interactionCount: (documentation.interactions || []).length,
115
116
  canActions: documentation.can.map((item) => item.action),
116
117
  mightActions: documentation.might.map((item) => item.action),
118
+ interactionActions: (documentation.interactions || []).map((item) => item.action),
119
+ qualityNotes: documentation.qualityNotes || [],
117
120
  filePath,
118
121
  });
119
122
  documented.add(pageKey);
120
- const nextPaths = this.extractNextPaths(state, baseUrl, research);
123
+ const nextPaths = this.extractNextPaths(state, baseUrl, research, documentation);
124
+ const interactionPriorityPaths = new Set(this.extractInteractionPaths(baseUrl, documentation));
121
125
  for (const nextPath of nextPaths) {
122
126
  if (documented.has(this.getPageKey(nextPath))) {
123
127
  continue;
124
128
  }
125
- if (stateManager.hasVisitedState(nextPath)) {
129
+ if (!interactionPriorityPaths.has(nextPath) && stateManager.hasVisitedState(nextPath)) {
126
130
  continue;
127
131
  }
128
132
  this.enqueuePath(nextPath, queue, queued);
@@ -162,9 +166,16 @@ class DocBot {
162
166
  }
163
167
  return true;
164
168
  }
165
- extractNextPaths(state, baseUrl, research) {
169
+ extractNextPaths(state, baseUrl, research, documentation) {
166
170
  const paths = [];
167
171
  const seen = new Set();
172
+ for (const interactionPath of this.extractInteractionPaths(baseUrl, documentation)) {
173
+ if (seen.has(interactionPath)) {
174
+ continue;
175
+ }
176
+ seen.add(interactionPath);
177
+ paths.push(interactionPath);
178
+ }
168
179
  for (const link of state.links || []) {
169
180
  const nextPath = this.resolveLink(link, baseUrl);
170
181
  if (!nextPath) {
@@ -197,10 +208,51 @@ class DocBot {
197
208
  }
198
209
  return paths;
199
210
  }
211
+ extractInteractionPaths(baseUrl, documentation) {
212
+ const paths = [];
213
+ const seen = new Set();
214
+ const interactions = documentation?.interactions;
215
+ for (const interaction of interactions || []) {
216
+ if (interaction.targetUrl) {
217
+ const nextPath = this.resolveRawUrl(interaction.targetUrl, baseUrl);
218
+ if (nextPath && this.isEligibleNextPath(nextPath) && !seen.has(nextPath)) {
219
+ seen.add(nextPath);
220
+ paths.push(nextPath);
221
+ }
222
+ }
223
+ for (const discoveredUrl of interaction.discoveredUrls || []) {
224
+ const discoveredPath = this.resolveRawUrl(discoveredUrl, baseUrl);
225
+ if (!discoveredPath) {
226
+ continue;
227
+ }
228
+ if (!this.isEligibleNextPath(discoveredPath)) {
229
+ continue;
230
+ }
231
+ if (seen.has(discoveredPath)) {
232
+ continue;
233
+ }
234
+ seen.add(discoveredPath);
235
+ paths.push(discoveredPath);
236
+ }
237
+ }
238
+ return paths;
239
+ }
240
+ isEligibleNextPath(nextPath) {
241
+ if (!shouldCrawlDocPath(nextPath, this.config)) {
242
+ return false;
243
+ }
244
+ if (!this.isInScope(nextPath)) {
245
+ return false;
246
+ }
247
+ return true;
248
+ }
200
249
  resolveLink(link, baseUrl) {
250
+ return this.resolveRawUrl(link.url, baseUrl);
251
+ }
252
+ resolveRawUrl(rawUrl, baseUrl) {
201
253
  let resolved;
202
254
  try {
203
- resolved = new URL(link.url, baseUrl);
255
+ resolved = new URL(rawUrl, baseUrl);
204
256
  }
205
257
  catch {
206
258
  return null;
@@ -11,6 +11,26 @@ function renderPageDocumentation(state, documentation) {
11
11
  lines.push('');
12
12
  lines.push(ensureSentence(documentation.summary));
13
13
  lines.push('');
14
+ const interactions = documentation.interactions;
15
+ if (interactions && interactions.length > 0) {
16
+ lines.push('## State Transitions');
17
+ lines.push('');
18
+ for (const transition of interactions) {
19
+ lines.push(`### ${transition.action}`);
20
+ lines.push('');
21
+ lines.push(`**Before:** ${transition.before}`);
22
+ lines.push('');
23
+ lines.push(`**After:** ${transition.after}`);
24
+ lines.push('');
25
+ if (transition.newCapabilities && transition.newCapabilities.length > 0) {
26
+ lines.push('**Observed changes:**');
27
+ for (const cap of transition.newCapabilities) {
28
+ lines.push(`- ${cap}`);
29
+ }
30
+ lines.push('');
31
+ }
32
+ }
33
+ }
14
34
  lines.push('## User Can');
15
35
  lines.push('');
16
36
  if (documentation.can.length === 0) {
@@ -37,6 +57,15 @@ function renderPageDocumentation(state, documentation) {
37
57
  if (documentation.might.length > 0) {
38
58
  lines.push('');
39
59
  }
60
+ const qualityNotes = documentation.qualityNotes;
61
+ if (qualityNotes && qualityNotes.length > 0) {
62
+ lines.push('## Coverage Notes');
63
+ lines.push('');
64
+ for (const note of qualityNotes) {
65
+ lines.push(`- ${ensureSentence(note)}`);
66
+ }
67
+ lines.push('');
68
+ }
40
69
  return `${lines.join('\n').trimEnd()}\n`;
41
70
  }
42
71
  function renderSpecIndex(outputDir, startPath, pages, skipped, maxPages) {
@@ -63,6 +92,9 @@ function renderSpecIndex(outputDir, startPath, pages, skipped, maxPages) {
63
92
  lines.push(`Purpose: ${ensureSentence(page.summary)}`);
64
93
  lines.push(`Proven actions: ${page.canCount}`);
65
94
  lines.push(`Possible actions: ${page.mightCount}`);
95
+ if (page.interactionCount > 0) {
96
+ lines.push(`Interactive transitions: ${page.interactionCount}`);
97
+ }
66
98
  if (page.title) {
67
99
  lines.push(`Title: ${normalizeInlineText(page.title)}`);
68
100
  }
@@ -81,6 +113,20 @@ function renderSpecIndex(outputDir, startPath, pages, skipped, maxPages) {
81
113
  }
82
114
  lines.push('');
83
115
  }
116
+ if (page.interactionActions.length > 0) {
117
+ lines.push('Interactive Findings:');
118
+ for (const action of page.interactionActions.slice(0, 3)) {
119
+ lines.push(`- ${normalizeInlineText(action)}`);
120
+ }
121
+ lines.push('');
122
+ }
123
+ if (page.qualityNotes.length > 0) {
124
+ lines.push('Coverage Notes:');
125
+ for (const note of page.qualityNotes) {
126
+ lines.push(`- ${ensureSentence(note)}`);
127
+ }
128
+ lines.push('');
129
+ }
84
130
  }
85
131
  if (skipped.length > 0) {
86
132
  lines.push('## Skipped');
package/dist/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "explorbot",
3
- "version": "0.1.20",
3
+ "version": "0.1.21",
4
4
  "description": "CLI app built with React Ink, CodeceptJS, and Playwright",
5
5
  "license": "Elastic-2.0",
6
6
  "type": "module",