@aayambansal/squint 0.2.3 → 0.2.4

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.
@@ -62,6 +62,19 @@ var VIEWPORTS = [
62
62
  { name: "tablet", width: 768, height: 1024 },
63
63
  { name: "desktop", width: 1440, height: 900 }
64
64
  ];
65
+ function loadRoutes(cwd) {
66
+ let lines = [];
67
+ try {
68
+ lines = fs2.readFileSync(path2.join(cwd, ".squint", "routes"), "utf8").split("\n").map((l) => l.trim()).filter((l) => l.length > 0 && !l.startsWith("#"));
69
+ } catch {
70
+ }
71
+ const routes = ["/", ...lines.filter((l) => l !== "/")];
72
+ return routes.slice(0, 6).map((r) => r.startsWith("/") ? r : `/${r}`);
73
+ }
74
+ function routeShotName(route) {
75
+ const clean = route.replace(/^\/+|\/+$/g, "").replace(/[^a-zA-Z0-9]+/g, "-");
76
+ return clean.length > 0 ? clean : "root";
77
+ }
65
78
  function previewDir(cwd) {
66
79
  const dir = path2.join(cwd, ".squint", "preview");
67
80
  fs2.mkdirSync(dir, { recursive: true });
@@ -72,10 +85,26 @@ async function captureViewports(cwd, url) {
72
85
  const chrome = findChrome();
73
86
  if (!chrome) return null;
74
87
  const dir = previewDir(cwd);
88
+ const routes = loadRoutes(cwd);
89
+ const base = url.replace(/\/+$/, "");
75
90
  if (hasWebSocket()) {
76
91
  try {
77
92
  const { report, shots: shots2, a11y } = await cdpCapture(chrome, url, dir, VIEWPORTS, 2500, true);
78
- return { shots: shots2, errors: [], runtime: report, a11y };
93
+ const errors2 = [];
94
+ for (const route of routes.slice(1)) {
95
+ try {
96
+ const routeCapture = await cdpCapture(chrome, `${base}${route}`, dir, [
97
+ { name: routeShotName(route), width: 1440, height: 900 }
98
+ ]);
99
+ shots2.push(...routeCapture.shots);
100
+ for (const err of routeCapture.report.pageErrors.slice(0, 3)) {
101
+ errors2.push(`${route}: ${err.split("\n")[0]}`);
102
+ }
103
+ } catch {
104
+ errors2.push(`${route}: capture failed`);
105
+ }
106
+ }
107
+ return { shots: shots2, errors: errors2, runtime: report, a11y };
79
108
  } catch {
80
109
  }
81
110
  }
@@ -178,6 +207,8 @@ export {
178
207
  saveState,
179
208
  clearState,
180
209
  VIEWPORTS,
210
+ loadRoutes,
211
+ routeShotName,
181
212
  previewDir,
182
213
  captureViewports,
183
214
  runtimeSummary,
package/dist/cli.js CHANGED
@@ -42,7 +42,7 @@ import {
42
42
  probeRuntime,
43
43
  runtimeSummary,
44
44
  saveState
45
- } from "./chunk-SU6YRRMF.js";
45
+ } from "./chunk-CIQ3OUAF.js";
46
46
  import "./chunk-OC6RU6XH.js";
47
47
  import {
48
48
  findChrome
@@ -80,6 +80,8 @@ var ConfigSchema = z.object({
80
80
  autoCheck: z.boolean().optional(),
81
81
  /** Terminal bell when a turn finishes (default on). */
82
82
  bell: z.boolean().optional(),
83
+ /** Session budget in USD; crossing it warns (never blocks). */
84
+ budgetUsd: z.number().positive().optional(),
83
85
  /** TUI theme name (amber, ocean, moss, rose, mono). */
84
86
  theme: z.string().optional()
85
87
  });
@@ -129,13 +131,19 @@ function setConfigValue(file, key, value) {
129
131
  throw new Error(`"${key}" must be true or false`);
130
132
  }
131
133
  next = { ...current, [key]: value === "true" };
134
+ } else if (key === "budgetUsd") {
135
+ const budget = Number.parseFloat(value);
136
+ if (!Number.isFinite(budget) || budget <= 0) {
137
+ throw new Error('"budgetUsd" must be a positive number');
138
+ }
139
+ next = { ...current, budgetUsd: budget };
132
140
  } else if (key.startsWith("models.")) {
133
141
  const engineId = key.slice("models.".length);
134
142
  if (!engineId) throw new Error("Usage: squint config set models.<engineId> <model>");
135
143
  next = { ...current, models: { ...current.models, [engineId]: value } };
136
144
  } else {
137
145
  throw new Error(
138
- `Unknown config key "${key}". Supported: engine, theme, autoDev, autoFix, autoProbe, autoCheck, bell, models.<engineId>`
146
+ `Unknown config key "${key}". Supported: engine, theme, autoDev, autoFix, autoProbe, autoCheck, bell, budgetUsd, models.<engineId>`
139
147
  );
140
148
  }
141
149
  fs.mkdirSync(path.dirname(file), { recursive: true });
@@ -347,16 +355,32 @@ var Session = class {
347
355
  }
348
356
  turnEdits = 0;
349
357
  turnTools = 0;
358
+ toolStreak = 0;
359
+ collapsedTools = 0;
360
+ /**
361
+ * Long tool cascades collapse: the first three of a consecutive burst
362
+ * render, the rest fold into one "+N more" line pushed when the burst
363
+ * ends — append-only, so the Static transcript stays valid.
364
+ */
365
+ flushToolCollapse() {
366
+ if (this.collapsedTools > 0) {
367
+ this.push("tool", `+${this.collapsedTools} more tool call${this.collapsedTools === 1 ? "" : "s"}`);
368
+ this.collapsedTools = 0;
369
+ }
370
+ this.toolStreak = 0;
371
+ }
350
372
  handleEvent = (event) => {
351
373
  switch (event.type) {
352
374
  case "status":
353
375
  this.commitLive();
376
+ this.flushToolCollapse();
354
377
  this.push("status", event.text);
355
378
  break;
356
379
  case "delta":
357
380
  this.setLive(this.live + event.text);
358
381
  break;
359
382
  case "text":
383
+ this.flushToolCollapse();
360
384
  if (event.streamed) {
361
385
  this.live = "";
362
386
  this.state = { ...this.state, liveText: "" };
@@ -367,17 +391,24 @@ var Session = class {
367
391
  break;
368
392
  case "thinking":
369
393
  this.commitLive();
394
+ this.flushToolCollapse();
370
395
  this.push("thinking", event.text);
371
396
  break;
372
397
  case "tool": {
373
398
  this.commitLive();
374
399
  this.turnTools += 1;
400
+ this.toolStreak += 1;
375
401
  if (/edit|write|patch|apply/i.test(event.name)) this.turnEdits += 1;
376
- this.push("tool", event.detail ? `${event.name} \xB7 ${event.detail}` : event.name);
402
+ if (this.toolStreak <= 3) {
403
+ this.push("tool", event.detail ? `${event.name} \xB7 ${event.detail}` : event.name);
404
+ } else {
405
+ this.collapsedTools += 1;
406
+ }
377
407
  break;
378
408
  }
379
409
  case "error":
380
410
  this.commitLive();
411
+ this.flushToolCollapse();
381
412
  this.push("error", event.text);
382
413
  break;
383
414
  case "result":
@@ -410,6 +441,7 @@ var Session = class {
410
441
  );
411
442
  this.abort = null;
412
443
  this.commitLive();
444
+ this.flushToolCollapse();
413
445
  if (result.ok) {
414
446
  const cost = result.costUsd !== void 0 ? ` \xB7 $${result.costUsd.toFixed(2)}` : "";
415
447
  const secs = result.durationMs !== void 0 ? ` \xB7 ${(result.durationMs / 1e3).toFixed(0)}s` : "";
@@ -417,12 +449,20 @@ var Session = class {
417
449
  const stat = checkpoint ? diffStatSince(this.opts.cwd, checkpoint.snapshot) : null;
418
450
  const work = stat ? ` \xB7 ${stat}` : this.turnEdits > 0 ? ` \xB7 ${this.turnEdits} edit${this.turnEdits === 1 ? "" : "s"}` : this.turnTools > 0 ? ` \xB7 ${this.turnTools} tool call${this.turnTools === 1 ? "" : "s"}` : "";
419
451
  this.push("status", `done${secs}${cost}${work}`);
452
+ const before = this.state.totals.costUsd;
420
453
  this.notify({
421
454
  totals: {
422
- costUsd: this.state.totals.costUsd + (result.costUsd ?? 0),
455
+ costUsd: before + (result.costUsd ?? 0),
423
456
  turns: this.state.totals.turns + 1
424
457
  }
425
458
  });
459
+ const budget = this.opts.budgetUsd;
460
+ if (budget && before < budget && this.state.totals.costUsd >= budget) {
461
+ this.push(
462
+ "error",
463
+ `session cost $${this.state.totals.costUsd.toFixed(2)} crossed your $${budget.toFixed(2)} budget \u2014 squint keeps working, this is just the flag you asked for`
464
+ );
465
+ }
426
466
  if (this.sessionId) {
427
467
  saveState(this.opts.cwd, {
428
468
  engine: this.state.engineId,
@@ -1227,6 +1267,7 @@ function App({
1227
1267
  autoProbe,
1228
1268
  autoCheck,
1229
1269
  bell,
1270
+ budgetUsd,
1230
1271
  initialTheme
1231
1272
  }) {
1232
1273
  const { exit } = useApp();
@@ -1242,6 +1283,7 @@ function App({
1242
1283
  autoFix,
1243
1284
  autoProbe,
1244
1285
  autoCheck,
1286
+ budgetUsd,
1245
1287
  // Delay lets the goodbye summary land in the Static scrollback.
1246
1288
  onQuit: () => setTimeout(() => exit(), 60)
1247
1289
  });
@@ -1431,7 +1473,7 @@ function App({
1431
1473
 
1432
1474
  // src/cli.tsx
1433
1475
  import { jsx as jsx4 } from "react/jsx-runtime";
1434
- var VERSION = "0.2.3";
1476
+ var VERSION = "0.2.4";
1435
1477
  var program = new Command();
1436
1478
  program.name("squint").description("Lovable for your terminal \u2014 a frontend harness on top of Claude Code, Codex, and friends.").version(VERSION);
1437
1479
  program.command("run").description("Run one prompt headlessly and stream the result").argument("<prompt...>", "what to build or change").option("-e, --engine <id>", "engine to use (claude, codex, gemini, opencode)").option("-m, --model <name>", "model override for the engine").option("--no-brief", "send the prompt without the squint design brief").option("--json", "emit normalized agent events as ndjson").action(
@@ -1728,7 +1770,7 @@ program.command("check").description("Run this project\u2019s quality gates (typ
1728
1770
  if (results.some((r) => !r.ok)) process.exitCode = 1;
1729
1771
  });
1730
1772
  program.command("shot").description("Screenshot a running app at mobile/tablet/desktop viewports").argument("<url>", "URL of the running app (e.g. http://localhost:5173)").action(async (url) => {
1731
- const { captureViewports: captureViewports2 } = await import("./preview-UX4VS4SX.js");
1773
+ const { captureViewports: captureViewports2 } = await import("./preview-LZDH65ID.js");
1732
1774
  const result = await captureViewports2(process.cwd(), url);
1733
1775
  if (!result) {
1734
1776
  console.error(pc.red("\u2717 no Chrome/Chromium found"));
@@ -1772,6 +1814,7 @@ program.action(() => {
1772
1814
  autoProbe: config.autoProbe,
1773
1815
  autoCheck: config.autoCheck,
1774
1816
  bell: config.bell,
1817
+ budgetUsd: config.budgetUsd,
1775
1818
  initialTheme: config.theme
1776
1819
  }
1777
1820
  )
@@ -5,10 +5,12 @@ import {
5
5
  buildRuntimeFixPrompt,
6
6
  captureViewports,
7
7
  comparePulse,
8
+ loadRoutes,
8
9
  previewDir,
9
10
  probeRuntime,
11
+ routeShotName,
10
12
  runtimeSummary
11
- } from "./chunk-SU6YRRMF.js";
13
+ } from "./chunk-CIQ3OUAF.js";
12
14
  import "./chunk-OC6RU6XH.js";
13
15
  import "./chunk-P3V5CWH5.js";
14
16
  import "./chunk-2LRIKWBU.js";
@@ -18,7 +20,9 @@ export {
18
20
  buildRuntimeFixPrompt,
19
21
  captureViewports,
20
22
  comparePulse,
23
+ loadRoutes,
21
24
  previewDir,
22
25
  probeRuntime,
26
+ routeShotName,
23
27
  runtimeSummary
24
28
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aayambansal/squint",
3
- "version": "0.2.3",
3
+ "version": "0.2.4",
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",