@aayambansal/squint 0.1.3 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -86,10 +86,11 @@ From source: `git clone https://github.com/aayambansal/squint.git && cd squint &
86
86
  and catches what the server never prints: blank pages, exceptions, failed requests.
87
87
  5. **The agent looks at its work.** `/review` screenshots mobile/tablet/desktop and
88
88
  re-prompts the engine to critique what it can see — then fix it.
89
- 6. **Gates keep it honest.** `/check` runs typecheck, lint, test, build failures come
90
- back with orders not to weaken the checks.
91
- 7. **Everything is reversible.** Each ask is snapshotted via git plumbing; `/undo` reverts
92
- the whole turn while your own uncommitted work survives.
89
+ 6. **Gates keep it honest.** Typecheck + lint run automatically after *every* turn and
90
+ auto-fix (capped); `/check` adds tests and the build — failures come back with orders
91
+ not to weaken the checks.
92
+ 7. **Everything is reversible.** Every ask records a checkpoint; `/undo` pops the last,
93
+ `/restore <n>` rewinds files to any earlier point — your own uncommitted work survives.
93
94
  8. **Point at things.** Alt+S in the browser, click any element, and a self-locating
94
95
  reference lands on your clipboard to paste into squint.
95
96
  9. **Explore in parallel.** `squint variants gen 3 "<ask>"` builds the same ask three ways —
@@ -129,11 +130,24 @@ squint config set engine claude
129
130
  squint config set models.claude claude-sonnet-5
130
131
  squint config set autoDev true # dev server starts with the TUI
131
132
  squint config set autoFix true # errors auto-route back (max 2 tries)
133
+ squint config set autoCheck false # skip the per-turn typecheck+lint pass
134
+ squint config set theme ocean # amber · ocean · moss · rose · mono
135
+ squint config set bell false # no bell on turn completion
132
136
  squint doctor # engines + Chrome + WebSocket check
133
137
  ```
134
138
 
135
- Inside the TUI: `/dev` `/check` `/fix` `/shot` `/review [focus]` `/undo` `/resume`
136
- `/engine <id>` `/model <name>` `/clear` — `Esc` interrupts, arrows recall history.
139
+ **Inside the TUI:**
140
+
141
+ - **Modes**: `shift+tab` cycles `safe` (edits auto-approved) → `plan` (read-only
142
+ investigation) → `yolo` (no friction), mapped to each engine's native permission flags.
143
+ - **Type ahead**: keep typing while the agent works — Enter queues asks that dispatch in
144
+ order; `/queue clear` drops them. `Esc` interrupts the current turn.
145
+ - **Editing**: a real line editor — arrows move, `alt+←/→` jump words, `ctrl+a/e/k/u/w`,
146
+ `↑/↓` history. `ctrl+c` twice exits with a session summary.
147
+ - **Commands**: `/dev` `/check` `/fix` `/shot` `/review [focus]` `/undo` `/checkpoints`
148
+ `/restore <n>` `/mode` `/theme` `/resume` `/engine <id>` `/model <name>` `/clear`.
149
+ - Assistant output renders as markdown; the footer tracks session turns and cost; a bell
150
+ rings when a turn finishes.
137
151
 
138
152
  ## Design directions
139
153
 
@@ -2,7 +2,7 @@
2
2
  import {
3
3
  cdpCapture,
4
4
  hasWebSocket
5
- } from "./chunk-ABF4QAMZ.js";
5
+ } from "./chunk-VJGE7HSP.js";
6
6
  export {
7
7
  cdpCapture,
8
8
  hasWebSocket
@@ -2,8 +2,8 @@
2
2
  import {
3
3
  findChrome,
4
4
  screenshot
5
- } from "./chunk-MURKQ7SI.js";
6
- import "./chunk-YIVPCWSG.js";
5
+ } from "./chunk-P3V5CWH5.js";
6
+ import "./chunk-2LRIKWBU.js";
7
7
  export {
8
8
  findChrome,
9
9
  screenshot
@@ -1,7 +1,17 @@
1
1
  #!/usr/bin/env node
2
+ import {
3
+ variantsRoot
4
+ } from "./chunk-LCS47EAG.js";
5
+ import {
6
+ findChrome,
7
+ screenshot
8
+ } from "./chunk-P3V5CWH5.js";
2
9
  import {
3
10
  lineSplitter
4
- } from "./chunk-YIVPCWSG.js";
11
+ } from "./chunk-2LRIKWBU.js";
12
+
13
+ // src/variants/shots.ts
14
+ import path2 from "path";
5
15
 
