@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.
Files changed (3) hide show
  1. package/README.md +99 -1
  2. package/dist/index.js +169 -16
  3. 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 preserve the rendered page without added blur or masking. Field values
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 = /password|credential|secret|token|otp|one.time|username|user.?id|api.?key|pin/i;
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) => /password|credential|secret|token|otp|one.time|username|user.?id|api.?key|pin/i.test(node.outerHTML + Array.from(node.labels ?? []).map((label) => label.innerText).join(" ")));
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
- if (command.reference) {
772
- await (await this.resolve(command.reference)).evaluate((node, offsets) => node.scrollBy({ left: offsets.x, top: offsets.y, behavior: "instant" }), command);
773
- } else {
774
- await this.page.evaluate((offsets) => window.scrollBy({ left: offsets.x, top: offsets.y, behavior: "instant" }), command);
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
- return page.screenshot({
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);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ai-digital-factory/lmt-web-cli",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "lmt-web": "dist/index.js"