@aayambansal/squint 0.1.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.
@@ -0,0 +1,156 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ lineSplitter
4
+ } from "./chunk-YIVPCWSG.js";
5
+
6
+ // src/devserver/devserver.ts
7
+ import { spawn } from "child_process";
8
+ import fs from "fs";
9
+ import path from "path";
10
+
11
+ // src/devserver/errors.ts
12
+ var ERROR_SIGNATURES = [
13
+ /\berror\b.*\bTS\d+/i,
14
+ // tsc: error TS2304
15
+ /✘ \[ERROR\]/,
16
+ // esbuild
17
+ /\[vite\].*error/i,
18
+ /Internal server error/i,
19
+ /Failed to compile/i,
20
+ // webpack / next
21
+ /Module not found/i,
22
+ /Cannot find module/i,
23
+ /SyntaxError:/,
24
+ /ReferenceError:/,
25
+ /TypeError:/,
26
+ /Unhandled Rejection/i,
27
+ /^error\b/i,
28
+ // generic leading "error:"
29
+ /\bERROR\b/
30
+ ];
31
+ var FALSE_POSITIVES = [
32
+ /0 errors?/i,
33
+ /no errors?/i,
34
+ /error-free/i,
35
+ /warnings? and 0/i
36
+ ];
37
+ function isErrorLine(line) {
38
+ if (FALSE_POSITIVES.some((re) => re.test(line))) return false;
39
+ return ERROR_SIGNATURES.some((re) => re.test(line));
40
+ }
41
+ var URL_RE = /(https?:\/\/(?:localhost|127\.0\.0\.1|0\.0\.0\.0):\d+[^\s]*)/;
42
+ function extractUrl(line) {
43
+ const match = URL_RE.exec(line);
44
+ if (!match || !match[1]) return null;
45
+ return match[1].replace(/\[[0-9;]*m/g, "").replace(/[.,)\]']+$/, "");
46
+ }
47
+
48
+ // src/devserver/devserver.ts
49
+ function detectDevCommand(cwd) {
50
+ let pkg;
51
+ try {
52
+ pkg = JSON.parse(fs.readFileSync(path.join(cwd, "package.json"), "utf8"));
53
+ } catch {
54
+ return null;
55
+ }
56
+ const script = pkg.scripts?.dev ? "dev" : pkg.scripts?.start ? "start" : null;
57
+ if (!script) return null;
58
+ const manager = fs.existsSync(path.join(cwd, "pnpm-lock.yaml")) ? "pnpm" : fs.existsSync(path.join(cwd, "yarn.lock")) ? "yarn" : fs.existsSync(path.join(cwd, "bun.lockb")) || fs.existsSync(path.join(cwd, "bun.lock")) ? "bun" : "npm";
59
+ const args = manager === "yarn" ? [script] : ["run", script];
60
+ return { command: manager, args, display: `${manager} ${args.join(" ")}` };
61
+ }
62
+ var BUFFER_LIMIT = 400;
63
+ var DevServer = class {
64
+ constructor(cwd, callbacks = {}) {
65
+ this.cwd = cwd;
66
+ this.callbacks = callbacks;
67
+ }
68
+ cwd;
69
+ callbacks;
70
+ child = null;
71
+ lines = [];
72
+ state = "stopped";
73
+ url = null;
74
+ start(command) {
75
+ if (this.child) return true;
76
+ const cmd = command ?? detectDevCommand(this.cwd);
77
+ if (!cmd) return false;
78
+ this.setState("starting");
79
+ this.lines = [];
80
+ this.url = null;
81
+ const child = spawn(cmd.command, cmd.args, {
82
+ cwd: this.cwd,
83
+ env: { ...process.env, FORCE_COLOR: "0" },
84
+ stdio: ["ignore", "pipe", "pipe"],
85
+ detached: false
86
+ });
87
+ this.child = child;
88
+ const capture = (chunk) => splitter.push(chunk);
89
+ const splitter = lineSplitter((line) => {
90
+ const clean = line.replace(/\[[0-9;]*m/g, "");
91
+ this.record(clean);
92
+ if (!this.url) {
93
+ const url = extractUrl(clean);
94
+ if (url) {
95
+ this.url = url;
96
+ this.setState("running");
97
+ this.callbacks.onUrl?.(url);
98
+ }
99
+ }
100
+ });
101
+ child.stdout?.setEncoding("utf8");
102
+ child.stdout?.on("data", capture);
103
+ child.stderr?.setEncoding("utf8");
104
+ child.stderr?.on("data", capture);
105
+ child.on("error", () => {
106
+ this.child = null;
107
+ this.setState("crashed");
108
+ });
109
+ child.on("close", () => {
110
+ this.child = null;
111
+ if (this.state !== "stopped") this.setState("crashed");
112
+ });
113
+ return true;
114
+ }
115
+ stop() {
116
+ if (!this.child) return;
117
+ this.setState("stopped");
118
+ this.child.kill("SIGTERM");
119
+ this.child = null;
120
+ }
121
+ /** Error lines captured after `since` (ms epoch), for fix-loop prompts. */
122
+ errorsSince(since) {
123
+ return this.lines.filter((l) => l.isError && l.at >= since).map((l) => l.text);
124
+ }
125
+ /** Recent raw output, newest last — context for fix prompts. */
126
+ tail(count) {
127
+ return this.lines.slice(-count).map((l) => l.text);
128
+ }
129
+ record(text) {
130
+ this.lines.push({ at: Date.now(), text, isError: isErrorLine(text) });
131
+ if (this.lines.length > BUFFER_LIMIT) this.lines.splice(0, this.lines.length - BUFFER_LIMIT);
132
+ }
133
+ setState(state) {
134
+ this.state = state;
135
+ this.callbacks.onStateChange?.(state);
136
+ }
137
+ };
138
+ function buildFixPrompt(errors, recentOutput) {
139
+ const errorBlock = errors.slice(-20).join("\n");
140
+ const context = recentOutput.slice(-30).join("\n");
141
+ return `The dev server reports errors after your last change. Diagnose and fix them. Do not paper over an error with try/catch \u2014 find the root cause.
142
+
143
+ ## Dev server errors
144
+
145
+ ${errorBlock}
146
+
147
+ ## Recent dev server output
148
+
149
+ ${context}`;
150
+ }
151
+
152
+ export {
153
+ detectDevCommand,
154
+ DevServer,
155
+ buildFixPrompt
156
+ };
@@ -0,0 +1,86 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ findEngineBinary,
4
+ lineSplitter,
5
+ truncate
6
+ } from "./chunk-YIVPCWSG.js";
7
+
8
+ // src/runner/run.ts
9
+ import { spawn } from "child_process";
10
+ function runAgent(engine, opts, onEvent, signal) {
11
+ return new Promise((resolve) => {
12
+ const binaryPath = findEngineBinary(engine);
13
+ if (!binaryPath) {
14
+ const error = `${engine.name} not found on PATH. Install it: ${engine.install}`;
15
+ onEvent({ type: "error", text: error });
16
+ resolve({ ok: false, error });
17
+ return;
18
+ }
19
+ const startedAt = Date.now();
20
+ let result = null;
21
+ let stderrTail = "";
22
+ const child = spawn(binaryPath, engine.buildArgs(opts), {
23
+ cwd: opts.cwd,
24
+ env: process.env,
25
+ stdio: ["ignore", "pipe", "pipe"]
26
+ });
27
+ const emit = (event) => {
28
+ if (event.type === "result") {
29
+ result = {
30
+ ok: event.ok,
31
+ sessionId: event.sessionId,
32
+ costUsd: event.costUsd,
33
+ durationMs: event.durationMs
34
+ };
35
+ }
36
+ onEvent(event);
37
+ };
38
+ const parse = engine.createParser?.();
39
+ const stdout = lineSplitter((line) => {
40
+ if (parse) {
41
+ for (const event of parse(line)) emit(event);
42
+ } else {
43
+ emit({ type: "text", text: line });
44
+ }
45
+ });
46
+ child.stdout.setEncoding("utf8");
47
+ child.stdout.on("data", (chunk) => stdout.push(chunk));
48
+ child.stderr.setEncoding("utf8");
49
+ child.stderr.on("data", (chunk) => {
50
+ stderrTail = truncate(stderrTail + chunk, 2e3);
51
+ });
52
+ const onAbort = () => child.kill("SIGTERM");
53
+ signal?.addEventListener("abort", onAbort, { once: true });
54
+ child.on("error", (err) => {
55
+ signal?.removeEventListener("abort", onAbort);
56
+ const error = `Failed to start ${engine.name}: ${err.message}`;
57
+ onEvent({ type: "error", text: error });
58
+ resolve({ ok: false, error });
59
+ });
60
+ child.on("close", (code) => {
61
+ signal?.removeEventListener("abort", onAbort);
62
+ stdout.flush();
63
+ if (signal?.aborted) {
64
+ onEvent({ type: "status", text: "interrupted" });
65
+ resolve({ ok: false, error: "interrupted", durationMs: Date.now() - startedAt });
66
+ return;
67
+ }
68
+ if (result) {
69
+ resolve({ ...result, durationMs: result.durationMs ?? Date.now() - startedAt });
70
+ return;
71
+ }
72
+ if (code === 0) {
73
+ resolve({ ok: true, durationMs: Date.now() - startedAt });
74
+ return;
75
+ }
76
+ const error = `${engine.name} exited with code ${code}${stderrTail ? `
77
+ ${stderrTail.trim()}` : ""}`;
78
+ onEvent({ type: "error", text: error });
79
+ resolve({ ok: false, error, durationMs: Date.now() - startedAt });
80
+ });
81
+ });
82
+ }
83
+
84
+ export {
85
+ runAgent
86
+ };
@@ -0,0 +1,74 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ findBinary
4
+ } from "./chunk-YIVPCWSG.js";
5
+
6
+ // src/preview/chrome.ts
7
+ import { spawn } from "child_process";
8
+ import fs from "fs";
9
+ var MAC_CHROMES = [
10
+ "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
11
+ "/Applications/Chromium.app/Contents/MacOS/Chromium",
12
+ "/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge",
13
+ "/Applications/Brave Browser.app/Contents/MacOS/Brave Browser"
14
+ ];
15
+ var PATH_CHROMES = ["google-chrome", "google-chrome-stable", "chromium", "chromium-browser"];
16
+ function findChrome() {
17
+ if (process.platform === "darwin") {
18
+ for (const candidate of MAC_CHROMES) {
19
+ try {
20
+ fs.accessSync(candidate, fs.constants.X_OK);
21
+ return candidate;
22
+ } catch {
23
+ }
24
+ }
25
+ }
26
+ for (const name of PATH_CHROMES) {
27
+ const found = findBinary(name);
28
+ if (found) return found;
29
+ }
30
+ return null;
31
+ }
32
+ function screenshot(chromePath, url, outPath, opts) {
33
+ return new Promise((resolve) => {
34
+ const args = [
35
+ "--headless=new",
36
+ "--disable-gpu",
37
+ // CI runners commonly need this; local runs keep the sandbox.
38
+ ...process.env.CI ? ["--no-sandbox"] : [],
39
+ "--hide-scrollbars",
40
+ "--force-device-scale-factor=1",
41
+ `--window-size=${opts.width},${opts.height}`,
42
+ `--virtual-time-budget=${opts.settleMs ?? 4e3}`,
43
+ `--screenshot=${outPath}`,
44
+ url
45
+ ];
46
+ const child = spawn(chromePath, args, { stdio: ["ignore", "ignore", "pipe"] });
47
+ let stderr = "";
48
+ child.stderr.setEncoding("utf8");
49
+ child.stderr.on("data", (chunk) => {
50
+ stderr += chunk;
51
+ });
52
+ const timer = setTimeout(() => {
53
+ child.kill("SIGKILL");
54
+ resolve({ ok: false, error: "screenshot timed out" });
55
+ }, opts.timeoutMs ?? 2e4);
56
+ child.on("error", (err) => {
57
+ clearTimeout(timer);
58
+ resolve({ ok: false, error: err.message });
59
+ });
60
+ child.on("close", () => {
61
+ clearTimeout(timer);
62
+ if (fs.existsSync(outPath) && fs.statSync(outPath).size > 0) {
63
+ resolve({ ok: true });
64
+ } else {
65
+ resolve({ ok: false, error: stderr.trim().split("\n").slice(-2).join(" ") || "no image produced" });
66
+ }
67
+ });
68
+ });
69
+ }
70
+
71
+ export {
72
+ findChrome,
73
+ screenshot
74
+ };
@@ -0,0 +1,63 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/prompt/brief.ts
4
+ import fs from "fs";
5
+ import path from "path";
6
+ var DEFAULT_BRIEF = `You are acting as a senior product designer and frontend engineer. Treat every change as production work, not a demo.
7
+
8
+ Direction before code:
9
+ - Commit to one specific visual direction derived from what this product is for, and state it in one sentence before implementing. If the direction could be guessed from the product category alone, sharpen it until it couldn't.
10
+ - Choose a color strategy deliberately: restrained (neutrals + one accent), committed (one dominant color owning 30\u201360% of the surface), or a small palette of 3\u20135 named roles. Never purple/violet gradients unless asked. Body text contrast stays at least 4.5:1 \u2014 no light gray body copy "for elegance".
11
+ - Typography: at most two families, paired on contrast (display serif + geometric sans, or sans + mono). Never Inter/Roboto/Open Sans/system defaults for display type. Pair weight extremes (300 against 700\u2013900); display sizes jump 3x over body, not 1.5x.
12
+
13
+ Tokens are the system:
14
+ - Define or extend design tokens (CSS variables / theme config) first, then compose the UI from them. Never scatter literal colors or one-off spacing values through components.
15
+ - One spacing rhythm on a 4/8px grid; one type scale with a ratio of at least 1.25 between steps.
16
+
17
+ Banned tells \u2014 these read instantly as machine-generated:
18
+ - Purple gradient on white; glassmorphism cards; cream/beige page background as a reflex "warmth" move
19
+ - Emoji as icons; identical icon-topped card grids; stat banner rows; numbered 01/02/03 section markers
20
+ - Tiny all-caps tracked eyebrow labels over every section; gradient text; colored left-border strips on cards
21
+ - Centered hero + badge + three feature cards; unmodified component-library defaults
22
+ If someone could look at the result and say "AI made that" without doubt, it has failed.
23
+
24
+ Craft details:
25
+ - Every interactive element gets hover, focus-visible, and active treatment; keyboard focus stays visible.
26
+ - Anything that loads data gets loading, empty, and error states.
27
+ - Motion: 150\u2013250ms ease-out; one orchestrated entrance with staggered reveals beats scattered micro-interactions; honor prefers-reduced-motion; never leave content invisible until a scroll observer fires.
28
+ - Body line length 65\u201375ch; text-wrap: balance on headings.
29
+
30
+ Engineering:
31
+ - Follow the repo's existing conventions and extend its patterns; new components in new files, small and focused; semantic HTML.
32
+ - Responsive from 360px up with no horizontal overflow \u2014 check intermediate widths, not just phone and desktop.
33
+ - Let errors surface instead of swallowing them in try/catch; log clearly so failures can be traced.
34
+ - Not done until the app builds cleanly, typechecks, and renders without console errors.`;
35
+ var FIRST_TURN_ADDENDUM = `This is the opening move on this task: establish the design foundation before building. State the visual direction, set up the tokens/theme first, then build components from them. The first render should feel like a designed product, not a scaffold \u2014 impressive on sight.`;
36
+ function loadBrief(cwd) {
37
+ const custom = path.join(cwd, ".squint", "brief.md");
38
+ try {
39
+ const text = fs.readFileSync(custom, "utf8").trim();
40
+ if (text.length > 0) return text;
41
+ } catch {
42
+ }
43
+ return DEFAULT_BRIEF;
44
+ }
45
+ function composePrompt(ask, opts) {
46
+ if (opts.noBrief) return ask;
47
+ const brief = loadBrief(opts.cwd);
48
+ const firstTurn = opts.firstTurn ?? true;
49
+ const addendum = firstTurn ? `
50
+
51
+ ${FIRST_TURN_ADDENDUM}` : "";
52
+ return `${brief}${addendum}
53
+
54
+ ## Task
55
+
56
+ ${ask}`;
57
+ }
58
+
59
+ export {
60
+ DEFAULT_BRIEF,
61
+ FIRST_TURN_ADDENDUM,
62
+ composePrompt
63
+ };
@@ -0,0 +1,197 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/preview/cdp.ts
4
+ import { spawn } from "child_process";
5
+ import fs from "fs";
6
+ import os from "os";
7
+ import path from "path";
8
+ function hasWebSocket() {
9
+ return typeof globalThis.WebSocket === "function";
10
+ }
11
+ var CdpConnection = class _CdpConnection {
12
+ ws;
13
+ nextId = 1;
14
+ pending = /* @__PURE__ */ new Map();
15
+ listeners = /* @__PURE__ */ new Map();
16
+ constructor(ws) {
17
+ this.ws = ws;
18
+ ws.addEventListener("message", (event) => {
19
+ let data;
20
+ try {
21
+ data = JSON.parse(String(event.data));
22
+ } catch {
23
+ return;
24
+ }
25
+ if (typeof data.id === "number") {
26
+ const entry = this.pending.get(data.id);
27
+ if (entry) {
28
+ this.pending.delete(data.id);
29
+ if (data.error) entry.reject(new Error(data.error.message ?? "CDP error"));
30
+ else entry.resolve(data.result);
31
+ }
32
+ } else if (typeof data.method === "string") {
33
+ this.listeners.get(data.method)?.(data.params, data.sessionId);
34
+ }
35
+ });
36
+ }
37
+ static connect(url, timeoutMs) {
38
+ return new Promise((resolve, reject) => {
39
+ const ws = new WebSocket(url);
40
+ const timer = setTimeout(() => {
41
+ ws.close();
42
+ reject(new Error("CDP connect timeout"));
43
+ }, timeoutMs);
44
+ ws.addEventListener("open", () => {
45
+ clearTimeout(timer);
46
+ resolve(new _CdpConnection(ws));
47
+ });
48
+ ws.addEventListener("error", () => {
49
+ clearTimeout(timer);
50
+ reject(new Error("CDP connect failed"));
51
+ });
52
+ });
53
+ }
54
+ send(method, params = {}, sessionId) {
55
+ const id = this.nextId++;
56
+ return new Promise((resolve, reject) => {
57
+ this.pending.set(id, { resolve, reject });
58
+ this.ws.send(JSON.stringify({ id, method, params, ...sessionId ? { sessionId } : {} }));
59
+ setTimeout(() => {
60
+ if (this.pending.has(id)) {
61
+ this.pending.delete(id);
62
+ reject(new Error(`CDP timeout: ${method}`));
63
+ }
64
+ }, 15e3);
65
+ });
66
+ }
67
+ on(method, handler) {
68
+ this.listeners.set(method, handler);
69
+ }
70
+ close() {
71
+ try {
72
+ this.ws.close();
73
+ } catch {
74
+ }
75
+ }
76
+ };
77
+ function launchChrome(chromePath) {
78
+ return new Promise((resolve, reject) => {
79
+ const profileDir = fs.mkdtempSync(path.join(os.tmpdir(), "squint-chrome-"));
80
+ const child = spawn(
81
+ chromePath,
82
+ [
83
+ "--headless=new",
84
+ "--disable-gpu",
85
+ ...process.env.CI ? ["--no-sandbox"] : [],
86
+ "--no-first-run",
87
+ "--remote-debugging-port=0",
88
+ `--user-data-dir=${profileDir}`,
89
+ "about:blank"
90
+ ],
91
+ { stdio: ["ignore", "ignore", "pipe"] }
92
+ );
93
+ let stderr = "";
94
+ const timer = setTimeout(() => {
95
+ child.kill("SIGKILL");
96
+ reject(new Error("Chrome did not announce DevTools endpoint"));
97
+ }, 15e3);
98
+ child.stderr.setEncoding("utf8");
99
+ child.stderr.on("data", (chunk) => {
100
+ stderr += chunk;
101
+ const match = /DevTools listening on (ws:\/\/[^\s]+)/.exec(stderr);
102
+ if (match && match[1]) {
103
+ clearTimeout(timer);
104
+ resolve({ child, wsUrl: match[1], profileDir });
105
+ }
106
+ });
107
+ child.on("error", (err) => {
108
+ clearTimeout(timer);
109
+ reject(err);
110
+ });
111
+ child.on("close", () => clearTimeout(timer));
112
+ });
113
+ }
114
+ var describe = (value) => {
115
+ if (value == null) return "";
116
+ if (typeof value === "object") {
117
+ return String(value.description ?? value.value ?? JSON.stringify(value));
118
+ }
119
+ return String(value);
120
+ };
121
+ async function cdpCapture(chromePath, url, outDir, viewports, settleMs = 2500) {
122
+ const { child, wsUrl, profileDir } = await launchChrome(chromePath);
123
+ const report = { consoleErrors: [], pageErrors: [], failedRequests: [] };
124
+ const shots = [];
125
+ const requests = /* @__PURE__ */ new Map();
126
+ let connection = null;
127
+ try {
128
+ connection = await CdpConnection.connect(wsUrl, 1e4);
129
+ const { targetId } = await connection.send("Target.createTarget", { url: "about:blank" });
130
+ const { sessionId } = await connection.send("Target.attachToTarget", { targetId, flatten: true });
131
+ connection.on("Runtime.consoleAPICalled", (params) => {
132
+ if (params.type === "error" || params.type === "assert") {
133
+ const text = (params.args ?? []).map(describe).join(" ");
134
+ if (text) report.consoleErrors.push(text);
135
+ }
136
+ });
137
+ connection.on("Runtime.exceptionThrown", (params) => {
138
+ const detail = params.exceptionDetails;
139
+ const text = detail?.exception?.description ?? detail?.text;
140
+ if (text) report.pageErrors.push(String(text).split("\n").slice(0, 3).join("\n"));
141
+ });
142
+ connection.on("Network.requestWillBeSent", (params) => {
143
+ requests.set(params.requestId, params.request?.url ?? "unknown");
144
+ });
145
+ connection.on("Network.responseReceived", (params) => {
146
+ const status = params.response?.status ?? 0;
147
+ if (status >= 400) {
148
+ report.failedRequests.push(`${status} ${params.response?.url ?? requests.get(params.requestId) ?? ""}`);
149
+ }
150
+ });
151
+ connection.on("Network.loadingFailed", (params) => {
152
+ if (params.canceled) return;
153
+ const target = requests.get(params.requestId);
154
+ if (target) report.failedRequests.push(`${params.errorText ?? "failed"} ${target}`);
155
+ });
156
+ let loaded = false;
157
+ connection.on("Page.loadEventFired", () => {
158
+ loaded = true;
159
+ });
160
+ await connection.send("Runtime.enable", {}, sessionId);
161
+ await connection.send("Network.enable", {}, sessionId);
162
+ await connection.send("Page.enable", {}, sessionId);
163
+ await connection.send("Page.navigate", { url }, sessionId);
164
+ const deadline = Date.now() + 12e3;
165
+ while (!loaded && Date.now() < deadline) {
166
+ await new Promise((resolve) => setTimeout(resolve, 100));
167
+ }
168
+ await new Promise((resolve) => setTimeout(resolve, settleMs));
169
+ for (const viewport of viewports) {
170
+ await connection.send(
171
+ "Emulation.setDeviceMetricsOverride",
172
+ {
173
+ width: viewport.width,
174
+ height: viewport.height,
175
+ deviceScaleFactor: 1,
176
+ mobile: viewport.width < 500
177
+ },
178
+ sessionId
179
+ );
180
+ await new Promise((resolve) => setTimeout(resolve, 250));
181
+ const { data } = await connection.send("Page.captureScreenshot", { format: "png" }, sessionId);
182
+ const outPath = path.join(outDir, `${viewport.name}.png`);
183
+ fs.writeFileSync(outPath, Buffer.from(data, "base64"));
184
+ shots.push({ name: viewport.name, path: outPath });
185
+ }
186
+ } finally {
187
+ connection?.close();
188
+ child.kill("SIGKILL");
189
+ setTimeout(() => fs.rmSync(profileDir, { recursive: true, force: true }), 500).unref?.();
190
+ }
191
+ return { report, shots };
192
+ }
193
+
194
+ export {
195
+ hasWebSocket,
196
+ cdpCapture
197
+ };