6
16
  // src/devserver/devserver.ts
7
17
  import { spawn } from "child_process";
@@ -149,8 +159,55 @@ ${errorBlock}
149
159
  ${context}`;
150
160
  }
151
161
 
162
+ // src/variants/shots.ts
163
+ var URL_TIMEOUT_MS = 45e3;
164
+ function waitForUrl(server) {
165
+ return new Promise((resolve) => {
166
+ const startedAt = Date.now();
167
+ const poll = setInterval(() => {
168
+ if (server.url) {
169
+ clearInterval(poll);
170
+ resolve(server.url);
171
+ } else if (Date.now() - startedAt > URL_TIMEOUT_MS || server.state === "crashed") {
172
+ clearInterval(poll);
173
+ resolve(null);
174
+ }
175
+ }, 200);
176
+ });
177
+ }
178
+ async function screenshotVariants(cwd, variants) {
179
+ const chrome = findChrome();
180
+ if (!chrome) {
181
+ return variants.map((v) => ({ familyId: v.family.id, error: "no Chrome found" }));
182
+ }
183
+ const shots = [];
184
+ for (const variant of variants) {
185
+ const command = detectDevCommand(variant.dir);
186
+ if (!command) {
187
+ shots.push({ familyId: variant.family.id, error: "no dev script" });
188
+ continue;
189
+ }
190
+ const server = new DevServer(variant.dir);
191
+ server.start(command);
192
+ const url = await waitForUrl(server);
193
+ if (!url) {
194
+ server.stop();
195
+ shots.push({ familyId: variant.family.id, error: "dev server did not announce a URL" });
196
+ continue;
197
+ }
198
+ const outPath = path2.join(variantsRoot(cwd), `${variant.family.id}.png`);
199
+ const result = await screenshot(chrome, url, outPath, { width: 1440, height: 900 });
200
+ server.stop();
201
+ shots.push(
202
+ result.ok ? { familyId: variant.family.id, path: outPath } : { familyId: variant.family.id, error: result.error }
203
+ );
204
+ }
205
+ return shots;
206
+ }
207
+
152
208
  export {
153
209
  detectDevCommand,
154
210
  DevServer,
155
- buildFixPrompt
211
+ buildFixPrompt,
212
+ screenshotVariants
156
213
  };
@@ -1,23 +1,5 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- // src/engines/registry.ts
4
- import fs from "fs";
5
- import path from "path";
6
-
7
- // src/engines/aider.ts
8
- var aider = {
9
- id: "aider",
10
- name: "Aider",
11
- binary: "aider",
12
- install: "python -m pip install aider-install && aider-install",
13
- supportsResume: false,
14
- buildArgs(opts) {
15
- const args = ["--message", opts.prompt, "--yes-always", "--no-auto-commits"];
16
- if (opts.model) args.push("--model", opts.model);
17
- return args;
18
- }
19
- };
20
-
21
3
  // src/util/stream.ts
22
4
  function lineSplitter(onLine) {
23
5
  let buffer = "";
@@ -42,6 +24,24 @@ function truncate(text, max) {
42
24
  return text.slice(0, max - 1) + "\u2026";
43
25
  }
44
26
 
27
+ // src/engines/registry.ts
28
+ import fs from "fs";
29
+ import path from "path";
30
+
31
+ // src/engines/aider.ts
32
+ var aider = {
33
+ id: "aider",
34
+ name: "Aider",
35
+ binary: "aider",
36
+ install: "python -m pip install aider-install && aider-install",
37
+ supportsResume: false,
38
+ buildArgs(opts) {
39
+ const args = ["--message", opts.prompt, "--yes-always", "--no-auto-commits"];
40
+ if (opts.model) args.push("--model", opts.model);
41
+ return args;
42
+ }
43
+ };
44
+
45
45
  // src/engines/claudeProtocol.ts
46
46
  function createClaudeStreamParser(readyLabel) {
47
47
  let sawTextDelta = false;
@@ -139,6 +139,7 @@ var claude = {
139
139
  install: "npm install -g @anthropic-ai/claude-code",
140
140
  supportsResume: true,
141
141
  buildArgs(opts) {
142
+ const permissionMode = opts.mode === "plan" ? "plan" : opts.mode === "yolo" ? "bypassPermissions" : "acceptEdits";
142
143
  const args = [
143
144
  "-p",
144
145
  opts.prompt,
@@ -147,7 +148,7 @@ var claude = {
147
148
  "--verbose",
148
149
  "--include-partial-messages",
149
150
  "--permission-mode",
150
- "acceptEdits"
151
+ permissionMode
151
152
  ];
152
153
  if (opts.model) args.push("--model", opts.model);
153
154
  if (opts.sessionId) args.push("--resume", opts.sessionId);
@@ -164,9 +165,10 @@ var codex = {
164
165
  install: "npm install -g @openai/codex",
165
166
  supportsResume: true,
166
167
  buildArgs(opts) {
168
+ const sandbox = opts.mode === "plan" ? "read-only" : opts.mode === "yolo" ? "danger-full-access" : "workspace-write";
167
169
  const args = ["exec"];
168
170
  if (opts.sessionId) args.push("resume", opts.sessionId);
169
- args.push("--json", "--sandbox", "workspace-write", "--skip-git-repo-check");
171
+ args.push("--json", "--sandbox", sandbox, "--skip-git-repo-check");
170
172
  if (opts.model) args.push("--model", opts.model);
171
173
  args.push(opts.prompt);
172
174
  return args;
@@ -278,7 +280,9 @@ var cursor = {
278
280
  install: "curl https://cursor.com/install -fsS | bash",
279
281
  supportsResume: true,
280
282
  buildArgs(opts) {
281
- const args = ["-p", opts.prompt, "--output-format", "stream-json", "--force"];
283
+ const args = ["-p", opts.prompt, "--output-format", "stream-json"];
284
+ if (opts.mode === "plan") args.push("--mode", "plan");
285
+ else args.push("--force");
282
286
  if (opts.model) args.push("--model", opts.model);
283
287
  if (opts.sessionId) args.push(`--resume=${opts.sessionId}`);
284
288
  return args;
@@ -294,7 +298,8 @@ var gemini = {
294
298
  install: "npm install -g @google/gemini-cli",
295
299
  supportsResume: false,
296
300
  buildArgs(opts) {
297
- const args = ["-p", opts.prompt, "--yolo"];
301
+ const approval = opts.mode === "plan" ? "plan" : opts.mode === "yolo" ? "yolo" : "auto_edit";
302
+ const args = ["-p", opts.prompt, "--approval-mode", approval];
298
303
  if (opts.model) args.push("-m", opts.model);
299
304
  return args;
300
305
  }
@@ -394,7 +399,6 @@ function detectEngines() {
394
399
  export {
395
400
  lineSplitter,
396
401
  truncate,
397
- engines,
398
402
  getEngine,
399
403
  findBinary,
400
404
  findEngineBinary,
@@ -48,6 +48,9 @@ function detectGates(cwd) {
48
48
  }
49
49
  return gates;
50
50
  }
51
+ function detectFastGates(cwd) {
52
+ return detectGates(cwd).filter((gate) => gate.id === "typecheck" || gate.id === "lint");
53
+ }
51
54
  var TAIL_LINES = 40;
52
55
  var GATE_TIMEOUT_MS = 5 * 60 * 1e3;
53
56
  function runGate(cwd, gate) {
@@ -112,6 +115,7 @@ After fixing, the failing commands above must pass.`;
112
115
 
