@aayambansal/squint 0.2.2 → 0.2.3

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
@@ -151,9 +151,12 @@ squint doctor --probe # run every engine end to end, verify auth act
151
151
  `/fix <n>` targets one. The footer counts what's open.
152
152
  - **Variants without leaving**: `/variants 3 <ask>` runs parallel explorations with
153
153
  streaming per-family status; `/variants apply <id>` keeps the winner.
154
- - **Commands**: `/dev` `/check` `/problems` `/fix [n]` `/shot` `/review [focus]`
155
- `/variants` `/undo` `/checkpoints` `/restore <n>` `/mode` `/theme` `/resume`
156
- `/engine <id>` `/model <name>` `/clear`.
154
+ - **Commands**: type `/` and matching commands appear with descriptions; tab completes.
155
+ `/dev` `/check` `/problems` `/fix [n]` `/shot` `/review [focus]` `/variants` `/undo`
156
+ `/checkpoints` `/restore <n>` `/mode` `/theme` `/copy` `/save` `/resume` `/clear`.
157
+ - **Visual pulse**: every clean turn is screenshotted and pixel-compared with the last —
158
+ drift shows up as a number, not a surprise. `.squint/locks` lists paths the engine must
159
+ never touch; `/save` exports the transcript as markdown.
157
160
  - Assistant output renders as markdown; the done line measures real work via git
158
161
  (`3 files +42 −7`); the footer tracks session turns and cost; a bell rings when a
159
162
  turn finishes.
@@ -51,12 +51,29 @@ function matchSkills(skills, ask) {
51
51
  const haystack = ask.toLowerCase();
52
52
  return skills.filter((skill) => skill.triggers.some((t) => haystack.includes(t)));
53
53
  }
