zefiro 0.9.0 → 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.
package/dist/mcp.js CHANGED
@@ -36,18 +36,20 @@ import {
36
36
  AgentBrowser,
37
37
  authenticate
38
38
  } from "./cli-b26q1e27.js";
39
+ import {
40
+ generateIndexHtml,
41
+ generateReadme,
42
+ generateSectionMarkdown
43
+ } from "./cli-kjyet1n8.js";
39
44
  import {
40
45
  ensureDir,
41
46
  writeFile
42
47
  } from "./cli-zvk8gwe4.js";
43
48
  import {
44
49
  buildTopology,
45
- generateIndexHtml,
46
- generateReadme,
47
- generateSectionMarkdown,
48
50
  normalizeUrl,
49
51
  urlToSlug
50
- } from "./cli-6qr9gvkp.js";
52
+ } from "./cli-5w708rbb.js";
51
53
  import {
52
54
  __commonJS,
53
55
  __require,
@@ -17936,18 +17938,72 @@ function broadcastState() {
17936
17938
  function startCompanionServer(mode, port = 0) {
17937
17939
  state = createCompanionState(mode);
17938
17940
  return new Promise((resolvePromise, reject) => {
17941
+ const startTime = Date.now();
17939
17942
  const httpServer = createServer((req, res) => {
17940
- if (req.url === "/" || req.url === "/companion") {
17943
+ const url = req.url?.split("?")[0];
17944
+ const cors = { "Access-Control-Allow-Origin": "*" };
17945
+ if (url === "/" || url === "/companion") {
17941
17946
  const htmlPath = resolve(uiDir, "companion.html");
17942
17947
  if (existsSync2(htmlPath)) {
17943
- res.writeHead(200, { "Content-Type": "text/html", "Access-Control-Allow-Origin": "*" });
17948
+ res.writeHead(200, { "Content-Type": "text/html; charset=utf-8", ...cors });
17944
17949
  res.end(readFileSync2(htmlPath));
17945
17950
  } else {
17946
- res.writeHead(200, { "Content-Type": "text/html", "Access-Control-Allow-Origin": "*" });
17951
+ res.writeHead(200, { "Content-Type": "text/html", ...cors });
17947
17952
  res.end("<html><body><h1>Companion UI not found</h1></body></html>");
17948
17953
  }
17949
17954
  return;
17950
17955
  }
17956
+ if (url === "/logo.svg") {
17957
+ const svgPath = resolve(uiDir, "logo.svg");
17958
+ if (existsSync2(svgPath)) {
17959
+ res.writeHead(200, { "Content-Type": "image/svg+xml", ...cors });
17960
+ res.end(readFileSync2(svgPath));
17961
+ } else {
17962
+ res.writeHead(404);
17963
+ res.end("Not found");
17964
+ }
17965
+ return;
17966
+ }
17967
+ if (url === "/health") {
17968
+ res.writeHead(200, { "Content-Type": "application/json", ...cors });
17969
+ res.end(JSON.stringify({ status: "ok", uptime: Math.floor((Date.now() - startTime) / 1000) }));
17970
+ return;
17971
+ }
17972
+ if (url === "/track" && req.method === "POST") {
17973
+ let body = "";
17974
+ req.on("data", (chunk) => {
17975
+ body += chunk.toString();
17976
+ });
17977
+ req.on("end", () => {
17978
+ try {
17979
+ const data = JSON.parse(body);
17980
+ const entry = {
17981
+ action: data.action ?? "unknown",
17982
+ timestamp: new Date().toISOString(),
17983
+ result: data.result,
17984
+ source: data.source ?? "agent"
17985
+ };
17986
+ state.actionLog.push(entry);
17987
+ if (state.actionLog.length > 100)
17988
+ state.actionLog = state.actionLog.slice(-100);
17989
+ if (data.url) {
17990
+ state.currentPage = { url: data.url, title: data.title ?? data.url };
17991
+ }
17992
+ broadcastState();
17993
+ res.writeHead(200, { "Content-Type": "application/json", ...cors });
17994
+ res.end(JSON.stringify({ success: true, tracked_at: entry.timestamp }));
17995
+ } catch {
17996
+ res.writeHead(400, { "Content-Type": "application/json", ...cors });
17997
+ res.end(JSON.stringify({ error: "Invalid JSON" }));
17998
+ }
17999
+ });
18000
+ return;
18001
+ }
18002
+ if (url === "/track" && req.method === "OPTIONS") {
18003
+ res.writeHead(204, { ...cors, "Access-Control-Allow-Methods": "POST", "Access-Control-Allow-Headers": "Content-Type" });
18004
+ res.end();
18005
+ return;
18006
+ }
17951
18007
  res.writeHead(404);
17952
18008
  res.end("Not found");
17953
18009
  });
@@ -18075,24 +18131,19 @@ import { homedir } from "node:os";
18075
18131
  import { spawn } from "node:child_process";
18076
18132
 
18077
18133
  // src/copilot/overlay-download.ts
18078
- import { createWriteStream, mkdirSync, existsSync as existsSync3, chmodSync } from "node:fs";
18079
- import { join as join3 } from "node:path";
18080
- import { pipeline as pipeline2 } from "node:stream/promises";
18081
18134
  import { createHash } from "node:crypto";
18135
+ import { chmodSync, createWriteStream, existsSync as existsSync3, mkdirSync } from "node:fs";
18082
18136
  import { readFile, unlink } from "node:fs/promises";
18137
+ import { join as join3 } from "node:path";
18083
18138
  import { createInterface } from "node:readline";
18084
18139
  import { Readable } from "node:stream";
18140
+ import { pipeline as pipeline2 } from "node:stream/promises";
18085
18141
  var OVERLAY_VERSION = "0.1.0";
18086
- var GITHUB_REPO = "anthropics/qa-intelligence";
18142
+ var GITHUB_REPO = "davide97g/qaligent";
18087
18143
  function resolveArch() {
18088
- switch (process.arch) {
18089
- case "arm64":
18090
- return "aarch64";
18091
- case "x64":
18092
- return "x86_64";
18093
- default:
18094
- return null;
18095
- }
18144
+ if (process.arch === "arm64")
18145
+ return "aarch64";
18146
+ return null;
18096
18147
  }
18097
18148
  async function confirm(message) {
18098
18149
  if (!process.stdin.isTTY)
@@ -18152,7 +18203,7 @@ async function downloadOverlay(binDir, options) {
18152
18203
  const arch = resolveArch();
18153
18204
  if (!arch) {
18154
18205
  if (!options?.silent) {
18155
- console.log(` Unsupported architecture: ${process.arch}`);
18206
+ console.log(" Native overlay is only available on Apple Silicon Macs (M1/M2/M3/M4).");
18156
18207
  }
18157
18208
  return false;
18158
18209
  }
@@ -18259,27 +18310,31 @@ async function handleCopilotStart(params) {
18259
18310
  const cwd = process.cwd();
18260
18311
  const outputDir = params.outputDir ? join5(cwd, params.outputDir) : join5(cwd, "copilot-report");
18261
18312
  const session = createSession(params.baseUrl, mode, outputDir);
18262
- const browser = new AgentBrowser({
18263
- session: `copilot-${session.id}`,
18264
- headed: params.headed ?? true
18265
- });
18266
- browsers.set(session.id, browser);
18267
- if (params.authState) {
18268
- const config = {
18269
- baseUrl: params.baseUrl,
18270
- outputDir,
18271
- maxPages: params.maxPages ?? 100,
18272
- excludePatterns: [],
18273
- headed: params.headed ?? true,
18274
- waitStrategy: "networkidle",
18275
- waitDelay: 1500,
18276
- pageTimeout: 30000,
18277
- auth: { method: "state-file", stateFile: params.authState },
18278
- sessionName: `copilot-${session.id}`
18279
- };
18280
- await authenticate(browser, config);
18313
+ if (params.browser !== "chrome") {
18314
+ const browser = new AgentBrowser({
18315
+ session: `copilot-${session.id}`,
18316
+ headed: params.headed ?? true
18317
+ });
18318
+ browsers.set(session.id, browser);
18319
+ if (params.authState) {
18320
+ const config = {
18321
+ baseUrl: params.baseUrl,
18322
+ outputDir,
18323
+ maxPages: params.maxPages ?? 100,
18324
+ excludePatterns: [],
18325
+ headed: params.headed ?? true,
18326
+ waitStrategy: "networkidle",
18327
+ waitDelay: 1500,
18328
+ pageTimeout: 30000,
18329
+ auth: { method: "state-file", stateFile: params.authState },
18330
+ sessionName: `copilot-${session.id}`
18331
+ };
18332
+ await authenticate(browser, config);
18333
+ }
18334
+ await browser.setViewport(1440, 900);
18335
+ await browser.open(params.baseUrl);
18336
+ await browser.waitForLoad("networkidle", 1500);
18281
18337
  }
18282
- await browser.setViewport(1440, 900);
18283
18338
  const companion = await startCompanionServer(mode, 0);
18284
18339
  session.companionPort = companion.port;
18285
18340
  const overlayBin = await resolveOverlayBinary({ silent: true });
@@ -18288,8 +18343,6 @@ async function handleCopilotStart(params) {
18288
18343
  if (child?.pid)
18289
18344
  session.overlayPid = child.pid;
18290
18345
  }
18291
- await browser.open(params.baseUrl);
18292
- await browser.waitForLoad("networkidle", 1500);
18293
18346
  logAction(session, `copilot_start: ${params.baseUrl} (mode: ${mode})`);
18294
18347
  syncAction(`copilot_start: ${params.baseUrl} (mode: ${mode})`);
18295
18348
  return {
@@ -18297,7 +18350,8 @@ async function handleCopilotStart(params) {
18297
18350
  mode: session.mode,
18298
18351
  base_url: params.baseUrl,
18299
18352
  output_dir: outputDir,
18300
- companion_url: `http://localhost:${companion.port}`
18353
+ companion_url: `http://localhost:${companion.port}`,
18354
+ browser: params.browser ?? "playwright"
18301
18355
  };
18302
18356
  }
18303
18357
  async function handleCopilotStop(params) {
@@ -18432,6 +18486,7 @@ async function handleCopilotRecordPage(params) {
18432
18486
  path: new URL(normalizedUrl).pathname,
18433
18487
  title,
18434
18488
  slug: params.slug,
18489
+ stableId: `page:${params.slug}`,
18435
18490
  depth: new URL(normalizedUrl).pathname.split("/").filter(Boolean).length,
18436
18491
  screenshot: `screenshots/${params.slug}/default.png`,
18437
18492
  elements: [],
@@ -18499,6 +18554,21 @@ async function handleCopilotProposeAction(params) {
18499
18554
  message: "Proposal sent to companion UI. Call copilot_check_input to get the result."
18500
18555
  };
18501
18556
  }
18557
+ async function handleCopilotTrack(params) {
18558
+ const session = getSession(params.session_id);
18559
+ if (!session)
18560
+ throw new Error(`Session not found: ${params.session_id}`);
18561
+ logAction(session, params.action, params.result);
18562
+ syncAction(params.action, params.result);
18563
+ if (params.url) {
18564
+ updateCurrentPage(params.url, params.title ?? params.url);
18565
+ }
18566
+ return {
18567
+ success: true,
18568
+ action: params.action,
18569
+ tracked_at: new Date().toISOString()
18570
+ };
18571
+ }
18502
18572
 
18503
18573
  // src/mcp.ts
18504
18574
  var SERVER_INSTRUCTIONS = `
@@ -18515,6 +18585,7 @@ It produces markdown documentation organized as a knowledge network, with an int
18515
18585
 
18516
18586
  1. **Explore.** \`zefiro_explore({ baseUrl: "http://localhost:3000" })\` — BFS exploration of the application
18517
18587
  2. **Read.** \`zefiro_read_docs()\` — read the generated documentation (README or specific sections)
18588
+ 3. **Push.** \`zefiro_push({ apiKey: "..." })\` — push report to QA Intelligence (builds graph automatically)
18518
18589
 
18519
18590
  ## Output Structure
18520
18591
 
@@ -18530,6 +18601,7 @@ app-report/
18530
18601
 
18531
18602
  - \`zefiro_explore\` — explore a web application via BFS, generating documentation and screenshots
18532
18603
  - \`zefiro_read_docs\` — read generated documentation (README or specific section files)
18604
+ - \`zefiro_push\` — push exploration report to QA Intelligence (auto-builds graph with stable IDs)
18533
18605
  - \`zefiro_scan_codebase\` — scan project for test files, configs, and path aliases (static analysis helper)
18534
18606
 
18535
18607
  ## Copilot — Interactive Browser Co-Pilot
@@ -18556,8 +18628,16 @@ Use these tools when the user asks to explore, map, or document a running web ap
18556
18628
  - \`zefiro_copilot_get_session\` — Get session state (explored pages, stats)
18557
18629
  - \`zefiro_copilot_check_input\` — Read pending human input (voice/text)
18558
18630
  - \`zefiro_copilot_propose_action\` — ASSIST mode: propose action for approval
18631
+ - \`zefiro_copilot_track\` — Chrome Bridge: sync an external browser action to the session log
18632
+
18633
+ ### Chrome Bridge Mode
18634
+ Use \`browser: "chrome"\` with \`copilot_start\` to track actions from an external browser (e.g., Claude-in-Chrome).
18635
+ 1. \`zefiro_copilot_start({ baseUrl, mode: "assist", browser: "chrome" })\` — starts companion only
18636
+ 2. Open the \`companion_url\` in your browser
18637
+ 3. Use Claude-in-Chrome tools to browse; call \`zefiro_copilot_track\` after each action
18638
+ 4. \`zefiro_copilot_stop\` — generates report
18559
18639
  `.trim();
18560
- var server = new McpServer({ name: "zefiro", version: "0.7.4" }, { instructions: SERVER_INSTRUCTIONS });
18640
+ var server = new McpServer({ name: "zefiro", version: "0.10.0" }, { instructions: SERVER_INSTRUCTIONS });
18561
18641
  server.registerTool("zefiro_explore", {
18562
18642
  title: "Explore Application",
18563
18643
  description: "Explore a web application using browser automation (BFS). " + "Visits pages, takes screenshots, and generates markdown documentation. " + "Requires agent-browser to be installed globally.",
@@ -18569,8 +18649,8 @@ server.registerTool("zefiro_explore", {
18569
18649
  })
18570
18650
  }, async ({ baseUrl, maxPages, outputDir, headed }) => {
18571
18651
  try {
18572
- const { runExploration } = await import("./explorer-g6bmbak1.js");
18573
- const { generateReadme: generateReadme2, generateIndexHtml: generateIndexHtml2 } = await import("./report-sdtah1f4.js");
18652
+ const { runExploration } = await import("./explorer-ckk9bkff.js");
18653
+ const { generateReadme: generateReadme2, generateIndexHtml: generateIndexHtml2 } = await import("./report-wh0mnnxb.js");
18574
18654
  const { writeFile: writeFile2, ensureDir: ensureDir2 } = await import("./fs-9dhtxzmg.js");
18575
18655
  const cwd = process.cwd();
18576
18656
  const outDir = outputDir ? join6(cwd, outputDir) : join6(cwd, "app-report");
@@ -18588,6 +18668,8 @@ server.registerTool("zefiro_explore", {
18588
18668
  sessionName: "zefiro"
18589
18669
  };
18590
18670
  const state2 = await runExploration(config);
18671
+ const serializableState = { ...state2, pages: Object.fromEntries(state2.pages) };
18672
+ writeFile2(join6(outDir, ".exploration-state.json"), JSON.stringify(serializableState, null, 2));
18591
18673
  const readme = generateReadme2(state2);
18592
18674
  writeFile2(join6(outDir, "README.md"), readme);
18593
18675
  const html = generateIndexHtml2(state2);
@@ -18674,12 +18756,107 @@ server.registerTool("zefiro_scan_codebase", {
18674
18756
  return { content: [{ type: "text", text: `Error scanning codebase: ${err.message}` }], isError: true };
18675
18757
  }
18676
18758
  });
18759
+ server.registerTool("zefiro_push", {
18760
+ title: "Push Report",
18761
+ description: "Push the exploration report to QA Intelligence. " + "Builds a graph from the exploration state, encodes screenshots, and pushes to the API. " + "Requires a prior zefiro_explore run and an API key.",
18762
+ inputSchema: exports_external.object({
18763
+ apiKey: exports_external.string().describe("App API key for QA Intelligence"),
18764
+ apiUrl: exports_external.string().optional().describe("API base URL (default: https://qaligent.space)"),
18765
+ outputDir: exports_external.string().optional().describe("Exploration output directory (default: app-report)"),
18766
+ commitSha: exports_external.string().optional().describe("Git commit SHA to associate")
18767
+ })
18768
+ }, async ({ apiKey, apiUrl, outputDir, commitSha }) => {
18769
+ try {
18770
+ const { buildReportGraph, buildReportSections } = await import("./graph-builder-ca71yjkc.js");
18771
+ const cwd = process.cwd();
18772
+ const outDir = outputDir ? join6(cwd, outputDir) : join6(cwd, "app-report");
18773
+ const url = apiUrl ?? "https://qaligent.space";
18774
+ const statePath = join6(outDir, ".exploration-state.json");
18775
+ if (!existsSync5(statePath)) {
18776
+ return {
18777
+ content: [{ type: "text", text: "No exploration state found. Run zefiro_explore first." }],
18778
+ isError: true
18779
+ };
18780
+ }
18781
+ const raw = JSON.parse(readFileSync3(statePath, "utf-8"));
18782
+ const pages = new Map(Object.entries(raw.pages));
18783
+ const state2 = { ...raw, pages };
18784
+ const visitedPages = Array.from(pages.values()).filter((p) => p.status === "visited");
18785
+ const graph = buildReportGraph(state2);
18786
+ const sectionsDir = join6(outDir, "sections");
18787
+ const markdowns = new Map;
18788
+ if (existsSync5(sectionsDir)) {
18789
+ for (const file of readdirSync2(sectionsDir)) {
18790
+ if (file.endsWith(".md")) {
18791
+ markdowns.set(file.replace(/\.md$/, ""), readFileSync3(join6(sectionsDir, file), "utf-8"));
18792
+ }
18793
+ }
18794
+ }
18795
+ const sections = buildReportSections(visitedPages, markdowns);
18796
+ const screenshotsDir = join6(outDir, "screenshots");
18797
+ const screenshots = [];
18798
+ if (existsSync5(screenshotsDir)) {
18799
+ for (const slugDir of readdirSync2(screenshotsDir)) {
18800
+ const fullDir = join6(screenshotsDir, slugDir);
18801
+ try {
18802
+ const files = readdirSync2(fullDir);
18803
+ for (let i = 0;i < files.length; i++) {
18804
+ const file = files[i];
18805
+ if (!file.endsWith(".png") && !file.endsWith(".jpg"))
18806
+ continue;
18807
+ screenshots.push({
18808
+ sectionSlug: slugDir,
18809
+ filename: file,
18810
+ label: file.replace(/^\d+-/, "").replace(/\.\w+$/, ""),
18811
+ sortOrder: i,
18812
+ data: readFileSync3(join6(fullDir, file)).toString("base64")
18813
+ });
18814
+ }
18815
+ } catch {}
18816
+ }
18817
+ }
18818
+ const readmePath = join6(outDir, "README.md");
18819
+ const readme = existsSync5(readmePath) ? readFileSync3(readmePath, "utf-8") : `# Application Map
18820
+
18821
+ ${visitedPages.length} pages explored.`;
18822
+ const payload = {
18823
+ title: `Zefiro Scan — ${new Date().toISOString().split("T")[0]}`,
18824
+ readme,
18825
+ sections,
18826
+ graph,
18827
+ screenshots,
18828
+ commitSha: commitSha || undefined
18829
+ };
18830
+ const res = await fetch(`${url}/api/v2/push-report`, {
18831
+ method: "POST",
18832
+ headers: {
18833
+ "Content-Type": "application/json",
18834
+ Authorization: `Bearer ${apiKey}`
18835
+ },
18836
+ body: JSON.stringify(payload)
18837
+ });
18838
+ if (!res.ok) {
18839
+ const body = await res.text();
18840
+ return { content: [{ type: "text", text: `Push failed (${res.status}): ${body}` }], isError: true };
18841
+ }
18842
+ const result = await res.json();
18843
+ return {
18844
+ content: [{
18845
+ type: "text",
18846
+ text: JSON.stringify(result, null, 2)
18847
+ }]
18848
+ };
18849
+ } catch (err) {
18850
+ return { content: [{ type: "text", text: `Error pushing report: ${err.message}` }], isError: true };
18851
+ }
18852
+ });
18677
18853
  server.registerTool("zefiro_copilot_start", {
18678
18854
  title: "Start Copilot Session",
18679
18855
  description: "Start an interactive copilot session. Launches a browser and creates a session. " + "Returns a session_id to use with all other copilot tools.",
18680
18856
  inputSchema: exports_external.object({
18681
18857
  baseUrl: exports_external.string().describe("Base URL of the application (e.g., http://localhost:3000)"),
18682
18858
  mode: exports_external.enum(["auto", "assist", "manual"]).optional().describe("Copilot mode: auto (AI drives), assist (AI proposes, human approves), manual (human drives)"),
18859
+ browser: exports_external.enum(["playwright", "chrome"]).optional().describe('Browser engine: "playwright" (default, launches new browser) or "chrome" (companion-only, use with Claude-in-Chrome)'),
18683
18860
  headed: exports_external.boolean().optional().describe("Show browser window (default: true)"),
18684
18861
  authState: exports_external.string().optional().describe("Path to Playwright auth state JSON file"),
18685
18862
  outputDir: exports_external.string().optional().describe("Output directory for report (default: copilot-report)")
@@ -18877,6 +19054,24 @@ server.registerTool("zefiro_copilot_propose_action", {
18877
19054
  return { content: [{ type: "text", text: `Error: ${err.message}` }], isError: true };
18878
19055
  }
18879
19056
  });
19057
+ server.registerTool("zefiro_copilot_track", {
19058
+ title: "Track Chrome Action",
19059
+ description: "Track a browser action performed via external browser (e.g., Claude-in-Chrome). " + "Syncs the action to the companion UI and session log. Use after each chrome tool call.",
19060
+ inputSchema: exports_external.object({
19061
+ session_id: exports_external.string().describe("Session ID"),
19062
+ action: exports_external.string().describe('Description of the action (e.g., "navigate: https://example.com", "click: Submit button")'),
19063
+ url: exports_external.string().optional().describe("Current page URL after the action (updates companion page display)"),
19064
+ title: exports_external.string().optional().describe("Current page title"),
19065
+ result: exports_external.string().optional().describe("Action result or details")
19066
+ })
19067
+ }, async (params) => {
19068
+ try {
19069
+ const result = await handleCopilotTrack(params);
19070
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
19071
+ } catch (err) {
19072
+ return { content: [{ type: "text", text: `Error: ${err.message}` }], isError: true };
19073
+ }
19074
+ });
18880
19075
  async function main() {
18881
19076
  const transport = new StdioServerTransport;
18882
19077
  await server.connect(transport);
@@ -0,0 +1,12 @@
1
+ import {
2
+ generateIndexHtml,
3
+ generateReadme,
4
+ generateSectionMarkdown
5
+ } from "./cli-kjyet1n8.js";
6
+ import"./cli-5w708rbb.js";
7
+ import"./cli-wckvcay0.js";
8
+ export {
9
+ generateSectionMarkdown,
10
+ generateReadme,
11
+ generateIndexHtml
12
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zefiro",
3
- "version": "0.9.0",
3
+ "version": "0.10.0",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "zefiro": "./dist/cli.js",
@@ -29,20 +29,16 @@
29
29
  "devDependencies": {
30
30
  "@inquirer/prompts": "^8.3.0",
31
31
  "@qai/cli-auth": "workspace:*",
32
+ "@types/ws": "^8.18.1",
32
33
  "@types/node": "^24.0.14",
33
- "@types/react": "^18",
34
34
  "bun-types": "^1.3.11",
35
35
  "commander": "^13.1.0",
36
- "ink": "^5",
37
- "ink-text-input": "^6",
38
36
  "picocolors": "^1.1.1",
39
- "react": "^18",
40
37
  "typescript": "^5.8.3",
41
38
  "zod": "^4.0.5"
42
39
  },
43
40
  "dependencies": {
44
41
  "@modelcontextprotocol/sdk": "^1.27.1",
45
- "@types/ws": "^8.18.1",
46
42
  "ws": "^8.20.0"
47
43
  }
48
44
  }