@aayambansal/squint 0.3.4 → 0.4.1

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/README.md CHANGED
@@ -133,6 +133,7 @@ squint config set autoDev true # dev server starts with the TUI
133
133
  squint config set autoFix true # errors auto-route back (max 2 tries)
134
134
  squint config set autoCheck false # skip the per-turn typecheck+lint pass
135
135
  squint config set autoReview true # big visual change → automatic self-critique
136
+ squint config set fixModel haiku # mechanical fix turns run on the cheap tier
136
137
  squint config set theme ocean # amber · ocean · moss · rose · mono
137
138
  squint config set bell false # no bell on turn completion
138
139
  squint doctor # engines + Chrome + WebSocket check
@@ -147,7 +148,10 @@ squint doctor --probe # run every engine end to end, verify auth act
147
148
  order; `/queue clear` drops them. `Esc` interrupts the current turn.
148
149
  - **Editing**: a real line editor — arrows move, `alt+←/→` jump words, `ctrl+a/e/k/u/w`,
149
150
  `↑/↓` history. `ctrl+c` twice exits with a session summary.
150
- - **Problems**: findings from gates, the dev server, the runtime probe, and a11y sweeps
151
+ - **Flows**: declare user journeys as six readable lines in `.squint/flows/`; `/flows`
152
+ replays them headlessly and failing steps join the fix loop. `/score` snapshots quality
153
+ deterministically.
154
+ - **Problems**: findings from gates, the dev server, the runtime probe, a11y sweeps, and flows
151
155
  collect into a list — `/problems` shows it, `/fix` sends everything as one turn,
152
156
  `/fix <n>` targets one. The footer counts what's open.
153
157
  - **Sandbox**: `/sandbox on` and asks accumulate in a shadow worktree — the dev server,
@@ -2,10 +2,12 @@
2
2
  import {
3
3
  cdpCapture,
4
4
  hasWebSocket,
5
- pixelDiffPct
6
- } from "./chunk-XOVOQJFL.js";
5
+ pixelDiffPct,
6
+ runFlow
7
+ } from "./chunk-7YKO5MMN.js";
7
8
  export {
8
9
  cdpCapture,
9
10
  hasWebSocket,
10
- pixelDiffPct
11
+ pixelDiffPct,
12
+ runFlow
11
13
  };
@@ -178,6 +178,64 @@ var describe = (value) => {
178
178
  }
179
179
  return String(value);
180
180
  };