113
116
  export {
114
117
  detectGates,
118
+ detectFastGates,
115
119
  runGate,
116
120
  runGates,
117
121
  buildGatePrompt
@@ -1,14 +1,12 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  FAMILIES,
4
+ FIRST_TURN_ADDENDUM,
4
5
  renderFamilyBrief
5
- } from "./chunk-5IR2AZVK.js";
6
+ } from "./chunk-P3H4N2EN.js";
6
7
  import {
7
8
  runAgent
8
- } from "./chunk-MGHJSERZ.js";
9
- import {
10
- FIRST_TURN_ADDENDUM
11
- } from "./chunk-NT2HR4RD.js";
9
+ } from "./chunk-OTG4ZB4R.js";
12
10
 
13
11
  // src/variants/variants.ts
14
12
  import { execFileSync } from "child_process";
@@ -3,7 +3,7 @@ import {
3
3
  findEngineBinary,
4
4
  lineSplitter,
5
5
  truncate
6
- } from "./chunk-YIVPCWSG.js";
6
+ } from "./chunk-2LRIKWBU.js";
7
7
 
8
8
  // src/runner/run.ts
9
9
  import { spawn } from "child_process";
@@ -1,7 +1,60 @@
1
1
  #!/usr/bin/env node
2
- import {
3
- DEFAULT_BRIEF
4
- } from "./chunk-NT2HR4RD.js";
2
+
3
+ // src/prompt/brief.ts
4
+ import fs from "fs";
5
+ import path from "path";
6
+ var DEFAULT_BRIEF = `You are acting as a senior product designer and frontend engineer. Treat every change as production work, not a demo.
7
+
8
+ Direction before code:
9
+ - Commit to one specific visual direction derived from what this product is for, and state it in one sentence before implementing. If the direction could be guessed from the product category alone, sharpen it until it couldn't.
10
+ - Choose a color strategy deliberately: restrained (neutrals + one accent), committed (one dominant color owning 30\u201360% of the surface), or a small palette of 3\u20135 named roles. Never purple/violet gradients unless asked. Body text contrast stays at least 4.5:1 \u2014 no light gray body copy "for elegance".
11
+ - Typography: at most two families, paired on contrast (display serif + geometric sans, or sans + mono). Never Inter/Roboto/Open Sans/system defaults for display type. Pair weight extremes (300 against 700\u2013900); display sizes jump 3x over body, not 1.5x.
12
+
13
+ Tokens are the system:
14
+ - Define or extend design tokens (CSS variables / theme config) first, then compose the UI from them. Never scatter literal colors or one-off spacing values through components.
15
+ - One spacing rhythm on a 4/8px grid; one type scale with a ratio of at least 1.25 between steps.
16
+
17
+ Banned tells \u2014 these read instantly as machine-generated:
18
+ - Purple gradient on white; glassmorphism cards; cream/beige page background as a reflex "warmth" move
19
+ - Emoji as icons; identical icon-topped card grids; stat banner rows; numbered 01/02/03 section markers
20
+ - Tiny all-caps tracked eyebrow labels over every section; gradient text; colored left-border strips on cards
21
+ - Centered hero + badge + three feature cards; unmodified component-library defaults
22
+ If someone could look at the result and say "AI made that" without doubt, it has failed.
23
+
24
+ Craft details:
25
+ - Every interactive element gets hover, focus-visible, and active treatment; keyboard focus stays visible.
26
+ - Anything that loads data gets loading, empty, and error states.
27
+ - Motion: 150\u2013250ms ease-out; one orchestrated entrance with staggered reveals beats scattered micro-interactions; honor prefers-reduced-motion; never leave content invisible until a scroll observer fires.
28
+ - Body line length 65\u201375ch; text-wrap: balance on headings.
29
+
30
+ Engineering:
31
+ - Follow the repo's existing conventions and extend its patterns; new components in new files, small and focused; semantic HTML.
32
+ - Responsive from 360px up with no horizontal overflow \u2014 check intermediate widths, not just phone and desktop.
33
+ - Let errors surface instead of swallowing them in try/catch; log clearly so failures can be traced.
34
+ - Not done until the app builds cleanly, typechecks, and renders without console errors.`;
35
+ var FIRST_TURN_ADDENDUM = `This is the opening move on this task: establish the design foundation before building. State the visual direction, set up the tokens/theme first, then build components from them. The first render should feel like a designed product, not a scaffold \u2014 impressive on sight.`;
36
+ function loadBrief(cwd) {
37
+ const custom = path.join(cwd, ".squint", "brief.md");
38
+ try {
39
+ const text = fs.readFileSync(custom, "utf8").trim();
40
+ if (text.length > 0) return text;
41
+ } catch {
42
+ }
43
+ return DEFAULT_BRIEF;
44
+ }
45
+ function composePrompt(ask, opts) {
46
+ if (opts.noBrief) return ask;
47
+ const brief = loadBrief(opts.cwd);
48
+ const firstTurn = opts.firstTurn ?? true;
49
+ const addendum = firstTurn ? `
50
+
51
+ ${FIRST_TURN_ADDENDUM}` : "";
52
+ return `${brief}${addendum}
53
+
54
+ ## Task
55
+
56
+ ${ask}`;
57
+ }
5
58
 
