@ai-digital-factory/lmt-web-cli 0.1.1 → 0.1.3
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/README.md +99 -1
- package/dist/index.js +169 -16
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -5,6 +5,67 @@ browser interaction with the configured local or dev Live Market Terminal Web UR
|
|
|
5
5
|
keeps browser dependencies and session lifetime out of the browser-free `lmt`
|
|
6
6
|
HTTP CLI.
|
|
7
7
|
|
|
8
|
+
## Agent guide (Dev only)
|
|
9
|
+
|
|
10
|
+
Agents working with a deployed Live Market Terminal must use the **Dev** environment.
|
|
11
|
+
Do not target production. The examples here use the published CLI through `npx` and
|
|
12
|
+
open a headless desktop browser by default.
|
|
13
|
+
|
|
14
|
+
Install Chromium once on the machine that will own the browser session:
|
|
15
|
+
|
|
16
|
+
```sh
|
|
17
|
+
npx --yes @ai-digital-factory/lmt-web-cli@latest install-browser
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
If Operator login needs the Gate Operator Directory, provision this CLI's SMS read
|
|
21
|
+
credential once from a secure input source. The Web CLI keeps its credential
|
|
22
|
+
separately from `lmt-cli` and does not use profiles:
|
|
23
|
+
|
|
24
|
+
```sh
|
|
25
|
+
npx --yes @ai-digital-factory/lmt-web-cli@latest credential provision --env dev --credential-stdin
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Start a fresh desktop session against Dev and keep the returned `sessionId` for every
|
|
29
|
+
later command:
|
|
30
|
+
|
|
31
|
+
```sh
|
|
32
|
+
npx --yes @ai-digital-factory/lmt-web-cli@latest start --env dev --device desktop
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
The session starts on the Home Page. Use the same session to log into the Operator;
|
|
36
|
+
if a Zerodha login is required, start that flow and submit the current OTP through
|
|
37
|
+
standard input only, using the `attemptId` returned by `zerodha-login`:
|
|
38
|
+
|
|
39
|
+
```sh
|
|
40
|
+
npx --yes @ai-digital-factory/lmt-web-cli@latest operator-login --session SESSION_ID
|
|
41
|
+
npx --yes @ai-digital-factory/lmt-web-cli@latest zerodha-login --session SESSION_ID
|
|
42
|
+
npx --yes @ai-digital-factory/lmt-web-cli@latest zerodha-otp --session SESSION_ID --attempt ATTEMPT_ID --stdin
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
The user supplies the OTP for the active attempt. Never put it in a command-line
|
|
46
|
+
argument or ask the CLI to retrieve it. Inspect the latest JSON result after each
|
|
47
|
+
command. Control references are short-lived: every new observation or action returns
|
|
48
|
+
fresh references, so use only references from the most recent result.
|
|
49
|
+
|
|
50
|
+
For example, to add an instrument to Market Watch, inspect the page, select `NSE`
|
|
51
|
+
in an empty row, enter the symbol, then apply that row. Use the current row's control
|
|
52
|
+
references from each immediately preceding result. `Apply` configures the Market
|
|
53
|
+
Watch row; it does not submit a trade.
|
|
54
|
+
|
|
55
|
+
Each command returns JSON with the page state and screenshot path. Review the result
|
|
56
|
+
before continuing, especially if an action is `uncertain`; inspect the page instead
|
|
57
|
+
of repeating that action automatically. When finished, stop only the session you
|
|
58
|
+
started:
|
|
59
|
+
|
|
60
|
+
```sh
|
|
61
|
+
npx --yes @ai-digital-factory/lmt-web-cli@latest stop --session SESSION_ID
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Stopping releases the browser process but does not log out the Operator or stop
|
|
65
|
+
Broker activity. The CLI also supports `inspect`, `click`, `fill`, `select`, and
|
|
66
|
+
`navigate`; run `npx --yes @ai-digital-factory/lmt-web-cli@latest --help` for the
|
|
67
|
+
complete command list.
|
|
68
|
+
|
|
8
69
|
## Install
|
|
9
70
|
|
|
10
71
|
Requires Node.js 22 or later, Chromium, and a macOS or Linux session owner.
|
|
@@ -54,6 +115,11 @@ Quitting the browser from its Dock menu or closing its session window also ends
|
|
|
54
115
|
the browser session and its local owner. This does not log out the Operator or
|
|
55
116
|
stop Broker activity. A later `inspect` reports `session_not_found`.
|
|
56
117
|
|
|
118
|
+
On Dev Deployment, Dev Operator opens the Web UI with `?captureSafe=1` on the first
|
|
119
|
+
navigation (for example `https://dev-live-market-terminal.piyush-arora.in/?captureSafe=1`)
|
|
120
|
+
so Capture-safe Mode stays enabled for that tab through navigation and refresh.
|
|
121
|
+
`lmt-web` screenshots still mask access-token value fields automatically without that flag.
|
|
122
|
+
|
|
57
123
|
```sh
|
|
58
124
|
lmt-web start --env dev
|
|
59
125
|
lmt-web inspect --session SESSION_ID
|
|
@@ -80,7 +146,10 @@ Each command writes one versioned JSON result to stdout and diagnostics to
|
|
|
80
146
|
stderr. Results contain the action outcome, current URL, visible page text and
|
|
81
147
|
controls, emulated device and actual viewport, and screenshot outcome. Images
|
|
82
148
|
are written to unique absolute paths in an owner-only temporary directory;
|
|
83
|
-
screenshots
|
|
149
|
+
screenshots solid-mask only Zerodha access-token value fields marked with
|
|
150
|
+
`data-lmt-sensitive="zerodha-access-token"`; other controls and metadata stay visible.
|
|
151
|
+
If protected capture fails, the screenshot outcome is `failed` with no unmasked fallback.
|
|
152
|
+
Field values
|
|
84
153
|
are omitted from JSON observations. Captures wait for document loading, visible
|
|
85
154
|
session-loading or busy indicators, fonts, visible images, and finite animations
|
|
86
155
|
to finish. The page must render text, controls, or visual content; an empty app
|
|
@@ -260,3 +329,32 @@ sets up its own Chromium runtime, verifies packaged runtime modules/configuratio
|
|
|
260
329
|
and reuses all browser acceptance tests. Installed commands and their live owners
|
|
261
330
|
run outside the checkout. Test-only fixture servers provide repeatable responses;
|
|
262
331
|
this check performs no npm publication or live dev login.
|
|
332
|
+
|
|
333
|
+
## Explicit values, equality, scrolling, and full content
|
|
334
|
+
|
|
335
|
+
```sh
|
|
336
|
+
lmt-web read --session ID --ref FIELD
|
|
337
|
+
lmt-web clipboard --session ID
|
|
338
|
+
lmt-web check --session ID --ref FIELD --stdin < /path/to/protected-expectation
|
|
339
|
+
lmt-web clipboard-check --session ID --ref FIELD
|
|
340
|
+
lmt-web scroll --session ID --ref FIELD
|
|
341
|
+
lmt-web scroll --session ID --y 600
|
|
342
|
+
lmt-web inspect --session ID --full-page
|
|
343
|
+
```
|
|
344
|
+
|
|
345
|
+
read returns an ordinary field value. clipboard returns content only when it matches
|
|
346
|
+
a verified ordinary field on the current page; unknown or secret content is refused.
|
|
347
|
+
check compares the field with one stdin-supplied expectation (an optional final newline
|
|
348
|
+
is removed). clipboard-check compares clipboard and field inside the browser session.
|
|
349
|
+
Comparisons return only matches: true or matches: false. An unreadable field/clipboard
|
|
350
|
+
or an expired reference is an error without a matches result. Inspect/action observations
|
|
351
|
+
continue to redact secrets, including revealed fields.
|
|
352
|
+
|
|
353
|
+
Referenced scrolling brings the control into view through its ancestors. Pixel scrolling
|
|
354
|
+
targets the largest scrolling content area. Results include before/after positions and
|
|
355
|
+
moved, already_visible, or at_boundary. Each observation expires older references.
|
|
356
|
+
|
|
357
|
+
Full-page capture temporarily expands inner scrolling content and restores its styles
|
|
358
|
+
and positions afterward. A screenshot with unexpanded clipped content reports failed;
|
|
359
|
+
browser-action and screenshot outcomes remain separate. Screenshots retain the existing
|
|
360
|
+
rendered-page confidentiality behavior: protect their files as you would the visible UI.
|
package/dist/index.js
CHANGED
|
@@ -511,6 +511,13 @@ var ACTIONS = {
|
|
|
511
511
|
return { action: "zerodha-otp", attemptId, otp: await input.readOtp() };
|
|
512
512
|
}
|
|
513
513
|
},
|
|
514
|
+
read: { control: true, flags: [], values: ["--ref"], build: (input) => ({ action: "read", reference: required(input, "--ref") }) },
|
|
515
|
+
clipboard: { control: true, flags: [], values: [], build: () => ({ action: "clipboard" }) },
|
|
516
|
+
check: { control: true, flags: ["--stdin"], values: ["--ref"], build: async (input) => {
|
|
517
|
+
if (!input.flags.has("--stdin")) throw new InvocationError("check requires --stdin.");
|
|
518
|
+
return { action: "check", reference: required(input, "--ref"), expected: await input.readInput() };
|
|
519
|
+
} },
|
|
520
|
+
"clipboard-check": { control: true, flags: [], values: ["--ref"], build: (input) => ({ action: "clipboard-check", reference: required(input, "--ref") }) },
|
|
514
521
|
click: { control: true, flags: [], values: ["--ref"], build: (input) => ({ action: "click", reference: required(input, "--ref") }) },
|
|
515
522
|
fill: {
|
|
516
523
|
control: true,
|
|
@@ -614,6 +621,52 @@ import os from "node:os";
|
|
|
614
621
|
import path4 from "node:path";
|
|
615
622
|
import { chromium as chromium2 } from "playwright";
|
|
616
623
|
|
|
624
|
+
// ../../packages/crawler/src/full-content.ts
|
|
625
|
+
async function captureFullContent(browserPage, options) {
|
|
626
|
+
const page = browserPage;
|
|
627
|
+
if (!page.evaluate) throw new Error("Full-content capture is unavailable.");
|
|
628
|
+
try {
|
|
629
|
+
await page.evaluate(() => {
|
|
630
|
+
const state = [];
|
|
631
|
+
window.__lmtCapture = state;
|
|
632
|
+
const scrolling = Array.from(document.querySelectorAll("body *")).filter((element) => {
|
|
633
|
+
const style = getComputedStyle(element);
|
|
634
|
+
return /(auto|scroll)/.test(style.overflowY) && element.scrollHeight > element.clientHeight;
|
|
635
|
+
});
|
|
636
|
+
const expand = /* @__PURE__ */ new Set();
|
|
637
|
+
for (const element of scrolling) {
|
|
638
|
+
for (let parent = element; parent; parent = parent.parentElement) expand.add(parent);
|
|
639
|
+
}
|
|
640
|
+
for (const element of expand) {
|
|
641
|
+
state.push({ element, style: element.getAttribute("style"), x: element.scrollLeft, y: element.scrollTop });
|
|
642
|
+
element.style.setProperty("height", "auto", "important");
|
|
643
|
+
element.style.setProperty("max-height", "none", "important");
|
|
644
|
+
element.style.setProperty("overflow", "visible", "important");
|
|
645
|
+
element.style.setProperty("flex-shrink", "0", "important");
|
|
646
|
+
element.scrollTop = 0;
|
|
647
|
+
}
|
|
648
|
+
});
|
|
649
|
+
await page.evaluate(() => new Promise((resolve) => requestAnimationFrame(() => requestAnimationFrame(() => resolve()))));
|
|
650
|
+
const complete = await page.evaluate(() => Array.from(document.querySelectorAll("body *")).every((element) => {
|
|
651
|
+
const style = getComputedStyle(element);
|
|
652
|
+
return style.display === "none" || element.clientHeight === 0 || !/(auto|scroll|hidden|clip)/.test(style.overflowY) || element.scrollHeight <= element.clientHeight + 1;
|
|
653
|
+
}));
|
|
654
|
+
if (!complete) throw new Error("Full-content capture could not expand all clipped content.");
|
|
655
|
+
return await page.screenshot({ ...options, fullPage: true });
|
|
656
|
+
} finally {
|
|
657
|
+
await page.evaluate(() => {
|
|
658
|
+
const owner = window;
|
|
659
|
+
for (const { element, style, x, y } of (owner.__lmtCapture ?? []).reverse()) {
|
|
660
|
+
if (style === null) element.removeAttribute("style");
|
|
661
|
+
else element.setAttribute("style", style);
|
|
662
|
+
element.scrollLeft = x;
|
|
663
|
+
element.scrollTop = y;
|
|
664
|
+
}
|
|
665
|
+
delete owner.__lmtCapture;
|
|
666
|
+
}).catch(() => void 0);
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
|
|
617
670
|
// ../../packages/crawler/src/controls.ts
|
|
618
671
|
import { randomBytes } from "node:crypto";
|
|
619
672
|
var ControlError = class extends Error {
|
|
@@ -623,6 +676,7 @@ var ControlError = class extends Error {
|
|
|
623
676
|
}
|
|
624
677
|
code;
|
|
625
678
|
};
|
|
679
|
+
var SENSITIVE_FIELD_PATTERN = "password|credential|secret|token|otp|one.time|username|user.?id|api.?key|pin";
|
|
626
680
|
function controlSignature(node) {
|
|
627
681
|
if (!node.isConnected) return null;
|
|
628
682
|
return JSON.stringify([
|
|
@@ -657,8 +711,8 @@ var PageControls = class {
|
|
|
657
711
|
return result;
|
|
658
712
|
}
|
|
659
713
|
async learnSecrets() {
|
|
660
|
-
const values = await this.page.evaluate(() => {
|
|
661
|
-
const sensitive =
|
|
714
|
+
const values = await this.page.evaluate((pattern) => {
|
|
715
|
+
const sensitive = new RegExp(pattern, "i");
|
|
662
716
|
const found = Array.from(document.querySelectorAll("input, textarea")).filter((element) => sensitive.test([
|
|
663
717
|
element.outerHTML.replace(/value=("[^"]*"|'[^']*')/g, ""),
|
|
664
718
|
...Array.from(element.labels ?? []).map((label) => label.innerText)
|
|
@@ -667,15 +721,16 @@ var PageControls = class {
|
|
|
667
721
|
if (match[1]) found.push(match[1]);
|
|
668
722
|
}
|
|
669
723
|
return found;
|
|
670
|
-
});
|
|
724
|
+
}, SENSITIVE_FIELD_PATTERN);
|
|
671
725
|
this.registerSecrets(...values);
|
|
672
726
|
}
|
|
673
727
|
async screenshotMasks() {
|
|
728
|
+
const stable = this.page.locator('[data-lmt-sensitive="zerodha-access-token"]');
|
|
674
729
|
try {
|
|
675
730
|
await this.learnSecrets();
|
|
676
731
|
} catch (error) {
|
|
677
732
|
if (this.secrets.size) throw error;
|
|
678
|
-
else return [];
|
|
733
|
+
else return [stable];
|
|
679
734
|
}
|
|
680
735
|
await this.page.evaluate((secrets) => {
|
|
681
736
|
document.querySelectorAll("[data-lmt-secret-mask]").forEach((element) => element.removeAttribute("data-lmt-secret-mask"));
|
|
@@ -685,7 +740,7 @@ var PageControls = class {
|
|
|
685
740
|
if (secrets.some((secret) => directText.includes(secret) || (element.textContent ?? "").includes(secret) && !Array.from(element.children).some((child) => (child.textContent ?? "").includes(secret)))) element.setAttribute("data-lmt-secret-mask", "");
|
|
686
741
|
}
|
|
687
742
|
}, [...this.secrets]);
|
|
688
|
-
return [this.page.locator("[data-lmt-secret-mask]")];
|
|
743
|
+
return [stable, this.page.locator("[data-lmt-secret-mask]")];
|
|
689
744
|
}
|
|
690
745
|
async inspect() {
|
|
691
746
|
await this.learnSecrets();
|
|
@@ -735,9 +790,52 @@ var PageControls = class {
|
|
|
735
790
|
async act(command, timeoutMs, onSubmit = () => void 0) {
|
|
736
791
|
this.page.setDefaultTimeout(timeoutMs);
|
|
737
792
|
this.page.setDefaultNavigationTimeout(timeoutMs);
|
|
793
|
+
if (command.action === "read" || command.action === "check" || command.action === "clipboard-check" || command.action === "clipboard") {
|
|
794
|
+
await this.learnSecrets();
|
|
795
|
+
if (command.action === "check") this.registerSecrets(command.expected);
|
|
796
|
+
try {
|
|
797
|
+
const field = command.action === "clipboard" ? void 0 : await this.resolve(command.reference);
|
|
798
|
+
if (command.action === "clipboard-check" || command.action === "clipboard") await this.page.context().grantPermissions(["clipboard-read", "clipboard-write"], { origin: this.origin });
|
|
799
|
+
if (command.action === "clipboard-check") {
|
|
800
|
+
const matches = await field.evaluate(async (node) => {
|
|
801
|
+
if (!(node instanceof HTMLInputElement || node instanceof HTMLTextAreaElement || node instanceof HTMLSelectElement)) throw new Error("Unreadable field");
|
|
802
|
+
return await navigator.clipboard.readText() === node.value;
|
|
803
|
+
});
|
|
804
|
+
return { matches };
|
|
805
|
+
}
|
|
806
|
+
const value = command.action === "clipboard" ? await this.page.evaluate(() => navigator.clipboard.readText()) : await field.evaluate((node) => {
|
|
807
|
+
if (!(node instanceof HTMLInputElement || node instanceof HTMLTextAreaElement || node instanceof HTMLSelectElement)) throw new Error("Unreadable field");
|
|
808
|
+
return node.value;
|
|
809
|
+
});
|
|
810
|
+
if (command.action === "check") {
|
|
811
|
+
this.registerSecrets(value);
|
|
812
|
+
return { matches: value === command.expected };
|
|
813
|
+
}
|
|
814
|
+
const sensitive = field && await field.evaluate((node, pattern) => new RegExp(pattern, "i").test(node.outerHTML.replace(/value=("[^"]*"|'[^']*')/g, "") + Array.from(node.labels ?? []).map((label) => label.innerText).join(" ")), SENSITIVE_FIELD_PATTERN);
|
|
815
|
+
if (command.action === "clipboard") {
|
|
816
|
+
const ordinary = await this.page.evaluate(({ value: value2, pattern }) => {
|
|
817
|
+
const sensitive2 = new RegExp(pattern, "i");
|
|
818
|
+
return Array.from(document.querySelectorAll("input,textarea,select")).some((node) => {
|
|
819
|
+
if (!(node instanceof HTMLInputElement || node instanceof HTMLTextAreaElement || node instanceof HTMLSelectElement)) return false;
|
|
820
|
+
const description = node.outerHTML.replace(/value=("[^"]*"|'[^']*')/g, "") + Array.from(node.labels ?? []).map((label) => label.innerText).join(" ");
|
|
821
|
+
return !sensitive2.test(description) && node.value === value2;
|
|
822
|
+
});
|
|
823
|
+
}, { value, pattern: SENSITIVE_FIELD_PATTERN });
|
|
824
|
+
if (!ordinary) {
|
|
825
|
+
this.registerSecrets(value);
|
|
826
|
+
throw new ControlError("secret_read_refused", "Clipboard content has no verified ordinary field source. Use clipboard-check for secret Copy.");
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
if (sensitive || [...this.secrets].some((secret) => value.includes(secret))) throw new ControlError("secret_read_refused", "Use an equality check for secret values.");
|
|
830
|
+
return { value };
|
|
831
|
+
} catch (error) {
|
|
832
|
+
if (error instanceof ControlError) throw error;
|
|
833
|
+
throw new ControlError("value_unreadable", "The field or clipboard could not be read. No comparison result is available.");
|
|
834
|
+
}
|
|
835
|
+
}
|
|
738
836
|
if (command.action === "fill") {
|
|
739
837
|
const element = await this.resolve(command.reference);
|
|
740
|
-
const sensitive = await element.evaluate((node) =>
|
|
838
|
+
const sensitive = await element.evaluate((node, pattern) => new RegExp(pattern, "i").test(node.outerHTML.replace(/value=("[^"]*"|'[^']*')/g, "") + Array.from(node.labels ?? []).map((label) => label.innerText).join(" ")), SENSITIVE_FIELD_PATTERN);
|
|
741
839
|
if (command.secret || sensitive) this.registerSecrets(command.value);
|
|
742
840
|
await element.fill(command.value, { timeout: timeoutMs });
|
|
743
841
|
return;
|
|
@@ -747,6 +845,7 @@ var PageControls = class {
|
|
|
747
845
|
return;
|
|
748
846
|
}
|
|
749
847
|
if (command.action === "click") {
|
|
848
|
+
await this.page.context().grantPermissions(["clipboard-read", "clipboard-write"], { origin: this.origin });
|
|
750
849
|
const element = await this.resolve(command.reference);
|
|
751
850
|
const deadlineAt = Date.now() + timeoutMs;
|
|
752
851
|
await element.click({ trial: true, timeout: timeoutMs });
|
|
@@ -768,12 +867,33 @@ var PageControls = class {
|
|
|
768
867
|
}
|
|
769
868
|
if (command.action === "scroll") {
|
|
770
869
|
if (!Number.isFinite(command.x) || !Number.isFinite(command.y)) throw new ControlError("invalid_invocation", "Scroll offsets must be finite pixel values.");
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
870
|
+
const element = command.reference ? await this.resolve(command.reference) : void 0;
|
|
871
|
+
const scroll = await this.page.evaluate(({ element: node, offsets }) => {
|
|
872
|
+
const scrollable = (item) => {
|
|
873
|
+
const style = getComputedStyle(item);
|
|
874
|
+
return /(auto|scroll)/.test(style.overflow + style.overflowY + style.overflowX) && (item.scrollHeight > item.clientHeight || item.scrollWidth > item.clientWidth);
|
|
875
|
+
};
|
|
876
|
+
const containers = [];
|
|
877
|
+
if (node) {
|
|
878
|
+
for (let parent = node.parentElement; parent; parent = parent.parentElement) if (scrollable(parent)) containers.push(parent);
|
|
879
|
+
if (document.scrollingElement && !containers.includes(document.scrollingElement)) containers.push(document.scrollingElement);
|
|
880
|
+
} else {
|
|
881
|
+
const candidates = Array.from(document.querySelectorAll("body *")).filter(scrollable);
|
|
882
|
+
const root = document.scrollingElement;
|
|
883
|
+
if (root && !/(hidden|clip)/.test(getComputedStyle(root).overflowY) && (root.scrollHeight > root.clientHeight || root.scrollWidth > root.clientWidth)) candidates.push(root);
|
|
884
|
+
candidates.sort((a, b) => b.clientWidth * b.clientHeight - a.clientWidth * a.clientHeight);
|
|
885
|
+
containers.push(candidates[0] ?? document.scrollingElement);
|
|
886
|
+
}
|
|
887
|
+
const positions = () => containers.map((item) => ({ x: item.scrollLeft, y: item.scrollTop }));
|
|
888
|
+
const before = positions();
|
|
889
|
+
if (node) node.scrollIntoView({ block: "nearest", inline: "nearest", behavior: "instant" });
|
|
890
|
+
else containers[0].scrollBy({ left: offsets.x, top: offsets.y, behavior: "instant" });
|
|
891
|
+
const after = positions();
|
|
892
|
+
const moved = before.some((position, i) => position.x !== after[i].x || position.y !== after[i].y);
|
|
893
|
+
return { outcome: moved ? "moved" : node ? "already_visible" : "at_boundary", before, after };
|
|
894
|
+
}, { element: element ?? null, offsets: command });
|
|
776
895
|
await this.page.evaluate(() => new Promise((resolve) => requestAnimationFrame(() => requestAnimationFrame(() => resolve()))));
|
|
896
|
+
return { scroll };
|
|
777
897
|
}
|
|
778
898
|
}
|
|
779
899
|
};
|
|
@@ -1915,6 +2035,13 @@ async function inspectPage(page, mode, crawler) {
|
|
|
1915
2035
|
}
|
|
1916
2036
|
};
|
|
1917
2037
|
}
|
|
2038
|
+
var ACCESS_TOKEN_SCREENSHOT_MASK = '[data-lmt-sensitive="zerodha-access-token"]';
|
|
2039
|
+
async function screenshotMaskOptions(page) {
|
|
2040
|
+
const playwrightPage = page;
|
|
2041
|
+
const locator = playwrightPage.locator(ACCESS_TOKEN_SCREENSHOT_MASK);
|
|
2042
|
+
if (!await locator.count()) return {};
|
|
2043
|
+
return { mask: [locator], maskColor: "#000000" };
|
|
2044
|
+
}
|
|
1918
2045
|
async function captureScreenshot(page, sessionId, action, fullPage, timeoutMs, onTimeout) {
|
|
1919
2046
|
if (!page) return { outcome: "unavailable", reason: "No browser page was available." };
|
|
1920
2047
|
if (!page.screenshot) return { outcome: "failed", error: "The browser does not support screenshots." };
|
|
@@ -1930,12 +2057,15 @@ async function captureScreenshot(page, sessionId, action, fullPage, timeoutMs, o
|
|
|
1930
2057
|
await withDeadline(
|
|
1931
2058
|
(async () => {
|
|
1932
2059
|
await waitForRenderedPage(page);
|
|
1933
|
-
|
|
2060
|
+
const maskOptions = await screenshotMaskOptions(page);
|
|
2061
|
+
const options = {
|
|
1934
2062
|
path: screenshotPath,
|
|
1935
2063
|
fullPage,
|
|
1936
2064
|
animations: "disabled",
|
|
1937
|
-
timeout: timeoutMs
|
|
1938
|
-
|
|
2065
|
+
timeout: timeoutMs,
|
|
2066
|
+
...maskOptions
|
|
2067
|
+
};
|
|
2068
|
+
return fullPage ? captureFullContent(page, options) : page.screenshot(options);
|
|
1939
2069
|
})(),
|
|
1940
2070
|
timeoutMs,
|
|
1941
2071
|
onTimeout
|
|
@@ -2189,11 +2319,12 @@ async function runSessionOwner(options) {
|
|
|
2189
2319
|
guidedError = error2;
|
|
2190
2320
|
}
|
|
2191
2321
|
let inspected = null;
|
|
2322
|
+
let controlResult;
|
|
2192
2323
|
let actionError;
|
|
2193
2324
|
try {
|
|
2194
2325
|
if (isControlAction(request)) {
|
|
2195
2326
|
const budget = Math.max(1, remainingMs(deadlineAt) - Math.min(2e3, request.timeoutMs / 4));
|
|
2196
|
-
await withDeadline(crawler.controls().act(request, budget, () => {
|
|
2327
|
+
controlResult = await withDeadline(crawler.controls().act(request, budget, () => {
|
|
2197
2328
|
submitted = true;
|
|
2198
2329
|
}), budget, () => void 0);
|
|
2199
2330
|
}
|
|
@@ -2235,6 +2366,7 @@ async function runSessionOwner(options) {
|
|
|
2235
2366
|
sessionId: options.sessionId,
|
|
2236
2367
|
action: request.action,
|
|
2237
2368
|
outcome: error instanceof ControlError && error.code === "action_uncertain" ? "uncertain" : error ? submitted && !(error instanceof OperatorActionError) && !(error instanceof LoginAttemptError) ? "uncertain" : "failure" : "success",
|
|
2369
|
+
...!actionError ? controlResult : {},
|
|
2238
2370
|
...loginAttempt ? { loginAttempt: { attemptId: loginAttempt.attemptId, status: loginAttempt.status } } : {},
|
|
2239
2371
|
url: inspected?.url ?? (page ? safeUrl(crawler?.controls().redact(page.url()) ?? page.url()) : null),
|
|
2240
2372
|
visibleState: inspected?.visibleState ?? null,
|
|
@@ -2387,6 +2519,10 @@ Usage:
|
|
|
2387
2519
|
lmt-web inspect --session ID [--full-page] [--timeout 30s]
|
|
2388
2520
|
lmt-web click --session ID --ref REF
|
|
2389
2521
|
lmt-web fill --session ID --ref REF --value TEXT [--secret]
|
|
2522
|
+
lmt-web read --session ID --ref REF
|
|
2523
|
+
lmt-web clipboard --session ID
|
|
2524
|
+
lmt-web check --session ID --ref REF --stdin
|
|
2525
|
+
lmt-web clipboard-check --session ID --ref REF
|
|
2390
2526
|
lmt-web select --session ID --ref REF --value OPTION
|
|
2391
2527
|
lmt-web scroll --session ID [--ref REF] [--x PIXELS] [--y PIXELS]
|
|
2392
2528
|
lmt-web navigate --session ID --url PATH_OR_URL
|
|
@@ -2474,7 +2610,7 @@ async function parseCommand(argv) {
|
|
|
2474
2610
|
}
|
|
2475
2611
|
let payload;
|
|
2476
2612
|
try {
|
|
2477
|
-
payload = await definition.build({ values, flags, readOtp: () => readOtp(timeoutMs) });
|
|
2613
|
+
payload = await definition.build({ values, flags, readOtp: () => readOtp(timeoutMs), readInput: () => readInput(timeoutMs) });
|
|
2478
2614
|
} catch (error) {
|
|
2479
2615
|
throw new InvocationError(error instanceof Error ? error.message : "The action input could not be read.", error instanceof InvocationError ? error.code : "action_failed", action);
|
|
2480
2616
|
}
|
|
@@ -2487,6 +2623,23 @@ function emit(result, diagnostic) {
|
|
|
2487
2623
|
if (diagnostic) process.stderr.write(`${diagnostic}
|
|
2488
2624
|
`);
|
|
2489
2625
|
}
|
|
2626
|
+
async function readInput(timeoutMs) {
|
|
2627
|
+
if (process.stdin.isTTY) throw new InvocationError("Pipe the expected value on stdin.");
|
|
2628
|
+
let input = "";
|
|
2629
|
+
const timer = setTimeout(() => process.stdin.destroy(new Error("Input deadline exceeded.")), timeoutMs);
|
|
2630
|
+
try {
|
|
2631
|
+
for await (const chunk of process.stdin) {
|
|
2632
|
+
input += chunk.toString();
|
|
2633
|
+
if (Buffer.byteLength(input) > 16384) throw new InvocationError("Expected value is too large.");
|
|
2634
|
+
}
|
|
2635
|
+
return input.replace(/\r?\n$/, "");
|
|
2636
|
+
} catch {
|
|
2637
|
+
throw new InvocationError("Expected value could not be read from stdin.");
|
|
2638
|
+
} finally {
|
|
2639
|
+
input = "";
|
|
2640
|
+
clearTimeout(timer);
|
|
2641
|
+
}
|
|
2642
|
+
}
|
|
2490
2643
|
async function readOtp(timeoutMs) {
|
|
2491
2644
|
let input = "";
|
|
2492
2645
|
const timer = setTimeout(() => process.stdin.destroy(new Error("OTP input exceeded its deadline.")), timeoutMs);
|