@olomonhelpdesk2/helpdesk 0.1.17 → 0.1.18

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.
Files changed (2) hide show
  1. package/helpdesk-capture.mjs +110 -50
  2. package/package.json +1 -1
@@ -248,6 +248,11 @@ function helpdeskCommand(command, args = []) {
248
248
 
249
249
  // core/capture/classify.ts
250
250
  var THIN_TEXT_CHARS = 25;
251
+ var ERROR_PAGE_PHRASES = [
252
+ /\bapplication error: a (server|client)-side exception has occurred\b/i,
253
+ /\bthis screen did not load\b/i,
254
+ /\bdigest\s*:?\s*[\w-]{6,}\s+copy reference\b/i
255
+ ];
251
256
  var BOUNDARY_PHRASES = [
252
257
  /\bsomething went wrong\b/i,
253
258
  /\bapplication error\b/i,
@@ -256,8 +261,12 @@ var BOUNDARY_PHRASES = [
256
261
  /\binternal server error\b/i,
257
262
  /\bthis page (could not|couldn'?t) be loaded\b/i,
258
263
  /\bwe('| a)re sorry[ —,-]+something\b/i,
259
- /\berror (id|digest)\b/i
264
+ /\berror (id|digest)\b/i,
265
+ ...ERROR_PAGE_PHRASES
260
266
  ];
267
+ function showsErrorPage(text2) {
268
+ return ERROR_PAGE_PHRASES.some((phrase) => phrase.test(text2));
269
+ }
261
270
  var NOT_FOUND_PHRASES = [
262
271
  /\b(page|it) (was )?not found\b/i,
263
272
  /\b404\b/,
@@ -1010,6 +1019,7 @@ async function readPaintedMarkup(page, regions) {
1010
1019
  const style = window.getComputedStyle(element);
1011
1020
  if (style.display === "none" || style.visibility === "hidden" || style.visibility === "collapse") return false;
1012
1021
  if (style.display === "contents") return true;
1022
+ if (typeof element.checkVisibility === "function" && !element.checkVisibility()) return false;
1013
1023
  for (const box of Array.from(element.getClientRects())) {
1014
1024
  if (box.width > 0 && box.height > 0) return true;
1015
1025
  }
@@ -1090,6 +1100,7 @@ async function readPageLayout(page, cover = []) {
1090
1100
  };
1091
1101
  const unseen = (element) => {
1092
1102
  if (element.closest('[aria-hidden="true"], [inert]')) return true;
1103
+ if (typeof element.checkVisibility === "function" && !element.checkVisibility()) return true;
1093
1104
  for (let node = element; node; node = node.parentElement) {
1094
1105
  const style = window.getComputedStyle(node);
1095
1106
  if (style.display === "none" || style.visibility === "hidden" || style.opacity === "0") return true;
@@ -1572,11 +1583,17 @@ function readStep(value, at) {
1572
1583
  if (action !== "auto" && !typed && given !== void 0) return `step ${at} gives a value to ${action}, which takes none`;
1573
1584
  if (action === "auto" && given !== void 0 && !valued) return `step ${at} has a value nothing can type`;
1574
1585
  if (step["follows"] !== void 0 && step["follows"] !== true) return `step ${at} says neither that it follows the step before it nor that it does not`;
1586
+ const demo = step["demo"] === void 0 ? void 0 : cleanText(step["demo"]);
1587
+ const opens = step["opens"] === void 0 ? void 0 : cleanText(step["opens"]);
1588
+ if (demo !== void 0 && (!demo || demo.length > MAX_TEXT2 || action !== "auto" && action !== "fill")) return `step ${at} has a demo value nothing types`;
1589
+ if (opens !== void 0 && (!isPathLike(opens) || !fillsPattern(route, opens.split("?")[0]))) return `step ${at} opens at ${opens || "nowhere"}, which is not ${route}`;
1575
1590
  return {
1576
1591
  route,
1577
1592
  control: { ...role === void 0 ? {} : { role }, name, ...scope ? { scope } : {} },
1578
1593
  action,
1579
1594
  ...valued && (typed || action === "auto") ? { value: given } : {},
1595
+ ...demo ? { demo } : {},
1596
+ ...opens ? { opens } : {},
1580
1597
  says,
1581
1598
  ...step["follows"] === true ? { follows: true } : {}
1582
1599
  };
@@ -1677,7 +1694,7 @@ function inChrome(manifest, control) {
1677
1694
  }
1678
1695
  return false;
1679
1696
  }
1680
- function resolveStep(manifest, step) {
1697
+ function resolveStep(manifest, step, options = {}) {
1681
1698
  const name = comparable(step.control.name);
1682
1699
  const where = manifest.screen.path;
1683
1700
  const role = step.control.role;
@@ -1691,16 +1708,18 @@ function resolveStep(manifest, step) {
1691
1708
  const scoped = matches.filter((control) => within(manifest, control, regions));
1692
1709
  if (scoped.length) matches = scoped;
1693
1710
  }
1694
- if (!role && matches.length > 1) {
1695
- const chrome = IN_CHROME.test(step.says);
1696
- const meant = matches.filter((control) => inChrome(manifest, control) === chrome);
1697
- if (meant.length === 1) matches = meant;
1698
- }
1699
- if (matches.length === 0) return { ok: false, because: `no ${what} "${step.control.name}" on ${where}` };
1711
+ const narrow = (keep) => {
1712
+ const kept = matches.filter(keep);
1713
+ if (kept.length === 1) matches = kept;
1714
+ };
1715
+ if (!role && matches.length > 1 && options.after) narrow((control) => within(manifest, control, /* @__PURE__ */ new Set([options.after])));
1716
+ if (!role && matches.length > 1) narrow((control) => inChrome(manifest, control) === IN_CHROME.test(step.says));
1717
+ if (!role && matches.length > 1) narrow((control) => !control.state?.disabled);
1718
+ if (matches.length === 0) return { ok: false, kind: "missing", because: `no ${what} "${step.control.name}" on ${where}` };
1700
1719
  if (matches.length > 1) {
1701
- return { ok: false, because: `${matches.length} of ${what} "${step.control.name}" on ${where}${scope ? ` in ${scope}` : ""}, so the step does not say which` };
1720
+ return { ok: false, kind: "ambiguous", because: `${matches.length} of ${what} "${step.control.name}" on ${where}${scope ? ` in ${scope}` : ""}, so the step does not say which` };
1702
1721
  }
1703
- if (matches[0].state?.disabled) return { ok: false, because: `${what} "${step.control.name}" on ${where} is disabled` };
1722
+ if (matches[0].state?.disabled) return { ok: false, kind: "disabled", because: `${what} "${step.control.name}" on ${where} is disabled` };
1704
1723
  return { ok: true, control: matches[0] };
1705
1724
  }
1706
1725
  function onScreen(route, location2) {
@@ -1715,6 +1734,7 @@ function clipFingerprint(script, touched) {
1715
1734
  role: step.control.role ?? null,
1716
1735
  name: step.control.name,
1717
1736
  ...step.control.scope ? { scope: step.control.scope } : {},
1737
+ ...step.demo ? { demo: step.demo } : {},
1718
1738
  action: step.action,
1719
1739
  value: step.value ?? null,
1720
1740
  says: step.says
@@ -3195,7 +3215,14 @@ async function playwrightBrowser(options = {}) {
3195
3215
  return;
3196
3216
  }
3197
3217
  await page.mouse.click(action.at.x, action.at.y);
3198
- if (action.kind === "focus") await page.keyboard.press("ControlOrMeta+A");
3218
+ if (action.kind !== "focus") return;
3219
+ const field = await page.evaluate(() => {
3220
+ const focused = document.activeElement;
3221
+ const kind = focused instanceof HTMLInputElement ? focused.type : "";
3222
+ return Boolean(focused && (focused.isContentEditable || focused instanceof HTMLTextAreaElement || focused instanceof HTMLInputElement && !["button", "checkbox", "color", "file", "image", "radio", "range", "reset", "submit"].includes(kind)));
3223
+ });
3224
+ if (!field) throw new Error(`the press at (${action.at.x}, ${action.at.y}) focused no field, so nothing could be typed`);
3225
+ await page.keyboard.press("ControlOrMeta+A");
3199
3226
  },
3200
3227
  async type(text2) {
3201
3228
  await page.keyboard.type(text2);
@@ -3476,20 +3503,22 @@ async function recordClip(options, script, touched) {
3476
3503
  let last = new Uint8Array();
3477
3504
  const judge = async (where) => {
3478
3505
  const route = where.split("?")[0];
3479
- const verdict = gateFrame(await page.text(), {
3506
+ const text2 = await page.text();
3507
+ if (showsErrorPage(text2)) return fail(`${route} answered with its error page, not the screen`);
3508
+ const verdict = gateFrame(text2, {
3480
3509
  route,
3481
3510
  ...options.brands ? { brands: options.brands } : {},
3482
3511
  ...options.clearance?.expectedBrands?.length ? { expected: options.clearance.expectedBrands } : {}
3483
3512
  });
3484
- if (!verdict.safe) return verdict.reason ?? `the gate refused ${route}`;
3513
+ if (!verdict.safe) return refuse(verdict.reason ?? `the gate refused ${route}`);
3485
3514
  if (!options.clearance) return null;
3486
3515
  const clearance = options.clearance;
3487
3516
  const reading = await readScreen(page, clearance);
3488
- if (!reading) return "this browser cannot read a screen against a clearance";
3489
- if (reading.inventory.unaccounted.length) return `${reading.inventory.unaccounted.length} value(s) on ${route} are outside every region the clearance names`;
3517
+ if (!reading) return refuse("this browser cannot read a screen against a clearance");
3518
+ if (reading.inventory.unaccounted.length) return refuse(`${reading.inventory.unaccounted.length} value(s) on ${route} are outside every region the clearance names`);
3490
3519
  const approved = new Set(clearance.approved);
3491
3520
  const loose = reading.surfaces.filter((surface) => !surface.read && !clearance.covers.includes(surface.where) && !approved.has(surface.where));
3492
- if (loose.length && !clearance.coverUnnamed) return `${loose.length} surface(s) on ${route} are neither approved nor covered`;
3521
+ if (loose.length && !clearance.coverUnnamed) return refuse(`${loose.length} surface(s) on ${route} are neither approved nor covered`);
3493
3522
  cover = reading.surfaces.filter((surface) => !surface.read && !approved.has(surface.where)).map((surface) => surface.where);
3494
3523
  return null;
3495
3524
  };
@@ -3516,6 +3545,8 @@ async function recordClip(options, script, touched) {
3516
3545
  return true;
3517
3546
  };
3518
3547
  const pause = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
3548
+ let redirected;
3549
+ const isOn = (route, where) => onScreen(route, where) || redirected?.route === route && where.split("?")[0] === redirected.path;
3519
3550
  const manifestOf = (raw, step, where) => buildLayoutManifest(raw, {
3520
3551
  shotId: script.id,
3521
3552
  route: step.route,
@@ -3534,12 +3565,13 @@ async function recordClip(options, script, touched) {
3534
3565
  }
3535
3566
  };
3536
3567
  let resting = START;
3568
+ let lastRegion;
3537
3569
  const find = async (step, where, filming) => {
3538
3570
  const raw = await layout(cover);
3539
- const shown = resolveStep(manifestOf(raw, step, where), step);
3571
+ const shown = resolveStep(manifestOf(raw, step, where), step, { after: lastRegion });
3540
3572
  if (shown.ok) return shown;
3541
- const whole = resolveStep(manifestOf(wholePage(raw), step, where), step);
3542
- if (!whole.ok) return shown;
3573
+ const whole = resolveStep(manifestOf(wholePage(raw), step, where), step, { after: lastRegion });
3574
+ if (!whole.ok) return whole.kind === "missing" ? shown : whole;
3543
3575
  const box = whole.control.box;
3544
3576
  const by = Math.round(box.y - WHOLE_SHIFT + box.height / 2 - CLIP_VIEWPORT.height * SCROLL_TO);
3545
3577
  const wheel = { x: Math.round(Math.min(Math.max(box.x + box.width / 2, 1), CLIP_VIEWPORT.width - 1)), y: Math.round(CLIP_VIEWPORT.height / 2) };
@@ -3560,15 +3592,20 @@ async function recordClip(options, script, touched) {
3560
3592
  } else {
3561
3593
  await steady();
3562
3594
  }
3563
- return resolveStep(manifestOf(await layout(cover), step, where), step);
3595
+ return resolveStep(manifestOf(await layout(cover), step, where), step, { after: lastRegion });
3564
3596
  };
3565
- const reach = async (route) => {
3566
- let where = await location2();
3567
- for (let waited = 0; !onScreen(route, where) && waited < STEP_WAIT_MS; waited += FRAME_MS) {
3597
+ const open = async (route, opens, where) => {
3598
+ if (!opens || isOn(route, where)) return where;
3599
+ await page.goto(new URL(opens, options.baseUrl).toString());
3600
+ return location2();
3601
+ };
3602
+ const reach = async (route, opens) => {
3603
+ let where = await open(route, opens, await location2());
3604
+ for (let waited = 0; !isOn(route, where) && waited < STEP_WAIT_MS; waited += FRAME_MS) {
3568
3605
  await pause(FRAME_MS);
3569
3606
  where = await location2();
3570
3607
  }
3571
- if (onScreen(route, where)) {
3608
+ if (isOn(route, where)) {
3572
3609
  await page.quiet?.(SETTLE_MS);
3573
3610
  await steady();
3574
3611
  }
@@ -3579,22 +3616,29 @@ async function recordClip(options, script, touched) {
3579
3616
  await steady();
3580
3617
  return location2();
3581
3618
  };
3582
- const performedOn = (step, where) => onScreen(step.route, where) ? step.route : [...script.before ?? [], ...script.steps].map((each) => each.route).find((route) => onScreen(route, where)) ?? where.split("?")[0];
3619
+ const focus = (at2) => pointer({ kind: "focus", at: at2 }).then(
3620
+ () => null,
3621
+ (error) => error instanceof Error ? error.message : String(error)
3622
+ );
3623
+ const performedOn = (step, where) => isOn(step.route, where) ? step.route : [...script.before ?? [], ...script.steps].map((each) => each.route).find((route) => isOn(route, where)) ?? where.split("?")[0];
3583
3624
  const lead = async (step) => {
3584
- const where = step.follows ? await settled() : await reach(step.route);
3585
- if (!step.follows && !onScreen(step.route, where)) return `the browser is on ${where}, not ${step.route}`;
3625
+ const where = step.follows ? await settled() : await reach(step.route, step.opens);
3626
+ if (!step.follows && !isOn(step.route, where)) return `the browser is on ${where}, not ${step.route}`;
3586
3627
  let resolution = await find(step, where, false);
3587
3628
  for (let waited = 0; !resolution.ok && waited < STEP_WAIT_MS; waited += FRAME_MS) {
3588
3629
  await pause(FRAME_MS);
3589
3630
  resolution = await find(step, where, false);
3590
3631
  }
3591
3632
  if (!resolution.ok) return resolution.because;
3633
+ lastRegion = resolution.control.region;
3592
3634
  const target = centreOf(resolution.control.box);
3593
3635
  const action = step.action === "auto" ? actionForRole(resolution.control.role, step.value) : step.action;
3636
+ const typed = step.value ?? step.demo;
3594
3637
  if (action === "fill") {
3595
- if (!step.value) return `it types into ${resolution.control.name}, and says nothing to type`;
3596
- await pointer({ kind: "focus", at: target });
3597
- await type(step.value);
3638
+ if (!typed) return `it types into ${resolution.control.name}, and says nothing to type`;
3639
+ const unfocused = await focus(target);
3640
+ if (unfocused) return unfocused;
3641
+ await type(typed);
3598
3642
  } else if (action === "select") {
3599
3643
  await pointer({ kind: "select", at: target, value: step.value ?? "" });
3600
3644
  } else {
@@ -3608,26 +3652,29 @@ async function recordClip(options, script, touched) {
3608
3652
  if (await page.isSignIn()) return refuse("its first screen asked for a sign-in, so this session is not the demo tenant's");
3609
3653
  const opened = await location2();
3610
3654
  const performedFirst = script.before?.[0] ?? script.steps[0];
3611
- if (!onScreen(performedFirst.route, opened)) return fail(`it opened on ${opened}, not ${performedFirst.route}`);
3655
+ if (!onScreen(performedFirst.route, opened)) {
3656
+ if (!options.screens?.some((route) => onScreen(route, opened))) return fail(`it opened on ${opened}, not ${performedFirst.route}`);
3657
+ redirected = { route: performedFirst.route, path: opened.split("?")[0] };
3658
+ }
3612
3659
  for (const [index, step] of (script.before ?? []).entries()) {
3613
3660
  const problem = await lead(step);
3614
3661
  if (problem) return fail(`before the recording, step ${index + 1}: ${problem}`);
3615
3662
  }
3616
3663
  const first = script.steps[0];
3617
- const starting = !script.before?.length ? opened : first.follows ? await settled() : await reach(first.route);
3618
- if (!first.follows && !onScreen(first.route, starting)) return fail(`once the steps before it were done, the browser is on ${starting}, not ${first.route}`);
3664
+ const starting = !script.before?.length ? opened : first.follows ? await settled() : await reach(first.route, first.opens);
3665
+ if (!first.follows && !isOn(first.route, starting)) return fail(`once the steps before it were done, the browser is on ${starting}, not ${first.route}`);
3619
3666
  const before = await judge(starting);
3620
- if (before) return refuse(before);
3667
+ if (before) return before;
3621
3668
  let poster;
3622
3669
  const steps = [];
3623
- const arrive = async (route, follows = false) => {
3624
- let where = await location2();
3625
- for (let waited = 0; !follows && !onScreen(route, where) && waited < STEP_WAIT_MS; waited += FRAME_MS) {
3670
+ const arrive = async (route, follows = false, opens) => {
3671
+ let where = follows ? await location2() : await open(route, opens, await location2());
3672
+ for (let waited = 0; !follows && !isOn(route, where) && waited < STEP_WAIT_MS; waited += FRAME_MS) {
3626
3673
  await cursor(resting);
3627
3674
  await film();
3628
3675
  where = await location2();
3629
3676
  }
3630
- if (!follows && !onScreen(route, where)) return where;
3677
+ if (!follows && !isOn(route, where)) return where;
3631
3678
  await page.quiet?.(SETTLE_MS);
3632
3679
  await cursor(resting);
3633
3680
  for (let waited = 0; waited < SETTLE_MS; waited += FRAME_MS) {
@@ -3638,10 +3685,10 @@ async function recordClip(options, script, touched) {
3638
3685
  await cursor(resting);
3639
3686
  checked.add(await shoot(OPENING_MS));
3640
3687
  for (const [index, step] of script.steps.entries()) {
3641
- const where = await arrive(step.route, step.follows === true);
3642
- if (!step.follows && !onScreen(step.route, where)) return fail(`step ${index + 1}: the browser is on ${where}, not ${step.route}`);
3688
+ const where = await arrive(step.route, step.follows === true, step.opens);
3689
+ if (!step.follows && !isOn(step.route, where)) return fail(`step ${index + 1}: the browser is on ${where}, not ${step.route}`);
3643
3690
  const arrived = await judge(where);
3644
- if (arrived) return refuse(arrived);
3691
+ if (arrived) return arrived;
3645
3692
  let resolution = await find(step, where, true);
3646
3693
  for (let waited = 0; !resolution.ok && waited < STEP_WAIT_MS; waited += FRAME_MS) {
3647
3694
  await cursor(resting);
@@ -3649,10 +3696,12 @@ async function recordClip(options, script, touched) {
3649
3696
  resolution = await find(step, where, true);
3650
3697
  }
3651
3698
  if (!resolution.ok) return fail(`step ${index + 1}: ${resolution.because}`);
3699
+ lastRegion = resolution.control.region;
3652
3700
  const target = centreOf(resolution.control.box);
3653
3701
  const startedAt = at;
3654
3702
  const action = step.action === "auto" ? actionForRole(resolution.control.role, step.value) : step.action;
3655
- if (action === "fill" && !step.value) return fail(`step ${index + 1}: it types into ${resolution.control.name}, and says nothing to type`);
3703
+ const typed = step.value ?? step.demo;
3704
+ if (action === "fill" && !typed) return fail(`step ${index + 1}: it types into ${resolution.control.name}, and says nothing to type`);
3656
3705
  for (const point of cursorPath(resting, target, MOVE_FRAMES)) {
3657
3706
  await cursor(point);
3658
3707
  await shoot(FRAME_MS);
@@ -3663,8 +3712,9 @@ async function recordClip(options, script, touched) {
3663
3712
  await cursor(target, true);
3664
3713
  await shoot(PRESS_MS);
3665
3714
  if (action === "fill") {
3666
- const value = step.value ?? "";
3667
- await pointer({ kind: "focus", at: target });
3715
+ const value = typed ?? "";
3716
+ const unfocused = await focus(target);
3717
+ if (unfocused) return fail(`step ${index + 1}: ${unfocused}`);
3668
3718
  await cursor(target, false);
3669
3719
  for (const chunk of chunksOf(value, Math.max(CHARS_PER_FRAME, Math.ceil(value.length / TYPING_FRAMES)))) {
3670
3720
  await type(chunk);
@@ -3685,7 +3735,7 @@ async function recordClip(options, script, touched) {
3685
3735
  checked.add(frames[frames.length - 1].file);
3686
3736
  const now = await location2();
3687
3737
  const after = await judge(now);
3688
- if (after) return refuse(after);
3738
+ if (after) return after;
3689
3739
  steps.push({
3690
3740
  route: performedOn(step, where),
3691
3741
  role: resolution.control.role,
@@ -3698,9 +3748,9 @@ async function recordClip(options, script, touched) {
3698
3748
  }
3699
3749
  if (script.lands) {
3700
3750
  const landed = await arrive(script.lands);
3701
- if (!onScreen(script.lands, landed)) return fail(`the last step left the browser on ${landed}, not ${script.lands}`);
3751
+ if (!isOn(script.lands, landed)) return fail(`the last step left the browser on ${landed}, not ${script.lands}`);
3702
3752
  const seen = await judge(landed);
3703
- if (seen) return refuse(seen);
3753
+ if (seen) return seen;
3704
3754
  }
3705
3755
  hold(CLOSING_MS);
3706
3756
  checked.add(frames[frames.length - 1].file);
@@ -3761,7 +3811,7 @@ async function runClips(options) {
3761
3811
  } else if (!options.force && options.held.get(script.id) === fingerprint) {
3762
3812
  outcome = { ...base, status: "unchanged", reason: "neither its steps nor a screen they touch has changed" };
3763
3813
  } else {
3764
- const result = await recordClip(options, script, touched);
3814
+ const result = await recordClip({ ...options, screens: options.screens ?? [...options.frames.keys()] }, script, touched);
3765
3815
  if (!result.ok) {
3766
3816
  outcome = { ...base, status: result.status, reason: result.because };
3767
3817
  } else {
@@ -4952,11 +5002,21 @@ function articleClipsToRecord(raw, examples) {
4952
5002
  if (!Array.isArray(raw)) return readClipScripts(raw, CLIP_MAX_ARTICLE_SCRIPTS);
4953
5003
  const named = examples ?? { paths: /* @__PURE__ */ new Map(), refused: [] };
4954
5004
  const unopened = [];
5005
+ const recordOf3 = (value) => typeof value === "object" && value !== null && !Array.isArray(value) ? value : {};
5006
+ const withOpens = (steps, from) => Array.isArray(steps) ? steps.map((value, index) => {
5007
+ const step = recordOf3(value);
5008
+ const later = from + index > 0 && step["follows"] !== true;
5009
+ const resolved = later && typeof step["route"] === "string" ? resolveVisit(step["route"], named) : void 0;
5010
+ return resolved && "visit" in resolved && resolved.visit.includes("?") ? { ...step, opens: resolved.visit } : value;
5011
+ }) : steps;
4955
5012
  const opened = raw.map((value) => {
4956
- const script = typeof value === "object" && value !== null && !Array.isArray(value) ? value : {};
5013
+ const script = recordOf3(value);
4957
5014
  const visit = typeof script["visit"] === "string" ? script["visit"] : "";
4958
5015
  const resolved = resolveVisit(visit, named);
4959
- if (!("needs" in resolved)) return { ...script, visit: resolved.visit };
5016
+ const lead = Array.isArray(script["before"]) ? script["before"].length : 0;
5017
+ if (!("needs" in resolved)) {
5018
+ return { ...script, visit: resolved.visit, ...lead ? { before: withOpens(script["before"], 0) } : {}, steps: withOpens(script["steps"], lead) };
5019
+ }
4960
5020
  unopened.push({ id: typeof script["id"] === "string" ? script["id"] : "a clip", because: describeMissingExample(visit, resolved.needs) });
4961
5021
  return void 0;
4962
5022
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@olomonhelpdesk2/helpdesk",
3
- "version": "0.1.17",
3
+ "version": "0.1.18",
4
4
  "description": "Reads a product's screens, plans its help articles, and photographs them.",
5
5
  "type": "module",
6
6
  "engines": {