@aayambansal/squint 0.2.0 → 0.2.2

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/cli.js CHANGED
@@ -2,14 +2,21 @@
2
2
  import {
3
3
  DevServer,
4
4
  buildFixPrompt,
5
- detectDevCommand
6
- } from "./chunk-ZJOU46X4.js";
5
+ detectDevCommand,
6
+ screenshotVariants
7
+ } from "./chunk-2IW2MMCY.js";
7
8
  import {
8
- runAgent
9
- } from "./chunk-FPTBBHOH.js";
9
+ applyVariant,
10
+ cleanVariants,
11
+ listVariants,
12
+ runVariants
13
+ } from "./chunk-LCS47EAG.js";
10
14
  import {
11
15
  composePrompt
12
- } from "./chunk-NT2HR4RD.js";
16
+ } from "./chunk-P3H4N2EN.js";
17
+ import {
18
+ runAgent
19
+ } from "./chunk-OTG4ZB4R.js";
13
20
  import {
14
21
  buildGatePrompt,
15
22
  detectFastGates,
@@ -21,21 +28,29 @@ import {
21
28
  buildRuntimeFixPrompt,
22
29
  captureViewports,
23
30
  clearState,
31
+ comparePulse,
24
32
  loadState,
25
33
  probeRuntime,
26
34
  runtimeSummary,
27
35
  saveState
28
- } from "./chunk-4MMIJXYD.js";
29
- import "./chunk-VJGE7HSP.js";
30
- import "./chunk-Z46MXU64.js";
36
+ } from "./chunk-73ULM6HF.js";
37
+ import "./chunk-OC6RU6XH.js";
38
+ import {
39
+ findChrome
40
+ } from "./chunk-P3V5CWH5.js";
31
41
  import {
32
42
  detectEngines,
33
43
  getEngine
34
- } from "./chunk-MCYQXOE7.js";
44
+ } from "./chunk-2LRIKWBU.js";
45
+ import {
46
+ enrich
47
+ } from "./chunk-XZKQZKEE.js";
35
48
  import {
49
+ diffStatSince,
50
+ isGitRepo,
36
51
  restoreSnapshot,
37
52
  takeSnapshot
38
- } from "./chunk-WTA6YYBY.js";
53
+ } from "./chunk-YHRAOBI2.js";
39
54
 
40
55
  // src/cli.tsx
41
56
  import { Command } from "commander";
@@ -148,7 +163,8 @@ var Session = class {
148
163
  devUrl: null,
149
164
  totals: { costUsd: 0, turns: 0 },
150
165
  queue: [],
151
- mode: "safe"
166
+ mode: "safe",
167
+ problems: []
152
168
  };
