glassbox 0.20.0-beta.4 → 1.0.0

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
@@ -87,7 +87,7 @@ Then you run `glassbox` again. Your previous annotations carry forward — match
87
87
 
88
88
  - **Split and unified diffs** with syntax-colored add/remove/context lines
89
89
  - **Line-level annotations** — click any line to add feedback with a category
90
- - **Image and SVG diffs** — binary images and SVGs render in a **side-by-side** layout (with a left/right or over/under toggle), a **difference overlay**, a **swipeable slice tool**, and pixel-precision synced zoom/pan. SVGs offer a *Code* / *Rendered* toggle so you can review either the source or the rasterized output.
90
+ - **Image and SVG diffs** — binary images and SVGs render in a **side-by-side** layout (with a left/right or over/under toggle), a **difference overlay**, a **swipeable slice tool**, single-side **A / B focus** modes, and pixel-precision synced zoom/pan. SVGs offer a *Code* / *Rendered* toggle so you can review either the source or the live-rendered output.
91
91
  - **Comment on images** — leave a general comment on any image, or **draw a rectangle region** to anchor feedback to a specific spot (per A/B side), just like a line annotation.
92
92
  - **Ground-truth image comparison** — `glassbox --ground-truth <manifest>` compares an **actual** render against an **expected** one (a design spec, a reference, or a previous baseline — even outside the repo), scores the perceptual difference, sorts most-changed-first, and lets you **promote** a new actual to the baseline. Great for catching visual regressions.
93
93
  - **Attach files to any comment** — drag, paste, or browse to attach screenshots, logs, or any file to a line comment, image comment, or note reply. Image attachments show a thumbnail and open in a lightbox; the export lists each file's path so your AI tool can read it.
@@ -99,12 +99,13 @@ Then you run `glassbox` again. Your previous annotations carry forward — match
99
99
  - **Resizable sidebar** and word wrap toggle
100
100
  - **Keyboard navigation** — `j`/`k` to move between files, `Cmd+Enter` to save
101
101
  - **Go-to-definition and a nav stack** — click any symbol to jump to its definition, with back/forward through your trail
102
- - **Themes** — ten built-in themes (Dark, Light, High Contrast Dark/Light, Dracula, Tokyo Night, One Dark Pro, Solarized Dark/Light, Monokai), plus a custom theme editor with live preview
102
+ - **Themes** — thirteen built-in themes (Dark, Light, High Contrast Dark/Light, Dracula, Tokyo Night, One Dark Pro, Solarized Dark/Light, Monokai, Nord, Gruvbox Dark/Light), plus a custom theme editor with live preview
103
103
  - **Session persistence** — reviews survive restarts, pick up where you left off
104
104
  - **Smart review reuse** — re-running `glassbox` on the same commit updates diffs in place and migrates annotations to their new line positions
105
105
  - **Stale annotation detection** — comments that can't be matched to the updated diff are flagged with a visual indicator
106
106
  - **Review history** — browse, reopen, or delete past reviews
107
107
  - **Structured export** — markdown output with file paths, line numbers, categories, and instructions for AI consumption
108
+ - **Content plugins** — opt-in renderers turn diagram source (Graphviz, PlantUML, Mermaid) into pictures: diagram files diff like images, and diagram artifacts on AI review notes render inline
108
109
  - **Automatic .gitignore** — keeps `.glassbox/` out of version control for you at launch, while leaving the per-project `.glassbox/settings.json` tracked
109
110
  - **Auto port selection** — if the default port is busy, it finds an open one
110
111
  - **Fully local** — no network calls (unless you opt into AI features), no accounts, no telemetry. Your code stays on your machine.
