@aayambansal/squint 0.8.2 → 0.9.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.
@@ -1,11 +1,11 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  App
4
- } from "./chunk-PPC2KM7P.js";
5
- import "./chunk-B356UR44.js";
4
+ } from "./chunk-6M54VGLM.js";
5
+ import "./chunk-MWPZCXN7.js";
6
6
  import "./chunk-YKEAKVIS.js";
7
7
  import "./chunk-LNJ7O5O4.js";
8
- import "./chunk-3ZHETLUC.js";
8
+ import "./chunk-KPLEM7SV.js";
9
9
  import "./chunk-UBADCBPT.js";
10
10
  import "./chunk-IEQGTVWN.js";
11
11
  import "./chunk-AUJJGMZG.js";
@@ -16,10 +16,10 @@ import "./chunk-K5QJMSJH.js";
16
16
  import "./chunk-WAJXATCO.js";
17
17
  import "./chunk-62JNF5M2.js";
18
18
  import "./chunk-7CAGWFAQ.js";
19
- import "./chunk-4DZAWPS2.js";
19
+ import "./chunk-MITMSC2W.js";
20
20
  import "./chunk-55JZKI2M.js";
21
21
  import "./chunk-HIUQ4FXO.js";
22
- import "./chunk-6RMF4NAW.js";
22
+ import "./chunk-AYJQYDN6.js";
23
23
  export {
24
24
  App
25
25
  };
@@ -6,7 +6,7 @@ import {
6
6
  pixelDiffPct,
7
7
  runFlow,
8
8
  summarizeSoftNav
9
- } from "./chunk-4DZAWPS2.js";
9
+ } from "./chunk-MITMSC2W.js";
10
10
  export {
11
11
  cdpCapture,
12
12
  hasWebSocket,
@@ -2,7 +2,7 @@
2
2
  import {
3
3
  loadChecks,
4
4
  runIntervalSweep
5
- } from "./chunk-6RMF4NAW.js";
5
+ } from "./chunk-AYJQYDN6.js";
6
6
  export {
7
7
  loadChecks,
8
8
  runIntervalSweep
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  Session
4
- } from "./chunk-B356UR44.js";
4
+ } from "./chunk-MWPZCXN7.js";
5
5
  import {
6
6
  completeCommand
7
7
  } from "./chunk-LNJ7O5O4.js";
@@ -44,7 +44,7 @@ async function runIntervalSweep(cwd, url, lastRun) {
44
44
  const chrome = findChrome();
45
45
  if (!chrome) return [];
46
46
  const os = await import("os");
47
- const { cdpCapture } = await import("./cdp-6YBMFE7M.js");
47
+ const { cdpCapture } = await import("./cdp-E6VAQ7XL.js");
48
48
  const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "squint-interval-"));
