@swmansion/argent 0.30.2-next.2 → 0.30.2-next.21

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -24566,6 +24566,7 @@ function spawnToolsServer(paths, port, options = {}) {
24566
24566
  stdio: ["ignore", "pipe", logFd],
24567
24567
  env: buildToolsServerEnv(paths, port, process.env, options)
24568
24568
  });
24569
+ fs.closeSync(logFd);
24569
24570
  child.unref();
24570
24571
  const pid = child.pid;
24571
24572
  if (!pid) {
@@ -25313,6 +25314,7 @@ var FAILURE_CODES = {
25313
25314
  REACT_PROFILER_DEVTOOLS_RENDERER_MISSING: "REACT_PROFILER_DEVTOOLS_RENDERER_MISSING",
25314
25315
  REACT_PROFILER_START_FAILED: "REACT_PROFILER_START_FAILED",
25315
25316
  REACT_PROFILER_START_VERIFY_FAILED: "REACT_PROFILER_START_VERIFY_FAILED",
25317
+ REACT_PROFILER_SESSION_TORN_DOWN: "REACT_PROFILER_SESSION_TORN_DOWN",
25316
25318
  REACT_PROFILER_HOOK_ERROR: "REACT_PROFILER_HOOK_ERROR",
25317
25319
  REACT_PROFILER_ANALYZE_NO_DATA: "REACT_PROFILER_ANALYZE_NO_DATA",
25318
25320
  NATIVE_PROFILER_FACTORY_OPTIONS_MISSING: "NATIVE_PROFILER_FACTORY_OPTIONS_MISSING",
@@ -25577,7 +25579,7 @@ async function getResolvedToolsUrl() {
25577
25579
  const link2 = await readLinkConfig();
25578
25580
  const envToken = process.env[AUTH_TOKEN_ENV];
25579
25581
  return {
25580
- url: envUrl,
25582
+ url: envUrl.replace(/\/+$/, ""),
25581
25583
  source: "env",
25582
25584
  ...envToken ? { token: envToken } : {},
25583
25585
  ...link2 ? { shadowedLink: link2 } : {}
@@ -26485,8 +26487,13 @@ async function consumeToolStream(name, body, onProgress, signal) {
26485
26487
  if (msg.event === "progress") {
26486
26488
  progress++;
26487
26489
  onProgress(msg.data);
26488
- } else if (msg.event === "result") final = { data: msg.data, note: msg.note };
26489
- else if (msg.event === "error") {
26490
+ } else if (msg.event === "result") {
26491
+ final = {
26492
+ data: msg.data,
26493
+ note: msg.note,
26494
+ ...msg.profiling === true ? { profiling: true } : {}
26495
+ };
26496
+ } else if (msg.event === "error") {
26490
26497
  throw new ToolInvocationError(msg.error ?? "tool invocation failed", {
26491
26498
  errorCode: msg.error_code,
26492
26499
  errorKind: msg.error_kind
@@ -26533,7 +26540,11 @@ async function consumeToolStream(name, body, onProgress, signal) {
26533
26540
  if (!final) {
26534
26541
  throw brokenStream(name, "the stream ended without a result", progress);
26535
26542
  }
26536
- return { data: final.data, note: final.note };
26543
+ return {
26544
+ data: final.data,
26545
+ note: final.note,
26546
+ ...final.profiling ? { profiling: true } : {}
26547
+ };
26537
26548
  }
26538
26549
  function errorBodyMessage(body) {
26539
26550
  if (Array.isArray(body.issues) && typeof body.message === "string") return body.message;
@@ -26706,7 +26717,8 @@ function createToolsClient(options = {}) {
26706
26717
  return {
26707
26718
  data: await settle2(streamed.data),
26708
26719
  note: streamed.note,
26709
- outputHint: meta2?.outputHint
26720
+ outputHint: meta2?.outputHint,
26721
+ ...streamed.profiling ? { profiling: true } : {}
26710
26722
  };
26711
26723
  }
26712
26724
  let json;
@@ -26731,7 +26743,12 @@ function createToolsClient(options = {}) {
26731
26743
  });
26732
26744
  }
26733
26745
  opts?.signal?.throwIfAborted();
26734
- return { data: await settle2(json.data), note: json.note, outputHint: meta2?.outputHint };
26746
+ return {
26747
+ data: await settle2(json.data),
26748
+ note: json.note,
26749
+ outputHint: meta2?.outputHint,
26750
+ ...json.profiling === true ? { profiling: true } : {}
26751
+ };
26735
26752
  }
26736
26753
  return { fetchTools, fetchTool, callTool, baseUrl };
26737
26754
  }
@@ -28485,7 +28502,9 @@ var AUTO_SCREENSHOT_TOOLS = /* @__PURE__ */ new Set([
28485
28502
  "restart-app",
28486
28503
  "open-url",
28487
28504
  "describe",
28488
- "run-sequence"
28505
+ "run-sequence",
28506
+ // A wait is almost always followed by a look at the screen it waited for.
28507
+ "await-ui-element"
28489
28508
  ]);
28490
28509
  var AUTO_SCREENSHOT_DELAY_MS_BY_TOOL = {
28491
28510
  "launch-app": 3e3,
@@ -28506,9 +28525,13 @@ var AUTO_SCREENSHOT_DELAY_MS_BY_TOOL = {
28506
28525
  "fold": 1500,
28507
28526
  "keyboard": 300,
28508
28527
  "paste": 300,
28509
- "describe": 100
28528
+ "describe": 100,
28529
+ // The wait returns on the first match, often mid-transition; the cap must
28530
+ // leave `await-screen-idle` room to see two identical reads.
28531
+ "await-ui-element": 1500
28510
28532
  };
28511
28533
  var DEFAULT_DELAY_MS = 1400;
28534
+ var CAPTURE_TIMEOUT_MS = 15e3;
28512
28535
  function autoScreenshotEnabled(options) {
28513
28536
  return !isFlagEnabled("disable-auto-screenshot", options);
28514
28537
  }
@@ -28528,11 +28551,13 @@ var AUTO_DESCRIBE_TOOLS = /* @__PURE__ */ new Set([
28528
28551
  "launch-app",
28529
28552
  "restart-app",
28530
28553
  "open-url",
28531
- "run-sequence"
28554
+ "run-sequence",
28555
+ "await-ui-element"
28532
28556
  ]);
28533
28557
  function autoDescribeEnabled(options) {
28534
28558
  return !isFlagEnabled("disable-auto-describe", options);
28535
28559
  }
28560
+ var PROFILING_CAPTURE_SKIPPED_NOTE = "Auto-screenshot and element tree skipped while a profiler is recording on this device. Call screenshot or describe if you need to see the screen.";
28536
28561
  var AUTO_DESCRIBE_HEADER = "--- Elements after action (describe) ---";
28537
28562
  function shouldAutoDescribe(toolName) {
28538
28563
  return AUTO_DESCRIBE_TOOLS.has(normalizeToolName(toolName));
@@ -28588,6 +28613,7 @@ function toMcpTool(t) {
28588
28613
  name: t.name,
28589
28614
  description: t.description,
28590
28615
  inputSchema: { type: "object", ...withoutRetiredParams(t.inputSchema) },
28616
+ ...t.readOnly ? { annotations: { readOnlyHint: true } } : {},
28591
28617
  ...Object.keys(meta2).length > 0 ? { _meta: meta2 } : {}
28592
28618
  };
28593
28619
  }
@@ -28623,22 +28649,50 @@ async function fetchWithReconnect(getUrl, reconnect, config2) {
28623
28649
  for (let attempt = 0; attempt <= maxRetries; attempt++) {
28624
28650
  const controller = new AbortController();
28625
28651
  const timer = fetchTimeoutMs !== null ? setTimeout(() => controller.abort(), fetchTimeoutMs) : void 0;
28652
+ let callerSignal;
28626
28653
  try {
28627
28654
  const attemptInit = typeof init === "function" ? init() : init;
28628
- return await fetch(getUrl(), { ...attemptInit, signal: controller.signal });
28655
+ callerSignal = attemptInit?.signal;
28656
+ const signal = callerSignal ? AbortSignal.any([controller.signal, callerSignal]) : controller.signal;
28657
+ return await fetch(getUrl(), { ...attemptInit, signal });
28629
28658
  } catch (err) {
28630
28659
  lastError = err;
28631
- if (attempt === maxRetries) break;
28632
- if (attempt === 0) {
28633
- await reconnect();
28660
+ if (callerSignal?.aborted || attempt === maxRetries) break;
28661
+ const pause = (async () => {
28662
+ if (attempt === 0) {
28663
+ await reconnect();
28664
+ }
28665
+ await new Promise((r) => setTimeout(r, expBackoffBase * Math.pow(2, attempt)));
28666
+ })();
28667
+ await untilAborted(pause, callerSignal);
28668
+ if (callerSignal?.aborted) {
28669
+ lastError = callerSignal.reason;
28670
+ break;
28634
28671
  }
28635
- await new Promise((r) => setTimeout(r, expBackoffBase * Math.pow(2, attempt)));
28636
28672
  } finally {
28637
28673
  clearTimeout(timer);
28638
28674
  }
28639
28675
  }
28640
28676
  throw lastError;
28641
28677
  }
28678
+ async function untilAborted(work, signal) {
28679
+ if (!signal) {
28680
+ await work;
28681
+ return;
28682
+ }
28683
+ let onAbort = () => {
28684
+ };
28685
+ const aborted2 = new Promise((resolve10) => {
28686
+ onAbort = resolve10;
28687
+ if (signal.aborted) resolve10();
28688
+ else signal.addEventListener("abort", onAbort, { once: true });
28689
+ });
28690
+ try {
28691
+ await Promise.race([work, aborted2]);
28692
+ } finally {
28693
+ signal.removeEventListener("abort", onAbort);
28694
+ }
28695
+ }
28642
28696
  function parseUrl(url) {
28643
28697
  let parsed;
28644
28698
  try {
@@ -28683,9 +28737,9 @@ function createToolCaller(deps) {
28683
28737
  });
28684
28738
  return {
28685
28739
  fetchTools: client.fetchTools,
28686
- async callTool(name, args) {
28687
- const { data, note, outputHint } = await client.callTool(name, args);
28688
- return { result: data, outputHint, note };
28740
+ async callTool(name, args, opts) {
28741
+ const { data, note, outputHint, profiling } = await client.callTool(name, args, opts);
28742
+ return { result: data, outputHint, note, ...profiling ? { profiling } : {} };
28689
28743
  }
28690
28744
  };
28691
28745
  }
@@ -28699,6 +28753,7 @@ async function startMcpServer(options) {
28699
28753
  }
28700
28754
  const autoScreenshotOn = autoScreenshotEnabled();
28701
28755
  const autoDescribeOn = autoDescribeEnabled();
28756
+ const captureTimeoutMs = options.captureTimeoutMs ?? CAPTURE_TIMEOUT_MS;
28702
28757
  let TOOLS_URL;
28703
28758
  let AUTH_TOKEN;
28704
28759
  const resolved = await getResolvedToolsUrl();
@@ -28758,11 +28813,75 @@ async function startMcpServer(options) {
28758
28813
  reconnect,
28759
28814
  extraHeaders: aiClientHeaders
28760
28815
  });
28816
+ async function awaitScreenSettled(udid, toolName) {
28817
+ const maxWaitMs = getAutoScreenshotDelayMs(toolName);
28818
+ if (maxWaitMs === 0) return true;
28819
+ const started = Date.now();
28820
+ try {
28821
+ const idle = await callTool(
28822
+ "await-screen-idle",
28823
+ { udid, timeoutMs: maxWaitMs },
28824
+ // The tool bounds its poll to maxWaitMs; the rest covers its setup.
28825
+ { signal: AbortSignal.timeout(maxWaitMs + captureTimeoutMs) }
28826
+ );
28827
+ await spyLog({
28828
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
28829
+ event: "auto_screenshot_readiness",
28830
+ name: toolName,
28831
+ maxWaitMs,
28832
+ ...idle.result
28833
+ });
28834
+ return idle.result?.settled === true;
28835
+ } catch {
28836
+ await new Promise((r) => setTimeout(r, Math.max(0, maxWaitMs - (Date.now() - started))));
28837
+ return false;
28838
+ }
28839
+ }
28840
+ async function captureScreenshotBlocks(udid, signal) {
28841
+ try {
28842
+ const { result } = await callTool("screenshot", { udid }, { signal });
28843
+ const blocks = await toMcpContent(result, "image", {
28844
+ toolsUrl: TOOLS_URL,
28845
+ authToken: AUTH_TOKEN,
28846
+ deviceId: udid
28847
+ });
28848
+ if (!blocks.some((b) => b.type === "image")) return [];
28849
+ return [{ type: "text", text: "--- Screen after action ---" }, ...blocks];
28850
+ } catch {
28851
+ return [];
28852
+ }
28853
+ }
28854
+ async function captureTreeBlocks(udid, toolName, signal) {
28855
+ const t1 = Date.now();
28856
+ try {
28857
+ const d = await callTool("describe", { udid }, { signal });
28858
+ const desc = d.result?.description;
28859
+ await spyLog({
28860
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
28861
+ event: "auto_describe",
28862
+ name: toolName,
28863
+ durationMs: Date.now() - t1,
28864
+ chars: typeof desc === "string" ? desc.length : 0
28865
+ });
28866
+ if (typeof desc !== "string" || desc.length === 0) return [];
28867
+ return [{ type: "text", text: `${AUTO_DESCRIBE_HEADER}
28868
+ ${desc}` }];
28869
+ } catch (e) {
28870
+ await spyLog({
28871
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
28872
+ event: "auto_describe",
28873
+ name: toolName,
28874
+ durationMs: Date.now() - t1,
28875
+ error: String(e instanceof Error ? e.message : e)
28876
+ });
28877
+ return [];
28878
+ }
28879
+ }
28761
28880
  const server = new Server(
28762
28881
  { name: "argent", version: getInstalledVersion() },
28763
28882
  {
28764
28883
  capabilities: { tools: {} },
28765
- instructions: "Argent \u2014 iOS Simulator, Android Emulator, and Chromium app control for interacting, testing, profiling and debugging mobile and Chromium applications. Interaction tools return the screen after the action: a screenshot plus the accessibility element tree with normalized tap frames. Take coordinates from that tree; call describe (or debugger-component-tree) only when no fresh tree is available \u2014 never guess coordinates from pixels. On session end: call stop-all-simulator-servers with devices: [...] naming the devices this session used, and perform any necessary cleanup. One tool-server is shared by every agent using this argent install, so an unscoped call tears down their devices too \u2014 reserve it for a deliberate machine-wide cleanup. Full guidance is in the argent rule loaded from .claude/rules/argent.md."
28884
+ instructions: "Argent \u2014 iOS Simulator, Android Emulator, and Chromium app control for interacting, testing, profiling and debugging mobile and Chromium applications. Interaction tools return the screen after the action: a screenshot plus the accessibility element tree with normalized tap frames. Take coordinates from that tree; call describe (or debugger-component-tree) only when no fresh tree is available \u2014 never guess coordinates from pixels. Do not stop simulator servers when a task finishes: the next task reuses them. Call stop-all-simulator-servers when the user asks to clean up or ends the session, or when a skill tells you to recycle a device's services, always with devices: [...] naming the devices this session used. Never stop a Metro the user started unless they ask. One tool-server is shared by every agent using this argent install, so an unscoped call tears down their devices too \u2014 reserve it for a deliberate machine-wide cleanup. Full guidance is in the argent rule loaded from .claude/rules/argent.md."
28766
28885
  }
28767
28886
  );
28768
28887
  server.setRequestHandler(ListToolsRequestSchema, async () => {
@@ -28791,7 +28910,7 @@ async function startMcpServer(options) {
28791
28910
  args: params.arguments
28792
28911
  });
28793
28912
  try {
28794
- const { result, outputHint, note } = await callTool(params.name, params.arguments);
28913
+ const { result, outputHint, note, profiling } = await callTool(params.name, params.arguments);
28795
28914
  await spyLog({
28796
28915
  ts: (/* @__PURE__ */ new Date()).toISOString(),
28797
28916
  event: "tool_result",
@@ -28824,73 +28943,18 @@ async function startMcpServer(options) {
28824
28943
  text: "Auto-screenshot and element tree skipped: the input contains a {{secret:\u2026}} placeholder, and a capture of this screen could reveal the typed secret. The secret is already typed \u2014 do not send the typing step again, or the field will hold two copies of it. Submit or navigate away, then verify the resulting screen as usual. Only this call is covered: the next call is captured normally, and shows the secret if the field is still on screen. To cover the submit as well, put the typing and the submit in ONE `run-sequence` the next time you type a secret."
28825
28944
  }
28826
28945
  ];
28827
- } else if (udid && (wantScreenshot || wantTree)) {
28828
- const maxWaitMs = getAutoScreenshotDelayMs(params.name);
28829
- if (maxWaitMs > 0) {
28830
- try {
28831
- const idle = await callTool("await-screen-idle", { udid, timeoutMs: maxWaitMs });
28832
- await spyLog({
28833
- ts: (/* @__PURE__ */ new Date()).toISOString(),
28834
- event: "auto_screenshot_readiness",
28835
- name: params.name,
28836
- maxWaitMs,
28837
- ...idle.result
28838
- });
28839
- } catch {
28840
- await new Promise((r) => setTimeout(r, maxWaitMs));
28841
- }
28842
- }
28843
- if (wantScreenshot) {
28844
- try {
28845
- const screenshotResult = await callTool("screenshot", { udid });
28846
- const screenshotContent = await toMcpContent(screenshotResult.result, "image", {
28847
- toolsUrl: TOOLS_URL,
28848
- authToken: AUTH_TOKEN,
28849
- deviceId: udid
28850
- });
28851
- const hasImage = screenshotContent.some((b) => b.type === "image");
28852
- if (hasImage) {
28853
- content = [
28854
- ...content,
28855
- {
28856
- type: "text",
28857
- text: "--- Screen after action ---"
28858
- },
28859
- ...screenshotContent
28860
- ];
28861
- }
28862
- } catch {
28863
- }
28864
- }
28865
- if (wantTree) {
28866
- const t1 = Date.now();
28867
- try {
28868
- const d = await callTool("describe", { udid });
28869
- const desc = d.result?.description;
28870
- if (typeof desc === "string" && desc.length > 0) {
28871
- content = [
28872
- ...content,
28873
- { type: "text", text: `${AUTO_DESCRIBE_HEADER}
28874
- ${desc}` }
28875
- ];
28876
- }
28877
- await spyLog({
28878
- ts: (/* @__PURE__ */ new Date()).toISOString(),
28879
- event: "auto_describe",
28880
- name: params.name,
28881
- durationMs: Date.now() - t1,
28882
- chars: typeof desc === "string" ? desc.length : 0
28883
- });
28884
- } catch (e) {
28885
- await spyLog({
28886
- ts: (/* @__PURE__ */ new Date()).toISOString(),
28887
- event: "auto_describe",
28888
- name: params.name,
28889
- durationMs: Date.now() - t1,
28890
- error: String(e instanceof Error ? e.message : e)
28891
- });
28892
- }
28946
+ } else if (udid && (wantScreenshot || wantTree) && profiling) {
28947
+ if (params.name !== "describe") {
28948
+ await awaitScreenSettled(udid, params.name);
28949
+ content = [...content, { type: "text", text: PROFILING_CAPTURE_SKIPPED_NOTE }];
28893
28950
  }
28951
+ } else if (udid && (wantScreenshot || wantTree)) {
28952
+ const screenSettled = await awaitScreenSettled(udid, params.name);
28953
+ const signal = AbortSignal.timeout(captureTimeoutMs);
28954
+ const screenshot = wantScreenshot ? captureScreenshotBlocks(udid, signal) : Promise.resolve([]);
28955
+ if (!screenSettled) await screenshot;
28956
+ const tree = wantTree ? captureTreeBlocks(udid, params.name, signal) : Promise.resolve([]);
28957
+ content = [...content, ...await screenshot, ...await tree];
28894
28958
  }
28895
28959
  if (note) {
28896
28960
  content = [{ type: "text", text: note }, ...content];
@@ -53,6 +53,8 @@ export interface ArgentToolArgs {
53
53
  force?: boolean;
54
54
  /** Android only: boot the emulator with audio output enabled. Defaults to false: argent boots emulators MUTED so several agent-driven devices don't all play sound on the host machine; pass `true` when the task involves playing, hearing, or testing audio. Takes effect at boot: if the emulator is already running muted, add `force: true` to reboot it with sound. A boot snapshot saved in the other audio mode can't be reused, so the first boot after toggling is a slower cold boot. The `boot-sound` argent flag flips this default to true. Ignored on iOS/Vega/HarmonyOS/Electron, which argent never mutes. */
55
55
  sound?: boolean;
56
+ /** Android on a macOS host only: boot the emulator with guest Vulkan. Defaults to false: guest Vulkan crashes Chrome's GPU process, which breaks Chrome and Custom Tab sign-in flows. Without it apps find no Vulkan device, although the guest still lists the Vulkan features, so a Vulkan-only renderer such as WebGPU (react-native-wgpu) draws nothing; pass `true` to test one. Takes effect at boot: an emulator already running in the other mode is reused with a `note`, and `force: true` reboots it. A boot snapshot saved in the other mode can't be reused, so the first boot after toggling is a slower cold boot. The `ARGENT_EMULATOR_GUEST_VULKAN` env var flips this default to true. Ignored on other hosts and platforms. */
57
+ guestVulkan?: boolean;
56
58
  /** iOS only: boot the simulator core WITHOUT opening its GUI window (Simulator.app, or Device Hub on Xcode 27+). The device still streams via simulator-server; used by Argent Lens. Set the `ARGENT_SIMULATOR_NO_WINDOW` env var (1/true/yes) to force this host-wide without passing the flag per call (the iOS analog of `ARGENT_EMULATOR_NO_WINDOW`). Ignored on Android/Vega/HarmonyOS/Electron, which have no equivalent GUI step. */
57
59
  headless?: boolean;
58
60
  /** Electron: path to the Electron app to launch. Either a packaged .app bundle / executable, or a project directory whose package.json points the Electron binary at the entry script. Provide exactly one of `udid`, `avdName`, `vvdImage`, `harmonyInstance`, or `electronAppPath`. */