181
+ async function runFlow(chromePath, baseUrl, flow, outDir) {
182
+ const { stepExpression } = await import("./flows-6LK7NGWS.js");
183
+ const { child, wsUrl, profileDir } = await launchChrome(chromePath);
184
+ const shots = [];
185
+ let connection = null;
186
+ try {
187
+ connection = await CdpConnection.connect(wsUrl, 1e4);
188
+ const { targetId } = await connection.send("Target.createTarget", { url: "about:blank" });
189
+ const { sessionId } = await connection.send("Target.attachToTarget", { targetId, flatten: true });
190
+ await connection.send("Page.enable", {}, sessionId);
191
+ await connection.send(
192
+ "Emulation.setDeviceMetricsOverride",
193
+ { width: 1280, height: 800, deviceScaleFactor: 1, mobile: false },
194
+ sessionId
195
+ );
196
+ const base = baseUrl.replace(/\/+$/, "");
197
+ let stepNumber = 0;
198
+ for (const step of flow.steps) {
199
+ stepNumber += 1;
200
+ if (step.kind === "goto") {
201
+ const url = step.route === "/" ? base : `${base}${step.route}`;
202
+ await connection.send("Page.navigate", { url }, sessionId);
203
+ await new Promise((resolve) => setTimeout(resolve, 1800));
204
+ continue;
205
+ }
206
+ if (step.kind === "wait") {
207
+ await new Promise((resolve) => setTimeout(resolve, step.ms));
208
+ continue;
209
+ }
210
+ if (step.kind === "shot") {
211
+ const { data } = await connection.send("Page.captureScreenshot", { format: "png" }, sessionId);
212
+ const outPath = path.join(outDir, `flow-${flow.name}-${step.name}.png`);
213
+ fs.writeFileSync(outPath, Buffer.from(data, "base64"));
214
+ shots.push(outPath);
215
+ continue;
216
+ }
217
+ const expression = stepExpression(step);
218
+ if (!expression) continue;
219
+ const { result } = await connection.send(
220
+ "Runtime.evaluate",
221
+ { expression, returnByValue: true },
222
+ sessionId
223
+ );
224
+ const value = result?.value;
225
+ if (!value?.ok) {
226
+ return { ok: false, failedStep: stepNumber, detail: value?.detail ?? "step failed", shots };
227
+ }
228
+ await new Promise((resolve) => setTimeout(resolve, 300));
229
+ }
230
+ return { ok: true, shots };
231
+ } catch (err) {
232
+ return { ok: false, detail: err instanceof Error ? err.message : String(err), shots };
233
+ } finally {
234
+ connection?.close();
235
+ child.kill("SIGKILL");
236
+ setTimeout(() => fs.rmSync(profileDir, { recursive: true, force: true }), 500).unref?.();
237
+ }
238
+ }
181
239
  async function pixelDiffPct(chromePath, pngA, pngB) {
182
240
  const { child, wsUrl, profileDir } = await launchChrome(chromePath);
183
241
  let connection = null;
@@ -420,6 +478,7 @@ async function cdpCapture(chromePath, url, outDir, viewports, settleMs = 2500, a
420
478
 
421
479
  export {
422
480
  hasWebSocket,
481
+ runFlow,
423
482
  pixelDiffPct,
424
483
  cdpCapture
425
484
  };
@@ -14,6 +14,8 @@ var COMMANDS = [
14
14
  { name: "shot", args: "[url]", group: "verify", description: "screenshot the app (or any url) at mobile/tablet/desktop" },
15
15
  { name: "review", args: "[focus]", group: "verify", description: "screenshots + the engine critiques its own rendered work" },
16
16
  { name: "polish", args: "[1-5]", group: "verify", description: "unattended rounds of review \u2192 fix (default 2)" },
17
+ { name: "score", group: "verify", description: "deterministic quality snapshot (problems, a11y, tells, runtime, LCP)" },
18
+ { name: "flows", args: "[name]", group: "verify", description: "replay declared .squint/flows/ journeys headlessly" },
17
19
  { name: "variants", args: "<2-4> <ask>", group: "explore", description: "parallel design explorations; apply/list/clean" },
18
20
  { name: "sandbox", args: "[on|diff|apply|discard]", group: "explore", description: "asks accumulate in a shadow worktree until you apply" },
19
21
  { name: "undo", group: "explore", description: "revert the last ask (files only)" },
@@ -1,4 +1,7 @@
1
1
  #!/usr/bin/env node
2
+ import {
3
+ ensureSquintIgnore
4
+ } from "./chunk-O2S6PAJE.js";
2
5
  import {
3
6
  findChrome,
4
7
  screenshot
@@ -6,10 +9,7 @@ import {
6
9
  import {
7
10
  cdpCapture,
8
11
  hasWebSocket
9
- } from "./chunk-XOVOQJFL.js";
10
- import {
11
- ensureSquintIgnore
12
- } from "./chunk-O2S6PAJE.js";
12
+ } from "./chunk-7YKO5MMN.js";
13
13
 
14
14
  // src/preview/preview.ts
15
15
  import fs from "fs";
@@ -133,7 +133,7 @@ async function probeRuntime(url, cwd) {
133
133
  async function comparePulse(previous, current) {
134
134
  const chrome = findChrome();
135
135
  if (!chrome || !hasWebSocket()) return null;
136
- const { pixelDiffPct } = await import("./cdp-UOIP4ZQZ.js");
136
+ const { pixelDiffPct } = await import("./cdp-UGLMW5R5.js");
137
137
  return pixelDiffPct(chrome, previous, current);
138
138
  }
139
139
  function buildRuntimeFixPrompt(report) {
package/dist/cli.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  completeCommand
4
- } from "./chunk-BNJGHNXB.js";
4
+ } from "./chunk-SVKV77TO.js";
5
5
  import {
6
6
  applySandbox,
7
7
  discardSandbox,
@@ -42,7 +42,12 @@ import {
42
42
  comparePulse,
43
43
  probeRuntime,
44
44
  runtimeSummary
45
- } from "./chunk-SXF7CVRW.js";
45
+ } from "./chunk-UTBTLCUO.js";
46
+ import {
47
+ clearState,
48
+ loadState,
49
+ saveState
50
+ } from "./chunk-O2S6PAJE.js";
46
51
  import {
47
52
  runAgent
48
53
  } from "./chunk-VH7OOFQP.js";
@@ -53,18 +58,13 @@ import {
53
58
  detectEngines,
54
59
  getEngine
55
60
  } from "./chunk-KVYGPLWW.js";
56
- import "./chunk-XOVOQJFL.js";
61
+ import "./chunk-7YKO5MMN.js";
57
62
  import {
58
63
  enrich
59
64
  } from "./chunk-H5K55LXY.js";
60
65
  import {
61
66
  composePrompt
62
67
  } from "./chunk-P3H4N2EN.js";
63
- import {
64
- clearState,
65
- loadState,
66
- saveState
67
- } from "./chunk-O2S6PAJE.js";
68
68
 
69
69
  // src/cli.tsx
70
70
  import { Command } from "commander";
@@ -238,7 +238,7 @@ function registerEnv(program2) {
238
238
  }
239
239
  }
240
240
  const { findChrome: findChrome2 } = await import("./chrome-SBV3H77F.js");
241
- const { hasWebSocket } = await import("./cdp-UOIP4ZQZ.js");
241
+ const { hasWebSocket } = await import("./cdp-UGLMW5R5.js");
242
242
  const chrome = findChrome2();
243
243
  console.log(
244
244
  chrome ? `${pc2.green("\u2713")} Chrome ${pc2.dim(chrome)}` : `${pc2.yellow("\u25CB")} Chrome ${pc2.dim("\u2014 screenshots and runtime probing disabled")}`
@@ -461,7 +461,7 @@ function registerQuality(program2) {
461
461
  if (results.some((r) => !r.ok)) process.exitCode = 1;
462
462
  });
463
463
  program2.command("shot").description("Screenshot a running app at mobile/tablet/desktop viewports (+ .squint/routes)").argument("<url>", "URL of the running app (e.g. http://localhost:5173)").action(async (url) => {
464
- const { captureViewports: captureViewports2 } = await import("./preview-S6FOTRRR.js");
464
+ const { captureViewports: captureViewports2 } = await import("./preview-6MML2NEG.js");
465
465
  const result = await captureViewports2(process.cwd(), url);
466
466
  if (!result) {
467
467
  console.error(pc4.red("\u2717 no Chrome/Chromium found"));
@@ -554,7 +554,7 @@ function registerRun(program2) {
554
554
  import pc6 from "picocolors";
555
555
  function registerScaffold(program2) {
556
556
  program2.command("init").description("Scaffold a new Vite + React + TS + Tailwind app with token-first CSS").argument("[dir]", "target directory", ".").option("--force", "write into a non-empty directory").option("--no-install", "skip npm install").action(async (dir, options) => {
557
- const { installDependencies, writeTemplate } = await import("./init-LSYB32NS.js");
557
+ const { installDependencies, writeTemplate } = await import("./init-YB2IMA4N.js");
558
558
  let result;
559
559
  try {
560
560
  result = writeTemplate(dir, { force: options.force });
@@ -1474,6 +1474,81 @@ They are objective defects, not style preferences.`
1474
1474
  })();
1475
1475
  break;
1476
1476
  }
1477
+ case "flows": {
1478
+ if (!this.state.devUrl) {
1479
+ this.push("error", "dev server not running \u2014 /dev first");
1480
+ break;
1481
+ }
1482
+ void (async () => {
1483
+ const { loadFlows } = await import("./flows-6LK7NGWS.js");
1484
+ const flows = loadFlows(this.opts.cwd);
1485
+ if (flows.length === 0) {
1486
+ this.push("status", "no flows \u2014 add .squint/flows/<name>.flow (goto/click/fill/press/expect/shot lines), or ask the engine to write one");
1487
+ return;
1488
+ }
1489
+ const chrome = findChrome();
1490
+ if (!chrome) {
1491
+ this.push("error", "no Chrome/Chromium found for flows");
1492
+ return;
1493
+ }
1494
+ const { runFlow } = await import("./cdp-UGLMW5R5.js");
1495
+ const { previewDir } = await import("./preview-6MML2NEG.js");
1496
+ this.push("status", `replaying ${flows.length} flow(s)\u2026`);
1497
+ this.notify({ running: true, runStartedAt: Date.now() });
1498
+ const failures = [];
1499
+ for (const flow of flows) {
1500
+ const wanted = arg.trim();
1501
+ if (wanted && flow.name !== wanted) continue;
1502
+ const result = await runFlow(chrome, this.state.devUrl, flow, previewDir(this.opts.cwd));
1503
+ if (result.ok) {
1504
+ this.push("status", `\u2713 flow ${flow.name} \xB7 ${flow.steps.length} steps${result.shots.length > 0 ? ` \xB7 ${result.shots.length} shot(s)` : ""}`);
1505
+ for (const shot of result.shots) this.push("image", shot);
1506
+ } else {
1507
+ const where = result.failedStep ? ` at step ${result.failedStep}` : "";
1508
+ this.push("error", `\u2717 flow ${flow.name}${where}: ${result.detail}`);
1509
+ failures.push(`Flow "${flow.name}" fails${where}: ${result.detail}. The flow file is .squint/flows/${flow.name}.flow \u2014 fix the app (or the flow if the UI legitimately changed).`);
1510
+ }
1511
+ }
1512
+ this.notify({ running: false });
1513
+ if (failures.length > 0) {
1514
+ this.addProblem("flow", `${failures.length} flow(s) failing`, failures.join("\n\n"));
1515
+ this.push("status", "/fix sends open problems to the engine \xB7 /problems lists them");
1516
+ } else {
1517
+ this.clearProblems("flow");
1518
+ }
1519
+ this.drainQueue();
1520
+ })();
1521
+ break;
1522
+ }
1523
+ case "score": {
1524
+ if (!this.state.devUrl) {
1525
+ this.push("error", "dev server not running \u2014 /dev first");
1526
+ break;
1527
+ }
1528
+ void (async () => {
1529
+ const result = await this.capture();
1530
+ if (!result) return;
1531
+ const a11yCount = result.a11y?.length ?? 0;
1532
+ const slopCount = result.slop?.length ?? 0;
1533
+ const runtimeBad = result.runtime ? runtimeSummary(result.runtime) ? 1 : 0 : 0;
1534
+ const problems = this.state.problems.length;
1535
+ const score = Math.max(
1536
+ 0,
1537
+ 5 - problems * 0.75 - Math.min(a11yCount, 4) * 0.25 - Math.min(slopCount, 4) * 0.25 - runtimeBad
1538
+ );
1539
+ const lcp = this.lastPerf?.lcpMs !== void 0 ? ` \xB7 LCP ${this.lastPerf.lcpMs}ms` : "";
1540
+ this.push(
1541
+ "status",
1542
+ [
1543
+ `score: ${score.toFixed(2)}/5 (deterministic axes)`,
1544
+ ` open problems: ${problems}${problems > 0 ? ` (${this.state.problems.map((p) => p.source).join(", ")})` : ""}`,
1545
+ ` a11y findings: ${a11yCount} \xB7 distinctiveness tells: ${slopCount} \xB7 runtime ${runtimeBad ? "dirty" : "clean"}${lcp}`,
1546
+ " /review judges what numbers cannot \u2014 hierarchy, taste, coherence"
1547
+ ].join("\n")
1548
+ );
1549
+ })();
1550
+ break;
1551
+ }
1477
1552
  case "polish": {
1478
1553
  const rounds = arg ? Number.parseInt(arg, 10) : 2;
1479
1554
  if (!Number.isInteger(rounds) || rounds < 1 || rounds > 5) {
@@ -1739,7 +1814,7 @@ ${sandboxFiles(this.opts.cwd).join("\n")}`);
1739
1814
  this.notify({ items: [], totals: { costUsd: 0, turns: 0 } });
1740
1815
  break;
1741
1816
  case "help": {
1742
- void import("./commands-3HHBHBQV.js").then(({ commandHelp }) => this.push("status", commandHelp()));
1817
+ void import("./commands-DE5Q7VWY.js").then(({ commandHelp }) => this.push("status", commandHelp()));
1743
1818
  break;
1744
1819
  }
1745
1820
  case "quit":
@@ -2358,7 +2433,7 @@ function registerTui(program2) {
2358
2433
  }
2359
2434
 
2360
2435
  // src/cli.tsx
2361
- var VERSION = true ? "0.3.4" : "0.0.0-dev";
2436
+ var VERSION = true ? "0.4.1" : "0.0.0-dev";
2362
2437
  var program = new Command();
2363
2438
  program.name("squint").description("Lovable for your terminal \u2014 a frontend harness on top of Claude Code, Codex, and friends.").version(VERSION);
2364
2439
  registerRun(program);
@@ -3,7 +3,7 @@ import {
3
3
  COMMANDS,
4
4
  commandHelp,
5
5
  completeCommand
6
- } from "./chunk-BNJGHNXB.js";
6
+ } from "./chunk-SVKV77TO.js";
7
7
  export {
8
8
  COMMANDS,
9
9
  commandHelp,
@@ -0,0 +1,147 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/preview/flows.ts
4
+ import fs from "fs";
5
+ import path from "path";
6
+ function parseFlow(name, text) {
7
+ const steps = [];
8
+ for (const rawLine of text.split("\n")) {
9
+ const line = rawLine.trim();
10
+ if (line.length === 0 || line.startsWith("#")) continue;
11
+ const [verb, ...rest] = line.split(/\s+/);
12
+ const arg = rest.join(" ");
13
+ switch (verb) {
14
+ case "goto":
15
+ steps.push({ kind: "goto", route: arg.startsWith("/") ? arg : `/${arg}` });
16
+ break;
17
+ case "click":
18
+ steps.push({ kind: "click", target: arg });
19
+ break;
20
+ case "fill": {
21
+ const [selector, ...valueParts] = rest;
22
+ if (!selector || valueParts.length === 0) return null;
23
+ steps.push({ kind: "fill", selector, value: valueParts.join(" ") });
24
+ break;
25
+ }
26
+ case "press":
27
+ steps.push({ kind: "press", key: arg });
28
+ break;
29
+ case "expect":
30
+ steps.push({ kind: "expect", text: arg });
31
+ break;
32
+ case "shot":
33
+ steps.push({ kind: "shot", name: arg.replace(/[^a-zA-Z0-9-]/g, "-") });
34
+ break;
35
+ case "hover":
36
+ steps.push({ kind: "hover", target: arg });
37
+ break;
38
+ case "scroll":
39
+ steps.push({ kind: "scroll", target: arg || "bottom" });
40
+ break;
41
+ case "wait": {
42
+ const ms = Number.parseInt(arg, 10);
43
+ if (!Number.isInteger(ms) || ms < 0 || ms > 1e4) return null;
44
+ steps.push({ kind: "wait", ms });
45
+ break;
46
+ }
47
+ default:
48
+ return null;
49
+ }
50
+ }
51
+ return steps.length > 0 ? { name, steps } : null;
52
+ }
53
+ function loadFlows(cwd) {
54
+ const dir = path.join(cwd, ".squint", "flows");
55
+ let entries;
56
+ try {
57
+ entries = fs.readdirSync(dir).filter((f) => f.endsWith(".flow"));
58
+ } catch {
59
+ return [];
60
+ }
61
+ const flows = [];
62
+ for (const entry of entries.sort()) {
63
+ try {
64
+ const flow = parseFlow(entry.replace(/\.flow$/, ""), fs.readFileSync(path.join(dir, entry), "utf8"));
65
+ if (flow) flows.push(flow);
66
+ } catch {
67
+ }
68
+ }
69
+ return flows;
70
+ }
71
+ function stepExpression(step) {
72
+ switch (step.kind) {
73
+ case "click":
74
+ return `(() => {
75
+ const target = ${JSON.stringify(step.target)};
76
+ let el = null;
77
+ try { el = document.querySelector(target); } catch {}
78
+ if (!el) {
79
+ const all = [...document.querySelectorAll('a, button, [role=button], input[type=submit], summary, label')];
80
+ el = all.find((e) => (e.textContent || '').trim().toLowerCase().includes(target.toLowerCase()));
81
+ }
82
+ if (!el) return { ok: false, detail: 'no element matching ' + target };
83
+ el.click();
84
+ return { ok: true };
85
+ })()`;
86
+ case "fill":
87
+ return `(() => {
88
+ const el = document.querySelector(${JSON.stringify(step.selector)});
89
+ if (!el) return { ok: false, detail: 'no element matching ${step.selector}' };
90
+ const setter = Object.getOwnPropertyDescriptor(el.constructor.prototype, 'value')?.set;
91
+ if (setter) setter.call(el, ${JSON.stringify(step.value)}); else el.value = ${JSON.stringify(step.value)};
92
+ el.dispatchEvent(new Event('input', { bubbles: true }));
93
+ el.dispatchEvent(new Event('change', { bubbles: true }));
94
+ return { ok: true };
95
+ })()`;
96
+ case "expect":
97
+ return `(() => {
98
+ const wanted = ${JSON.stringify(step.text)}.toLowerCase();
99
+ const ok = (document.body.innerText || '').toLowerCase().includes(wanted);
100
+ return ok ? { ok: true } : { ok: false, detail: 'page does not show: ' + ${JSON.stringify(step.text)} };
101
+ })()`;
102
+ case "press":
103
+ return `(() => {
104
+ const key = ${JSON.stringify(step.key)};
105
+ const el = document.activeElement || document.body;
106
+ for (const type of ['keydown', 'keypress', 'keyup']) {
107
+ el.dispatchEvent(new KeyboardEvent(type, { key, bubbles: true, cancelable: true }));
108
+ }
109
+ return { ok: true };
110
+ })()`;
111
+ case "hover":
112
+ return `(() => {
113
+ const target = ${JSON.stringify(step.target)};
114
+ let el = null;
115
+ try { el = document.querySelector(target); } catch {}
116
+ if (!el) {
117
+ const all = [...document.querySelectorAll('*')];
118
+ el = all.find((e) => e.children.length === 0 && (e.textContent || '').trim().toLowerCase().includes(target.toLowerCase()));
119
+ }
120
+ if (!el) return { ok: false, detail: 'no element matching ' + target };
121
+ for (const type of ['pointerover', 'mouseover', 'mouseenter']) {
122
+ el.dispatchEvent(new MouseEvent(type, { bubbles: type !== 'mouseenter' }));
123
+ }
124
+ return { ok: true };
125
+ })()`;
126
+ case "scroll":
127
+ return `(() => {
128
+ const target = ${JSON.stringify(step.target)};
129
+ if (target === 'bottom') { window.scrollTo(0, document.body.scrollHeight); return { ok: true }; }
130
+ if (target === 'top') { window.scrollTo(0, 0); return { ok: true }; }
131
+ let el = null;
132
+ try { el = document.querySelector(target); } catch {}
133
+ if (!el) return { ok: false, detail: 'no element matching ' + target };
134
+ el.scrollIntoView({ block: 'center' });
135
+ return { ok: true };
136
+ })()`;
137
+ case "goto":
138
+ case "shot":
139
+ case "wait":
140
+ return null;
141
+ }
142
+ }
143
+ export {
144
+ loadFlows,
145
+ parseFlow,
146
+ stepExpression
147
+ };
@@ -139,6 +139,12 @@ body {
139
139
  dist/
140
140
  *.log
141
141
  .DS_Store
142
+ `,
143
+ ".squint/flows/home.flow": `# The starter journey: replayed headlessly by /flows after every change.
144
+ # Grow this file as the app grows \u2014 or ask the engine to.
145
+ goto /
146
+ expect Ready
147
+ shot home
142
148
  `
143
149
  };
144
150
  }
@@ -10,11 +10,11 @@ import {
10
10
  probeRuntime,
11
11
  routeShotName,
12
12
  runtimeSummary
13
- } from "./chunk-SXF7CVRW.js";
13
+ } from "./chunk-UTBTLCUO.js";
14
+ import "./chunk-O2S6PAJE.js";
14
15
  import "./chunk-IMDRXXFU.js";
15
16
  import "./chunk-KVYGPLWW.js";
16
- import "./chunk-XOVOQJFL.js";
17
- import "./chunk-O2S6PAJE.js";
17
+ import "./chunk-7YKO5MMN.js";
18
18
  export {
19
19
  VIEWPORTS,
20
20
  buildReviewPrompt,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aayambansal/squint",
3
- "version": "0.3.4",
3
+ "version": "0.4.1",
4
4
  "description": "Lovable for your terminal — a frontend harness on top of Claude Code, Codex, and other coding agents.",
5
5
  "type": "module",
6
6
  "license": "MIT",