@@ -123,6 +124,18 @@ And when "correct" means "matches the design," **ground-truth comparison** (`gla
123
124
 
124
125
  <img src="assets/demo-ground-truth.png" alt="Ground-truth comparison: expected vs actual with a perceptual difference score" width="720">
125
126
 
127
+ ### Diagrams too, via content plugins
128
+
129
+ Reviewing a diagram's source text tells you what changed; seeing the diagram tells you what it _means_. **Content plugins** render specialized formats server-side — install one and a `.dot`, `.puml`, or `.mmd` file gets a **Code | Rendered** toggle: _Code_ is the ordinary text diff, _Rendered_ treats the diagram **like an image**, with the full image-review toolkit — side-by-side, difference overlay, slice, synced zoom.
130
+
131
+ <img src="assets/demo-plugin-rendered.png" alt="A Graphviz .dot file diffed as an image: old and new pipeline diagrams side by side" width="720">
132
+
133
+ The same plugins render diagram-source **proof artifacts** attached to [AI review notes](#ai-review-notes) — so an AI's sequence-diagram evidence shows up as an actual diagram, inline with the diff:
134
+
135
+ <img src="assets/demo-plugin-mermaid-note.png" alt="A Mermaid sequence-diagram proof artifact rendered inline in an AI review note" width="720">
136
+
137
+ Graphviz ships built-in (WASM, no dependencies). PlantUML and Mermaid are one click away in **Settings → Plugins** (they use a local Java / headless-Chromium render, so they're opt-in), alongside an image-codecs plugin that adds WebP/AVIF support to ground-truth scoring. Everything stays local, and everything fails soft — without a plugin you simply see the source text. Want to render your own format? See the [plugin development guide](docs/plugin-development-guide.md).
138
+
126
139
  ---
127
140
 
128
141
  ## AI-Powered Review Intelligence
@@ -192,7 +205,7 @@ When an AI writes or modifies code, it knows _why_ each non-obvious change is th
192
205
  **AI Review Notes** give the generating AI a structured, line-anchored channel to record that reasoning. The AI emits notes with the `glassbox note` command; they're stored as committed SARIF under `.pr-notes/` — tool-neutral and travelling with the repo — and Glassbox renders them review-comment-style at the exact line they apply to:
193
206
 
194
207
  - **Rationale** — why this change, and what alternatives were rejected
195
- - **Proof** — the test that passed, a measurement, or a before/after screenshot (notes can carry text and image artifacts)
208
+ - **Proof** — the test that passed, a measurement, or a before/after screenshot (notes can carry text, image, and diagram-source artifacts — diagrams render inline via [content plugins](#diagrams-too-via-content-plugins))
196
209
  - **Risk / assumption** — what a reviewer should double-check
197
210
 
198
211
  <img src="assets/demo-review-notes.png" alt="AI-authored review notes rendered inline with the diff, with a threaded reply" width="720">
@@ -293,8 +306,10 @@ glassbox --resume
293
306
  | `--diff <a> <b>` | Compare two arbitrary files or folders by path — no git repo required |
294
307
  | `--ground-truth <m>` | Compare actual vs expected images from a manifest — no git repo required |
295
308
  | `--port <number>` | Port to run on (default: 4183) |
309
+ | `--data-dir <path>` | Store review data somewhere other than `.glassbox/` |
296
310
  | `--resume` | Resume the latest in-progress review for this mode |
297
- | `--browser` | Open in browser instead of desktop window |
311
+ | `--no-open` | Don't open the browser automatically |
312
+ | `--on-complete <cmd>` | Run a local command when a review is completed |
298
313
  | `--check-for-updates` | Check for a newer version on npm |
299
314
  | `--debug` | Show build timestamp and debug info |
300
315
  | `--help` | Show help |
package/dist/channel.js CHANGED
@@ -4,11 +4,62 @@ import { createRequire as __createRequire } from 'module';
4
4
  const require = __createRequire(import.meta.url);
5
5
 
6
6
  // src/channel.ts
7
+ import { randomBytes } from "crypto";
7
8
  import { Server } from "@modelcontextprotocol/sdk/server/index.js";
8
9
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
9
10
  import { unlinkSync, writeFileSync } from "fs";
10
11
  import { createServer } from "http";
11
12
  import { join } from "path";
13
+
14
+ // src/channel-server.ts
15
+ import { timingSafeEqual } from "crypto";
16
+ var MAX_BODY_BYTES = 1048576;
17
+ function secretMatches(provided, secret2) {
18
+ if (typeof provided !== "string") return false;
19
+ const a = Buffer.from(provided);
20
+ const b = Buffer.from(secret2);
21
+ return a.length === b.length && timingSafeEqual(a, b);
22
+ }
23
+ function createChannelHandler(deps) {
24
+ return async (req, res) => {
25
+ if (req.method === "GET" && req.url === "/health") {
26
+ res.writeHead(200, { "Content-Type": "application/json" });
27
+ res.end(JSON.stringify({ ok: true }));
28
+ return;
29
+ }
30
+ if (req.method === "POST" && req.url === "/trigger") {
31
+ if (!secretMatches(req.headers["x-glassbox-secret"], deps.secret)) {
32
+ res.writeHead(403, { "Content-Type": "application/json" });
33
+ res.end(JSON.stringify({ error: "Missing or invalid X-Glassbox-Secret" }));
34
+ return;
35
+ }
36
+ let body = "";
37
+ let bodySize = 0;
38
+ for await (const chunk of req) {
39
+ bodySize += chunk.length;
40
+ if (bodySize > MAX_BODY_BYTES) {
41
+ res.writeHead(413);
42
+ res.end("Payload too large");
43
+ return;
44
+ }
45
+ body += String(chunk);
46
+ }
47
+ try {
48
+ await deps.notify(body || "Read .glassbox/latest-review.md and apply the feedback.");
49
+ res.writeHead(200, { "Content-Type": "application/json" });
50
+ res.end(JSON.stringify({ ok: true }));
51
+ } catch (err) {
52
+ res.writeHead(500, { "Content-Type": "application/json" });
53
+ res.end(JSON.stringify({ error: String(err) }));
54
+ }
55
+ return;
56
+ }
57
+ res.writeHead(404);
58
+ res.end("not found");
59
+ };
60
+ }
61
+
62
+ // src/channel.ts
12
63
  var dataDir = ".glassbox";
13
64
  var args = process.argv.slice(2);
14
65
  for (let i = 0; i < args.length; i++) {
@@ -18,6 +69,8 @@ for (let i = 0; i < args.length; i++) {
18
69
  }
19
70
  }
20
71
  var portFile = join(dataDir, "channel-port");
72
+ var secretFile = join(dataDir, "channel-secret");
73
+ var secret = randomBytes(32).toString("hex");
21
74
  var mcp = new Server(
22
75
  { name: "glassbox-channel", version: "0.1.0" },
23
76
  {
@@ -35,69 +88,47 @@ var mcp = new Server(
35
88
  ].join(" ")
36
89
  }
37
90
  );
38
- await mcp.connect(new StdioServerTransport());
39
- var httpServer = createServer(async (req, res) => {
40
- res.setHeader("Access-Control-Allow-Origin", "*");
41
- res.setHeader("Access-Control-Allow-Methods", "POST, GET, OPTIONS");
42
- res.setHeader("Access-Control-Allow-Headers", "Content-Type");
43
- if (req.method === "OPTIONS") {
44
- res.writeHead(200);
45
- res.end();
46
- return;
47
- }
48
- if (req.method === "GET" && req.url === "/health") {
49
- res.writeHead(200, { "Content-Type": "application/json" });
50
- res.end(JSON.stringify({ ok: true }));
51
- return;
52
- }
53
- if (req.method === "POST" && req.url === "/trigger") {
54
- let body = "";
55
- let bodySize = 0;
56
- for await (const chunk of req) {
57
- bodySize += chunk.length;
58
- if (bodySize > 1048576) {
59
- res.writeHead(413);
60
- res.end("Payload too large");
61
- return;
62
- }
63
- body += String(chunk);
64
- }
65
- try {
66
- await mcp.notification({
67
- method: "notifications/claude/channel",
68
- params: {
69
- content: body || "Read .glassbox/latest-review.md and apply the feedback.",
70
- meta: { type: "review-feedback" }
71
- }
72
- });
73
- res.writeHead(200, { "Content-Type": "application/json" });
74
- res.end(JSON.stringify({ ok: true }));
75
- } catch (err) {
76
- res.writeHead(500, { "Content-Type": "application/json" });
77
- res.end(JSON.stringify({ error: String(err) }));
78
- }
79
- return;
91
+ try {
92
+ await mcp.connect(new StdioServerTransport());
93
+ } catch (err) {
94
+ process.stderr.write(`glassbox-channel: failed to connect MCP stdio transport: ${String(err)}
95
+ `);
96
+ process.exit(1);
97
+ }
98
+ var httpServer = createServer(createChannelHandler({
99
+ secret,
100
+ notify: async (content) => {
101
+ await mcp.notification({
102
+ method: "notifications/claude/channel",
103
+ params: { content, meta: { type: "review-feedback" } }
104
+ });
80
105
  }
81
- res.writeHead(404);
82
- res.end("not found");
83
- });
106
+ }));
84
107
  httpServer.listen(0, "127.0.0.1", () => {
85
108
  const addr = httpServer.address();
86
109
  if (addr !== null && typeof addr !== "string") {
87
110
  const port = addr.port;
88
111
  try {
89
112
  writeFileSync(portFile, String(port), "utf-8");
113
+ writeFileSync(secretFile, secret, { encoding: "utf-8", mode: 384 });
90
114
  } catch {
91
115
  }
92
116
  process.stderr.write(`glassbox-channel listening on port ${port}
93
117
  `);
94
118
  }
95
119
  });
96
- function cleanup() {
120
+ function removeStateFiles() {
97
121
  try {
98
122
  unlinkSync(portFile);
99
123
  } catch {
100
124
  }
125
+ try {
126
+ unlinkSync(secretFile);
127
+ } catch {
128
+ }
129
+ }
130
+ function cleanup() {
131
+ removeStateFiles();
101
132
  process.exit(0);
102
133
  }
103
134
  process.on("SIGTERM", () => {
@@ -107,9 +138,6 @@ process.on("SIGINT", () => {
107
138
  cleanup();
108
139
  });
109
140
  process.on("exit", () => {
110
- try {
111
- unlinkSync(portFile);
112
- } catch {
113
- }
141
+ removeStateFiles();
114
142
  });
115
143
  //# sourceMappingURL=channel.js.map