@swmansion/argent 0.8.1 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -3225,8 +3225,8 @@ var require_utils = __commonJS({
3225
3225
  }
3226
3226
  return ind;
3227
3227
  }
3228
- function removeDotSegments(path2) {
3229
- let input = path2;
3228
+ function removeDotSegments(path3) {
3229
+ let input = path3;
3230
3230
  const output = [];
3231
3231
  let nextSlash = -1;
3232
3232
  let len = 0;
@@ -3478,8 +3478,8 @@ var require_schemes = __commonJS({
3478
3478
  wsComponent.secure = void 0;
3479
3479
  }
3480
3480
  if (wsComponent.resourceName) {
3481
- const [path2, query] = wsComponent.resourceName.split("?");
3482
- wsComponent.path = path2 && path2 !== "/" ? path2 : void 0;
3481
+ const [path3, query] = wsComponent.resourceName.split("?");
3482
+ wsComponent.path = path3 && path3 !== "/" ? path3 : void 0;
3483
3483
  wsComponent.query = query;
3484
3484
  wsComponent.resourceName = void 0;
3485
3485
  }
@@ -6886,9 +6886,9 @@ var require_dist = __commonJS({
6886
6886
  });
6887
6887
 
6888
6888
  // ../argent-mcp/src/mcp-server.ts
6889
- import { appendFile, mkdir as mkdir2 } from "node:fs/promises";
6889
+ import { appendFile, mkdir as mkdir3 } from "node:fs/promises";
6890
6890
  import { dirname } from "node:path";
6891
- import { homedir as homedir2 } from "node:os";
6891
+ import { homedir as homedir3 } from "node:os";
6892
6892
 
6893
6893
  // ../../node_modules/@modelcontextprotocol/sdk/dist/esm/server/stdio.js
6894
6894
  import process3 from "node:process";
@@ -7133,10 +7133,10 @@ function mergeDefs(...defs) {
7133
7133
  function cloneDef(schema) {
7134
7134
  return mergeDefs(schema._zod.def);
7135
7135
  }
7136
- function getElementAtPath(obj, path2) {
7137
- if (!path2)
7136
+ function getElementAtPath(obj, path3) {
7137
+ if (!path3)
7138
7138
  return obj;
7139
- return path2.reduce((acc, key) => acc?.[key], obj);
7139
+ return path3.reduce((acc, key) => acc?.[key], obj);
7140
7140
  }
7141
7141
  function promiseAllObject(promisesObj) {
7142
7142
  const keys = Object.keys(promisesObj);
@@ -7545,11 +7545,11 @@ function explicitlyAborted(x, startIndex = 0) {
7545
7545
  }
7546
7546
  return false;
7547
7547
  }
7548
- function prefixIssues(path2, issues) {
7548
+ function prefixIssues(path3, issues) {
7549
7549
  return issues.map((iss) => {
7550
7550
  var _a3;
7551
7551
  (_a3 = iss).path ?? (_a3.path = []);
7552
- iss.path.unshift(path2);
7552
+ iss.path.unshift(path3);
7553
7553
  return iss;
7554
7554
  });
7555
7555
  }
@@ -7696,16 +7696,16 @@ function flattenError(error2, mapper = (issue2) => issue2.message) {
7696
7696
  }
7697
7697
  function formatError(error2, mapper = (issue2) => issue2.message) {
7698
7698
  const fieldErrors = { _errors: [] };
7699
- const processError = (error3, path2 = []) => {
7699
+ const processError = (error3, path3 = []) => {
7700
7700
  for (const issue2 of error3.issues) {
7701
7701
  if (issue2.code === "invalid_union" && issue2.errors.length) {
7702
- issue2.errors.map((issues) => processError({ issues }, [...path2, ...issue2.path]));
7702
+ issue2.errors.map((issues) => processError({ issues }, [...path3, ...issue2.path]));
7703
7703
  } else if (issue2.code === "invalid_key") {
7704
- processError({ issues: issue2.issues }, [...path2, ...issue2.path]);
7704
+ processError({ issues: issue2.issues }, [...path3, ...issue2.path]);
7705
7705
  } else if (issue2.code === "invalid_element") {
7706
- processError({ issues: issue2.issues }, [...path2, ...issue2.path]);
7706
+ processError({ issues: issue2.issues }, [...path3, ...issue2.path]);
7707
7707
  } else {
7708
- const fullpath = [...path2, ...issue2.path];
7708
+ const fullpath = [...path3, ...issue2.path];
7709
7709
  if (fullpath.length === 0) {
7710
7710
  fieldErrors._errors.push(mapper(issue2));
7711
7711
  } else {
@@ -15457,17 +15457,30 @@ import * as path from "node:path";
15457
15457
  import * as readline from "node:readline";
15458
15458
  import { homedir } from "node:os";
15459
15459
  import { spawn } from "node:child_process";
15460
- import { mkdir, writeFile, readFile, unlink } from "node:fs/promises";
15460
+ import { randomBytes } from "node:crypto";
15461
+ import { mkdir, writeFile, readFile, unlink, rename, chmod } from "node:fs/promises";
15461
15462
  var STATE_DIR = path.join(homedir(), ".argent");
15462
15463
  var STATE_FILE = path.join(STATE_DIR, "tool-server.json");
15463
15464
  var LOG_FILE = path.join(STATE_DIR, "tool-server.log");
15464
- function buildToolsServerEnv(paths, port, baseEnv = process.env) {
15465
- return {
15465
+ var AUTH_TOKEN_BYTES = 32;
15466
+ var AUTH_TOKEN_ENV = "ARGENT_AUTH_TOKEN";
15467
+ var AUTOSPAWN_IDLE_TIMEOUT_MINUTES = 30;
15468
+ function buildToolsServerEnv(paths, port, baseEnv = process.env, options = {}) {
15469
+ const env = {
15466
15470
  ...baseEnv,
15467
- PORT: String(port),
15471
+ ARGENT_PORT: String(port),
15468
15472
  ARGENT_SIMULATOR_SERVER_DIR: paths.simulatorServerDir,
15469
15473
  ARGENT_NATIVE_DEVTOOLS_DIR: paths.nativeDevtoolsDir
15470
15474
  };
15475
+ if (options.host !== void 0) env.ARGENT_HOST = options.host;
15476
+ if (options.idleTimeoutMinutes !== void 0) {
15477
+ env.ARGENT_IDLE_TIMEOUT_MINUTES = String(options.idleTimeoutMinutes);
15478
+ }
15479
+ if (options.token) env[AUTH_TOKEN_ENV] = options.token;
15480
+ return env;
15481
+ }
15482
+ function generateToken() {
15483
+ return randomBytes(AUTH_TOKEN_BYTES).toString("hex");
15471
15484
  }
15472
15485
  function findFreePort() {
15473
15486
  return new Promise((resolve, reject) => {
@@ -15495,12 +15508,25 @@ function isProcessAlive(pid) {
15495
15508
  return false;
15496
15509
  }
15497
15510
  }
15498
- async function isHealthy(port) {
15511
+ function healthCheckHost(host) {
15512
+ if (host === "0.0.0.0" || host === "") return "127.0.0.1";
15513
+ if (host === "::" || host === "::0") return "::1";
15514
+ return host;
15515
+ }
15516
+ function formatUrl(host, port) {
15517
+ const h = host.includes(":") && !host.startsWith("[") ? `[${host}]` : host;
15518
+ return `http://${h}:${port}`;
15519
+ }
15520
+ function authHeaders(token) {
15521
+ return token ? { Authorization: `Bearer ${token}` } : {};
15522
+ }
15523
+ async function isToolsServerHealthy(port, host = "127.0.0.1", timeoutMs = 2e3, token) {
15499
15524
  const controller = new AbortController();
15500
- const timer = setTimeout(() => controller.abort(), 2e3);
15525
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
15501
15526
  try {
15502
- const res = await fetch(`http://127.0.0.1:${port}/tools`, {
15503
- signal: controller.signal
15527
+ const res = await fetch(`${formatUrl(healthCheckHost(host), port)}/tools`, {
15528
+ signal: controller.signal,
15529
+ headers: authHeaders(token)
15504
15530
  });
15505
15531
  return res.ok;
15506
15532
  } catch {
@@ -15509,7 +15535,7 @@ async function isHealthy(port) {
15509
15535
  clearTimeout(timer);
15510
15536
  }
15511
15537
  }
15512
- function spawnToolsServer(paths, port) {
15538
+ function spawnToolsServer(paths, port, options = {}) {
15513
15539
  return new Promise((resolve, reject) => {
15514
15540
  let logFd;
15515
15541
  try {
@@ -15521,7 +15547,7 @@ function spawnToolsServer(paths, port) {
15521
15547
  const child = spawn("node", [paths.bundlePath, "start"], {
15522
15548
  detached: true,
15523
15549
  stdio: ["ignore", "pipe", logFd],
15524
- env: buildToolsServerEnv(paths, port)
15550
+ env: buildToolsServerEnv(paths, port, process.env, options)
15525
15551
  });
15526
15552
  child.unref();
15527
15553
  const pid = child.pid;
@@ -15537,7 +15563,7 @@ function spawnToolsServer(paths, port) {
15537
15563
  };
15538
15564
  const rl = readline.createInterface({ input: child.stdout });
15539
15565
  rl.on("line", (line) => {
15540
- const match = line.match(/Tools server listening on http:\/\/127\.0\.0\.1:(\d+)/);
15566
+ const match = line.match(/Tools server listening on http:\/\/.+:(\d+)/);
15541
15567
  if (match) {
15542
15568
  const actualPort = parseInt(match[1], 10);
15543
15569
  rl.close();
@@ -15560,7 +15586,7 @@ function spawnToolsServer(paths, port) {
15560
15586
  rl.on("close", () => clearTimeout(timer));
15561
15587
  });
15562
15588
  }
15563
- async function readState() {
15589
+ async function readToolsServerState() {
15564
15590
  try {
15565
15591
  const raw = await readFile(STATE_FILE, "utf8");
15566
15592
  return JSON.parse(raw);
@@ -15568,40 +15594,113 @@ async function readState() {
15568
15594
  return null;
15569
15595
  }
15570
15596
  }
15571
- async function writeState(state) {
15597
+ async function writeToolsServerState(state) {
15572
15598
  await mkdir(STATE_DIR, { recursive: true });
15573
- await writeFile(STATE_FILE, JSON.stringify(state, null, 2) + "\n", "utf8");
15599
+ const tmp = `${STATE_FILE}.${process.pid}.tmp`;
15600
+ try {
15601
+ await writeFile(tmp, JSON.stringify(state, null, 2) + "\n", {
15602
+ encoding: "utf8",
15603
+ mode: 384
15604
+ });
15605
+ await chmod(tmp, 384);
15606
+ await rename(tmp, STATE_FILE);
15607
+ } catch (err) {
15608
+ await unlink(tmp).catch(() => {
15609
+ });
15610
+ throw err;
15611
+ }
15574
15612
  }
15575
- async function clearState() {
15613
+ async function clearToolsServerState() {
15576
15614
  try {
15577
15615
  await unlink(STATE_FILE);
15578
15616
  } catch {
15579
15617
  }
15580
15618
  }
15619
+ var readState = readToolsServerState;
15620
+ var writeState = writeToolsServerState;
15621
+ var clearState = clearToolsServerState;
15581
15622
  async function ensureToolsServer(paths) {
15582
15623
  const state = await readState();
15583
15624
  if (state) {
15584
15625
  const alive = isProcessAlive(state.pid);
15585
15626
  if (alive) {
15586
- const healthy = await isHealthy(state.port);
15627
+ const host = state.host ?? "127.0.0.1";
15628
+ const healthy = await isToolsServerHealthy(state.port, host, 2e3, state.token);
15587
15629
  if (healthy) {
15588
- return `http://127.0.0.1:${state.port}`;
15630
+ return {
15631
+ url: formatUrl(healthCheckHost(host), state.port),
15632
+ token: state.token ?? ""
15633
+ };
15589
15634
  }
15590
15635
  }
15591
15636
  await clearState();
15592
15637
  }
15638
+ const token = generateToken();
15593
15639
  const port = await findFreePort();
15594
- const { port: actualPort, pid } = await spawnToolsServer(paths, port);
15640
+ const { port: actualPort, pid } = await spawnToolsServer(paths, port, {
15641
+ token,
15642
+ idleTimeoutMinutes: AUTOSPAWN_IDLE_TIMEOUT_MINUTES
15643
+ });
15595
15644
  await writeState({
15596
15645
  port: actualPort,
15597
15646
  pid,
15598
15647
  startedAt: (/* @__PURE__ */ new Date()).toISOString(),
15599
- bundlePath: paths.bundlePath
15648
+ bundlePath: paths.bundlePath,
15649
+ host: "127.0.0.1",
15650
+ token
15600
15651
  });
15601
- return `http://127.0.0.1:${actualPort}`;
15652
+ return { url: formatUrl("127.0.0.1", actualPort), token };
15653
+ }
15654
+
15655
+ // ../argent-tools-client/src/link-config.ts
15656
+ import * as path2 from "node:path";
15657
+ import { homedir as homedir2 } from "node:os";
15658
+ import { mkdir as mkdir2, writeFile as writeFile2, readFile as readFile2, unlink as unlink2, chmod as chmod2 } from "node:fs/promises";
15659
+ var LINK_DIR = path2.join(homedir2(), ".argent");
15660
+ var LINK_FILE = path2.join(LINK_DIR, "link.json");
15661
+ async function readLinkConfig() {
15662
+ try {
15663
+ const raw = await readFile2(LINK_FILE, "utf8");
15664
+ const parsed = JSON.parse(raw);
15665
+ if (!parsed || typeof parsed.url !== "string" || typeof parsed.host !== "string" || typeof parsed.port !== "number" || typeof parsed.createdAt !== "string") {
15666
+ return null;
15667
+ }
15668
+ return {
15669
+ url: parsed.url,
15670
+ host: parsed.host,
15671
+ port: parsed.port,
15672
+ createdAt: parsed.createdAt,
15673
+ ...typeof parsed.token === "string" ? { token: parsed.token } : {}
15674
+ };
15675
+ } catch {
15676
+ return null;
15677
+ }
15678
+ }
15679
+ async function getResolvedToolsUrl() {
15680
+ const envUrl = process.env.ARGENT_TOOLS_URL;
15681
+ if (envUrl) {
15682
+ const link2 = await readLinkConfig();
15683
+ const envToken = process.env[AUTH_TOKEN_ENV];
15684
+ return {
15685
+ url: envUrl,
15686
+ source: "env",
15687
+ ...envToken ? { token: envToken } : {},
15688
+ ...link2 ? { shadowedLink: link2 } : {}
15689
+ };
15690
+ }
15691
+ const link = await readLinkConfig();
15692
+ if (link) {
15693
+ return { url: link.url, source: "link", ...link.token ? { token: link.token } : {} };
15694
+ }
15695
+ return { url: null, source: "none" };
15696
+ }
15697
+ async function isRemoteRouted() {
15698
+ const { url } = await getResolvedToolsUrl();
15699
+ return url !== null;
15602
15700
  }
15603
15701
 
15604
15702
  // ../argent-mcp/src/content.ts
15703
+ import { readFile as readFile3 } from "node:fs/promises";
15605
15704
  var PNG_SIGNATURE = Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]);
15606
15705
  async function fetchPngBytes(url) {
15607
15706
  try {
@@ -15615,9 +15714,12 @@ async function fetchPngBytes(url) {
15615
15714
  return null;
15616
15715
  }
15617
15716
  }
15618
- async function toMcpContent(result, outputHint) {
15717
+ async function toMcpContent(result, outputHint, args) {
15619
15718
  if (outputHint === "image" && result && typeof result === "object" && "url" in result) {
15620
15719
  const r = result;
15720
+ if (isRecord(args) && args.includeImageInContext === false) {
15721
+ return [{ type: "text", text: `Saved: ${r.path}` }];
15722
+ }
15621
15723
  const buf = await fetchPngBytes(r.url);
15622
15724
  if (buf) {
15623
15725
  return [
@@ -15638,6 +15740,26 @@ async function toMcpContent(result, outputHint) {
15638
15740
  }
15639
15741
  return [{ type: "text", text: JSON.stringify(result, null, 2) }];
15640
15742
  }
15743
+ function isRecord(value) {
15744
+ return value !== null && typeof value === "object";
15745
+ }
15746
+ function isScreenshotDiffResult(value) {
15747
+ if (!isRecord(value)) return false;
15748
+ return typeof value.summary === "string";
15749
+ }
15750
+ async function screenshotDiffToMcpContent(result) {
15751
+ const blocks = [];
15752
+ if (typeof result.contextDiffPath === "string") {
15753
+ const buf = await readFile3(result.contextDiffPath);
15754
+ blocks.push({
15755
+ type: "image",
15756
+ data: buf.toString("base64"),
15757
+ mimeType: "image/png"
15758
+ });
15759
+ }
15760
+ blocks.push({ type: "text", text: result.summary });
15761
+ return blocks;
15762
+ }
15641
15763
  async function flowRunToMcpContent(result) {
15642
15764
  const blocks = [];
15643
15765
  if (result.executionPrerequisite) {
@@ -15662,7 +15784,7 @@ async function flowRunToMcpContent(result) {
15662
15784
  });
15663
15785
  } else {
15664
15786
  blocks.push({ type: "text", text: `[${num}] ${step.tool}` });
15665
- const stepContent = await toMcpContent(step.result, step.outputHint);
15787
+ const stepContent = await toMcpContent(step.result, step.outputHint, step.args);
15666
15788
  blocks.push(...stepContent);
15667
15789
  }
15668
15790
  }
@@ -15776,35 +15898,44 @@ async function fetchWithReconnect(getUrl, reconnect, config2) {
15776
15898
  }
15777
15899
  async function startMcpServer(options) {
15778
15900
  let TOOLS_URL;
15779
- if (process.env.ARGENT_TOOLS_URL) {
15780
- TOOLS_URL = process.env.ARGENT_TOOLS_URL;
15901
+ let AUTH_TOKEN;
15902
+ const resolved = await getResolvedToolsUrl();
15903
+ if (resolved.url) {
15904
+ TOOLS_URL = resolved.url;
15905
+ AUTH_TOKEN = resolved.token ?? "";
15781
15906
  } else {
15782
15907
  try {
15783
- TOOLS_URL = await ensureToolsServer(options.paths);
15908
+ const handle = await ensureToolsServer(options.paths);
15909
+ TOOLS_URL = handle.url;
15910
+ AUTH_TOKEN = handle.token;
15784
15911
  } catch (err) {
15785
15912
  process.stderr.write(`[argent] Failed to start tools server: ${err}
15786
15913
  `);
15787
15914
  process.exit(1);
15788
15915
  }
15789
15916
  }
15917
+ function authHeader() {
15918
+ return AUTH_TOKEN ? { Authorization: `Bearer ${AUTH_TOKEN}` } : {};
15919
+ }
15790
15920
  let reconnectPromise = null;
15791
15921
  async function reconnect() {
15792
- if (process.env.ARGENT_TOOLS_URL) return;
15922
+ if (await isRemoteRouted()) return;
15793
15923
  if (!reconnectPromise) {
15794
- reconnectPromise = ensureToolsServer(options.paths).then((url) => {
15795
- TOOLS_URL = url;
15924
+ reconnectPromise = ensureToolsServer(options.paths).then((handle) => {
15925
+ TOOLS_URL = handle.url;
15926
+ AUTH_TOKEN = handle.token;
15796
15927
  }).finally(() => {
15797
15928
  reconnectPromise = null;
15798
15929
  });
15799
15930
  }
15800
15931
  return reconnectPromise;
15801
15932
  }
15802
- const LOG_FILE2 = process.env.ARGENT_MCP_LOG ?? `${homedir2()}/.argent/mcp-calls.log`;
15933
+ const LOG_FILE2 = process.env.ARGENT_MCP_LOG ?? `${homedir3()}/.argent/mcp-calls.log`;
15803
15934
  let logDirReady = false;
15804
15935
  async function spyLog(entry) {
15805
15936
  try {
15806
15937
  if (!logDirReady) {
15807
- await mkdir2(dirname(LOG_FILE2), { recursive: true });
15938
+ await mkdir3(dirname(LOG_FILE2), { recursive: true });
15808
15939
  logDirReady = true;
15809
15940
  }
15810
15941
  await appendFile(LOG_FILE2, JSON.stringify(entry) + "\n");
@@ -15812,7 +15943,9 @@ async function startMcpServer(options) {
15812
15943
  }
15813
15944
  }
15814
15945
  async function fetchTools() {
15815
- const res = await fetchWithReconnect(() => `${TOOLS_URL}/tools`, reconnect);
15946
+ const res = await fetchWithReconnect(() => `${TOOLS_URL}/tools`, reconnect, {
15947
+ init: { headers: authHeader() }
15948
+ });
15816
15949
  const json = await res.json();
15817
15950
  return json.tools;
15818
15951
  }
@@ -15822,7 +15955,7 @@ async function startMcpServer(options) {
15822
15955
  const res = await fetchWithReconnect(() => `${TOOLS_URL}/tools/${name}`, reconnect, {
15823
15956
  init: {
15824
15957
  method: "POST",
15825
- headers: { "Content-Type": "application/json" },
15958
+ headers: { "Content-Type": "application/json", ...authHeader() },
15826
15959
  body: JSON.stringify(args ?? {})
15827
15960
  },
15828
15961
  fetchTimeoutMs: meta2?.longRunning ? null : FETCH_TIMEOUT_MS
@@ -15873,14 +16006,21 @@ async function startMcpServer(options) {
15873
16006
  isError: false,
15874
16007
  result
15875
16008
  });
15876
- let content = params.name === "flow-execute" && result && typeof result === "object" && "flow" in result && "steps" in result ? await flowRunToMcpContent(result) : await toMcpContent(result, outputHint);
16009
+ let content;
16010
+ if (params.name === "flow-execute" && result && typeof result === "object" && "flow" in result && "steps" in result) {
16011
+ content = await flowRunToMcpContent(result);
16012
+ } else if (params.name === "screenshot-diff" && isScreenshotDiffResult(result)) {
16013
+ content = await screenshotDiffToMcpContent(result);
16014
+ } else {
16015
+ content = await toMcpContent(result, outputHint, params.arguments);
16016
+ }
15877
16017
  const udid = getUdidFromArgs(params.arguments);
15878
16018
  if (autoScreenshotEnabled() && udid && shouldAutoScreenshot(params.name)) {
15879
16019
  const delayMs = getAutoScreenshotDelayMs(params.name);
15880
16020
  if (delayMs > 0) await new Promise((r) => setTimeout(r, delayMs));
15881
16021
  try {
15882
16022
  const screenshotResult = await callTool("screenshot", { udid });
15883
- const screenshotContent = await toMcpContent(screenshotResult.result, "image");
16023
+ const screenshotContent = await toMcpContent(screenshotResult.result, "image", { udid });
15884
16024
  const hasImage = screenshotContent.some((b) => b.type === "image");
15885
16025
  if (hasImage) {
15886
16026
  content = [
@@ -15920,14 +16060,17 @@ async function startMcpServer(options) {
15920
16060
  }
15921
16061
  });
15922
16062
  await server.connect(new StdioServerTransport());
15923
- if (!process.env.ARGENT_TOOLS_URL) {
16063
+ if (!await isRemoteRouted()) {
15924
16064
  const HEALTH_INTERVAL_MS = 3e4;
15925
16065
  const healthInterval = setInterval(async () => {
15926
16066
  try {
15927
16067
  const controller = new AbortController();
15928
16068
  const timer = setTimeout(() => controller.abort(), 3e3);
15929
16069
  try {
15930
- const res = await fetch(`${TOOLS_URL}/tools`, { signal: controller.signal });
16070
+ const res = await fetch(`${TOOLS_URL}/tools`, {
16071
+ signal: controller.signal,
16072
+ headers: authHeader()
16073
+ });
15931
16074
  if (!res.ok) throw new Error(`health check returned ${res.status}`);
15932
16075
  } finally {
15933
16076
  clearTimeout(timer);
@@ -15942,7 +16085,7 @@ async function startMcpServer(options) {
15942
16085
  }
15943
16086
  if (process.env.ARGENT_AUTO_SHUTDOWN === "1") {
15944
16087
  process.stdin.on("close", () => {
15945
- fetch(`${TOOLS_URL}/shutdown`, { method: "POST" }).catch(() => {
16088
+ fetch(`${TOOLS_URL}/shutdown`, { method: "POST", headers: authHeader() }).catch(() => {
15946
16089
  });
15947
16090
  setTimeout(() => process.exit(0), 5e3);
15948
16091
  });