54
+ function loadLocks(cwd) {
55
+ try {
56
+ return fs.readFileSync(path.join(cwd, ".squint", "locks"), "utf8").split("\n").map((line) => line.trim()).filter((line) => line.length > 0 && !line.startsWith("#"));
57
+ } catch {
58
+ return [];
59
+ }
60
+ }
54
61
  function enrich(cwd, ask) {
55
62
  const parts = [];
56
63
  const rules = loadRules(cwd);
57
64
  if (rules) parts.push(`## Project rules (always apply)
58
65
 
59
66
  ${rules}`);
67
+ const locks = loadLocks(cwd);
68
+ if (locks.length > 0) {
69
+ parts.push(
70
+ `## Locked files (hard constraint)
71
+
72
+ Never modify these paths, no matter what the task seems to need:
73
+ ${locks.map((l) => `- ${l}`).join("\n")}
74
+ If the task appears to require changing them, stop and explain instead.`
75
+ );
76
+ }
60
77
  const matched = matchSkills(loadSkills(cwd), ask);
61
78
  for (const skill of matched) {
62
79
  parts.push(`## Project notes: ${skill.name}
@@ -76,5 +93,6 @@ export {
76
93
  loadSkills,
77
94
  loadRules,
78
95
  matchSkills,
96
+ loadLocks,
79
97
  enrich
80
98
  };
@@ -0,0 +1,39 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/session/commands.ts
4
+ var COMMANDS = [
5
+ { name: "dev", description: "start/stop the project dev server" },
6
+ { name: "check", description: "run all quality gates (typecheck, lint, test, build)" },
7
+ { name: "problems", description: "list open findings from gates, dev server, runtime, a11y" },
8
+ { name: "fix", args: "[n]", description: "send all open problems to the engine, or just problem n" },
9
+ { name: "shot", description: "screenshot the app at mobile/tablet/desktop" },
10
+ { name: "review", args: "[focus]", description: "screenshots + the engine critiques its own rendered work" },
11
+ { name: "variants", args: "<2-4> <ask>", description: "parallel design explorations; apply/list/clean" },
12
+ { name: "undo", description: "revert the last ask (files only)" },
13
+ { name: "checkpoints", description: "list per-ask checkpoints" },
14
+ { name: "restore", args: "<n>", description: "rewind files to before ask n" },
15
+ { name: "mode", args: "plan|safe|yolo", description: "how much the engine may do (shift+tab cycles)" },
16
+ { name: "engine", args: "<id>", description: "switch backend (new session)" },
17
+ { name: "model", args: "[name]", description: "model override for the engine" },
18
+ { name: "theme", args: "[name]", description: "switch the TUI theme", viewLevel: true },
19
+ { name: "copy", description: "copy the last reply to the clipboard" },
20
+ { name: "save", description: "export the transcript to .squint/transcripts/" },
21
+ { name: "queue", args: "clear", description: "drop queued asks" },
22
+ { name: "resume", description: "pick up the previous session for this repo" },
23
+ { name: "clear", description: "new session (transcript, totals, persisted state)" },
24
+ { name: "help", description: "list commands" },
25
+ { name: "quit", description: "exit with a session summary" }
26
+ ];
27
+ function completeCommand(partial) {
28
+ const query = partial.toLowerCase();
29
+ return COMMANDS.filter((c) => c.name.startsWith(query));
30
+ }
31
+ function commandHelp() {
32
+ return COMMANDS.map((c) => `/${c.name}${c.args ? ` ${c.args}` : ""} \u2014 ${c.description}`).join("\n");
33
+ }
34
+
35
+ export {
36
+ COMMANDS,
37
+ completeCommand,
38
+ commandHelp
39
+ };
@@ -16,7 +16,7 @@ import path2 from "path";
16
16
  // src/state/state.ts
17
17
  import fs from "fs";
18
18
  import path from "path";
19
- var IGNORED = ["preview/", "state.json", "variants/"];
19
+ var IGNORED = ["preview/", "state.json", "variants/", "transcripts/"];
20
20
  function ensureSquintIgnore(cwd) {
21
21
  const dir = path.join(cwd, ".squint");
22
22
  fs.mkdirSync(dir, { recursive: true });
package/dist/cli.js CHANGED
@@ -1,4 +1,10 @@
1
1
  #!/usr/bin/env node
2
+ import {
3
+ diffStatSince,
4
+ isGitRepo,
5
+ restoreSnapshot,
6
+ takeSnapshot
7
+ } from "./chunk-YHRAOBI2.js";
2
8
  import {
3
9
  DevServer,
4
10
  buildFixPrompt,
@@ -17,6 +23,9 @@ import {
17
23
  import {
18
24
  runAgent
19
25
  } from "./chunk-OTG4ZB4R.js";
26
+ import {
27
+ completeCommand
28
+ } from "./chunk-EECGXFRX.js";
20
29
  import {
21
30
  buildGatePrompt,
22
31
  detectFastGates,
@@ -33,7 +42,7 @@ import {
33
42
  probeRuntime,
34
43
  runtimeSummary,
35
44
  saveState
36
- } from "./chunk-73ULM6HF.js";
45
+ } from "./chunk-SU6YRRMF.js";
37
46
  import "./chunk-OC6RU6XH.js";
38
47
  import {
39
48
  findChrome
@@ -44,13 +53,7 @@ import {
44
53
  } from "./chunk-2LRIKWBU.js";
45
54
  import {
46
55
  enrich
47
- } from "./chunk-XZKQZKEE.js";
48
- import {
49
- diffStatSince,
50
- isGitRepo,
51
- restoreSnapshot,
52
- takeSnapshot
53
- } from "./chunk-YHRAOBI2.js";
56
+ } from "./chunk-4LAU5TGK.js";
54
57
 
55
58
  // src/cli.tsx
56
59
  import { Command } from "commander";
@@ -669,6 +672,38 @@ They are objective defects, not style preferences.`
669
672
  }
670
673
  break;
671
674
  }
675
+ case "save": {
676
+ void (async () => {
677
+ const fs2 = await import("fs");
678
+ const path4 = await import("path");
679
+ const dir = path4.join(this.opts.cwd, ".squint", "transcripts");
680
+ fs2.mkdirSync(dir, { recursive: true });
681
+ const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-").slice(0, 19);
682
+ const file = path4.join(dir, `${stamp}.md`);
683
+ const lines = [`# squint session \u2014 ${(/* @__PURE__ */ new Date()).toISOString().slice(0, 16)}`, ""];
684
+ for (const item of this.state.items) {
685
+ switch (item.role) {
686
+ case "user":
687
+ lines.push(`## \u276F ${item.text}`, "");
688
+ break;
689
+ case "assistant":
690
+ lines.push(item.text, "");
691
+ break;
692
+ case "tool":
693
+ lines.push(`- \u2699 ${item.text}`);
694
+ break;
695
+ case "thinking":
696
+ break;
697
+ default:
698
+ lines.push(`> ${item.role === "error" ? "\u2717 " : ""}${item.text.split("\n").join("\n> ")}`);
699
+ }
700
+ }
701
+ lines.push("", `> ${this.summary()}`);
702
+ fs2.writeFileSync(file, lines.join("\n") + "\n");
703
+ this.push("status", `saved transcript \u2192 ${path4.relative(this.opts.cwd, file)}`);
704
+ })();
705
+ break;
706
+ }
672
707
  case "copy": {
673
708
  const last = this.state.items.findLast((i) => i.role === "assistant");
674
709
  if (!last) {
@@ -857,12 +892,10 @@ They are objective defects, not style preferences.`
857
892
  clearState(this.opts.cwd);
858
893
  this.notify({ items: [], totals: { costUsd: 0, turns: 0 } });
859
894
  break;
860
- case "help":
861
- this.push(
862
- "status",
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"
864
- );
895
+ case "help": {
896
+ void import("./commands-6XDXUVO6.js").then(({ commandHelp }) => this.push("status", commandHelp()));
865
897
  break;
898
+ }
866
899
  case "quit":
867
900
  case "exit":
868
901
  this.push("status", this.summary());
@@ -1248,6 +1281,13 @@ function App({
1248
1281
  session.cycleMode();
1249
1282
  return;
1250
1283
  }
1284
+ if (key.tab && line.text.startsWith("/") && !line.text.includes(" ")) {
1285
+ const matches = completeCommand(line.text.slice(1));
1286
+ if (matches.length > 0) {
1287
+ setLine(fromText(`/${matches[0].name}${matches[0].args ? " " : ""}`));
1288
+ }
1289
+ return;
1290
+ }
1251
1291
  if (key.return) {
1252
1292
  const value = line.text.trim();
1253
1293
  setLine(emptyLine);
@@ -1341,6 +1381,16 @@ function App({
1341
1381
  "\u22EF queued: ",
1342
1382
  queued
1343
1383
  ] }) }, index)),
1384
+ line.text.startsWith("/") && !line.text.includes(" ") && /* @__PURE__ */ jsx3(Box2, { flexDirection: "column", children: completeCommand(line.text.slice(1)).slice(0, 5).map((command, index) => /* @__PURE__ */ jsxs3(Text3, { color: index === 0 ? theme2.accent : theme2.dim, children: [
1385
+ "/",
1386
+ command.name,
1387
+ command.args ? ` ${command.args}` : "",
1388
+ " ",
1389
+ /* @__PURE__ */ jsxs3(Text3, { color: theme2.dim, children: [
1390
+ "\u2014 ",
1391
+ command.description
1392
+ ] })
1393
+ ] }, command.name)) }),
1344
1394
  /* @__PURE__ */ jsx3(Box2, { marginTop: state.running ? 0 : 1, children: /* @__PURE__ */ jsxs3(Text3, { children: [
1345
1395
  /* @__PURE__ */ jsx3(Text3, { color: theme2.accent, children: "\u276F " }),
1346
1396
  line.text.slice(0, line.cursor),
@@ -1381,7 +1431,7 @@ function App({
1381
1431
 
1382
1432
  // src/cli.tsx
1383
1433
  import { jsx as jsx4 } from "react/jsx-runtime";
1384
- var VERSION = "0.2.2";
1434
+ var VERSION = "0.2.3";
1385
1435
  var program = new Command();
1386
1436
  program.name("squint").description("Lovable for your terminal \u2014 a frontend harness on top of Claude Code, Codex, and friends.").version(VERSION);
1387
1437
  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(
@@ -1492,7 +1542,7 @@ Next: ${pc.bold(`${cd}${options.install ? "" : "npm install && "}squint`)}`);
1492
1542
  });
