@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 +20 -6
- package/dist/{cdp-R7Y2XSZQ.js → cdp-RTWPFIX5.js} +1 -1
- package/dist/{chrome-4WNYZZ42.js → chrome-RD7XQ767.js} +2 -2
- package/dist/{chunk-FQDSJRTY.js → chunk-2IW2MMCY.js} +59 -2
- package/dist/{chunk-YIVPCWSG.js → chunk-2LRIKWBU.js} +27 -23
- package/dist/{chunk-43VZK47W.js → chunk-DLCRYRWQ.js} +4 -0
- package/dist/{chunk-UHVE42IQ.js → chunk-LCS47EAG.js} +3 -5
- package/dist/{chunk-MGHJSERZ.js → chunk-OTG4ZB4R.js} +1 -1
- package/dist/{chunk-5IR2AZVK.js → chunk-P3H4N2EN.js} +58 -3
- package/dist/{chunk-MURKQ7SI.js → chunk-P3V5CWH5.js} +3 -3
- package/dist/{chunk-MOYLED4S.js → chunk-REDPR3KI.js} +2 -2
- package/dist/{chunk-ABF4QAMZ.js → chunk-VJGE7HSP.js} +8 -5
- package/dist/{chunk-W6MYXQIU.js → chunk-VNLM53RJ.js} +60 -10
- package/dist/chunk-XZKQZKEE.js +80 -0
- package/dist/cli.js +1148 -471
- package/dist/{families-RVP5BWQD.js → families-2G5SLLY7.js} +1 -2
- package/dist/{gates-G5DABJWQ.js → gates-I2GGQGMA.js} +3 -1
- package/dist/{init-7AYGAKOS.js → init-LSYB32NS.js} +1 -1
- package/dist/{preview-AA6YIE5W.js → preview-XQ7EOHV4.js} +4 -4
- package/dist/run-XHLCTF6V.js +8 -0
- package/dist/shots-LRYYMTPK.js +12 -0
- package/dist/skills-UGHU22BS.js +15 -0
- package/dist/{source-MEXHWVP4.js → source-ZZU245VN.js} +1 -1
- package/dist/{variants-VVIYT7WI.js → variants-7A7723L7.js} +4 -5
- package/package.json +1 -1
- package/dist/chunk-NT2HR4RD.js +0 -63
- package/dist/shots-ADBOBALL.js +0 -66
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.**
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
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
|
|
136
|
-
|
|
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
|
|
|
@@ -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-
|
|
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
|
-
|
|
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",
|
|
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"
|
|
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
|
|
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-
|
|
6
|
+
} from "./chunk-P3H4N2EN.js";
|
|
6
7
|
import {
|
|
7
8
|
runAgent
|
|
8
|
-
} from "./chunk-
|
|
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";
|
|
@@ -1,7 +1,60 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
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-
|
|
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
|
|
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-
|
|
5
|
+
} from "./chunk-VJGE7HSP.js";
|
|
6
6
|
import {
|
|
7
7
|
findChrome,
|
|
8
8
|
screenshot
|
|
9
|
-
} from "./chunk-
|
|
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
|
-
|
|
127
|
-
|
|
128
|
-
|
|
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
|
|
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;
|
|
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
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
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
|
+
};
|