6
59
  // src/prompt/families.ts
7
60
  var FAMILIES = [
@@ -121,6 +174,8 @@ ${coreStandards()}`;
121
174
  }
122
175
 
123
176
  export {
177
+ FIRST_TURN_ADDENDUM,
178
+ composePrompt,
124
179
  FAMILIES,
125
180
  getFamily,
126
181
  renderFamilyBrief
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  findBinary
4
- } from "./chunk-YIVPCWSG.js";
4
+ } from "./chunk-2LRIKWBU.js";
5
5
 
6
6
  // src/preview/chrome.ts
7
7
  import { spawn } from "child_process";
@@ -34,8 +34,8 @@ function screenshot(chromePath, url, outPath, opts) {
34
34
  const args = [
35
35
  "--headless=new",
36
36
  "--disable-gpu",
37
- // CI runners commonly need this; local runs keep the sandbox.
38
- ...process.env.CI ? ["--no-sandbox"] : [],
37
+ // CI containers need these; local runs keep the sandbox and shm.
38
+ ...process.env.CI ? ["--no-sandbox", "--disable-dev-shm-usage"] : [],
39
39
  "--hide-scrollbars",
40
40
  "--force-device-scale-factor=1",
41
41
  `--window-size=${opts.width},${opts.height}`,
@@ -2,11 +2,11 @@
2
2
  import {
3
3
  cdpCapture,
4
4
  hasWebSocket
5
- } from "./chunk-ABF4QAMZ.js";
5
+ } from "./chunk-VJGE7HSP.js";
6
6
  import {
7
7
  findChrome,
8
8
  screenshot
9
- } from "./chunk-MURKQ7SI.js";
9
+ } from "./chunk-P3V5CWH5.js";
10
10
 
11
11
  // src/preview/preview.ts
12
12
  import fs2 from "fs";
@@ -113,7 +113,7 @@ function launchChrome(chromePath) {
113
113
  [
114
114
  "--headless=new",
115
115
  "--disable-gpu",
116
- ...process.env.CI ? ["--no-sandbox"] : [],
116
+ ...process.env.CI ? ["--no-sandbox", "--disable-dev-shm-usage"] : [],
117
117
  "--no-first-run",
118
118
  "--remote-debugging-port=0",
119
119
  `--user-data-dir=${profileDir}`,
@@ -122,10 +122,13 @@ function launchChrome(chromePath) {
122
122
  { stdio: ["ignore", "ignore", "pipe"] }
123
123
  );
124
124
  let stderr = "";
125
- const timer = setTimeout(() => {
126
- child.kill("SIGKILL");
127
- reject(new Error("Chrome did not announce DevTools endpoint"));
128
- }, 15e3);
125
+ const timer = setTimeout(
126
+ () => {
127
+ child.kill("SIGKILL");
128
+ reject(new Error("Chrome did not announce DevTools endpoint"));
129
+ },
130
+ process.env.CI ? 45e3 : 15e3
131
+ );
129
132
  child.stderr.setEncoding("utf8");
130
133
  child.stderr.on("data", (chunk) => {
131
134
  stderr += chunk;
@@ -79,10 +79,11 @@ const PICKER = \`(() => {
79
79
  addEventListener('keydown', (e) => {
80
80
  if (e.altKey && (e.key === 's' || e.key === 'S' || e.code === 'KeyS')) {
81
81
  active = !active;
82
- if (!active) { box.style.display = 'none'; tip.style.display = 'none'; }
83
- else toast('squint picker on \u2014 click an element (esc to cancel)');
82
+ if (!active) { window.__squintFinishNote?.(true); window.__squintCompile?.(); box.style.display = 'none'; tip.style.display = 'none'; }
83
+ else toast('squint picker on \u2014 click elements to pin, alt+enter copies all (esc cancels)');
84
84
  } else if (e.key === 'Escape' && active) {
85
- active = false; box.style.display = 'none'; tip.style.display = 'none';
85
+ active = false; window.__squintFinishNote?.(false); window.__squintClearPins?.();
86
+ box.style.display = 'none'; tip.style.display = 'none';
86
87
  }
87
88
  });
88
89
  addEventListener('mousemove', (e) => {
@@ -97,16 +98,65 @@ const PICKER = \`(() => {
97
98
  tip.style.top = Math.max(r.top - 26, 2) + 'px';
98
99
  tip.textContent = '<' + s.tag + '> ' + rel(s.file);
99
100
  }, true);
101
+ // Multi-pin annotations: each click drops a numbered pin with a note;
102
+ // alt+enter (or toggling the picker off) compiles them into one blob.
103
+ const pins = [];
104
+ const badges = [];
105
+ const noteBox = document.createElement('input');
106
+ noteBox.style.cssText = 'position:fixed;z-index:2147483647;background:#1b1b1b;color:#fff;border:2px solid #e8a33d;border-radius:3px;padding:4px 8px;font:12px ui-monospace,monospace;display:none;width:280px;outline:none;';
107
+ noteBox.placeholder = 'note for this pin \u2014 enter to keep, esc to drop';
108
+ document.documentElement.append(noteBox);
109
+ let pending = null;
110
+ const addBadge = (el, n) => {
111
+ const r = el.getBoundingClientRect();
112
+ const b = document.createElement('div');
113
+ b.textContent = String(n);
114
+ b.style.cssText = 'position:fixed;z-index:2147483647;background:#e8a33d;color:#1b1b1b;font:bold 11px ui-monospace,monospace;border-radius:8px;min-width:16px;height:16px;text-align:center;line-height:16px;pointer-events:none;';
115
+ b.style.left = Math.max(r.left - 6, 2) + 'px';
116
+ b.style.top = Math.max(r.top - 6, 2) + 'px';
117
+ document.documentElement.append(b);
118
+ badges.push(b);
119
+ };
120
+ const clearPins = () => { pins.length = 0; badges.forEach((b) => b.remove()); badges.length = 0; };
121
+ const compile = () => {
122
+ if (pins.length === 0) return;
123
+ const blob = pins.length === 1 && !pins[0].note
124
+ ? pins[0].ref
125
+ : pins.map((p, i) => (i + 1) + '. ' + p.ref + (p.note ? ' \u2014 ' + p.note : '')).join('\\\\n');
126
+ window.__squintLastPick = blob;
127
+ const count = pins.length;
128
+ clearPins();
129
+ const done = () => toast('copied ' + count + ' annotation' + (count === 1 ? '' : 's') + ' \u2014 paste into squint');
130
+ if (navigator.clipboard?.writeText) navigator.clipboard.writeText(blob).then(done, done);
131
+ else done();
132
+ };
133
+ const finishNote = (keep) => {
134
+ if (!pending) return;
135
+ if (keep) { pins.push({ ref: pending.ref, note: noteBox.value.trim() }); addBadge(pending.el, pins.length); }
136
+ pending = null; noteBox.value = ''; noteBox.style.display = 'none';
137
+ };
138
+ noteBox.addEventListener('keydown', (e) => {
139
+ e.stopPropagation();
140
+ if (e.key === 'Enter') { finishNote(true); if (e.altKey) { compile(); active = false; box.style.display = 'none'; } }
141
+ else if (e.key === 'Escape') finishNote(false);
142
+ });
143
+ window.__squintFinishNote = finishNote;
144
+ window.__squintCompile = compile;
145
+ window.__squintClearPins = clearPins;
146
+ addEventListener('keydown', (e) => {
147
+ if (active && e.altKey && e.key === 'Enter') { finishNote(true); compile(); active = false; box.style.display = 'none'; tip.style.display = 'none'; }
148
+ });
100
149
  addEventListener('click', (e) => {
101
- if (!active) return;
150
+ if (!active || e.target === noteBox) return;
102
151
  e.preventDefault(); e.stopPropagation();
152
+ finishNote(true);
103
153
  const el = find(e.target); if (!el) return;
104
- const ref = describe(el);
105
- window.__squintLastPick = ref;
106
- const done = () => toast('copied \u2014 paste into squint: ' + ref);
107
- if (navigator.clipboard?.writeText) navigator.clipboard.writeText(ref).then(done, done);
108
- else done();
109
- active = false; box.style.display = 'none';
154
+ pending = { el, ref: describe(el) };
155
+ const r = el.getBoundingClientRect();
156
+ noteBox.style.display = 'block';
157
+ noteBox.style.left = Math.min(r.left, innerWidth - 310) + 'px';
158
+ noteBox.style.top = Math.min(r.bottom + 4, innerHeight - 34) + 'px';
159
+ noteBox.focus();
110
160
  }, true);
111
161
  })();\`
112
162
  `;
@@ -0,0 +1,80 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/prompt/skills.ts
4
+ import fs from "fs";
5
+ import path from "path";
6
+ var FRONTMATTER_RE = /^---\n([\s\S]*?)\n---\n?/;
7
+ function parseSkill(name, raw) {
8
+ const match = FRONTMATTER_RE.exec(raw);
9
+ if (!match) return null;
10
+ const body = raw.slice(match[0].length).trim();
11
+ const triggersLine = /(^|\n)triggers:[ \t]*(.*)/.exec(match[1]);
12
+ if (!triggersLine) return null;
13
+ let triggers;
14
+ const inline = triggersLine[2].trim();
15
+ if (inline.length > 0) {
16
+ triggers = inline.split(",").map((t) => t.trim().toLowerCase());
17
+ } else {
18
+ triggers = [...match[1].matchAll(/\n\s*-\s+(.+)/g)].map((m) => m[1].trim().toLowerCase());
19
+ }
20
+ triggers = triggers.filter((t) => t.length > 0);
21
+ if (triggers.length === 0 || body.length === 0) return null;
22
+ return { name, triggers, body };
23
+ }
24
+ function loadSkills(cwd) {
25
+ const dir = path.join(cwd, ".squint", "skills");
26
+ let entries;
27
+ try {
28
+ entries = fs.readdirSync(dir).filter((f) => f.endsWith(".md"));
29
+ } catch {
30
+ return [];
31
+ }
32
+ const skills = [];
33
+ for (const entry of entries.sort()) {
34
+ try {
35
+ const skill = parseSkill(entry.replace(/\.md$/, ""), fs.readFileSync(path.join(dir, entry), "utf8"));
36
+ if (skill) skills.push(skill);
37
+ } catch {
38
+ }
39
+ }
40
+ return skills;
41
+ }
42
+ function loadRules(cwd) {
43
+ try {
44
+ const text = fs.readFileSync(path.join(cwd, ".squint", "rules.md"), "utf8").trim();
45
+ return text.length > 0 ? text : null;
46
+ } catch {
47
+ return null;
48
+ }
49
+ }
50
+ function matchSkills(skills, ask) {
51
+ const haystack = ask.toLowerCase();
52
+ return skills.filter((skill) => skill.triggers.some((t) => haystack.includes(t)));
53
+ }
54
+ function enrich(cwd, ask) {
55
+ const parts = [];
56
+ const rules = loadRules(cwd);
57
+ if (rules) parts.push(`## Project rules (always apply)
58
+
59
+ ${rules}`);
60
+ const matched = matchSkills(loadSkills(cwd), ask);
61
+ for (const skill of matched) {
62
+ parts.push(`## Project notes: ${skill.name}
63
+
64
+ ${skill.body}`);
65
+ }
66
+ return {
67
+ sections: parts.length > 0 ? `
68
+
69
+ ${parts.join("\n\n")}` : "",
70
+ matchedSkills: matched.map((s) => s.name)
71
+ };
72
+ }
73
+
74
+ export {
75
+ parseSkill,
76
+ loadSkills,
77
+ loadRules,
78
+ matchSkills,
79
+ enrich
80
+ };