1493
1543
  var skillsCommand = program.command("skills").description("Project knowledge injected into asks (.squint/rules.md + .squint/skills/)");
1494
1544
  skillsCommand.command("list").description("Show always-on rules and trigger-matched skills").action(async () => {
1495
- const { loadRules, loadSkills } = await import("./skills-UGHU22BS.js");
1545
+ const { loadRules, loadSkills } = await import("./skills-DNBHZAAU.js");
1496
1546
  const cwd = process.cwd();
1497
1547
  const rules = loadRules(cwd);
1498
1548
  console.log(rules ? `${pc.green("\u2713")} rules.md ${pc.dim(`(${rules.split("\n").length} lines, always on)`)}` : pc.dim("\u25CB no .squint/rules.md"));
@@ -1678,7 +1728,7 @@ program.command("check").description("Run this project\u2019s quality gates (typ
1678
1728
  if (results.some((r) => !r.ok)) process.exitCode = 1;
1679
1729
  });
1680
1730
  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) => {
1681
- const { captureViewports: captureViewports2 } = await import("./preview-JJJLDF7H.js");
1731
+ const { captureViewports: captureViewports2 } = await import("./preview-UX4VS4SX.js");
1682
1732
  const result = await captureViewports2(process.cwd(), url);
1683
1733
  if (!result) {
1684
1734
  console.error(pc.red("\u2717 no Chrome/Chromium found"));
@@ -0,0 +1,11 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ COMMANDS,
4
+ commandHelp,
5
+ completeCommand
6
+ } from "./chunk-EECGXFRX.js";
7
+ export {
8
+ COMMANDS,
9
+ commandHelp,
10
+ completeCommand
11
+ };
@@ -8,7 +8,7 @@ import {
8
8
  previewDir,
9
9
  probeRuntime,
10
10
  runtimeSummary
11
- } from "./chunk-73ULM6HF.js";
11
+ } from "./chunk-SU6YRRMF.js";
12
12
  import "./chunk-OC6RU6XH.js";
13
13
  import "./chunk-P3V5CWH5.js";
14
14
  import "./chunk-2LRIKWBU.js";
@@ -1,13 +1,15 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  enrich,
4
+ loadLocks,
4
5
  loadRules,
5
6
  loadSkills,
6
7
  matchSkills,
7
8
  parseSkill
8
- } from "./chunk-XZKQZKEE.js";
9
+ } from "./chunk-4LAU5TGK.js";
9
10
  export {
10
11
  enrich,
12
+ loadLocks,
11
13
  loadRules,
12
14
  loadSkills,
13
15
  matchSkills,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aayambansal/squint",
3
- "version": "0.2.2",
3
+ "version": "0.2.3",
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",