elementus-ai 1.2.0 → 1.5.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/CHANGELOG.md +43 -0
- package/README.md +90 -1
- package/elementus.js +77 -2
- package/index.d.ts +27 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,49 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes are documented here. This project adheres to [Semantic Versioning](https://semver.org).
|
|
4
4
|
|
|
5
|
+
## 1.5.0
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- `HealEvent.suggestion` — a ready-to-use, framework-native, properly-escaped suggested replacement
|
|
10
|
+
locator (`getByRole('link', { name: 'Save' })` on Playwright, `$('aria/Save')` on WDIO, `$('~Save')`
|
|
11
|
+
on Appium). Surface it verbatim. This moves suggestion-building into the library so consumers stop
|
|
12
|
+
hand-rolling it from `resolved` — hand-rolling re-introduced quote-escaping bugs (text with
|
|
13
|
+
apostrophes) and bad role mapping (form controls suggested as `getByText`). `resolved` stays for
|
|
14
|
+
consumers that want the raw facts.
|
|
15
|
+
|
|
16
|
+
### Changed
|
|
17
|
+
|
|
18
|
+
- Additive on the existing `onHeal` event; no behavior change.
|
|
19
|
+
|
|
20
|
+
## 1.4.0
|
|
21
|
+
|
|
22
|
+
### Added
|
|
23
|
+
|
|
24
|
+
- `HealEvent.resolved` — `{ tag, text, role }` of the element a heal resolved to (when known), so a
|
|
25
|
+
consumer can compose a **suggested replacement locator** (e.g. `getByRole(role, { name: text })`) and
|
|
26
|
+
show "selector X failed → replace with Y" in reports. Absent on coordinate-only vision heals.
|
|
27
|
+
|
|
28
|
+
### Changed
|
|
29
|
+
|
|
30
|
+
- Additive on the existing `onHeal` event; no behavior change.
|
|
31
|
+
|
|
32
|
+
## 1.3.0
|
|
33
|
+
|
|
34
|
+
### Added
|
|
35
|
+
|
|
36
|
+
- `onHeal(event)` config callback — fires once per heal with `{ description, selector, method, framework }`
|
|
37
|
+
so consumers can surface healed locators (report annotations, CI fail-on-heal). Framework-agnostic:
|
|
38
|
+
elementus emits the event; wiring it to a reporter is consumer-side (README "Detecting heals" has
|
|
39
|
+
Playwright + WebdriverIO/Appium recipes).
|
|
40
|
+
- `HealEvent` exported type (`index.d.ts`); `onHeal` added to `ElementusOptions`.
|
|
41
|
+
- Test `T47` covering onHeal; docs in README, `docs/API.md`, `docs/ARCHITECTURE.md`.
|
|
42
|
+
|
|
43
|
+
### Changed
|
|
44
|
+
|
|
45
|
+
- No change to existing behavior. `onHeal` is opt-in; unset ⇒ byte-for-byte the same as 1.2.0
|
|
46
|
+
(zero-overhead path untouched, single export preserved).
|
|
47
|
+
|
|
5
48
|
## 1.2.0
|
|
6
49
|
|
|
7
50
|
### Added
|
package/README.md
CHANGED
|
@@ -34,11 +34,24 @@ I just installed the npm package "elementus-ai" — a self-healing element resol
|
|
|
34
34
|
|
|
35
35
|
3. INTEGRATE BASED ON MY FRAMEWORK
|
|
36
36
|
|
|
37
|
+
First create the elementus instance using the provider chosen in step 2:
|
|
38
|
+
const { createElementus } = require('elementus-ai') // ESM/TS: import { createElementus } from 'elementus-ai'
|
|
39
|
+
const el = createElementus({ provider: 'gemini', geminiApiKey: process.env.GEMINI_API_KEY })
|
|
40
|
+
// LM Studio instead: createElementus({ provider: 'lmstudio', lmStudioUrl: 'http://localhost:1234/v1/chat/completions', model: 'holo-3.1-9b' })
|
|
41
|
+
For exact, copy-pasteable code (TypeScript fixture, onHeal recipe), read the installed package's
|
|
42
|
+
node_modules/elementus-ai/README.md ("Quick Start", "TypeScript", "Detecting heals") and docs/API.md.
|
|
43
|
+
|
|
37
44
|
For Playwright:
|
|
38
|
-
- Create or update a fixtures file that
|
|
45
|
+
- Create or update a fixtures file that overrides the page fixture with el.wrapPage(page)
|
|
39
46
|
- Make sure all tests import from the fixtures file instead of @playwright/test
|
|
40
47
|
- TypeScript projects: use import/export and type the override as base.extend<{ page: ElementusPage }>({ ... }) (import ElementusPage from elementus-ai) so { ai } is autocompleted and documented. Types are bundled — do NOT add @types or a "declare module" shim
|
|
41
48
|
- Set actionTimeout: 10000 in playwright config (Elementus respects framework timeouts)
|
|
49
|
+
- Optional heal visibility: only if the user wants healed locators surfaced, wire onHeal to the
|
|
50
|
+
framework's reporter. Use e.suggestion — a ready-to-use, escaped, framework-native replacement
|
|
51
|
+
locator — verbatim. Playwright: push a testInfo "healed" annotation (HTML report) AND
|
|
52
|
+
testInfo.attach (so it also reaches Allure + custom reporters); optionally fail CI on heal via an
|
|
53
|
+
env-gated afterEach/fixture. WebdriverIO/Appium: collect in onHeal, fail in afterTest. See
|
|
54
|
+
"Detecting heals" in the README. Default off
|
|
42
55
|
|
|
43
56
|
For WebDriverIO:
|
|
44
57
|
- In wdio.conf.js before hook, wrap browser and override global $:
|
|
@@ -320,9 +333,85 @@ createElementus({
|
|
|
320
333
|
|
|
321
334
|
// Custom stop words
|
|
322
335
|
stopWords: null, // Set of words to ignore in descriptions
|
|
336
|
+
|
|
337
|
+
// Heal telemetry (opt-in) — called once per heal with { description, selector, method, framework, resolved, suggestion }.
|
|
338
|
+
// Best-effort & isolated: a throwing callback never breaks a test. See "Detecting heals" below.
|
|
339
|
+
onHeal: null, // e.g. (e) => console.log('healed', e.selector, '→', e.description)
|
|
323
340
|
})
|
|
324
341
|
```
|
|
325
342
|
|
|
343
|
+
## Detecting heals (reporting & CI)
|
|
344
|
+
|
|
345
|
+
A heal is silent by default — the test still passes. To surface it, pass an `onHeal(event)` callback;
|
|
346
|
+
elementus calls it **once per heal** with `{ description, selector, method, framework, resolved, suggestion }`. The library only
|
|
347
|
+
*emits* the event — turning it into a report annotation or a CI failure is framework-specific (a few lines
|
|
348
|
+
on your side). It fires when the AI resolves a replacement element, so it signals "the selector drifted and
|
|
349
|
+
AI stepped in", not "the action ultimately passed".
|
|
350
|
+
|
|
351
|
+
**Playwright** — annotate the test (shows in the Playwright HTML report), and optionally fail
|
|
352
|
+
CI on heal:
|
|
353
|
+
|
|
354
|
+
```javascript
|
|
355
|
+
// fixtures.js
|
|
356
|
+
const { test: base } = require('@playwright/test')
|
|
357
|
+
const { createElementus } = require('elementus-ai')
|
|
358
|
+
|
|
359
|
+
const el = createElementus({
|
|
360
|
+
provider: 'gemini',
|
|
361
|
+
geminiApiKey: process.env.GEMINI_API_KEY,
|
|
362
|
+
onHeal: (e) => {
|
|
363
|
+
// e.suggestion is a ready-to-use, escaped, framework-native replacement locator — use it verbatim.
|
|
364
|
+
const line = `${e.selector} → ${e.suggestion ?? '(located visually — no DOM suggestion)'}`
|
|
365
|
+
try {
|
|
366
|
+
const info = base.info()
|
|
367
|
+
info.annotations.push({ type: 'healed', description: line }) // Playwright HTML report
|
|
368
|
+
void info.attach('healed', { body: line, contentType: 'text/plain' }) // Allure + custom reporters
|
|
369
|
+
} catch {} // ignored outside a running test
|
|
370
|
+
},
|
|
371
|
+
})
|
|
372
|
+
|
|
373
|
+
const test = base.extend({
|
|
374
|
+
page: async ({ page }, use) => { await use(el.wrapPage(page)) },
|
|
375
|
+
// Optional: fail CI when a selector only passed via self-healing (drift you should fix).
|
|
376
|
+
failOnHeal: [async ({}, use, testInfo) => {
|
|
377
|
+
await use()
|
|
378
|
+
const heals = testInfo.annotations.filter(a => a.type === 'healed')
|
|
379
|
+
if (process.env.ELEMENTUS_FAIL_ON_HEAL && heals.length) {
|
|
380
|
+
throw new Error('Passed only via self-healing — fix the selector(s):\n ' + heals.map(h => h.description).join('\n '))
|
|
381
|
+
}
|
|
382
|
+
}, { auto: true }],
|
|
383
|
+
})
|
|
384
|
+
module.exports = { test, expect: require('@playwright/test').expect }
|
|
385
|
+
```
|
|
386
|
+
|
|
387
|
+
(TypeScript: import the `HealEvent` type from `elementus-ai`; the annotation/fixture code is identical.)
|
|
388
|
+
|
|
389
|
+
**WebdriverIO / Appium** — collect in `onHeal`, fail (or report) in `afterTest`:
|
|
390
|
+
|
|
391
|
+
```javascript
|
|
392
|
+
// wdio.conf.js
|
|
393
|
+
const { createElementus } = require('elementus-ai')
|
|
394
|
+
let heals = []
|
|
395
|
+
const el = createElementus({ provider: 'gemini', geminiApiKey: '...', onHeal: (e) => heals.push(e) })
|
|
396
|
+
|
|
397
|
+
exports.config = {
|
|
398
|
+
beforeTest() { heals = [] },
|
|
399
|
+
afterTest() {
|
|
400
|
+
if (process.env.ELEMENTUS_FAIL_ON_HEAL && heals.length) {
|
|
401
|
+
throw new Error('healed: ' + heals.map(h => h.description).join(', '))
|
|
402
|
+
}
|
|
403
|
+
},
|
|
404
|
+
}
|
|
405
|
+
```
|
|
406
|
+
|
|
407
|
+
**Where each report shows it:** the annotation appears in the **Playwright HTML report** (test → _Annotations_).
|
|
408
|
+
In **Allure**, the heal shows in the test's **step tree** (the broken locator → the healed `[data-elementus=…]`
|
|
409
|
+
element) and, with fail-on-heal on, the **failure message** — note `allure-playwright` does not convert
|
|
410
|
+
runtime annotations into labels/steps, so for a dedicated Allure marker also
|
|
411
|
+
`testInfo.attach('healed', { body: \`${e.selector} → ${e.description}\`, contentType: 'text/plain' })`
|
|
412
|
+
(attachments surface in Allure and most custom reporters). A custom HTML reporter surfaces the heal via the
|
|
413
|
+
failure message and captured stdout.
|
|
414
|
+
|
|
326
415
|
## Security Notes
|
|
327
416
|
|
|
328
417
|
- **Debug screenshots** capture the full page — including any sensitive data visible on it. Keep `debugDir` out of version control.
|
package/elementus.js
CHANGED
|
@@ -377,6 +377,7 @@ const REGION_LABELS = [
|
|
|
377
377
|
* @param {number} [userConfig.visionMaxWidth=1280] - max screenshot width (px) sent to vision LLM
|
|
378
378
|
* @param {string|null} [userConfig.cacheFile=null] - opt-in fingerprint cache file (e.g. './elementus-cache.json')
|
|
379
379
|
* @param {string|null} [userConfig.embeddingModel=null] - opt-in embedding model for semantic matching
|
|
380
|
+
* @param {(event: { description: string, selector: string, method: string, framework: string, resolved?: { tag: string, text: string, role: string|null }, suggestion?: string }) => void} [userConfig.onHeal] - called after a heal resolves a replacement element (original selector failed, AI found it). `resolved` is the element it healed to; `suggestion` is a ready-to-use, framework-native replacement locator (use it verbatim). Best-effort; throwing never breaks a test.
|
|
380
381
|
* @returns {{ wrap, wrapPage, wrapBrowser, locate, find, click }}
|
|
381
382
|
*/
|
|
382
383
|
function createElementus(userConfig = {}) {
|
|
@@ -1988,6 +1989,64 @@ function createElementus(userConfig = {}) {
|
|
|
1988
1989
|
return { record: null, somCandidates: out.somCandidates || null }
|
|
1989
1990
|
}
|
|
1990
1991
|
|
|
1992
|
+
// Facts about the element the last heal resolved to (tag/text/role) — set during resolution,
|
|
1993
|
+
// read once by _notifyHeal. Framework-agnostic; consumers format a suggested replacement locator.
|
|
1994
|
+
let _lastResolved = null
|
|
1995
|
+
function _resolvedFacts(record) {
|
|
1996
|
+
if (!record) return null
|
|
1997
|
+
return { tag: record.tag || '', text: (record.text || '').trim(), role: record.role || null }
|
|
1998
|
+
}
|
|
1999
|
+
|
|
2000
|
+
// Implicit ARIA roles for the common interactive tags (used when the element has no explicit
|
|
2001
|
+
// role attribute). `input` is deliberately omitted — its role depends on its type.
|
|
2002
|
+
const ROLE_BY_TAG = { a: 'link', button: 'button', select: 'combobox', textarea: 'textbox' }
|
|
2003
|
+
// Escape a value for a single-quoted string literal in the suggested locator.
|
|
2004
|
+
function _escSingle(s) {
|
|
2005
|
+
return String(s).replace(/\\/g, '\\\\').replace(/'/g, "\\'")
|
|
2006
|
+
}
|
|
2007
|
+
// Collapse whitespace and cap length so the suggested locator stays one readable line.
|
|
2008
|
+
function _suggestName(s) {
|
|
2009
|
+
return String(s || '').replace(/\s+/g, ' ').trim().slice(0, 80)
|
|
2010
|
+
}
|
|
2011
|
+
// Build a ready-to-use, framework-native, properly-escaped suggested replacement locator from
|
|
2012
|
+
// the resolved element. Consumers should surface event.suggestion verbatim — do NOT hand-roll it
|
|
2013
|
+
// (raw text interpolation re-introduces quote-escaping and role-mapping bugs).
|
|
2014
|
+
function _suggestLocator(framework, resolved) {
|
|
2015
|
+
if (!resolved) return undefined
|
|
2016
|
+
const tag = resolved.tag || ''
|
|
2017
|
+
const name = _suggestName(resolved.text)
|
|
2018
|
+
const role = resolved.role || ROLE_BY_TAG[tag] || null
|
|
2019
|
+
if (framework === 'playwright') {
|
|
2020
|
+
if (role && name) return `getByRole('${_escSingle(role)}', { name: '${_escSingle(name)}' })`
|
|
2021
|
+
if (name) return `getByText('${_escSingle(name)}')`
|
|
2022
|
+
return `locator('${_escSingle(tag || '*')}')`
|
|
2023
|
+
}
|
|
2024
|
+
if (framework === 'appium') {
|
|
2025
|
+
return name ? `$('~${_escSingle(name)}')` : `$('${_escSingle(tag || '*')}')`
|
|
2026
|
+
}
|
|
2027
|
+
return name ? `$('aria/${_escSingle(name)}')` : `$('${_escSingle(tag || '*')}')`
|
|
2028
|
+
}
|
|
2029
|
+
|
|
2030
|
+
// Fire the user's onHeal(event) after a real heal — the original selector failed and the AI
|
|
2031
|
+
// pipeline resolved a replacement. Best-effort and fully isolated: a throwing or rejecting
|
|
2032
|
+
// callback must never break a test. No-op with zero overhead when onHeal is not configured.
|
|
2033
|
+
function _notifyHeal(ctx, description, selectorKey, method) {
|
|
2034
|
+
const resolved = _lastResolved
|
|
2035
|
+
_lastResolved = null
|
|
2036
|
+
const cb = config.onHeal
|
|
2037
|
+
if (typeof cb !== 'function') return
|
|
2038
|
+
const framework = _isNative(ctx) ? 'appium' : _isPlaywright(ctx) ? 'playwright' : 'wdio'
|
|
2039
|
+
try {
|
|
2040
|
+
const event = { description, selector: selectorKey, method, framework }
|
|
2041
|
+
if (resolved) {
|
|
2042
|
+
event.resolved = resolved
|
|
2043
|
+
event.suggestion = _suggestLocator(framework, resolved)
|
|
2044
|
+
}
|
|
2045
|
+
const r = cb(event)
|
|
2046
|
+
if (r && typeof r.then === 'function') r.then(undefined, () => {})
|
|
2047
|
+
} catch {}
|
|
2048
|
+
}
|
|
2049
|
+
|
|
1991
2050
|
async function _findByDescription(ctx, description, selectorKey = '') {
|
|
1992
2051
|
const { record, somCandidates } = await _resolveElement(ctx, description, selectorKey)
|
|
1993
2052
|
if (record) {
|
|
@@ -1995,6 +2054,7 @@ function createElementus(userConfig = {}) {
|
|
|
1995
2054
|
const mark = {}
|
|
1996
2055
|
const locator = record._locator || await markByElement(ctx, record, mark)
|
|
1997
2056
|
await _cacheStore(ctx, description, selectorKey, record, record._uid || mark.uid || null)
|
|
2057
|
+
_lastResolved = _resolvedFacts(record)
|
|
1998
2058
|
return locator
|
|
1999
2059
|
} catch (err) {
|
|
2000
2060
|
console.log(`[Resolve] Mark failed (${err.message}) — trying vision`)
|
|
@@ -2006,11 +2066,13 @@ function createElementus(userConfig = {}) {
|
|
|
2006
2066
|
const mark = {}
|
|
2007
2067
|
const locator = await markByElement(ctx, result.element, mark)
|
|
2008
2068
|
await _cacheStore(ctx, description, selectorKey, result.element, mark.uid || null)
|
|
2069
|
+
_lastResolved = _resolvedFacts(result.element)
|
|
2009
2070
|
return locator
|
|
2010
2071
|
}
|
|
2011
2072
|
const mark = {}
|
|
2012
2073
|
const locator = await markAtCoordinates(ctx, result.coords.docX, result.coords.docY, mark)
|
|
2013
2074
|
await _cacheStore(ctx, description, selectorKey, result.coords, mark.uid || null)
|
|
2075
|
+
_lastResolved = null
|
|
2014
2076
|
return locator
|
|
2015
2077
|
} catch (err) {
|
|
2016
2078
|
throw new Error(`All fallback paths exhausted for "${description}": ${err.message}`)
|
|
@@ -2038,7 +2100,10 @@ function createElementus(userConfig = {}) {
|
|
|
2038
2100
|
} catch {
|
|
2039
2101
|
console.log(`\u2717 Locator failed \u2014 searching for: "${description}"`)
|
|
2040
2102
|
}
|
|
2041
|
-
|
|
2103
|
+
const selectorKey = _selectorKey(locator)
|
|
2104
|
+
const resolved = await _findByDescription(ctx, description, selectorKey)
|
|
2105
|
+
_notifyHeal(ctx, description, selectorKey, 'locate')
|
|
2106
|
+
return resolved
|
|
2042
2107
|
}
|
|
2043
2108
|
|
|
2044
2109
|
/**
|
|
@@ -2088,6 +2153,8 @@ function createElementus(userConfig = {}) {
|
|
|
2088
2153
|
// Store before clicking \u2014 the click may navigate away from the page
|
|
2089
2154
|
await _cacheStore(ctx, description, selectorKey, record)
|
|
2090
2155
|
await scrollAndClick(ctx, record)
|
|
2156
|
+
_lastResolved = _resolvedFacts(record)
|
|
2157
|
+
_notifyHeal(ctx, description, selectorKey, 'click')
|
|
2091
2158
|
return
|
|
2092
2159
|
}
|
|
2093
2160
|
try {
|
|
@@ -2095,9 +2162,13 @@ function createElementus(userConfig = {}) {
|
|
|
2095
2162
|
if (result.element) {
|
|
2096
2163
|
await _cacheStore(ctx, description, selectorKey, result.element)
|
|
2097
2164
|
await scrollAndClick(ctx, result.element)
|
|
2165
|
+
_lastResolved = _resolvedFacts(result.element)
|
|
2166
|
+
_notifyHeal(ctx, description, selectorKey, 'click')
|
|
2098
2167
|
return
|
|
2099
2168
|
}
|
|
2100
2169
|
await clickAtCoords(ctx, result.coords)
|
|
2170
|
+
_lastResolved = null
|
|
2171
|
+
_notifyHeal(ctx, description, selectorKey, 'click')
|
|
2101
2172
|
} catch (err) {
|
|
2102
2173
|
throw new Error(`All fallback paths exhausted for "${description}": ${err.message}`)
|
|
2103
2174
|
}
|
|
@@ -2160,6 +2231,7 @@ function createElementus(userConfig = {}) {
|
|
|
2160
2231
|
if (!_resolved) {
|
|
2161
2232
|
console.log(`[AI] ${prop}() \u2014 resolving via AI first for "${description}"`)
|
|
2162
2233
|
_resolved = await _findByDescription(driverContext, description, wrapSelectorKey)
|
|
2234
|
+
_notifyHeal(driverContext, description, wrapSelectorKey, String(prop))
|
|
2163
2235
|
}
|
|
2164
2236
|
return _resolved[prop](...args)
|
|
2165
2237
|
}
|
|
@@ -2168,7 +2240,10 @@ function createElementus(userConfig = {}) {
|
|
|
2168
2240
|
return await original.apply(target, args)
|
|
2169
2241
|
} catch (firstError) {
|
|
2170
2242
|
console.log(`[AI] ${String(prop)}() failed \u2014 AI fallback for "${description}"`)
|
|
2171
|
-
if (!_resolved)
|
|
2243
|
+
if (!_resolved) {
|
|
2244
|
+
_resolved = await _findByDescription(driverContext, description, wrapSelectorKey)
|
|
2245
|
+
_notifyHeal(driverContext, description, wrapSelectorKey, String(prop))
|
|
2246
|
+
}
|
|
2172
2247
|
|
|
2173
2248
|
const resolvedMethod = _resolved[prop]
|
|
2174
2249
|
if (typeof resolvedMethod !== 'function') {
|
package/index.d.ts
CHANGED
|
@@ -36,6 +36,33 @@ export interface ElementusOptions {
|
|
|
36
36
|
cacheFile?: string | null
|
|
37
37
|
/** Opt-in embedding model for semantic paraphrase matching. @default null */
|
|
38
38
|
embeddingModel?: string | null
|
|
39
|
+
/**
|
|
40
|
+
* Called after a heal resolves a replacement element (the original selector failed and the AI
|
|
41
|
+
* pipeline found a match). Best-effort and isolated — a throwing/rejecting callback never breaks
|
|
42
|
+
* a test. Fires at resolution time, so it does NOT guarantee the subsequent action succeeded.
|
|
43
|
+
*/
|
|
44
|
+
onHeal?: (event: HealEvent) => void | Promise<void>
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Event passed to `onHeal` when a broken selector is healed. */
|
|
48
|
+
export interface HealEvent {
|
|
49
|
+
/** The natural-language `{ ai }` description that resolved the element. */
|
|
50
|
+
description: string
|
|
51
|
+
/** The original selector that failed (selector key). */
|
|
52
|
+
selector: string
|
|
53
|
+
/** The action that triggered the heal: 'click', 'fill', 'isVisible', 'locate', etc. */
|
|
54
|
+
method: string
|
|
55
|
+
/** Which driver the heal happened on. */
|
|
56
|
+
framework: 'playwright' | 'wdio' | 'appium'
|
|
57
|
+
/** The element elementus resolved to (when known — absent on coordinate-only vision heals). */
|
|
58
|
+
resolved?: { tag: string; text: string; role: string | null }
|
|
59
|
+
/**
|
|
60
|
+
* Ready-to-use, framework-native, properly-escaped suggested replacement locator for the failed
|
|
61
|
+
* selector (e.g. `getByRole('link', { name: 'Save' })` on Playwright, `$('aria/Save')` on WDIO).
|
|
62
|
+
* Surface it verbatim — don't hand-build one from `resolved` (that re-introduces quote-escaping
|
|
63
|
+
* and role-mapping bugs). Absent on coordinate-only vision heals.
|
|
64
|
+
*/
|
|
65
|
+
suggestion?: string
|
|
39
66
|
}
|
|
40
67
|
|
|
41
68
|
/**
|
package/package.json
CHANGED