49
49
  try {
50
50
  const result = await cdpCapture(chrome, url, tmp, [], 800, false, due);
@@ -143,8 +143,8 @@ function stepExpression(step) {
143
143
  async function suggestFlows(cwd, baseUrl, chromePath) {
144
144
  const fs2 = await import("fs");
145
145
  const path2 = await import("path");
146
- const { cdpCapture } = await import("./cdp-6YBMFE7M.js");
147
- const { loadRoutes } = await import("./preview-UV67QLVI.js");
146
+ const { cdpCapture } = await import("./cdp-E6VAQ7XL.js");
147
+ const { loadRoutes } = await import("./preview-M6VBOTQZ.js");
148
148
  const dir = path2.join(cwd, ".squint", "flows");
149
149
  fs2.mkdirSync(dir, { recursive: true });
150
150
  const created = [];
@@ -5,14 +5,14 @@ import {
5
5
  import {
6
6
  cdpCapture,
7
7
  hasWebSocket
8
- } from "./chunk-4DZAWPS2.js";
8
+ } from "./chunk-MITMSC2W.js";
9
9
  import {
10
10
  findChrome,
11
11
  screenshot
12
12
  } from "./chunk-55JZKI2M.js";
13
13
  import {
14
14
  loadChecks
15
- } from "./chunk-6RMF4NAW.js";
15
+ } from "./chunk-AYJQYDN6.js";
16
16
 
17
17
  // src/preview/preview.ts
18
18
  import fs from "fs";
@@ -51,7 +51,7 @@ async function captureViewports(cwd, url) {
51
51
  if (hasWebSocket()) {
52
52
  try {
53
53
  const checks = loadChecks(cwd);
54
- const { report, shots: shots2, a11y, slop, narration, phantoms, viewTransitions, components, checkFailures, webmcp, jank, locale } = await cdpCapture(chrome, url, dir, VIEWPORTS, 2500, true, checks);
54
+ const { report, shots: shots2, a11y, slop, narration, phantoms, viewTransitions, components, checkFailures, webmcp, jank, locale, speculation } = await cdpCapture(chrome, url, dir, VIEWPORTS, 2500, true, checks);
55
55
  const errors2 = [];
56
56
  for (const route of routes.slice(1)) {
57
57
  try {
@@ -66,7 +66,7 @@ async function captureViewports(cwd, url) {
66
66
  errors2.push(`${route}: capture failed`);
67
67
  }
68
68
  }
69
- return { shots: shots2, errors: errors2, runtime: report, a11y, slop, narration, phantoms, viewTransitions, components, checkFailures, webmcp, jank, locale };
69
+ return { shots: shots2, errors: errors2, runtime: report, a11y, slop, narration, phantoms, viewTransitions, components, checkFailures, webmcp, jank, locale, speculation };
70
70
  } catch {
71
71
  }
72
72
  }
@@ -139,13 +139,13 @@ async function probeRuntime(url, cwd) {
139
139
  async function comparePulse(previous, current) {
140
140
  const chrome = findChrome();
141
141
  if (!chrome || !hasWebSocket()) return null;
142
- const { pixelDiffPct } = await import("./cdp-6YBMFE7M.js");
142
+ const { pixelDiffPct } = await import("./cdp-E6VAQ7XL.js");
143
143
  return pixelDiffPct(chrome, previous, current);
144
144
  }
145
145
  async function comparePulseAttributed(previous, current, url, outPath) {
146
146
  const chrome = findChrome();
147
147
  if (!chrome || !hasWebSocket()) return null;
148
- const { pixelDiffAttributed } = await import("./cdp-6YBMFE7M.js");
148
+ const { pixelDiffAttributed } = await import("./cdp-E6VAQ7XL.js");
149
149
  return pixelDiffAttributed(chrome, previous, current, url, outPath);
150
150
  }
151
151
  function buildRuntimeFixPrompt(report) {
@@ -173,6 +173,16 @@ ${narration.join("\n")}
173
173
 
174
174
  Judge this narration as an experience: does the reading order make sense? do names actually describe their targets? is anything announced as "(no accessible name)"? Fix real incoherence \u2014 this is how non-visual users meet the page.`;
175
175
  }
176
+ function speculationSection(speculation) {
177
+ if (!speculation || speculation.length === 0) return "";
178
+ return `
179
+
180
+ ## Speculation-rules findings
181
+
182
+ ${speculation.join("\n")}
183
+
184
+ Invalid rule sets silently disable speculative loading; failed prefetch/prerender attempts waste the declaration. Fix the rules (or the target pages), don't remove them.`;
185
+ }
176
186
  function localeSection(locale) {
177
187
  if (!locale || locale.length === 0) return "";
178
188
  return `
@@ -243,13 +253,13 @@ ${findings.join("\n")}
243
253
 
244
254
  These patterns make the page read as template output. Rework them within the committed design direction \u2014 this is style debt, not a defect list.`;
245
255
  }
246
- function buildReviewPrompt(shots, extra, runtime, a11y, slop, narration, phantoms, viewTransitions, components, webmcp, jank, locale) {
256
+ function buildReviewPrompt(shots, extra, runtime, a11y, slop, narration, phantoms, viewTransitions, components, webmcp, jank, locale, speculation) {
247
257
  const list = shots.map((s) => `- ${s.name}: ${s.path}`).join("\n");
248
258
  return `Screenshots of the running app were just captured:
249
259
 
250
260
  ${list}
251
261
 
252
- Read each screenshot and review the rendered UI against the design standards you were given. Check: visual hierarchy and spacing rhythm, typography, color and contrast, alignment, empty-looking or broken regions, and whether the mobile capture shows horizontal overflow or cramped layout. List the concrete issues you can SEE (not hypothetical ones), ranked by visual impact${extra ? `, with special attention to: ${extra}` : ""}. Then fix them and verify the app still builds.${runtimeSection(runtime)}${a11ySection(a11y)}${slopSection(slop)}${narrationSection(narration)}${phantomSection(phantoms)}${vtSection(viewTransitions)}${componentSection(components)}${webmcpSection(webmcp)}${jankSection(jank)}${localeSection(locale)}`;
262
+ Read each screenshot and review the rendered UI against the design standards you were given. Check: visual hierarchy and spacing rhythm, typography, color and contrast, alignment, empty-looking or broken regions, and whether the mobile capture shows horizontal overflow or cramped layout. List the concrete issues you can SEE (not hypothetical ones), ranked by visual impact${extra ? `, with special attention to: ${extra}` : ""}. Then fix them and verify the app still builds.${runtimeSection(runtime)}${a11ySection(a11y)}${slopSection(slop)}${narrationSection(narration)}${phantomSection(phantoms)}${vtSection(viewTransitions)}${componentSection(components)}${webmcpSection(webmcp)}${jankSection(jank)}${localeSection(locale)}${speculationSection(speculation)}`;
253
263
  }
254
264
 
255
265
  export {
@@ -211,7 +211,7 @@ function summarizeSoftNav(entries) {
211
211
  });
212
212
  }
213
213
  async function runFlow(chromePath, baseUrl, flow, outDir) {
214
- const { stepExpression } = await import("./flows-4QS2ER6N.js");
214
+ const { stepExpression } = await import("./flows-EHUNUHED.js");
215
215
  const { child, wsUrl, profileDir } = await launchChrome(chromePath);
216
216
  const shots = [];
217
217
  let transitions = [];
@@ -879,6 +879,7 @@ async function cdpCapture(chromePath, url, outDir, viewports, settleMs = 2500, a
879
879
  const checkFailures = [];
880
880
  let webmcp = [];
881
881
  let locale = [];
882
+ const speculation = [];
882
883
  let jank = [];
883
884
  const requests = /* @__PURE__ */ new Map();
884
885
  let connection = null;
@@ -897,6 +898,22 @@ async function cdpCapture(chromePath, url, outDir, viewports, settleMs = 2500, a
897
898
  const text = detail?.exception?.description ?? detail?.text;
898
899
  if (text) report.pageErrors.push(String(text).split("\n").slice(0, 3).join("\n"));
899
900
  });
901
+ connection.on("Preload.ruleSetUpdated", (params) => {
902
+ const rs = params.ruleSet;
903
+ if (rs?.errorType) {
904
+ speculation.push(`speculation: rule set invalid (${rs.errorType}) \u2014 ${String(rs.errorMessage ?? "").slice(0, 120)}`);
905
+ }
906
+ });
907
+ connection.on("Preload.prefetchStatusUpdated", (params) => {
908
+ if (params.status === "Failure") {
909
+ speculation.push(`speculation: prefetch failed for ${params.prefetchUrl ?? "?"}`);
910
+ }
911
+ });
912
+ connection.on("Preload.prerenderStatusUpdated", (params) => {
913
+ if (params.status === "Failure") {
914
+ speculation.push(`speculation: prerender failed${params.disallowedMojoInterface ? ` (${params.disallowedMojoInterface})` : ""}${params.prerenderStatus ? ` \u2014 ${params.prerenderStatus}` : ""}`);
915
+ }
916
+ });
900
917
  connection.on("Network.requestWillBeSent", (params) => {
901
918
  requests.set(params.requestId, params.request?.url ?? "unknown");
902
919
  });
@@ -918,6 +935,7 @@ async function cdpCapture(chromePath, url, outDir, viewports, settleMs = 2500, a
918
935
  await connection.send("Runtime.enable", {}, sessionId);
919
936
  await connection.send("Network.enable", {}, sessionId);
920
937
  await connection.send("Page.enable", {}, sessionId);
938
+ await connection.send("Preload.enable", {}, sessionId).catch(() => null);
921
939
  await connection.send("Page.addScriptToEvaluateOnNewDocument", { source: WEBMCP_SHIM }, sessionId).catch(() => null);
922
940
  await connection.send("Page.addScriptToEvaluateOnNewDocument", { source: LOAF_SHIM }, sessionId).catch(() => null);
923
941
  await connection.send("Page.navigate", { url }, sessionId);
@@ -1201,7 +1219,7 @@ async function cdpCapture(chromePath, url, outDir, viewports, settleMs = 2500, a
1201
1219
  child.kill("SIGKILL");
1202
1220
  setTimeout(() => fs.rmSync(profileDir, { recursive: true, force: true }), 500).unref?.();
1203
1221
  }
1204
- return { report, shots, a11y, slop, perf, narration, phantoms, viewTransitions, components, checkFailures, webmcp, jank, locale };
1222
+ return { report, shots, a11y, slop, perf, narration, phantoms, viewTransitions, components, checkFailures, webmcp, jank, locale, speculation };
1205
1223
  }
1206
1224
 
1207
1225
  export {
@@ -12,7 +12,7 @@ import {
12
12
  comparePulseAttributed,
13
13
  probeRuntime,
14
14
  runtimeSummary
15
- } from "./chunk-3ZHETLUC.js";
15
+ } from "./chunk-KPLEM7SV.js";
16
16
  import {
17
17
  enrich
18
18
  } from "./chunk-UBADCBPT.js";
@@ -117,6 +117,7 @@ var Session = class {
117
117
  engineId: opts.engineId,
118
118
  model: opts.model,
119
119
  devState: "stopped",
120
+ pendingApproval: null,
120
121
  devUrl: null,
121
122
  totals: { costUsd: 0, turns: 0 },
122
123
  queue: [],
@@ -155,7 +156,6 @@ var Session = class {
155
156
  checkpoints = [];
156
157
  fixAttempts = 0;
157
158
  reviewTipShown = false;
158
- pendingApproval = null;
159
159
  goal = null;
160
160
  laneEnabled = false;
161
161
  inLane = false;
@@ -274,7 +274,7 @@ ${question}`,
274
274
  return;
275
275
  }
276
276
  await this.runTurn(
277
- buildReviewPrompt(result.shots, void 0, result.runtime, result.a11y, result.slop, result.narration, result.phantoms, result.viewTransitions, result.components, result.webmcp, result.jank, result.locale),
277
+ buildReviewPrompt(result.shots, void 0, result.runtime, result.a11y, result.slop, result.narration, result.phantoms, result.viewTransitions, result.components, result.webmcp, result.jank, result.locale, result.speculation),
278
278
  `\u{1F441} polish round ${round}/${rounds}`
279
279
  );
280
280
  }
@@ -496,7 +496,7 @@ ${question}`,
496
496
  const req = JSON.parse(fs2.readFileSync(reqPath, "utf8"));
497
497
  fs2.rmSync(reqPath, { force: true });
498
498
  if (typeof req?.summary === "string" && req.summary.length > 0) {
499
- this.pendingApproval = req.summary;
499
+ this.notify({ pendingApproval: req.summary });
500
500
  const shot = typeof req.screenshot === "string" ? path2.resolve(this.execCwd(), req.screenshot) : null;
501
501
  if (shot && fs2.existsSync(shot)) this.push("image", shot);
502
502
  this.push("status", `\u23F8 approval requested: ${req.summary}
@@ -665,7 +665,7 @@ ${probe.checkFailures.slice(0, 5).join("\n")}`);
665
665
  const captureResult = await this.capture();
666
666
  if (captureResult) {
667
667
  await this.runTurn(
668
- buildReviewPrompt(captureResult.shots, void 0, captureResult.runtime, captureResult.a11y, captureResult.slop, captureResult.narration, captureResult.phantoms, captureResult.viewTransitions, captureResult.components, captureResult.webmcp, captureResult.jank, captureResult.locale),
668
+ buildReviewPrompt(captureResult.shots, void 0, captureResult.runtime, captureResult.a11y, captureResult.slop, captureResult.narration, captureResult.phantoms, captureResult.viewTransitions, captureResult.components, captureResult.webmcp, captureResult.jank, captureResult.locale, captureResult.speculation),
669
669
  "\u{1F441} auto-review rendered UI"
670
670
  );
671
671
  }
@@ -812,7 +812,7 @@ squint verifies every turn \u2014 gates, runtime probe, page audits. Do not decl
812
812
  this.push("image", pulsePath);
813
813
  return null;
814
814
  }
815
- const { previewDir } = await import("./preview-UV67QLVI.js");
815
+ const { previewDir } = await import("./preview-M6VBOTQZ.js");
816
816
  const diff = await comparePulseAttributed(
817
817
  previous,
818
818
  current,
@@ -1069,12 +1069,12 @@ every ask now carries it; auto-fix presses to ${MAX_GOAL_FIX_ATTEMPTS} attempts
1069
1069
  }
1070
1070
  case "yes":
1071
1071
  case "no": {
1072
- if (!this.pendingApproval) {
1072
+ if (!this.state.pendingApproval) {
1073
1073
  this.push("status", `nothing awaiting approval \u2014 /${name} answers an engine's approval request`);
1074
1074
  break;
1075
1075
  }
1076
- const summary = this.pendingApproval;
1077
- this.pendingApproval = null;
1076
+ const summary = this.state.pendingApproval;
1077
+ this.notify({ pendingApproval: null });
1078
1078
  const approved = name === "yes";
1079
1079
  appendDecision(this.opts.cwd, {
1080
1080
  decision: `${approved ? "approved" : "rejected"}: ${summary}${arg ? ` \u2014 ${arg}` : ""}`,
@@ -1190,7 +1190,7 @@ every ask now carries it; auto-fix presses to ${MAX_GOAL_FIX_ATTEMPTS} attempts
1190
1190
  this.push("error", "no Chrome/Chromium found");
1191
1191
  return;
1192
1192
  }
1193
- const { suggestFlows } = await import("./flows-4QS2ER6N.js");
1193
+ const { suggestFlows } = await import("./flows-EHUNUHED.js");
1194
1194
  this.push("status", "drafting flows from the live routes\u2026");
1195
1195
  const { created, skipped } = await suggestFlows(this.opts.cwd, this.state.devUrl, chrome);
1196
1196
  if (created.length > 0) this.push("status", `drafted ${created.length} flow(s): ${created.join(", ")} \u2014 edit .squint/flows/*.flow or ask the engine to deepen them`);
@@ -1200,7 +1200,7 @@ every ask now carries it; auto-fix presses to ${MAX_GOAL_FIX_ATTEMPTS} attempts
1200
1200
  break;
1201
1201
  }
1202
1202
  void (async () => {
1203
- const { loadFlows } = await import("./flows-4QS2ER6N.js");
1203
+ const { loadFlows } = await import("./flows-EHUNUHED.js");
1204
1204
  const flows = loadFlows(this.opts.cwd);
1205
1205
  if (flows.length === 0) {
1206
1206
  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");
@@ -1211,8 +1211,8 @@ every ask now carries it; auto-fix presses to ${MAX_GOAL_FIX_ATTEMPTS} attempts
1211
1211
  this.push("error", "no Chrome/Chromium found for flows");
1212
1212
  return;
1213
1213
  }
1214
- const { runFlow } = await import("./cdp-6YBMFE7M.js");
1215
- const { previewDir } = await import("./preview-UV67QLVI.js");
1214
+ const { runFlow } = await import("./cdp-E6VAQ7XL.js");
1215
+ const { previewDir } = await import("./preview-M6VBOTQZ.js");
1216
1216
  this.push("status", `replaying ${flows.length} flow(s)\u2026`);
1217
1217
  this.notify({ running: true, runStartedAt: Date.now() });
1218
1218
  const failures = [];
@@ -1352,7 +1352,7 @@ every ask now carries it; auto-fix presses to ${MAX_GOAL_FIX_ATTEMPTS} attempts
1352
1352
  const result = await this.capture();
1353
1353
  if (result) {
1354
1354
  await this.runTurn(
1355
- buildReviewPrompt(result.shots, arg || void 0, result.runtime, result.a11y, result.slop, result.narration, result.phantoms, result.viewTransitions, result.components, result.webmcp, result.jank, result.locale),
1355
+ buildReviewPrompt(result.shots, arg || void 0, result.runtime, result.a11y, result.slop, result.narration, result.phantoms, result.viewTransitions, result.components, result.webmcp, result.jank, result.locale, result.speculation),
1356
1356
  `\u{1F441} review rendered UI${arg ? ` \xB7 ${arg}` : ""}`
1357
1357
  );
1358
1358
  }
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  Session
4
- } from "./chunk-B356UR44.js";
4
+ } from "./chunk-MWPZCXN7.js";
5
5
 
6
6
  // src/daemon/client.ts
7
7
  import net from "net";
@@ -57,7 +57,7 @@ function socketPath(cwd) {
57
57
  const hash = crypto.createHash("sha256").update(cwd).digest("hex").slice(0, 12);
58
58
  return path.join(os.tmpdir(), `squint-${hash}.sock`);
59
59
  }
60
- function startDaemon(opts) {
60
+ async function startDaemon(opts) {
61
61
  const session = new Session(opts);
62
62
  const sock = socketPath(opts.cwd);
63
63
  fs.mkdirSync(path.dirname(sock), { recursive: true });
@@ -122,7 +122,7 @@ function startDaemon(opts) {
122
122
  const state = session.getState();
123
123
  if (!state.devUrl || state.running) return;
124
124
  try {
125
- const { runIntervalSweep } = await import("./checks-GTKPEQ2L.js");
125
+ const { runIntervalSweep } = await import("./checks-Y73R4SS3.js");
126
126
  const failures = await runIntervalSweep(opts.cwd, state.devUrl, lastRun);
127
127
  if (failures.length > 0) {
128
128
  session.note(`\u23F0 interval check(s) failing:
@@ -132,6 +132,54 @@ ${failures.join("\n")}`);
132
132
  }
133
133
  }, opts.intervalSweepMs ?? 6e4);
134
134
  sweep.unref?.();
135
+ let relayServer = null;
136
+ let relayUrl;
137
+ let relayUnsub = null;
138
+ if (opts.approvalWebhook) {
139
+ const http = await import("http");
140
+ const crypto2 = await import("crypto");
141
+ const tokens = /* @__PURE__ */ new Map();
142
+ relayServer = http.createServer((req, res) => {
143
+ const token = (req.url ?? "").replace(/^\//, "");
144
+ const verdict = tokens.get(token);
145
+ if (!verdict) {
146
+ res.writeHead(404).end("unknown or already-used token");
147
+ return;
148
+ }
149
+ tokens.clear();
150
+ session.command(verdict === "yes" ? "/yes approved via webhook" : "/no rejected via webhook");
151
+ res.writeHead(200, { "content-type": "text/plain" });
152
+ res.end(verdict === "yes" ? "approved \u2014 the engine proceeds" : "rejected \u2014 the engine stands down");
153
+ });
154
+ await new Promise((resolve) => relayServer?.listen(opts.relayPort ?? 0, "127.0.0.1", resolve));
155
+ const addr = relayServer.address();
156
+ relayUrl = `http://127.0.0.1:${addr.port}`;
157
+ let lastSeen = null;
158
+ relayUnsub = session.subscribe(() => {
159
+ const pending = session.getState().pendingApproval;
160
+ if (!pending || pending === lastSeen) {
161
+ if (!pending) lastSeen = null;
162
+ return;
163
+ }
164
+ lastSeen = pending;
165
+ const approve = crypto2.randomBytes(16).toString("hex");
166
+ const reject = crypto2.randomBytes(16).toString("hex");
167
+ tokens.clear();
168
+ tokens.set(approve, "yes");
169
+ tokens.set(reject, "no");
170
+ fetch(opts.approvalWebhook, {
171
+ method: "POST",
172
+ headers: { "content-type": "application/json" },
173
+ body: JSON.stringify({
174
+ text: `squint approval requested: ${pending}`,
175
+ summary: pending,
176
+ approveUrl: `${relayUrl}/${approve}`,
177
+ rejectUrl: `${relayUrl}/${reject}`
178
+ }),
179
+ signal: AbortSignal.timeout(5e3)
180
+ }).catch(() => null);
181
+ });
182
+ }
135
183
  const unsubscribe = session.subscribe(() => {
136
184
  const payload = serialize();
137
185
  for (const client of clients) {
@@ -143,9 +191,12 @@ ${failures.join("\n")}`);
143
191
  server.listen(sock, () => {
144
192
  resolve({
145
193
  session,
194
+ relayUrl,
146
195
  clientCount: () => clients.length,
147
196
  close: () => {
148
197
  clearInterval(sweep);
198
+ relayUnsub?.();
199
+ relayServer?.close();
149
200
  unsubscribe();
150
201
  for (const client of clients) client.destroy();
151
202
  server.close();
package/dist/cli.js CHANGED
@@ -3,14 +3,14 @@ import {
3
3
  connectDaemon,
4
4
  socketPath,
5
5
  startDaemon
6
- } from "./chunk-MDLNTOGX.js";
6
+ } from "./chunk-QYK6OO46.js";
7
7
  import {
8
8
  App
9
- } from "./chunk-PPC2KM7P.js";
10
- import "./chunk-B356UR44.js";
9
+ } from "./chunk-6M54VGLM.js";
10
+ import "./chunk-MWPZCXN7.js";
11
11
  import "./chunk-YKEAKVIS.js";
12
12
  import "./chunk-LNJ7O5O4.js";
13
- import "./chunk-3ZHETLUC.js";
13
+ import "./chunk-KPLEM7SV.js";
14
14
  import "./chunk-UBADCBPT.js";
15
15
  import "./chunk-IEQGTVWN.js";
16
16
  import "./chunk-AUJJGMZG.js";
@@ -25,13 +25,13 @@ import {
25
25
  } from "./chunk-WAJXATCO.js";
26
26
  import "./chunk-62JNF5M2.js";
27
27
  import "./chunk-7CAGWFAQ.js";
28
- import "./chunk-4DZAWPS2.js";
28
+ import "./chunk-MITMSC2W.js";
29
29
  import "./chunk-55JZKI2M.js";
30
30
  import {
31
31
  detectEngines,
32
32
  getEngine
33
33
  } from "./chunk-HIUQ4FXO.js";
34
- import "./chunk-6RMF4NAW.js";
34
+ import "./chunk-AYJQYDN6.js";
35
35
 
36
36
  // src/cli.tsx
37
37
  import { Command } from "commander";
@@ -66,7 +66,9 @@ var ConfigSchema = z.object({
66
66
  /** Cheaper model used for auto-fix and /fix turns (mechanical work). */
67
67
  fixModel: z.string().optional(),
68
68
  /** TUI theme name (amber, ocean, moss, rose, mono). */
69
- theme: z.string().optional()
69
+ theme: z.string().optional(),
70
+ /** POSTed when an engine requests visual approval under squint serve. */
71
+ approvalWebhook: z.string().url().optional()
70
72
  });
71
73
  function defaultPaths(cwd) {
72
74
  const xdg = process.env.XDG_CONFIG_HOME;
@@ -114,6 +116,8 @@ function setConfigValue(file, key, value) {
114
116
  throw new Error(`"${key}" must be true or false`);
115
117
  }
116
118
  next = { ...current, [key]: value === "true" };
119
+ } else if (key === "approvalWebhook") {
120
+ next = { ...current, approvalWebhook: value };
117
121
  } else if (key === "budgetUsd") {
118
122
  const budget = Number.parseFloat(value);
119
123
  if (!Number.isFinite(budget) || budget <= 0) {
@@ -126,7 +130,7 @@ function setConfigValue(file, key, value) {
126
130
  next = { ...current, models: { ...current.models, [engineId]: value } };
127
131
  } else {
128
132
  throw new Error(
129
- `Unknown config key "${key}". Supported: engine, theme, autoDev, autoFix, autoProbe, autoCheck, autoReview, bell, budgetUsd, fixModel, models.<engineId>`
133
+ `Unknown config key "${key}". Supported: engine, theme, autoDev, autoFix, autoProbe, autoCheck, autoReview, bell, budgetUsd, fixModel, approvalWebhook, models.<engineId>`
130
134
  );
131
135
  }
132
136
  fs.mkdirSync(path.dirname(file), { recursive: true });
@@ -173,6 +177,7 @@ function registerDaemon(program2) {
173
177
  autoReview: config.autoReview,
174
178
  fixModel: config.fixModel,
175
179
  budgetUsd: config.budgetUsd,
180
+ approvalWebhook: config.approvalWebhook,
176
181
  onQuit: () => {
177
182
  daemon.close();
178
183
  process.exit(0);
@@ -180,6 +185,7 @@ function registerDaemon(program2) {
180
185
  });
181
186
  console.log(`squint daemon on ${socketPath(cwd)} (engine: ${engineId})`);
182
187
  console.log("attach from another terminal with: squint attach");
188
+ if (daemon.relayUrl) console.log(`approval relay on ${daemon.relayUrl} \u2192 ${config.approvalWebhook}`);
183
189
  const stop = () => {
184
190
  daemon.close();
185
191
  process.exit(0);
@@ -188,18 +194,18 @@ function registerDaemon(program2) {
188
194
  process.on("SIGTERM", stop);
189
195
  });
190
196
  program2.command("mcp").description("serve the gates as MCP tools over stdio (check, shot, flows, context)").action(async () => {
191
- const { runMcpServer } = await import("./server-WOYEQLAA.js");
197
+ const { runMcpServer } = await import("./server-V3DHLHPK.js");
192
198
  runMcpServer(process.cwd());
193
199
  });
194
200
  program2.command("attach").description("attach this terminal to a running squint daemon (full TUI; --plain for line mode)").option("--plain", "line-mode attach instead of the full TUI").action(async (opts) => {
195
201
  const cwd = process.cwd();
196
202
  if (!opts.plain) {
197
203
  try {
198
- const { RemoteSession } = await import("./remote-ICNK2PPK.js");
204
+ const { RemoteSession } = await import("./remote-VAIQNPAV.js");
199
205
  const remote = await RemoteSession.connect(cwd);
200
206
  const config = loadConfig(defaultPaths(cwd));
201
207
  const { render: render2 } = await import("ink");
202
- const { App: App2 } = await import("./App-C3LOYRTR.js");
208
+ const { App: App2 } = await import("./App-TEZ6E4R6.js");
203
209
  const React = await import("react");
204
210
  render2(
205
211
  React.createElement(App2, {
@@ -316,7 +322,7 @@ function registerEnv(program2) {
316
322
  }
317
323
  }
318
324
  const { findChrome } = await import("./chrome-BQ4WOCOH.js");
319
- const { hasWebSocket } = await import("./cdp-6YBMFE7M.js");
325
+ const { hasWebSocket } = await import("./cdp-E6VAQ7XL.js");
320
326
  const chrome = findChrome();
321
327
  console.log(
322
328
  chrome ? `${pc3.green("\u2713")} Chrome ${pc3.dim(chrome)}` : `${pc3.yellow("\u25CB")} Chrome ${pc3.dim("\u2014 screenshots and runtime probing disabled")}`
@@ -552,7 +558,7 @@ function registerQuality(program2) {
552
558
  report.gates = gateResults.map((r) => ({ id: r.gate.id, ok: r.ok, durationMs: r.durationMs, outputTail: r.ok ? void 0 : r.outputTail }));
553
559
  if (gateResults.some((r) => !r.ok)) failed = true;
554
560
  if (opts.url) {
555
- const { captureViewports, runtimeSummary, previewDir } = await import("./preview-UV67QLVI.js");
561
+ const { captureViewports, runtimeSummary, previewDir } = await import("./preview-M6VBOTQZ.js");
556
562
  const capture = await captureViewports(cwd, opts.url);
557
563
  if (!capture) {
558
564
  console.log(pc5.red("\u2717 audit: capture failed (Chrome missing or URL unreachable)"));
@@ -584,14 +590,14 @@ function registerQuality(program2) {
584
590
  console.log(`${pc5.green("\u2713")} audit ${pc5.dim(`(${advisories.a11y.length} a11y, ${advisories.slop.length} slop, ${advisories.jank.length} jank advisories)`)}`);
585
591
  }
586
592
  }
587
- const { loadFlows } = await import("./flows-4QS2ER6N.js");
593
+ const { loadFlows } = await import("./flows-EHUNUHED.js");
588
594
  const flows = loadFlows(cwd);
589
595
  if (flows.length > 0) {
590
596
  const { findChrome } = await import("./chrome-BQ4WOCOH.js");
591
597
  const chrome = findChrome();
592
598
  const flowReport = [];
593
599
  if (chrome) {
594
- const { runFlow } = await import("./cdp-6YBMFE7M.js");
600
+ const { runFlow } = await import("./cdp-E6VAQ7XL.js");
595
601
  for (const flow of flows) {
596
602
  const result = await runFlow(chrome, opts.url, flow, previewDir(cwd));
597
603
  flowReport.push({ name: flow.name, ok: result.ok, detail: result.ok ? void 0 : `step ${result.failedStep}: ${result.detail}` });
@@ -605,7 +611,7 @@ function registerQuality(program2) {
605
611
  report.ok = !failed;
606
612
  report.finishedAt = (/* @__PURE__ */ new Date()).toISOString();
607
613
  try {
608
- const { writeReceipt } = await import("./receipts-7PERXBJO.js");
614
+ const { writeReceipt } = await import("./receipts-KNOHIVCA.js");
609
615
  console.log(pc5.dim(`receipt \u2192 ${writeReceipt(cwd, report)}`));
610
616
  } catch {
611
617
  }
@@ -617,7 +623,7 @@ function registerQuality(program2) {
617
623
  if (failed) process.exitCode = 1;
618
624
  });
619
625
  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) => {
620
- const { captureViewports } = await import("./preview-UV67QLVI.js");
626
+ const { captureViewports } = await import("./preview-M6VBOTQZ.js");
621
627
  const result = await captureViewports(process.cwd(), url);
622
628
  if (!result) {
623
629
  console.error(pc5.red("\u2717 no Chrome/Chromium found"));
@@ -804,7 +810,7 @@ function registerTui(program2) {
804
810
  }
805
811
 
806
812
  // src/cli.tsx
807
- var VERSION = true ? "0.8.2" : "0.0.0-dev";
813
+ var VERSION = true ? "0.9.1" : "0.0.0-dev";
808
814
  var program = new Command();
809
815
  program.name("squint").description("Lovable for your terminal \u2014 a frontend harness on top of Claude Code, Codex, and friends.").version(VERSION);
810
816
  registerRun(program);
@@ -4,7 +4,7 @@ import {
4
4
  parseFlow,
5
5
  stepExpression,
6
6
  suggestFlows
7
- } from "./chunk-53DPUAXN.js";
7
+ } from "./chunk-FQYJ343N.js";
8
8
  export {
9
9
  loadFlows,
10
10
  parseFlow,
@@ -11,12 +11,12 @@ import {
11
11
  probeRuntime,
12
12
  routeShotName,
13
13
  runtimeSummary
14
- } from "./chunk-3ZHETLUC.js";
14
+ } from "./chunk-KPLEM7SV.js";
15
15
  import "./chunk-7CAGWFAQ.js";
16
- import "./chunk-4DZAWPS2.js";
16
+ import "./chunk-MITMSC2W.js";
17
17
  import "./chunk-55JZKI2M.js";
18
18
  import "./chunk-HIUQ4FXO.js";
19
- import "./chunk-6RMF4NAW.js";
19
+ import "./chunk-AYJQYDN6.js";
20
20
  export {
21
21
  VIEWPORTS,
22
22
  buildReviewPrompt,
@@ -31,7 +31,7 @@ function buildReceipt(cwd, report) {
31
31
  }
32
32
  }
33
33
  const unsigned = {
34
- version: true ? "0.8.2" : "0.0.0-dev",
34
+ version: true ? "0.9.1" : "0.0.0-dev",
35
35
  node: process.version,
36
36
  gitHead,
37
37
  report,
@@ -2,10 +2,10 @@
2
2
  import {
3
3
  connectDaemon,
4
4
  socketPath
5
- } from "./chunk-MDLNTOGX.js";
6
- import "./chunk-B356UR44.js";
5
+ } from "./chunk-QYK6OO46.js";
6
+ import "./chunk-MWPZCXN7.js";
7
7
  import "./chunk-YKEAKVIS.js";
8
- import "./chunk-3ZHETLUC.js";
8
+ import "./chunk-KPLEM7SV.js";
9
9
  import "./chunk-UBADCBPT.js";
10
10
  import "./chunk-IEQGTVWN.js";
11
11
  import "./chunk-AUJJGMZG.js";
@@ -16,10 +16,10 @@ import "./chunk-K5QJMSJH.js";
16
16
  import "./chunk-WAJXATCO.js";
17
17
  import "./chunk-62JNF5M2.js";
18
18
  import "./chunk-7CAGWFAQ.js";
19
- import "./chunk-4DZAWPS2.js";
19
+ import "./chunk-MITMSC2W.js";
20
20
  import "./chunk-55JZKI2M.js";
21
21
  import "./chunk-HIUQ4FXO.js";
22
- import "./chunk-6RMF4NAW.js";
22
+ import "./chunk-AYJQYDN6.js";
23
23
 
24
24
  // src/daemon/remote.ts
25
25
  var noteId = 0;
@@ -38,7 +38,8 @@ var RemoteSession = class _RemoteSession {
38
38
  queue: [],
39
39
  mode: "safe",
40
40
  problems: [],
41
- sandbox: false
41
+ sandbox: false,
42
+ pendingApproval: null
42
43
  };
43
44
  client.onMessage((msg) => {
44
45
  if (msg.type === "state" && msg.state) {
@@ -7,7 +7,7 @@ import {
7
7
  captureViewports,
8
8
  previewDir,
9
9
  runtimeSummary
10
- } from "./chunk-3ZHETLUC.js";
10
+ } from "./chunk-KPLEM7SV.js";
11
11
  import "./chunk-UBADCBPT.js";
12
12
  import "./chunk-IEQGTVWN.js";
13
13
  import "./chunk-WAJXATCO.js";
@@ -18,13 +18,13 @@ import {
18
18
  import "./chunk-7CAGWFAQ.js";
19
19
  import {
20
20
  loadFlows
21
- } from "./chunk-53DPUAXN.js";
22
- import "./chunk-4DZAWPS2.js";
21
+ } from "./chunk-FQYJ343N.js";
22
+ import "./chunk-MITMSC2W.js";
23
23
  import {
24
24
  findChrome
25
25
  } from "./chunk-55JZKI2M.js";
26
26
  import "./chunk-HIUQ4FXO.js";
27
- import "./chunk-6RMF4NAW.js";
27
+ import "./chunk-AYJQYDN6.js";
28
28
 
29
29
  // src/mcp/server.ts
30
30
  import readline from "readline";
@@ -59,6 +59,7 @@ ${r.outputTail.slice(-1500)}`}`).join("\n");
59
59
  ["view transitions", result.viewTransitions],
60
60
  ["jank", result.jank],
61
61
  ["locale", result.locale],
62
+ ["speculation", result.speculation],
62
63
  ["components", result.components]
63
64
  ]) {
64
65
  if (list && list.length > 0) sections.push(`${label}:
@@ -77,7 +78,7 @@ ${list.join("\n")}`);
77
78
  if (flows.length === 0) return "no flows declared \u2014 add .squint/flows/<name>.flow";
78
79
  const chrome = findChrome();
79
80
  if (!chrome) return "no Chrome/Chromium found";
80
- const { runFlow } = await import("./cdp-6YBMFE7M.js");
81
+ const { runFlow } = await import("./cdp-E6VAQ7XL.js");
81
82
  const lines = [];
82
83
  for (const flow of flows) {
83
84
  const result = await runFlow(chrome, url, flow, previewDir(cwd));
@@ -97,7 +98,7 @@ ${result.leaks.map((l) => ` ${l}`).join("\n")}` : ""}` : `\u2717 ${flow.name} a
97
98
  async run(args, cwd) {
98
99
  const chrome = findChrome();
99
100
  if (!chrome) return "no Chrome/Chromium found";
100
- const { suggestFlows } = await import("./flows-4QS2ER6N.js");
101
+ const { suggestFlows } = await import("./flows-EHUNUHED.js");
101
102
  const { created, skipped } = await suggestFlows(cwd, String(args.url ?? ""), chrome);
102
103
  return [
103
104
  created.length > 0 ? `drafted: ${created.join(", ")}` : "nothing drafted",
@@ -112,7 +113,7 @@ ${result.leaks.map((l) => ` ${l}`).join("\n")}` : ""}` : `\u2717 ${flow.name} a
112
113
  async run(args, cwd) {
113
114
  const fs = await import("fs");
114
115
  const path = await import("path");
115
- const { verifyReceipt } = await import("./receipts-7PERXBJO.js");
116
+ const { verifyReceipt } = await import("./receipts-KNOHIVCA.js");
116
117
  const file = path.isAbsolute(String(args.path)) ? String(args.path) : path.join(cwd, String(args.path));
117
118
  let receipt;
118
119
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aayambansal/squint",
3
- "version": "0.8.2",
3
+ "version": "0.9.1",
4
4
  "description": "Lovable for your terminal \u2014 a frontend harness on top of Claude Code, Codex, and other coding agents.",
5
5
  "type": "module",
6
6
  "license": "MIT",