153
169
  if (opts.autoDev && detectDevCommand(opts.cwd)) {
154
170
  this.devServer().start();
@@ -175,10 +191,13 @@ var Session = class {
175
191
  sessionId;
176
192
  dev = null;
177
193
  abort = null;
178
- pendingFix = null;
194
+ /** Full problem records; state carries the summaries. */
195
+ problemPrompts = /* @__PURE__ */ new Map();
196
+ nextProblemId = 0;
179
197
  checkpoints = [];
180
198
  fixAttempts = 0;
181
199
  reviewTipShown = false;
200
+ lastPulse = null;
182
201
  startedAt = Date.now();
183
202
  subscribe(listener) {
184
203
  this.listeners.add(listener);
@@ -198,6 +217,47 @@ var Session = class {
198
217
  note(text) {
199
218
  this.push("status", text);
200
219
  }
220
+ /** Register a finding; a fresh finding from a source supersedes its old one. */
221
+ addProblem(source, summary, prompt) {
222
+ const kept = this.state.problems.filter((p) => p.source !== source);
223
+ for (const gone of this.state.problems) {
224
+ if (gone.source === source) this.problemPrompts.delete(gone.id);
225
+ }
226
+ this.nextProblemId += 1;
227
+ this.problemPrompts.set(this.nextProblemId, prompt);
228
+ this.notify({ problems: [...kept, { id: this.nextProblemId, source, summary }] });
229
+ }
230
+ clearProblems(source) {
231
+ const removed = this.state.problems.filter((p) => p.source === source);
232
+ if (removed.length === 0) return;
233
+ for (const problem of removed) this.problemPrompts.delete(problem.id);
234
+ this.notify({ problems: this.state.problems.filter((p) => p.source !== source) });
235
+ }
236
+ /** One prompt covering the given problems, oldest first. */
237
+ combinedFixPrompt(problems) {
238
+ const sections = problems.map(
239
+ (p) => this.problemPrompts.get(p.id) ?? `Fix this reported problem: ${p.summary}`
240
+ );
241
+ return sections.join("\n\n---\n\n");
242
+ }
243
+ dispatchFix(problems) {
244
+ if (problems.length === 0) return;
245
+ const prompt = this.combinedFixPrompt(problems);
246
+ const display = `\u26D1 fix: ${problems.map((p) => p.source).join(" + ")}`;
247
+ for (const problem of problems) this.problemPrompts.delete(problem.id);
248
+ this.notify({ problems: this.state.problems.filter((p) => !problems.includes(p)) });
249
+ void this.runTurn(prompt, display);
250
+ }
251
+ /** Launch a capped auto-fix turn over all open problems. Returns true if launched. */
252
+ maybeAutoFix() {
253
+ if (!this.opts.autoFix || this.fixAttempts >= MAX_AUTO_FIX_ATTEMPTS) return false;
254
+ if (this.state.problems.length === 0) return false;
255
+ this.fixAttempts += 1;
256
+ this.push("status", `auto-fix attempt ${this.fixAttempts}/${MAX_AUTO_FIX_ATTEMPTS}`);
257
+ this.notify({ running: false });
258
+ this.dispatchFix([...this.state.problems]);
259
+ return true;
260
+ }
201
261
  setMode(mode) {
202
262
  this.notify({ mode });
203
263
  const hint = mode === "plan" ? "read-only: the engine investigates and proposes, edits nothing" : mode === "yolo" ? "no approval friction \u2014 the engine can do anything" : "edits auto-approved inside the workspace";
@@ -350,7 +410,9 @@ var Session = class {
350
410
  if (result.ok) {
351
411
  const cost = result.costUsd !== void 0 ? ` \xB7 $${result.costUsd.toFixed(2)}` : "";
352
412
  const secs = result.durationMs !== void 0 ? ` \xB7 ${(result.durationMs / 1e3).toFixed(0)}s` : "";
353
- const work = this.turnEdits > 0 ? ` \xB7 ${this.turnEdits} edit${this.turnEdits === 1 ? "" : "s"}` : this.turnTools > 0 ? ` \xB7 ${this.turnTools} tool call${this.turnTools === 1 ? "" : "s"}` : "";
413
+ const checkpoint = this.checkpoints.at(-1);
414
+ const stat = checkpoint ? diffStatSince(this.opts.cwd, checkpoint.snapshot) : null;
415
+ 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"}` : "";
354
416
  this.push("status", `done${secs}${cost}${work}`);
355
417
  this.notify({
356
418
  totals: {
@@ -374,26 +436,22 @@ var Session = class {
374
436
  const gateResults = await runGates(this.opts.cwd, fastGates);
375
437
  const failures = gateResults.filter((r) => !r.ok);
376
438
  if (failures.length > 0) {
377
- this.pendingFix = {
378
- prompt: buildGatePrompt(failures),
379
- display: `\u26D1 fix ${failures.map((f) => f.gate.id).join(" + ")} errors`
380
- };
439
+ this.addProblem(
440
+ "gates",
441
+ failures.map((f) => f.gate.id).join(" + "),
442
+ buildGatePrompt(failures)
443
+ );
381
444
  this.push(
382
445
  "error",
383
446
  failures.map((f) => `\u2717 ${f.gate.id} \xB7 ${f.outputTail.split("\n").slice(-3).join("\n")}`).join("\n")
384
447
  );
385
- if (this.opts.autoFix && this.fixAttempts < MAX_AUTO_FIX_ATTEMPTS) {
386
- this.fixAttempts += 1;
387
- this.push("status", `auto-fix attempt ${this.fixAttempts}/${MAX_AUTO_FIX_ATTEMPTS}`);
388
- this.notify({ running: false });
389
- await this.runTurn(this.pendingFix.prompt, this.pendingFix.display);
390
- return;
391
- }
392
- this.push("status", "type /fix to send them to the engine");
448
+ if (this.maybeAutoFix()) return;
449
+ this.push("status", "/fix sends open problems to the engine \xB7 /problems lists them");
393
450
  this.notify({ running: false });
394
451
  this.drainQueue();
395
452
  return;
396
453
  }
454
+ this.clearProblems("gates");
397
455
  }
398
456
  }
399
457
  const dev = this.dev;
@@ -401,39 +459,28 @@ var Session = class {
401
459
  await delay(1500);
402
460
  const errors = dev.errorsSince(runStart);
403
461
  if (errors.length > 0) {
404
- this.pendingFix = {
405
- prompt: buildFixPrompt(errors, dev.tail(30)),
406
- display: "\u26D1 fix dev server errors"
407
- };
462
+ this.addProblem(
463
+ "dev",
464
+ `${errors.length} dev server error line(s)`,
465
+ buildFixPrompt(errors, dev.tail(30))
466
+ );
408
467
  this.push("error", `dev server: ${errors.length} error line(s)
409
468
  ${errors.slice(-5).join("\n")}`);
410
- if (this.opts.autoFix && this.fixAttempts < MAX_AUTO_FIX_ATTEMPTS) {
411
- this.fixAttempts += 1;
412
- this.push("status", `auto-fix attempt ${this.fixAttempts}/${MAX_AUTO_FIX_ATTEMPTS}`);
413
- this.notify({ running: false });
414
- await this.runTurn(this.pendingFix.prompt, this.pendingFix.display);
415
- return;
416
- }
417
- this.push("status", "type /fix to send them to the engine");
469
+ if (this.maybeAutoFix()) return;
470
+ this.push("status", "/fix sends open problems to the engine \xB7 /problems lists them");
418
471
  } else {
419
- this.pendingFix = null;
472
+ this.clearProblems("dev");
420
473
  if (this.opts.autoProbe !== false && this.state.devUrl) {
421
- const report = await probeRuntime(this.state.devUrl);
422
- const summary = report ? runtimeSummary(report) : null;
423
- if (report && summary) {
424
- this.pendingFix = {
425
- prompt: buildRuntimeFixPrompt(report),
426
- display: "\u26D1 fix runtime errors"
427
- };
474
+ const probe = await probeRuntime(this.state.devUrl, this.opts.cwd);
475
+ const summary = probe ? runtimeSummary(probe.report) : null;
476
+ if (probe && summary) {
477
+ this.addProblem("runtime", summary, buildRuntimeFixPrompt(probe.report));
428
478
  this.push("error", `runtime: ${summary}`);
429
- if (this.opts.autoFix && this.fixAttempts < MAX_AUTO_FIX_ATTEMPTS) {
430
- this.fixAttempts += 1;
431
- this.push("status", `auto-fix attempt ${this.fixAttempts}/${MAX_AUTO_FIX_ATTEMPTS}`);
432
- this.notify({ running: false });
433
- await this.runTurn(this.pendingFix.prompt, this.pendingFix.display);
434
- return;
435
- }
436
- this.push("status", "type /fix to send them to the engine");
479
+ if (this.maybeAutoFix()) return;
480
+ this.push("status", "/fix sends open problems to the engine \xB7 /problems lists them");
481
+ } else if (probe) {
482
+ this.clearProblems("runtime");
483
+ await this.visualPulse(probe.pulsePath);
437
484
  }
438
485
  }
439
486
  if (!this.reviewTipShown && this.state.devUrl) {
@@ -457,7 +504,12 @@ ${errors.slice(-5).join("\n")}`);
457
504
  if (this.checkpoints.length > 20) this.checkpoints.shift();
458
505
  }
459
506
  const isFirstTurn = this.sessionId === void 0;
460
- const prompt = isFirstTurn ? composePrompt(ask, { cwd: this.opts.cwd, firstTurn: true }) : ask;
507
+ let prompt = isFirstTurn ? composePrompt(ask, { cwd: this.opts.cwd, firstTurn: true }) : ask;
508
+ const enrichment = enrich(this.opts.cwd, ask);
509
+ if (enrichment.matchedSkills.length > 0) {
510
+ this.push("status", `skills: ${enrichment.matchedSkills.join(", ")}`);
511
+ }
512
+ prompt += enrichment.sections;
461
513
  await this.runTurn(prompt, ask);
462
514
  }
463
515
  /** Restore files to the state before checkpoint `index`; drop it and everything after. */
@@ -479,6 +531,32 @@ ${errors.slice(-5).join("\n")}`);
479
531
  this.push("error", `restore failed: ${result.detail ?? "unknown error"}`);
480
532
  }
481
533
  }
534
+ /**
535
+ * Cross-turn visual drift check: compare this turn's pulse screenshot
536
+ * with the previous one and report how much of the page changed.
537
+ * Informational — changes are usually intended; surprises shouldn't be.
538
+ */
539
+ async visualPulse(pulsePath) {
540
+ if (!pulsePath) return;
541
+ let current;
542
+ try {
543
+ current = (await import("fs")).readFileSync(pulsePath);
544
+ } catch {
545
+ return;
546
+ }
547
+ const previous = this.lastPulse;
548
+ this.lastPulse = current;
549
+ if (!previous) {
550
+ this.push("status", "visual pulse: baseline captured");
551
+ return;
552
+ }
553
+ const pct = await comparePulse(previous, current);
554
+ if (pct === null) return;
555
+ this.push(
556
+ "status",
557
+ pct < 0.5 ? "visual pulse: stable vs last turn" : `visual pulse: ${pct.toFixed(1)}% of the page changed vs last turn`
558
+ );
559
+ }
482
560
  /** Screenshot the running app (and watch its runtime where CDP is available). */
483
561
  async capture() {
484
562
  if (!this.state.devUrl) {
@@ -502,16 +580,28 @@ ${errors.slice(-5).join("\n")}`);
502
580
  const summary = runtimeSummary(result.runtime);
503
581
  if (summary) {
504
582
  this.push("error", `runtime: ${summary}`);
505
- this.pendingFix = { prompt: buildRuntimeFixPrompt(result.runtime), display: "\u26D1 fix runtime errors" };
506
- this.push("status", "type /fix to send them to the engine");
583
+ this.addProblem("runtime", summary, buildRuntimeFixPrompt(result.runtime));
584
+ this.push("status", "/fix sends open problems to the engine");
507
585
  } else {
586
+ this.clearProblems("runtime");
508
587
  this.push("status", "runtime clean \u2014 no console errors, exceptions, or failed requests");
509
588
  }
510
589
  }
511
590
  if (result.a11y && result.a11y.length > 0) {
512
591
  this.push("error", `a11y: ${result.a11y.length} finding(s)
513
592
  ${result.a11y.slice(0, 5).join("\n")}`);
514
- this.push("status", "/review folds these into the fix pass");
593
+ this.addProblem(
594
+ "a11y",
595
+ `${result.a11y.length} accessibility finding(s)`,
596
+ `Fix these accessibility defects found by an automated sweep of the running app:
597
+
598
+ ${result.a11y.join("\n")}
599
+
600
+ They are objective defects, not style preferences.`
601
+ );
602
+ this.push("status", "/review folds these into the fix pass \xB7 /fix sends them directly");
603
+ } else if (result.runtime) {
604
+ this.clearProblems("a11y");
515
605
  }
516
606
  return result.shots.length > 0 ? result : null;
517
607
  }
@@ -561,11 +651,47 @@ ${result.a11y.slice(0, 5).join("\n")}`);
561
651
  }
562
652
  break;
563
653
  }
564
- case "fix":
565
- if (!this.pendingFix) {
566
- this.push("status", "nothing to fix \u2014 no captured errors or failed gates");
654
+ case "fix": {
655
+ if (this.state.problems.length === 0) {
656
+ this.push("status", "nothing to fix \u2014 no open problems");
657
+ break;
658
+ }
659
+ const index = Number.parseInt(arg, 10);
660
+ if (arg && Number.isInteger(index)) {
661
+ const target = this.state.problems[index - 1];
662
+ if (!target) {
663
+ this.push("status", `usage: /fix [1\u2013${this.state.problems.length}] \u2014 see /problems`);
664
+ break;
665
+ }
666
+ this.dispatchFix([target]);
567
667
  } else {
568
- void this.runTurn(this.pendingFix.prompt, this.pendingFix.display);
668
+ this.dispatchFix([...this.state.problems]);
669
+ }
670
+ break;
671
+ }
672
+ case "copy": {
673
+ const last = this.state.items.findLast((i) => i.role === "assistant");
674
+ if (!last) {
675
+ this.push("status", "nothing to copy yet");
676
+ break;
677
+ }
678
+ void import("child_process").then(({ spawn }) => {
679
+ const cmd = process.platform === "darwin" ? spawn("pbcopy") : process.platform === "win32" ? spawn("clip") : spawn("xclip", ["-selection", "clipboard"]);
680
+ cmd.on("error", () => this.push("error", "no clipboard tool found"));
681
+ cmd.on("close", (code) => {
682
+ if (code === 0) this.push("status", `copied last reply (${last.text.length} chars)`);
683
+ });
684
+ cmd.stdin?.end(last.text);
685
+ });
686
+ break;
687
+ }
688
+ case "problems":
689
+ if (this.state.problems.length === 0) {
690
+ this.push("status", "no open problems");
691
+ } else {
692
+ const lines = this.state.problems.map((p, i) => `${i + 1}. [${p.source}] ${p.summary}`);
693
+ this.push("status", `${lines.join("\n")}
694
+ /fix sends all \xB7 /fix <n> targets one`);
569
695
  }
570
696
  break;
571
697
  case "check":
@@ -587,12 +713,10 @@ ${result.a11y.slice(0, 5).join("\n")}`);
587
713
  this.drainQueue();
588
714
  const failures = results.filter((r) => !r.ok);
589
715
  if (failures.length > 0) {
590
- this.pendingFix = {
591
- prompt: buildGatePrompt(failures),
592
- display: `\u26D1 fix failing gates: ${failures.map((f) => f.gate.id).join(", ")}`
593
- };
594
- this.push("status", "type /fix to send failures to the engine");
716
+ this.addProblem("gates", failures.map((f) => f.gate.id).join(" + "), buildGatePrompt(failures));
717
+ this.push("status", "/fix sends open problems to the engine \xB7 /problems lists them");
595
718
  } else {
719
+ this.clearProblems("gates");
596
720
  this.push("status", "all gates passed");
597
721
  }
598
722
  })();
@@ -659,6 +783,75 @@ ${result.a11y.slice(0, 5).join("\n")}`);
659
783
  this.push("status", `resumed ${saved.engine} session${saved.lastAsk ? ` \xB7 "${saved.lastAsk}"` : ""}`);
660
784
  break;
661
785
  }
786
+ case "variants": {
787
+ const [sub, ...subRest] = arg.split(/\s+/);
788
+ if (sub === "apply") {
789
+ const id = subRest[0];
790
+ if (!id) {
791
+ this.push("status", "usage: /variants apply <id>");
792
+ break;
793
+ }
794
+ const applied = applyVariant(this.opts.cwd, id);
795
+ if (applied.ok) {
796
+ cleanVariants(this.opts.cwd);
797
+ this.push("status", `applied ${id} to the working tree \u2014 review with git diff`);
798
+ } else {
799
+ this.push("error", applied.detail ?? "apply failed");
800
+ }
801
+ break;
802
+ }
803
+ if (sub === "clean") {
804
+ this.push("status", `removed ${cleanVariants(this.opts.cwd)} variant(s)`);
805
+ break;
806
+ }
807
+ if (sub === "list") {
808
+ const ids = listVariants(this.opts.cwd);
809
+ this.push("status", ids.length > 0 ? ids.join(" \xB7 ") : "no variants \u2014 /variants <2-4> <ask>");
810
+ break;
811
+ }
812
+ const n = Number.parseInt(sub ?? "", 10);
813
+ const ask = subRest.join(" ").trim();
814
+ if (!Number.isInteger(n) || n < 2 || n > 4 || ask.length === 0) {
815
+ this.push("status", "usage: /variants <2-4> <ask> \xB7 /variants apply <id> \xB7 list \xB7 clean");
816
+ break;
817
+ }
818
+ if (!isGitRepo(this.opts.cwd)) {
819
+ this.push("error", "variants need a git repo with at least one commit");
820
+ break;
821
+ }
822
+ void (async () => {
823
+ cleanVariants(this.opts.cwd);
824
+ this.push("status", `generating ${n} directions in parallel \u2014 this runs ${n} engine sessions`);
825
+ this.notify({ running: true, runStartedAt: Date.now() });
826
+ const engine = getEngine(this.state.engineId);
827
+ const runs = await runVariants(
828
+ this.opts.cwd,
829
+ ask,
830
+ n,
831
+ engine,
832
+ this.state.model,
833
+ (familyId, text) => this.push("status", `[${familyId}] ${text}`)
834
+ );
835
+ const succeeded = runs.filter((r) => r.result.ok);
836
+ if (succeeded.length > 0 && findChrome() && detectDevCommand(this.opts.cwd)) {
837
+ this.push("status", "capturing one screenshot per variant\u2026");
838
+ const shots = await screenshotVariants(this.opts.cwd, succeeded.map((r) => r.variant));
839
+ for (const shot of shots) {
840
+ this.push(
841
+ shot.path ? "status" : "error",
842
+ `[${shot.familyId}] ${shot.path ?? shot.error ?? "screenshot failed"}`
843
+ );
844
+ }
845
+ }
846
+ this.notify({ running: false });
847
+ this.push(
848
+ succeeded.length > 0 ? "status" : "error",
849
+ `${succeeded.length}/${runs.length} variants ready \u2014 /variants apply <id> keeps one, /variants clean discards`
850
+ );
851
+ this.drainQueue();
852
+ })();
853
+ break;
854
+ }
662
855
  case "clear":
663
856
  this.sessionId = void 0;
664
857
  clearState(this.opts.cwd);
@@ -667,7 +860,7 @@ ${result.a11y.slice(0, 5).join("\n")}`);
667
860
  case "help":
668
861
  this.push(
669
862
  "status",
670
- "/engine <id> \xB7 /model <name> \xB7 /dev (start/stop server) \xB7 /check (quality gates) \xB7 /fix (send failures) \xB7 /shot (screenshots) \xB7 /review [focus] (visual self-critique) \xB7 /undo (revert last ask) \xB7 /checkpoints \xB7 /restore <n> \xB7 /resume (last session) \xB7 /clear (new session) \xB7 /quit"
863
+ "/engine <id> \xB7 /model <name> \xB7 /mode plan|safe|yolo \xB7 /dev \xB7 /check (gates) \xB7 /problems \xB7 /fix [n] \xB7 /shot \xB7 /review [focus] \xB7 /variants <2-4> <ask> \xB7 /undo \xB7 /checkpoints \xB7 /restore <n> \xB7 /copy (last reply) \xB7 /resume \xB7 /clear \xB7 /quit"
671
864
  );
672
865
  break;
673
866
  case "quit":
@@ -1138,6 +1331,11 @@ function App({
1138
1331
  return /* @__PURE__ */ jsx3(ThemeProvider, { value: theme2, children: /* @__PURE__ */ jsxs3(Box2, { flexDirection: "column", paddingX: 1, children: [
1139
1332
  /* @__PURE__ */ jsx3(Static, { items: state.items, children: (item) => /* @__PURE__ */ jsx3(Box2, { children: /* @__PURE__ */ jsx3(MessageLine, { message: item }) }, item.id) }),
1140
1333
  state.liveText.length > 0 && /* @__PURE__ */ jsx3(Box2, { children: /* @__PURE__ */ jsx3(Markdown, { text: state.liveText }) }),
1334
+ state.items.length === 0 && state.liveText.length === 0 && !state.running && /* @__PURE__ */ jsxs3(Box2, { flexDirection: "column", marginTop: 1, children: [
1335
+ /* @__PURE__ */ jsx3(Text3, { color: theme2.dim, children: "describe what to build \u2014 or try:" }),
1336
+ /* @__PURE__ */ jsx3(Text3, { color: theme2.dim, children: " /dev start the preview server \xB7 /review after a change \xB7 /variants 3 explore wide" }),
1337
+ /* @__PURE__ */ jsx3(Text3, { color: theme2.dim, children: " shift+tab cycles plan/safe/yolo \xB7 type while the agent works to queue asks" })
1338
+ ] }),
1141
1339
  state.running && /* @__PURE__ */ jsx3(Box2, { marginTop: 1, children: /* @__PURE__ */ jsx3(WorkingLine, { startedAt: state.runStartedAt }) }),
1142
1340
  state.queue.map((queued, index) => /* @__PURE__ */ jsx3(Box2, { children: /* @__PURE__ */ jsxs3(Text3, { color: theme2.dim, children: [
1143
1341
  "\u22EF queued: ",
@@ -1169,14 +1367,21 @@ function App({
1169
1367
  path3.basename(cwd),
1170
1368
  devBadge,
1171
1369
  totalsBadge,
1172
- " \xB7 shift+tab mode \xB7 /help"
1370
+ state.problems.length > 0 && /* @__PURE__ */ jsxs3(Text3, { color: theme2.error, children: [
1371
+ " \xB7 ",
1372
+ state.problems.length,
1373
+ " problem",
1374
+ state.problems.length === 1 ? "" : "s"
1375
+ ] }),
1376
+ " ",
1377
+ "\xB7 shift+tab mode \xB7 /help"
1173
1378
  ] }) })
1174
1379
  ] }) });
1175
1380
  }
1176
1381
 
1177
1382
  // src/cli.tsx
1178
1383
  import { jsx as jsx4 } from "react/jsx-runtime";
1179
- var VERSION = "0.2.0";
1384
+ var VERSION = "0.2.2";
1180
1385
  var program = new Command();
1181
1386
  program.name("squint").description("Lovable for your terminal \u2014 a frontend harness on top of Claude Code, Codex, and friends.").version(VERSION);
1182
1387
  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(
@@ -1210,18 +1415,41 @@ program.command("engines").description("List engines and whether they are instal
1210
1415
  console.log(`${status} ${engine.id.padEnd(10)} ${engine.name.padEnd(14)} ${location}`);
1211
1416
  }
1212
1417
  });
1213
- program.command("doctor").description("Check squint prerequisites and engine availability").action(async () => {
1418
+ program.command("doctor").description("Check squint prerequisites and engine availability").option("--probe", "run each detected engine with a one-word prompt to verify auth works").action(async (options) => {
1214
1419
  const [major] = process.versions.node.split(".");
1215
- const nodeOk = Number(major) >= 20;
1216
- console.log(`${nodeOk ? pc.green("\u2713") : pc.red("\u2717")} node ${process.versions.node}${nodeOk ? "" : " (need >= 20)"}`);
1420
+ const nodeOk = Number(major) >= 22;
1421
+ console.log(`${nodeOk ? pc.green("\u2713") : pc.red("\u2717")} node ${process.versions.node}${nodeOk ? "" : " (need >= 22)"}`);
1217
1422
  const detected = detectEngines();
1218
1423
  for (const { engine, path: binaryPath } of detected) {
1219
1424
  const status = binaryPath ? pc.green("\u2713") : pc.yellow("\u25CB");
1220
1425
  console.log(`${status} ${engine.name}${binaryPath ? "" : pc.dim(` \u2014 install: ${engine.install}`)}`);
1221
1426
  }
1222
- const { findChrome } = await import("./chrome-K4BKFOYS.js");
1223
- const { hasWebSocket } = await import("./cdp-RTWPFIX5.js");
1224
- const chrome = findChrome();
1427
+ if (options.probe) {
1428
+ const { runAgent: runAgent2 } = await import("./run-XHLCTF6V.js");
1429
+ console.log(pc.dim("\nprobing engines with a one-word prompt (verifies auth end to end)\u2026"));
1430
+ for (const { engine, path: binaryPath } of detected) {
1431
+ if (!binaryPath) continue;
1432
+ const startedAt = Date.now();
1433
+ const abort = new AbortController();
1434
+ const timer = setTimeout(() => abort.abort(), 9e4);
1435
+ const result = await runAgent2(
1436
+ engine,
1437
+ { prompt: "Reply with exactly: ok", cwd: process.cwd() },
1438
+ () => {
1439
+ },
1440
+ abort.signal
1441
+ );
1442
+ clearTimeout(timer);
1443
+ const secs = ((Date.now() - startedAt) / 1e3).toFixed(1);
1444
+ const detail = (result.error ?? "failed").split("\n").filter((l) => l.trim()).at(-1) ?? "failed";
1445
+ console.log(
1446
+ result.ok ? `${pc.green("\u2713")} ${engine.id.padEnd(10)} responded in ${secs}s` : `${pc.red("\u2717")} ${engine.id.padEnd(10)} ${pc.dim(detail.slice(0, 110))}`
1447
+ );
1448
+ }
1449
+ }
1450
+ const { findChrome: findChrome2 } = await import("./chrome-RD7XQ767.js");
1451
+ const { hasWebSocket } = await import("./cdp-SFWNP7OA.js");
1452
+ const chrome = findChrome2();
1225
1453
  console.log(
1226
1454
  chrome ? `${pc.green("\u2713")} Chrome ${pc.dim(chrome)}` : `${pc.yellow("\u25CB")} Chrome ${pc.dim("\u2014 screenshots and runtime probing disabled")}`
1227
1455
  );
@@ -1238,7 +1466,7 @@ ${available.length} engine(s) ready.`));
1238
1466
  }
1239
1467
  });
1240
1468
  program.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) => {
1241
- const { installDependencies, writeTemplate } = await import("./init-7AYGAKOS.js");
1469
+ const { installDependencies, writeTemplate } = await import("./init-LSYB32NS.js");
1242
1470
  let result;
1243
1471
  try {
1244
1472
  result = writeTemplate(dir, { force: options.force });
@@ -1262,10 +1490,49 @@ program.command("init").description("Scaffold a new Vite + React + TS + Tailwind
1262
1490
  Next: ${pc.bold(`${cd}${options.install ? "" : "npm install && "}squint`)}`);
1263
1491
  console.log(pc.dim("then describe what to build \u2014 /dev starts the preview server"));
1264
1492
  });
1493
+ var skillsCommand = program.command("skills").description("Project knowledge injected into asks (.squint/rules.md + .squint/skills/)");
1494
+ skillsCommand.command("list").description("Show always-on rules and trigger-matched skills").action(async () => {
1495
+ const { loadRules, loadSkills } = await import("./skills-UGHU22BS.js");
1496
+ const cwd = process.cwd();
1497
+ const rules = loadRules(cwd);
1498
+ console.log(rules ? `${pc.green("\u2713")} rules.md ${pc.dim(`(${rules.split("\n").length} lines, always on)`)}` : pc.dim("\u25CB no .squint/rules.md"));
1499
+ const skills = loadSkills(cwd);
1500
+ if (skills.length === 0) {
1501
+ console.log(pc.dim("\u25CB no skills \u2014 squint skills init writes an example"));
1502
+ return;
1503
+ }
1504
+ for (const skill of skills) {
1505
+ console.log(`${pc.green("\u2713")} ${skill.name.padEnd(20)} ${pc.dim(`triggers: ${skill.triggers.join(", ")}`)}`);
1506
+ }
1507
+ });
1508
+ skillsCommand.command("init").description("Scaffold .squint/rules.md and an example skill").action(async () => {
1509
+ const fs2 = await import("fs");
1510
+ const nodePath = await import("path");
1511
+ const cwd = process.cwd();
1512
+ const skillsDir = nodePath.join(cwd, ".squint", "skills");
1513
+ fs2.mkdirSync(skillsDir, { recursive: true });
1514
+ const rules = nodePath.join(cwd, ".squint", "rules.md");
1515
+ if (!fs2.existsSync(rules)) {
1516
+ fs2.writeFileSync(
1517
+ rules,
1518
+ "# Project rules\n\nThese ride along on every squint ask. Keep them short \u2014 cut anything that would not cause a mistake if removed.\n"
1519
+ );
1520
+ console.log(pc.green("\u2713 .squint/rules.md"));
1521
+ }
1522
+ const example = nodePath.join(skillsDir, "example.md");
1523
+ if (!fs2.existsSync(example)) {
1524
+ fs2.writeFileSync(
1525
+ example,
1526
+ "---\ntriggers: example, sample\n---\n\nThis note is injected only when an ask mentions one of the triggers above.\nDocument the parts of this repo an agent would otherwise rediscover every time:\nwhere state lives, which helpers to reuse, what not to touch.\n"
1527
+ );
1528
+ console.log(pc.green("\u2713 .squint/skills/example.md"));
1529
+ }
1530
+ console.log(pc.dim("rules are always-on; skills inject when an ask mentions a trigger"));
1531
+ });
1265
1532
  program.command("tag").description("Add the element picker to this Vite app (Alt+S in the browser \u2192 click \u2192 file:line:col)").action(async () => {
1266
1533
  const fs2 = await import("fs");
1267
1534
  const nodePath = await import("path");
1268
- const { patchViteConfig, TAGGER_FILENAME, TAGGER_SOURCE } = await import("./source-MEXHWVP4.js");
1535
+ const { patchViteConfig, TAGGER_FILENAME, TAGGER_SOURCE } = await import("./source-ZZU245VN.js");
1269
1536
  const cwd = process.cwd();
1270
1537
  const taggerPath = nodePath.join(cwd, TAGGER_FILENAME);
1271
1538
  fs2.writeFileSync(taggerPath, TAGGER_SOURCE);
@@ -1301,21 +1568,21 @@ variantsCommand.command("gen").description("Generate n variants of one ask in pa
1301
1568
  process.exitCode = 1;
1302
1569
  return;
1303
1570
  }
1304
- const { isGitRepo } = await import("./snapshot-KKUY5RR6.js");
1305
- if (!isGitRepo(cwd)) {
1571
+ const { isGitRepo: isGitRepo2 } = await import("./snapshot-H6VYFHLN.js");
1572
+ if (!isGitRepo2(cwd)) {
1306
1573
  console.error(pc.red("\u2717 variants need a git repo with at least one commit"));
1307
1574
  process.exitCode = 1;
1308
1575
  return;
1309
1576
  }
1310
- const { runVariants, cleanVariants } = await import("./variants-4NCQXTP7.js");
1577
+ const { runVariants: runVariants2, cleanVariants: cleanVariants2 } = await import("./variants-7A7723L7.js");
1311
1578
  const config = loadConfig(defaultPaths(cwd));
1312
1579
  const engineId = resolveEngineId(config, options.engine);
1313
1580
  const engine = getEngine(engineId);
1314
1581
  const model = resolveModel(config, engineId, options.model);
1315
1582
  const ask = promptWords.join(" ");
1316
- cleanVariants(cwd);
1583
+ cleanVariants2(cwd);
1317
1584
  console.log(pc.dim(`generating ${n} directions in parallel via ${engine.id} \u2014 this runs ${n} engine sessions`));
1318
- const runs = await runVariants(
1585
+ const runs = await runVariants2(
1319
1586
  cwd,
1320
1587
  ask,
1321
1588
  n,
@@ -1325,9 +1592,9 @@ variantsCommand.command("gen").description("Generate n variants of one ask in pa
1325
1592
  );
1326
1593
  const succeeded = runs.filter((r) => r.result.ok);
1327
1594
  if (options.shots && succeeded.length > 0) {
1328
- const { screenshotVariants } = await import("./shots-VJBLDMF3.js");
1595
+ const { screenshotVariants: screenshotVariants2 } = await import("./shots-LRYYMTPK.js");
1329
1596
  console.log(pc.dim("capturing screenshots\u2026"));
1330
- const shots = await screenshotVariants(cwd, succeeded.map((r) => r.variant));
1597
+ const shots = await screenshotVariants2(cwd, succeeded.map((r) => r.variant));
1331
1598
  for (const shot of shots) {
1332
1599
  console.log(`${pc.green("\u2713")} ${shot.familyId.padEnd(18)} ${shot.path ?? pc.dim(shot.error ?? "")}`);
1333
1600
  }
@@ -1340,8 +1607,8 @@ ${succeeded.length}/${runs.length} variants ready in .squint/variants/ \u2014 `
1340
1607
  }
1341
1608
  );
1342
1609
  variantsCommand.command("list").description("List generated variants").action(async () => {
1343
- const { listVariants } = await import("./variants-4NCQXTP7.js");
1344
- const ids = listVariants(process.cwd());
1610
+ const { listVariants: listVariants2 } = await import("./variants-7A7723L7.js");
1611
+ const ids = listVariants2(process.cwd());
1345
1612
  if (ids.length === 0) {
1346
1613
  console.log(pc.dim('no variants \u2014 squint variants gen <n> "<ask>"'));
1347
1614
  return;
@@ -1349,26 +1616,26 @@ variantsCommand.command("list").description("List generated variants").action(as
1349
1616
  for (const id of ids) console.log(id);
1350
1617
  });
1351
1618
  variantsCommand.command("apply").description("Apply one variant\u2019s changes to the main tree and discard the rest").argument("<id>", "family id of the winning variant").action(async (id) => {
1352
- const { applyVariant, cleanVariants } = await import("./variants-4NCQXTP7.js");
1619
+ const { applyVariant: applyVariant2, cleanVariants: cleanVariants2 } = await import("./variants-7A7723L7.js");
1353
1620
  const cwd = process.cwd();
1354
- const result = applyVariant(cwd, id);
1621
+ const result = applyVariant2(cwd, id);
1355
1622
  if (!result.ok) {
1356
1623
  console.error(pc.red(`\u2717 ${result.detail}`));
1357
1624
  process.exitCode = 1;
1358
1625
  return;
1359
1626
  }
1360
- cleanVariants(cwd);
1627
+ cleanVariants2(cwd);
1361
1628
  console.log(pc.green(`\u2713 applied ${id} to the working tree`) + pc.dim(" \u2014 review with git diff"));
1362
1629
  });
1363
1630
  variantsCommand.command("clean").description("Discard all variants").action(async () => {
1364
- const { cleanVariants } = await import("./variants-4NCQXTP7.js");
1365
- const count = cleanVariants(process.cwd());
1631
+ const { cleanVariants: cleanVariants2 } = await import("./variants-7A7723L7.js");
1632
+ const count = cleanVariants2(process.cwd());
1366
1633
  console.log(pc.dim(`removed ${count} variant(s)`));
1367
1634
  });
1368
1635
  program.command("brief").description("Set a committed design direction for this project (.squint/brief.md)").argument("[family]", "aesthetic family id (omit to list)").option("--force", "overwrite an existing project brief").action(async (familyId, options) => {
1369
1636
  const fs2 = await import("fs");
1370
1637
  const nodePath = await import("path");
1371
- const { FAMILIES, getFamily, renderFamilyBrief } = await import("./families-RVP5BWQD.js");
1638
+ const { FAMILIES, getFamily, renderFamilyBrief } = await import("./families-2G5SLLY7.js");
1372
1639
  if (!familyId) {
1373
1640
  console.log(pc.bold("Aesthetic families") + pc.dim(" \u2014 squint brief <id>\n"));
1374
1641
  for (const family2 of FAMILIES) {
@@ -1411,7 +1678,7 @@ program.command("check").description("Run this project\u2019s quality gates (typ
1411
1678
  if (results.some((r) => !r.ok)) process.exitCode = 1;
1412
1679
  });
1413
1680
  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) => {
1414
- const { captureViewports: captureViewports2 } = await import("./preview-LTWJK3LF.js");
1681
+ const { captureViewports: captureViewports2 } = await import("./preview-JJJLDF7H.js");
1415
1682
  const result = await captureViewports2(process.cwd(), url);
1416
1683
  if (!result) {
1417
1684
  console.error(pc.red("\u2717 no Chrome/Chromium found"));