@walterra/pi-graphviz 0.0.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 ADDED
@@ -0,0 +1,79 @@
1
+ # @walterra/pi-graphviz
2
+
3
+ Graphviz DOT diagram extension for [pi coding agent](https://github.com/badlogic/pi-mono) - render diagrams as inline terminal images.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ pi install @walterra/pi-graphviz
9
+ ```
10
+
11
+ Or add to your pi config manually:
12
+
13
+ ```bash
14
+ npm install @walterra/pi-graphviz
15
+ ```
16
+
17
+ Then in your pi config, add the extension path.
18
+
19
+ ## Features
20
+
21
+ - **DOT Language**: Full support for Graphviz DOT syntax
22
+ - **Auto Install**: Graphviz auto-installed via brew (macOS) or apt/dnf (Linux)
23
+ - **Multiple Engines**: dot, neato, fdp, circo, twopi layout engines
24
+ - **Inline Display**: Diagrams render directly in terminals supporting inline images
25
+ - **SVG/PNG Output**: Save diagrams in either format
26
+
27
+ ## Tool: `graphviz_chart`
28
+
29
+ Renders a Graphviz DOT specification as a PNG image.
30
+
31
+ ### Parameters
32
+
33
+ | Parameter | Type | Required | Description |
34
+ |-----------|------|----------|-------------|
35
+ | `dot` | string | ✅ | Graphviz DOT specification |
36
+ | `engine` | string | | Layout engine: dot (default), neato, fdp, circo, twopi |
37
+ | `width` | number | | Output width in pixels |
38
+ | `height` | number | | Output height in pixels |
39
+ | `save_path` | string | | Optional file path (.png or .svg) |
40
+
41
+ ### Example
42
+
43
+ ```dot
44
+ digraph G {
45
+ rankdir=LR;
46
+ node [shape=box style="rounded,filled" fillcolor=lightblue];
47
+
48
+ A [label="Start"];
49
+ B [label="Process"];
50
+ C [label="End" fillcolor=lightgreen];
51
+
52
+ A -> B [label="step 1"];
53
+ B -> C [label="step 2"];
54
+ }
55
+ ```
56
+
57
+ ## Use Cases
58
+
59
+ - Architecture diagrams
60
+ - Flowcharts
61
+ - State machines
62
+ - Dependency graphs
63
+ - ER diagrams
64
+ - Network topologies
65
+
66
+ ## Reference Documentation
67
+
68
+ See [graphviz-reference.md](./extensions/graphviz-chart/graphviz-reference.md) for comprehensive documentation on:
69
+
70
+ - DOT language syntax
71
+ - All node shapes and edge styles
72
+ - Clusters and subgraphs
73
+ - Layout engines
74
+ - Professional theming
75
+ - Common patterns
76
+
77
+ ## License
78
+
79
+ MIT
@@ -0,0 +1,907 @@
1
+ # Graphviz DOT Language Reference
2
+
3
+ Graphviz is a powerful tool for creating diagrams and visual representations of graphs using the DOT language. This reference covers the essential syntax, attributes, and patterns for creating effective diagrams.
4
+
5
+ ## Prerequisites
6
+
7
+ Graphviz is **auto-installed** when first using this extension:
8
+
9
+ - **macOS**: via Homebrew (`brew install graphviz`)
10
+ - **Linux**: via apt or dnf (`sudo apt install -y graphviz` / `sudo dnf install -y graphviz`)
11
+
12
+ If auto-install fails (e.g., missing package manager, permissions), manual installation is required:
13
+
14
+ - **macOS**: `brew install graphviz`
15
+ - **Ubuntu/Debian**: `sudo apt install graphviz`
16
+ - **Fedora/RHEL**: `sudo dnf install graphviz`
17
+ - **Windows**: Download from https://graphviz.org/download/
18
+
19
+ Verify installation: `dot -V`
20
+
21
+ If Graphviz cannot be installed, the tool returns an error with instructions. Do NOT fall back to ASCII art.
22
+
23
+ ## Basic Structure
24
+
25
+ ### Graph Types
26
+
27
+ ```dot
28
+ // Undirected graph
29
+ graph G {
30
+ A -- B;
31
+ B -- C;
32
+ }
33
+
34
+ // Directed graph (digraph)
35
+ digraph G {
36
+ A -> B;
37
+ B -> C;
38
+ }
39
+
40
+ // Strict graph (no multi-edges)
41
+ strict digraph G {
42
+ A -> B;
43
+ A -> B; // ignored, already exists
44
+ }
45
+ ```
46
+
47
+ ### Comments
48
+
49
+ ```dot
50
+ // Single line comment
51
+ /* Multi-line
52
+ comment */
53
+ # Preprocessor-style comment (line must start with #)
54
+ ```
55
+
56
+ ## Nodes
57
+
58
+ ### Basic Node Declaration
59
+
60
+ ```dot
61
+ digraph G {
62
+ // Implicit node creation
63
+ A -> B;
64
+
65
+ // Explicit node with label
66
+ C [label="Node C"];
67
+
68
+ // Node with attributes
69
+ D [label="Database" shape=cylinder color=blue];
70
+ }
71
+ ```
72
+
73
+ ### Node Shapes
74
+
75
+ **Common shapes:**
76
+
77
+ - `box`, `rect`, `rectangle` - Rectangular
78
+ - `ellipse`, `oval` - Elliptical (default)
79
+ - `circle`, `doublecircle` - Circular
80
+ - `diamond` - Diamond
81
+ - `plaintext`, `plain`, `none` - No border
82
+ - `point` - Small circle
83
+ - `triangle`, `invtriangle` - Triangular
84
+ - `house`, `invhouse` - House-shaped
85
+ - `pentagon`, `hexagon`, `septagon`, `octagon` - Polygons
86
+ - `cylinder` - Database/storage
87
+ - `note` - Document note
88
+ - `folder`, `tab` - File system
89
+ - `box3d` - 3D box
90
+ - `component` - UML component
91
+ - `star` - Star shape
92
+ - `record` - Record with fields (use HTML labels instead)
93
+
94
+ **Example:**
95
+
96
+ ```dot
97
+ digraph shapes {
98
+ database [shape=cylinder label="PostgreSQL"];
99
+ service [shape=box label="API Service"];
100
+ queue [shape=parallelogram label="Message Queue"];
101
+ decision [shape=diamond label="Is Valid?"];
102
+
103
+ service -> database;
104
+ service -> queue;
105
+ queue -> decision;
106
+ }
107
+ ```
108
+
109
+ ### Node Styles
110
+
111
+ ```dot
112
+ node [style=filled]; // Filled background
113
+ node [style=dashed]; // Dashed border
114
+ node [style=dotted]; // Dotted border
115
+ node [style=bold]; // Bold border
116
+ node [style=rounded]; // Rounded corners (for box)
117
+ node [style="filled,rounded"]; // Multiple styles
118
+ node [style=invis]; // Invisible
119
+ ```
120
+
121
+ ## Edges
122
+
123
+ ### Edge Types
124
+
125
+ ```dot
126
+ // Directed edge
127
+ A -> B;
128
+
129
+ // Undirected edge (in graph, not digraph)
130
+ A -- B;
131
+
132
+ // Chain of edges
133
+ A -> B -> C -> D;
134
+
135
+ // Multiple targets
136
+ A -> {B C D};
137
+
138
+ // Edge with label
139
+ A -> B [label="sends data"];
140
+ ```
141
+
142
+ ### Edge Attributes
143
+
144
+ ```dot
145
+ digraph G {
146
+ // Labeled edge
147
+ A -> B [label="HTTP"];
148
+
149
+ // Colored edge
150
+ A -> C [color=red];
151
+
152
+ // Styled edge
153
+ A -> D [style=dashed];
154
+ A -> E [style=dotted];
155
+ A -> F [style=bold];
156
+
157
+ // Weighted edge (affects layout)
158
+ A -> G [weight=2];
159
+
160
+ // Constraint (if false, doesn't affect ranking)
161
+ A -> H [constraint=false];
162
+ }
163
+ ```
164
+
165
+ ### Arrow Types
166
+
167
+ ```dot
168
+ digraph arrows {
169
+ // Arrow head styles
170
+ A -> B [arrowhead=normal]; // Default
171
+ A -> C [arrowhead=dot];
172
+ A -> D [arrowhead=odot]; // Open dot
173
+ A -> E [arrowhead=diamond];
174
+ A -> F [arrowhead=box];
175
+ A -> G [arrowhead=vee];
176
+ A -> H [arrowhead=none];
177
+
178
+ // Both ends (for digraph)
179
+ I -> J [dir=both arrowhead=normal arrowtail=dot];
180
+
181
+ // Reverse direction
182
+ K -> L [dir=back];
183
+ }
184
+ ```
185
+
186
+ **Arrow modifiers:**
187
+
188
+ - `o` prefix: open (hollow) - `odot`, `odiamond`
189
+ - `l`/`r` prefix: left/right half - `lnormal`, `rnormal`
190
+ - Combinations: `obox`, `olnormal`
191
+
192
+ ## Attributes
193
+
194
+ ### Graph Attributes
195
+
196
+ ```dot
197
+ digraph G {
198
+ // Layout direction
199
+ rankdir=LR; // Left to Right
200
+ rankdir=TB; // Top to Bottom (default)
201
+ rankdir=BT; // Bottom to Top
202
+ rankdir=RL; // Right to Left
203
+
204
+ // Spacing
205
+ ranksep=0.5; // Rank separation (inches)
206
+ nodesep=0.5; // Node separation (inches)
207
+
208
+ // Background
209
+ bgcolor=lightgrey;
210
+
211
+ // Size constraints
212
+ size="10,10"; // Max size in inches
213
+ ratio=fill; // Fill the size
214
+
215
+ // Font defaults
216
+ fontname="Helvetica";
217
+ fontsize=12;
218
+
219
+ // Label (graph title)
220
+ label="System Architecture";
221
+ labelloc=t; // Top (t), bottom (b)
222
+ labeljust=l; // Left (l), center (c), right (r)
223
+ }
224
+ ```
225
+
226
+ ### Node Attributes
227
+
228
+ | Attribute | Description | Example |
229
+ | ----------- | ---------------------------------- | --------------------- |
230
+ | `label` | Display text | `label="Web Server"` |
231
+ | `shape` | Node shape | `shape=box` |
232
+ | `color` | Border color | `color=red` |
233
+ | `fillcolor` | Fill color (requires style=filled) | `fillcolor=lightblue` |
234
+ | `style` | Visual style | `style=filled` |
235
+ | `fontname` | Font family | `fontname="Arial"` |
236
+ | `fontsize` | Font size in points | `fontsize=14` |
237
+ | `fontcolor` | Text color | `fontcolor=white` |
238
+ | `width` | Minimum width (inches) | `width=2` |
239
+ | `height` | Minimum height (inches) | `height=1` |
240
+ | `penwidth` | Border thickness | `penwidth=2.0` |
241
+ | `tooltip` | Hover text (SVG) | `tooltip="Click me"` |
242
+ | `URL` | Clickable link (SVG) | `URL="https://..."` |
243
+
244
+ ### Edge Attributes
245
+
246
+ | Attribute | Description | Example |
247
+ | ------------ | -------------------------------------------- | ------------------ |
248
+ | `label` | Edge label | `label="HTTP"` |
249
+ | `color` | Edge color | `color=blue` |
250
+ | `style` | Line style | `style=dashed` |
251
+ | `arrowhead` | Head arrow style | `arrowhead=vee` |
252
+ | `arrowtail` | Tail arrow style | `arrowtail=dot` |
253
+ | `dir` | Arrow direction | `dir=both` |
254
+ | `penwidth` | Line thickness | `penwidth=2.0` |
255
+ | `weight` | Layout weight (higher = shorter, straighter) | `weight=2` |
256
+ | `constraint` | Affects ranking | `constraint=false` |
257
+ | `minlen` | Minimum edge length (ranks) | `minlen=2` |
258
+ | `headlabel` | Label at head | `headlabel="1"` |
259
+ | `taillabel` | Label at tail | `taillabel="*"` |
260
+
261
+ ## Colors
262
+
263
+ ### Named Colors (X11 Scheme)
264
+
265
+ Common colors: `red`, `blue`, `green`, `yellow`, `orange`, `purple`, `pink`, `brown`, `black`, `white`, `gray`, `grey`
266
+
267
+ Light variants: `lightblue`, `lightgreen`, `lightyellow`, `lightgray`
268
+ Dark variants: `darkblue`, `darkgreen`, `darkred`, `darkgray`
269
+
270
+ ### Hex Colors
271
+
272
+ ```dot
273
+ node [fillcolor="#3b82f6"]; // Blue
274
+ node [color="#ef4444"]; // Red
275
+ ```
276
+
277
+ ### RGB/RGBA
278
+
279
+ ```dot
280
+ node [fillcolor="0.5 0.8 0.2"]; // HSB format
281
+ ```
282
+
283
+ ## Subgraphs and Clusters
284
+
285
+ ### Basic Subgraph
286
+
287
+ ```dot
288
+ digraph G {
289
+ subgraph {
290
+ A; B; C;
291
+ }
292
+ D -> A;
293
+ }
294
+ ```
295
+
296
+ ### Clusters (Named subgraphs starting with "cluster")
297
+
298
+ ```dot
299
+ digraph G {
300
+ subgraph cluster_frontend {
301
+ label="Frontend";
302
+ style=filled;
303
+ color=lightblue;
304
+
305
+ web [label="Web App"];
306
+ mobile [label="Mobile App"];
307
+ }
308
+
309
+ subgraph cluster_backend {
310
+ label="Backend";
311
+ style=filled;
312
+ color=lightgreen;
313
+
314
+ api [label="API Server"];
315
+ worker [label="Worker"];
316
+ }
317
+
318
+ subgraph cluster_data {
319
+ label="Data Layer";
320
+ style=filled;
321
+ color=lightyellow;
322
+
323
+ db [label="Database" shape=cylinder];
324
+ cache [label="Cache" shape=cylinder];
325
+ }
326
+
327
+ web -> api;
328
+ mobile -> api;
329
+ api -> db;
330
+ api -> cache;
331
+ worker -> db;
332
+ }
333
+ ```
334
+
335
+ ### Rank Constraints
336
+
337
+ ```dot
338
+ digraph G {
339
+ // Force nodes to same rank (horizontal alignment in TB)
340
+ {rank=same; A; B; C;}
341
+
342
+ // Source rank (top in TB)
343
+ {rank=source; Start;}
344
+
345
+ // Sink rank (bottom in TB)
346
+ {rank=sink; End;}
347
+
348
+ // Min/max rank
349
+ {rank=min; First;}
350
+ {rank=max; Last;}
351
+ }
352
+ ```
353
+
354
+ ## Common Patterns
355
+
356
+ ### Flowchart
357
+
358
+ ```dot
359
+ digraph flowchart {
360
+ rankdir=TB;
361
+ node [shape=box style="rounded,filled" fillcolor=lightblue];
362
+
363
+ start [shape=ellipse label="Start" fillcolor=lightgreen];
364
+ end [shape=ellipse label="End" fillcolor=lightcoral];
365
+ decision [shape=diamond label="Valid?" fillcolor=lightyellow];
366
+
367
+ start -> process1 [label="Begin"];
368
+ process1 [label="Process Data"];
369
+ process1 -> decision;
370
+ decision -> process2 [label="Yes"];
371
+ decision -> error [label="No"];
372
+ process2 [label="Save Results"];
373
+ error [label="Handle Error" fillcolor=lightcoral];
374
+ process2 -> end;
375
+ error -> end;
376
+ }
377
+ ```
378
+
379
+ ### Architecture Diagram
380
+
381
+ ```dot
382
+ digraph architecture {
383
+ rankdir=LR;
384
+ node [shape=box style=filled];
385
+
386
+ // External
387
+ user [label="User" shape=ellipse fillcolor=lightgray];
388
+
389
+ // Load Balancer
390
+ lb [label="Load Balancer" fillcolor=lightyellow];
391
+
392
+ // Application tier
393
+ subgraph cluster_app {
394
+ label="Application Tier";
395
+ color=lightblue;
396
+ style=filled;
397
+
398
+ app1 [label="App Server 1"];
399
+ app2 [label="App Server 2"];
400
+ }
401
+
402
+ // Data tier
403
+ subgraph cluster_data {
404
+ label="Data Tier";
405
+ color=lightgreen;
406
+ style=filled;
407
+
408
+ db [label="Primary DB" shape=cylinder];
409
+ replica [label="Replica DB" shape=cylinder];
410
+ cache [label="Redis Cache" shape=cylinder];
411
+ }
412
+
413
+ user -> lb;
414
+ lb -> app1;
415
+ lb -> app2;
416
+ app1 -> db;
417
+ app2 -> db;
418
+ app1 -> cache;
419
+ app2 -> cache;
420
+ db -> replica [style=dashed label="replication"];
421
+ }
422
+ ```
423
+
424
+ ### Sequence-like Diagram
425
+
426
+ ```dot
427
+ digraph sequence {
428
+ rankdir=LR;
429
+ node [shape=box];
430
+
431
+ // Actors
432
+ {rank=same; client; server; db;}
433
+
434
+ client [label="Client"];
435
+ server [label="Server"];
436
+ db [label="Database" shape=cylinder];
437
+
438
+ // Messages (use invisible nodes for spacing)
439
+ client -> server [label="1. Request"];
440
+ server -> db [label="2. Query"];
441
+ db -> server [label="3. Results" style=dashed];
442
+ server -> client [label="4. Response" style=dashed];
443
+ }
444
+ ```
445
+
446
+ ### State Machine
447
+
448
+ ```dot
449
+ digraph state_machine {
450
+ rankdir=LR;
451
+ node [shape=circle];
452
+
453
+ // Start state
454
+ start [shape=point width=0.2];
455
+
456
+ // End state
457
+ end [shape=doublecircle];
458
+
459
+ // States
460
+ idle [label="Idle"];
461
+ running [label="Running"];
462
+ paused [label="Paused"];
463
+
464
+ start -> idle;
465
+ idle -> running [label="start"];
466
+ running -> paused [label="pause"];
467
+ paused -> running [label="resume"];
468
+ running -> idle [label="stop"];
469
+ paused -> idle [label="stop"];
470
+ running -> end [label="complete"];
471
+ }
472
+ ```
473
+
474
+ ### Dependency Graph
475
+
476
+ ```dot
477
+ digraph dependencies {
478
+ rankdir=BT; // Bottom to top for dependencies
479
+ node [shape=box style="rounded,filled" fillcolor=lightblue];
480
+
481
+ app [label="Application" fillcolor=lightgreen];
482
+
483
+ // Direct dependencies
484
+ lodash [label="lodash"];
485
+ express [label="express"];
486
+ pg [label="pg"];
487
+
488
+ // Transitive dependencies
489
+ body_parser [label="body-parser" fillcolor=lightyellow];
490
+ qs [label="qs" fillcolor=lightyellow];
491
+
492
+ app -> lodash;
493
+ app -> express;
494
+ app -> pg;
495
+ express -> body_parser;
496
+ body_parser -> qs;
497
+ }
498
+ ```
499
+
500
+ ### ER Diagram (using HTML labels)
501
+
502
+ ```dot
503
+ digraph er {
504
+ rankdir=LR;
505
+ node [shape=none margin=0];
506
+
507
+ users [label=<
508
+ <TABLE BORDER="1" CELLBORDER="0" CELLSPACING="0" CELLPADDING="4">
509
+ <TR><TD BGCOLOR="lightblue"><B>users</B></TD></TR>
510
+ <TR><TD ALIGN="LEFT">id: INT PK</TD></TR>
511
+ <TR><TD ALIGN="LEFT">name: VARCHAR</TD></TR>
512
+ <TR><TD ALIGN="LEFT">email: VARCHAR</TD></TR>
513
+ </TABLE>
514
+ >];
515
+
516
+ orders [label=<
517
+ <TABLE BORDER="1" CELLBORDER="0" CELLSPACING="0" CELLPADDING="4">
518
+ <TR><TD BGCOLOR="lightgreen"><B>orders</B></TD></TR>
519
+ <TR><TD ALIGN="LEFT">id: INT PK</TD></TR>
520
+ <TR><TD ALIGN="LEFT">user_id: INT FK</TD></TR>
521
+ <TR><TD ALIGN="LEFT">total: DECIMAL</TD></TR>
522
+ </TABLE>
523
+ >];
524
+
525
+ users -> orders [label="1:N" arrowhead=crow arrowtail=none];
526
+ }
527
+ ```
528
+
529
+ ## Layout Engines
530
+
531
+ Graphviz includes several layout engines:
532
+
533
+ | Engine | Best For | Direction |
534
+ | ----------- | ------------------------------- | --------- |
535
+ | `dot` | Hierarchical graphs, DAGs | TB/LR |
536
+ | `neato` | Undirected graphs, spring model | Any |
537
+ | `fdp` | Large undirected graphs | Any |
538
+ | `sfdp` | Very large graphs | Any |
539
+ | `twopi` | Radial layouts | Radial |
540
+ | `circo` | Circular layouts | Circular |
541
+ | `osage` | Clustered graphs | - |
542
+ | `patchwork` | Treemaps | - |
543
+
544
+ Specify with command line: `dot -Kneato -Tpng input.dot -o output.png`
545
+
546
+ ## Best Practices
547
+
548
+ 1. **Use meaningful IDs**: `web_server` not `n1`
549
+ 2. **Group related nodes**: Use clusters for visual organization
550
+ 3. **Control direction**: `rankdir=LR` for wide diagrams, `TB` for tall
551
+ 4. **Use consistent styling**: Set defaults with `node [...]` and `edge [...]`
552
+ 5. **Label edges**: Make relationships clear
553
+ 6. **Use colors purposefully**: Highlight important nodes/paths
554
+ 7. **Keep it simple**: Don't overcrowd; split complex diagrams
555
+ 8. **Use constraint=false**: For edges that shouldn't affect layout
556
+ 9. **Adjust spacing**: Use `ranksep` and `nodesep` for readability
557
+ 10. **Test with different engines**: `dot` vs `neato` can give very different results
558
+
559
+ ## Critical Rendering Issues
560
+
561
+ ### HTML TABLE Labels Break Kerning
562
+
563
+ **Problem:** Using `<TABLE>` in HTML labels causes Pango to render each cell separately, destroying letter spacing (kerning).
564
+
565
+ **Symptom:** Text like "eddo-telegram-bot" has uneven letter spacing.
566
+
567
+ ```dot
568
+ // ❌ BAD - TABLE breaks kerning
569
+ node [label=<<TABLE><TR><TD><FONT>Service Name</FONT></TD></TR></TABLE>>]
570
+
571
+ // ✅ GOOD - Simple HTML with BR preserves kerning
572
+ node [label=<Service Name<BR/><FONT POINT-SIZE="10" COLOR="#737373">subtitle</FONT>>]
573
+ ```
574
+
575
+ **Rule:** Only use `<TABLE>` when you need multi-column alignment. For simple multi-line labels, use `<BR/>`.
576
+
577
+ ### DPI for Crisp Output
578
+
579
+ **Problem:** Default 96 DPI produces fuzzy text on modern displays.
580
+
581
+ **Solution:** Set high DPI in graph attributes:
582
+
583
+ ```dot
584
+ digraph G {
585
+ dpi=192; // 2x for retina displays
586
+ // or dpi=300 for print quality
587
+ }
588
+ ```
589
+
590
+ **Note:** Higher DPI increases file size proportionally.
591
+
592
+ ### Font Selection and Rendering
593
+
594
+ **Problem:** Graphviz uses fontconfig which matches font names loosely. "Helvetica Neue" might fall back to something unexpected.
595
+
596
+ **Solution:** Use widely-available fonts:
597
+
598
+ ```dot
599
+ graph [fontname="Arial" fontsize=14];
600
+ node [fontname="Arial" fontsize=12];
601
+ edge [fontname="Arial" fontsize=10];
602
+ ```
603
+
604
+ **Safe fonts:** Arial, Helvetica, Times, Courier, Georgia, Verdana
605
+
606
+ ### Edge Labels with Ortho Splines
607
+
608
+ **Problem:** Edge labels may not appear when using `splines=ortho`.
609
+
610
+ **Solution:** Use `splines=true` (curved) or `splines=line` (straight) when labels are important.
611
+
612
+ ## Visual Design Principles
613
+
614
+ ### Minimalist (Data-Ink Ratio)
615
+
616
+ Maximize data, minimize non-data ink:
617
+
618
+ - Remove decorative borders if they don't encode information
619
+ - Use line weight to encode traffic/volume
620
+ - Avoid chartjunk (3D effects, gradients, unnecessary fills)
621
+ - Every pixel should convey data
622
+
623
+ **When to apply:** Technical documentation, data-dense dashboards, publications.
624
+
625
+ **Limitation:** Can be too minimal for audiences who need visual anchors.
626
+
627
+ ### Communicative (Functional Clarity)
628
+
629
+ Emphasize communication over minimalism:
630
+
631
+ - **Functional decoration is not chartjunk** — borders and shapes aid parsing
632
+ - **Redundant encoding for critical data** — error states need color + icon + size
633
+ - **Know your audience** — label for the least technical viewer
634
+ - **Guide the eye** — create visual hierarchy that leads to the insight
635
+
636
+ **When to apply:** Operational dashboards, presentations to mixed audiences, alerting systems.
637
+
638
+ ### Modern SaaS (Clean Professional)
639
+
640
+ Clean, professional design for modern tooling:
641
+
642
+ - **White space is design** — let elements breathe
643
+ - **One accent color** — gray scale for structure, color for emphasis
644
+ - **No gradients, no shadows** — flat, honest design
645
+ - **Typography carries hierarchy** — weight and size, not decoration
646
+ - **Thin lines, subtle borders** — 1px borders, never black
647
+
648
+ **When to apply:** Product documentation, engineering blogs, modern tooling.
649
+
650
+ ### Choosing an Approach
651
+
652
+ | Audience | Approach | Style |
653
+ | ---------------------- | ------------- | ------------------------- |
654
+ | Engineers reading docs | Minimalist | Minimal, data-dense |
655
+ | Ops team monitoring | Communicative | Clear, redundant encoding |
656
+ | Product/stakeholders | Modern SaaS | Clean, professional |
657
+ | Print/publication | Minimalist | High information density |
658
+
659
+ ## Professional Theming
660
+
661
+ ### Light Theme (SaaS Style)
662
+
663
+ ```dot
664
+ digraph G {
665
+ bgcolor="white";
666
+ dpi=192;
667
+ pad=0.5;
668
+
669
+ graph [fontname="Arial" fontsize=14 fontcolor="#404040"];
670
+ node [fontname="Arial" fontsize=12 fontcolor="#171717"];
671
+ edge [fontname="Arial" fontsize=9 fontcolor="#525252" color="#a3a3a3"];
672
+
673
+ // Entry points (ellipse)
674
+ users [label="Users" shape=ellipse style=filled fillcolor="#f5f5f5" color="#d4d4d4"];
675
+
676
+ // Services (rounded box)
677
+ api [label=<api-server<BR/><FONT POINT-SIZE="10" COLOR="#525252">572K traces</FONT>>
678
+ shape=box style="rounded" color="#d4d4d4"];
679
+
680
+ // Error state (red accent)
681
+ problem [label=<problem-service<BR/><FONT POINT-SIZE="10" COLOR="#dc2626">8% errors</FONT>>
682
+ shape=box style="rounded,filled" fillcolor="#fef2f2" color="#dc2626" penwidth=2];
683
+
684
+ // Data layer (cylinder)
685
+ db [label="Database" shape=cylinder style=filled fillcolor="#fafafa" color="#d4d4d4"];
686
+
687
+ // External (dashed)
688
+ external [label="External APIs" shape=box style="dashed,rounded" color="#a3a3a3"];
689
+ }
690
+ ```
691
+
692
+ **Light palette:**
693
+ | Element | Color |
694
+ |---------|-------|
695
+ | Background | `#ffffff` |
696
+ | Node fill | `#f5f5f5` / `#fafafa` |
697
+ | Node border | `#d4d4d4` |
698
+ | Primary text | `#171717` |
699
+ | Secondary text | `#525252` |
700
+ | Muted text | `#737373` |
701
+ | Edges | `#a3a3a3` |
702
+ | Error fill | `#fef2f2` |
703
+ | Error border | `#dc2626` |
704
+
705
+ ### Dark Theme
706
+
707
+ ```dot
708
+ digraph G {
709
+ bgcolor="#0a0a0a";
710
+ dpi=192;
711
+ pad=0.5;
712
+
713
+ graph [fontname="Arial" fontsize=14 fontcolor="#a3a3a3"];
714
+ node [fontname="Arial" fontsize=12 fontcolor="#e5e5e5"];
715
+ edge [fontname="Arial" fontsize=9 fontcolor="#737373" color="#525252"];
716
+
717
+ // Entry points
718
+ users [label="Users" shape=ellipse style=filled fillcolor="#1c1c1c" color="#404040" fontcolor="#a3a3a3"];
719
+
720
+ // Services
721
+ api [label=<api-server<BR/><FONT POINT-SIZE="10" COLOR="#a3a3a3">572K traces</FONT>>
722
+ shape=box style="rounded,filled" fillcolor="#171717" color="#404040"];
723
+
724
+ // Error state (red glow on dark)
725
+ problem [label=<problem-service<BR/><FONT POINT-SIZE="10" COLOR="#f87171">8% errors</FONT>>
726
+ shape=box style="rounded,filled" fillcolor="#1f1315" color="#dc2626" penwidth=2 fontcolor="#fecaca"];
727
+
728
+ // Data layer
729
+ db [label="Database" shape=cylinder style=filled fillcolor="#1c1c1c" color="#404040"];
730
+
731
+ // External
732
+ external [label="External APIs" shape=box style="dashed,rounded,filled" fillcolor="#0a0a0a" color="#525252"];
733
+ }
734
+ ```
735
+
736
+ **Dark palette:**
737
+ | Element | Color |
738
+ |---------|-------|
739
+ | Background | `#0a0a0a` |
740
+ | Node fill | `#171717` / `#1c1c1c` |
741
+ | Node border | `#404040` |
742
+ | Primary text | `#e5e5e5` |
743
+ | Secondary text | `#a3a3a3` |
744
+ | Muted text | `#737373` |
745
+ | Edges | `#525252` |
746
+ | Error fill | `#1f1315` |
747
+ | Error border | `#dc2626` |
748
+ | Error text | `#f87171` (lighter for contrast) |
749
+
750
+ ## Edge Label Strategy
751
+
752
+ ### When to Label Edges
753
+
754
+ Apply the principle: **Label only what deviates from expectation.**
755
+
756
+ | Label Type | Keep? | Reason |
757
+ | ----------- | ----- | -------------------------------- |
758
+ | HTTP/REST | ❌ | Assumed between web services |
759
+ | CRUD/SQL | ❌ | Assumed for database connections |
760
+ | IPC | ✅ | Surprising — not HTTP |
761
+ | webhook | ✅ | Notable — async external |
762
+ | oauth | ✅ | Specific authentication flow |
763
+ | gRPC | ✅ | Different from default HTTP |
764
+ | async/queue | ✅ | Different execution model |
765
+
766
+ ### Edge Label Styling
767
+
768
+ Keep labels subordinate to nodes:
769
+
770
+ ```dot
771
+ edge [fontname="Arial" fontsize=9 fontcolor="#737373"];
772
+
773
+ A -> B [label="http"]; // Only if audience needs it
774
+ A -> C [label="ipc"]; // Notable - different protocol
775
+ A -> D [label="webhook"]; // Notable - async pattern
776
+ ```
777
+
778
+ ## Complete Architecture Example
779
+
780
+ ````dot
781
+ digraph EdDoArchitecture {
782
+ rankdir=TB;
783
+ splines=true;
784
+ nodesep=0.9;
785
+ ranksep=0.85;
786
+ bgcolor="white";
787
+ pad=0.5;
788
+ dpi=192;
789
+
790
+ // Typography
791
+ graph [fontname="Arial" fontsize=14 fontcolor="#404040"];
792
+ node [fontname="Arial" fontsize=12 fontcolor="#171717"];
793
+ edge [fontname="Arial" fontsize=9 fontcolor="#525252" color="#a3a3a3" arrowsize=0.6 arrowhead=vee];
794
+
795
+ // Title with insight (communicative: guide the eye)
796
+ labelloc=t;
797
+ label=<Service Architecture<BR/><FONT POINT-SIZE="11" COLOR="#737373">telegram-bot error rate needs attention</FONT>>;
798
+
799
+ // Entry points (distinct shape)
800
+ web_users [label="Web Users" shape=ellipse style=filled fillcolor="#f5f5f5" color="#d4d4d4" fontsize=11];
801
+ telegram_users [label="Telegram Users" shape=ellipse style=filled fillcolor="#f5f5f5" color="#d4d4d4" fontsize=11];
802
+
803
+ // Services - size encodes importance (minimalist: data in the ink)
804
+ web_client [
805
+ label=<web-client<BR/><FONT POINT-SIZE="10" COLOR="#525252">11K traces</FONT>>
806
+ shape=box style="rounded" color="#d4d4d4" penwidth=1
807
+ ];
808
+
809
+ // Error state: redundant encoding (communicative)
810
+ telegram_bot [
811
+ label=<telegram-bot<BR/><FONT POINT-SIZE="11" COLOR="#dc2626"><B>97K · 8% errors</B></FONT>>
812
+ shape=box style="rounded,filled" fillcolor="#fef2f2" color="#dc2626" penwidth=2
813
+ fontsize=13
814
+ ];
815
+
816
+ web_api [
817
+ label=<web-api<BR/><FONT POINT-SIZE="10" COLOR="#525252">572K traces</FONT>>
818
+ shape=box style="rounded" color="#d4d4d4" penwidth=1
819
+ fontsize=14
820
+ ];
821
+
822
+ mcp_server [
823
+ label=<mcp-server<BR/><FONT POINT-SIZE="10" COLOR="#525252">32K traces</FONT>>
824
+ shape=box style="rounded" color="#d4d4d4" penwidth=1
825
+ ];
826
+
827
+ // Data layer (distinct shape)
828
+ couchdb [
829
+ label=<CouchDB<BR/><FONT POINT-SIZE="10" COLOR="#525252">225K queries</FONT>>
830
+ shape=cylinder style=filled fillcolor="#fafafa" color="#d4d4d4"
831
+ ];
832
+
833
+ // External (clearly outside system boundary)
834
+ external [
835
+ label=<External APIs<BR/><FONT POINT-SIZE="9" COLOR="#737373">Telegram · Google · GitHub</FONT>>
836
+ shape=box style="dashed,rounded" color="#a3a3a3"
837
+ ];
838
+
839
+ // Layout control
840
+ {rank=same; web_users; telegram_users;}
841
+ {rank=same; web_client; telegram_bot;}
842
+ {rank=same; web_api; mcp_server;}
843
+
844
+ // Edges - weight encodes traffic (minimalist)
845
+ web_users -> web_client [penwidth=1];
846
+ telegram_users -> telegram_bot [penwidth=1];
847
+
848
+ // Label only what's notable (not "http" - assumed)
849
+ web_client -> web_api [penwidth=1.5 label="http"];
850
+ telegram_bot -> web_api [penwidth=2 label="http"];
851
+
852
+ // IPC is notable
853
+ web_api -> mcp_server [penwidth=1 dir=both arrowsize=0.45 constraint=false label="ipc"];
854
+
855
+ // Database (no labels - CRUD assumed)
856
+ web_api -> couchdb [penwidth=2.5];
857
+ telegram_bot -> couchdb [penwidth=1.2];
858
+ mcp_server -> couchdb [penwidth=1];
859
+
860
+ // External (dashed = crosses boundary)
861
+ telegram_bot -> external [penwidth=0.75 color="#b5b5b5" style=dashed constraint=false label="webhook"];
862
+ web_api -> external [penwidth=0.75 color="#b5b5b5" style=dashed constraint=false label="oauth"];
863
+ }
864
+
865
+ ## Output Formats
866
+
867
+ Common formats: `png`, `svg`, `pdf`, `ps`, `json`, `dot` (canonical)
868
+
869
+ ```bash
870
+ # PNG output
871
+ dot -Tpng input.dot -o output.png
872
+
873
+ # SVG output (best for web, scalable)
874
+ dot -Tsvg input.dot -o output.svg
875
+
876
+ # PDF output
877
+ dot -Tpdf input.dot -o output.pdf
878
+ ````
879
+
880
+ ## Quick Reference: Quality Checklist
881
+
882
+ Before finalizing any diagram, verify:
883
+
884
+ - [ ] **DPI set to 192+** for crisp text
885
+ - [ ] **Font explicitly set** to Arial or Helvetica
886
+ - [ ] **No TABLE in HTML labels** (use BR instead for kerning)
887
+ - [ ] **Font sizes 10pt+** for readability
888
+ - [ ] **Edge labels only for surprising protocols** (skip HTTP, CRUD)
889
+ - [ ] **Error states use redundant encoding** (color + fill + size)
890
+ - [ ] **Shapes distinguish categories** (ellipse=users, box=services, cylinder=data)
891
+ - [ ] **splines=true or splines=line** if edge labels needed
892
+ - [ ] **Accessibility:** contrast ratio 4.5:1+ for text
893
+
894
+ ## References
895
+
896
+ - [DOT Language Specification](https://graphviz.org/doc/info/lang.html)
897
+ - [Node Shapes](https://graphviz.org/doc/info/shapes.html)
898
+ - [Attributes Reference](https://graphviz.org/doc/info/attrs.html)
899
+ - [Color Names](https://graphviz.org/doc/info/colors.html)
900
+ - [Arrow Shapes](https://graphviz.org/doc/info/arrows.html)
901
+ - [Gallery](https://graphviz.org/gallery/)
902
+
903
+ ### Visual Design References
904
+
905
+ - _The Visual Display of Quantitative Information_ (data-ink ratio, minimalist approach)
906
+ - _The Functional Art_, _How Charts Lie_ (clarity over minimalism, communicative approach)
907
+ - [Observable](https://observablehq.com/) (modern SaaS aesthetics, D3.js patterns)
@@ -0,0 +1,369 @@
1
+ /**
2
+ * Graphviz Chart Extension
3
+ *
4
+ * Renders Graphviz DOT specifications as PNG or SVG images in terminals that support
5
+ * inline images (Ghostty, Kitty, iTerm2, WezTerm).
6
+ *
7
+ * Use cases:
8
+ * - Architecture diagrams
9
+ * - Flowcharts
10
+ * - State machines
11
+ * - Dependency graphs
12
+ * - ER diagrams
13
+ * - Network topologies
14
+ */
15
+
16
+ import type { ExtensionAPI } from '@mariozechner/pi-coding-agent';
17
+ import { Type } from '@sinclair/typebox';
18
+ import { execSync } from 'node:child_process';
19
+ import { existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from 'node:fs';
20
+ import { tmpdir } from 'node:os';
21
+ import { dirname, join } from 'node:path';
22
+ import { fileURLToPath } from 'node:url';
23
+
24
+ // Compute reference path using ESM import.meta.url
25
+ const GRAPHVIZ_REFERENCE_PATH = join(
26
+ dirname(fileURLToPath(import.meta.url)),
27
+ 'graphviz-reference.md',
28
+ );
29
+
30
+ export default function (pi: ExtensionAPI) {
31
+ pi.registerTool({
32
+ name: 'graphviz_chart',
33
+ label: 'Graphviz Chart',
34
+ description: `Render a Graphviz DOT specification as a PNG image.
35
+
36
+ Graphviz will be auto-installed if not present (via brew on macOS, apt/dnf on Linux).
37
+ If auto-install fails, the tool returns installation instructions - do NOT fall back to ASCII art.
38
+
39
+ IMPORTANT: Before using this tool, read the complete reference documentation at:
40
+ ${GRAPHVIZ_REFERENCE_PATH}
41
+
42
+ The reference contains critical information about:
43
+ - DOT language syntax for graphs, nodes, and edges
44
+ - All node shapes (box, cylinder, diamond, ellipse, etc.)
45
+ - Edge styles and arrow types
46
+ - Clusters and subgraphs
47
+ - Layout engines (dot, neato, fdp, circo, twopi)
48
+ - Professional theming (light/dark themes, SaaS aesthetics)
49
+ - Common patterns (flowcharts, architecture diagrams, state machines)
50
+
51
+ Pass a complete DOT graph definition. Supports:
52
+ - Graph types: graph (undirected), digraph (directed), strict
53
+ - Node shapes: box, ellipse, circle, diamond, cylinder, record, etc.
54
+ - Edge styles: solid, dashed, dotted, bold
55
+ - Arrows: normal, dot, diamond, box, vee, none, etc.
56
+ - Clusters: subgraph cluster_name { ... }
57
+ - Attributes: color, fillcolor, style, label, fontname, etc.
58
+ - Layout engines: dot (default), neato, fdp, circo, twopi
59
+
60
+ Example DOT syntax:
61
+ \`\`\`
62
+ digraph G {
63
+ rankdir=LR;
64
+ node [shape=box style="rounded,filled" fillcolor=lightblue];
65
+
66
+ A [label="Start"];
67
+ B [label="Process"];
68
+ C [label="End" fillcolor=lightgreen];
69
+
70
+ A -> B [label="step 1"];
71
+ B -> C [label="step 2"];
72
+ }
73
+ \`\`\`
74
+
75
+ Reference: https://graphviz.org/doc/info/lang.html`,
76
+ parameters: Type.Object({
77
+ dot: Type.String({
78
+ description: 'Graphviz DOT specification (complete graph definition)',
79
+ }),
80
+ engine: Type.Optional(
81
+ Type.String({
82
+ description:
83
+ 'Layout engine: dot (hierarchical, default), neato (spring), fdp (force-directed), circo (circular), twopi (radial)',
84
+ }),
85
+ ),
86
+ width: Type.Optional(
87
+ Type.Number({
88
+ description: 'Output width in pixels (default: auto based on graph)',
89
+ }),
90
+ ),
91
+ height: Type.Optional(
92
+ Type.Number({
93
+ description: 'Output height in pixels (default: auto based on graph)',
94
+ }),
95
+ ),
96
+ save_path: Type.Optional(
97
+ Type.String({
98
+ description:
99
+ 'Optional file path to save the chart. Format determined by extension: .svg for SVG, .png for PNG (default)',
100
+ }),
101
+ ),
102
+ }),
103
+
104
+ async execute(_toolCallId, params, _onUpdate, _ctx, signal) {
105
+ const {
106
+ dot,
107
+ engine = 'dot',
108
+ width,
109
+ height,
110
+ save_path,
111
+ } = params as {
112
+ dot: string;
113
+ engine?: string;
114
+ width?: number;
115
+ height?: number;
116
+ save_path?: string;
117
+ };
118
+
119
+ if (signal?.aborted) {
120
+ return { content: [{ type: 'text', text: 'Cancelled' }], details: {} };
121
+ }
122
+
123
+ // Validate engine
124
+ const validEngines = ['dot', 'neato', 'fdp', 'sfdp', 'circo', 'twopi', 'osage', 'patchwork'];
125
+ if (!validEngines.includes(engine)) {
126
+ return {
127
+ content: [
128
+ {
129
+ type: 'text',
130
+ text: `Invalid engine "${engine}". Valid engines: ${validEngines.join(', ')}`,
131
+ },
132
+ ],
133
+ details: { error: 'Invalid engine' },
134
+ isError: true,
135
+ };
136
+ }
137
+
138
+ // Determine output format from save_path extension
139
+ const isSvgOutput = save_path?.toLowerCase().endsWith('.svg') ?? false;
140
+ const outputFormat = isSvgOutput ? 'svg' : 'png';
141
+
142
+ try {
143
+ // Check if graphviz is installed, auto-install if not
144
+ try {
145
+ execSync('which dot', { encoding: 'utf-8' });
146
+ } catch {
147
+ // Try to auto-install graphviz
148
+ const platform = process.platform;
149
+ let installCmd: string | null = null;
150
+
151
+ if (platform === 'darwin') {
152
+ // macOS - check if brew is available
153
+ try {
154
+ execSync('which brew', { encoding: 'utf-8' });
155
+ installCmd = 'brew install graphviz';
156
+ } catch {
157
+ // brew not available
158
+ }
159
+ } else if (platform === 'linux') {
160
+ // Linux - try apt first, then dnf
161
+ try {
162
+ execSync('which apt', { encoding: 'utf-8' });
163
+ installCmd = 'sudo apt install -y graphviz';
164
+ } catch {
165
+ try {
166
+ execSync('which dnf', { encoding: 'utf-8' });
167
+ installCmd = 'sudo dnf install -y graphviz';
168
+ } catch {
169
+ // no supported package manager
170
+ }
171
+ }
172
+ }
173
+
174
+ if (installCmd) {
175
+ try {
176
+ execSync(installCmd, { encoding: 'utf-8', stdio: 'inherit' });
177
+ // Verify installation succeeded
178
+ execSync('which dot', { encoding: 'utf-8' });
179
+ } catch {
180
+ return {
181
+ content: [
182
+ {
183
+ type: 'text',
184
+ text: `Failed to auto-install Graphviz. Please install manually:\n- macOS: brew install graphviz\n- Ubuntu/Debian: sudo apt install graphviz\n- Fedora/RHEL: sudo dnf install graphviz`,
185
+ },
186
+ ],
187
+ details: { error: 'Graphviz installation failed' },
188
+ isError: true,
189
+ };
190
+ }
191
+ } else {
192
+ return {
193
+ content: [
194
+ {
195
+ type: 'text',
196
+ text: `Graphviz not found and auto-install not supported on this platform. Please install manually:\n- macOS: brew install graphviz\n- Ubuntu/Debian: sudo apt install graphviz\n- Fedora/RHEL: sudo dnf install graphviz\n- Windows: https://graphviz.org/download/`,
197
+ },
198
+ ],
199
+ details: { error: 'Graphviz not installed' },
200
+ isError: true,
201
+ };
202
+ }
203
+ }
204
+
205
+ const tmpDot = join(tmpdir(), `graphviz-${Date.now()}.dot`);
206
+ const tmpOutput = join(tmpdir(), `graphviz-${Date.now()}.${outputFormat}`);
207
+ // Always generate PNG for terminal display
208
+ const tmpPng = isSvgOutput
209
+ ? join(tmpdir(), `graphviz-${Date.now()}-display.png`)
210
+ : tmpOutput;
211
+
212
+ // Write DOT file
213
+ writeFileSync(tmpDot, dot);
214
+
215
+ // Build graphviz command for the requested output format
216
+ let cmd = `${engine} -T${outputFormat}`;
217
+
218
+ // Add size constraints if specified (applies to both formats)
219
+ if (width || height) {
220
+ // Graphviz uses inches, convert from pixels (assuming 96 DPI)
221
+ const dpi = 96;
222
+ if (width && height) {
223
+ cmd += ` -Gsize="${width / dpi},${height / dpi}!"`;
224
+ } else if (width) {
225
+ cmd += ` -Gsize="${width / dpi},1000!" -Gratio=compress`;
226
+ } else if (height) {
227
+ cmd += ` -Gsize="1000,${height / dpi}!" -Gratio=compress`;
228
+ }
229
+ if (outputFormat === 'png') {
230
+ cmd += ` -Gdpi=${dpi}`;
231
+ }
232
+ } else if (outputFormat === 'png') {
233
+ // Default: higher DPI for better quality (PNG only)
234
+ cmd += ` -Gdpi=150`;
235
+ }
236
+
237
+ cmd += ` "${tmpDot}" -o "${tmpOutput}"`;
238
+
239
+ // Execute graphviz for main output
240
+ try {
241
+ execSync(cmd, {
242
+ encoding: 'utf-8',
243
+ timeout: 30000,
244
+ maxBuffer: 10 * 1024 * 1024,
245
+ });
246
+ } catch (execError: any) {
247
+ const errorMsg = execError.stderr || execError.message;
248
+ // Clean up
249
+ try {
250
+ unlinkSync(tmpDot);
251
+ } catch {}
252
+ return {
253
+ content: [{ type: 'text', text: `Graphviz error: ${errorMsg}` }],
254
+ details: { error: errorMsg },
255
+ isError: true,
256
+ };
257
+ }
258
+
259
+ // Check if output was created
260
+ if (!existsSync(tmpOutput)) {
261
+ try {
262
+ unlinkSync(tmpDot);
263
+ } catch {}
264
+ return {
265
+ content: [
266
+ { type: 'text', text: 'Graphviz produced no output. Check your DOT syntax.' },
267
+ ],
268
+ details: { error: 'No output' },
269
+ isError: true,
270
+ };
271
+ }
272
+
273
+ // If SVG output, also generate PNG for terminal display
274
+ if (isSvgOutput) {
275
+ let pngCmd = `${engine} -Tpng`;
276
+ if (width || height) {
277
+ const dpi = 96;
278
+ if (width && height) {
279
+ pngCmd += ` -Gsize="${width / dpi},${height / dpi}!"`;
280
+ } else if (width) {
281
+ pngCmd += ` -Gsize="${width / dpi},1000!" -Gratio=compress`;
282
+ } else if (height) {
283
+ pngCmd += ` -Gsize="1000,${height / dpi}!" -Gratio=compress`;
284
+ }
285
+ pngCmd += ` -Gdpi=${dpi}`;
286
+ } else {
287
+ pngCmd += ` -Gdpi=150`;
288
+ }
289
+ pngCmd += ` "${tmpDot}" -o "${tmpPng}"`;
290
+
291
+ try {
292
+ execSync(pngCmd, {
293
+ encoding: 'utf-8',
294
+ timeout: 30000,
295
+ maxBuffer: 10 * 1024 * 1024,
296
+ });
297
+ } catch {
298
+ // If PNG generation fails, we can still save SVG but won't display
299
+ }
300
+ }
301
+
302
+ // Read the PNG file as base64 for terminal display
303
+ let base64Data: string | undefined;
304
+ if (existsSync(tmpPng)) {
305
+ const pngBuffer = readFileSync(tmpPng);
306
+ base64Data = pngBuffer.toString('base64');
307
+ }
308
+
309
+ // If save_path provided, copy the output to that location
310
+ let savedPath: string | undefined;
311
+ if (save_path) {
312
+ try {
313
+ // Ensure directory exists
314
+ mkdirSync(dirname(save_path), { recursive: true });
315
+ const outputBuffer = readFileSync(tmpOutput);
316
+ writeFileSync(save_path, outputBuffer);
317
+ savedPath = save_path;
318
+ } catch (saveErr: any) {
319
+ // Don't fail the whole operation, just note the error
320
+ console.error(`Failed to save to ${save_path}: ${saveErr.message}`);
321
+ }
322
+ }
323
+
324
+ // Clean up temp files
325
+ try {
326
+ unlinkSync(tmpDot);
327
+ } catch {}
328
+ try {
329
+ unlinkSync(tmpOutput);
330
+ } catch {}
331
+ if (isSvgOutput && tmpPng !== tmpOutput) {
332
+ try {
333
+ unlinkSync(tmpPng);
334
+ } catch {}
335
+ }
336
+
337
+ // Count nodes and edges (rough estimate)
338
+ const nodeCount =
339
+ (dot.match(/\w+\s*\[/g) || []).length + (dot.match(/^\s*\w+\s*;/gm) || []).length;
340
+ const edgeCount = (dot.match(/->/g) || []).length + (dot.match(/--/g) || []).length;
341
+
342
+ const formatInfo = isSvgOutput ? ` as ${outputFormat.toUpperCase()}` : '';
343
+ const textMsg = savedPath
344
+ ? `Rendered Graphviz chart (${engine} engine, ~${nodeCount} nodes, ~${edgeCount} edges) - saved${formatInfo} to ${savedPath}`
345
+ : `Rendered Graphviz chart (${engine} engine, ~${nodeCount} nodes, ~${edgeCount} edges)`;
346
+
347
+ const content: Array<{ type: string; text?: string; data?: string; mimeType?: string }> =
348
+ [];
349
+
350
+ // Add image for terminal display if available
351
+ if (base64Data) {
352
+ content.push({ type: 'image', data: base64Data, mimeType: 'image/png' });
353
+ }
354
+ content.push({ type: 'text', text: textMsg });
355
+
356
+ return {
357
+ content,
358
+ details: { engine, nodeCount, edgeCount, savedPath, format: outputFormat },
359
+ };
360
+ } catch (error: any) {
361
+ return {
362
+ content: [{ type: 'text', text: `Error rendering chart: ${error.message}` }],
363
+ details: { error: error.message },
364
+ isError: true,
365
+ };
366
+ }
367
+ },
368
+ });
369
+ }
package/package.json ADDED
@@ -0,0 +1,44 @@
1
+ {
2
+ "name": "@walterra/pi-graphviz",
3
+ "version": "0.0.1",
4
+ "description": "Graphviz chart extension for pi coding agent - render DOT diagrams as inline images",
5
+ "type": "module",
6
+ "files": [
7
+ "extensions",
8
+ "README.md"
9
+ ],
10
+ "pi": {
11
+ "extensions": [
12
+ "./extensions/graphviz-chart/index.ts"
13
+ ]
14
+ },
15
+ "repository": {
16
+ "type": "git",
17
+ "url": "https://github.com/walterra/agent-tools.git",
18
+ "directory": "packages/pi-graphviz"
19
+ },
20
+ "keywords": [
21
+ "pi",
22
+ "pi-coding-agent",
23
+ "extension",
24
+ "graphviz",
25
+ "dot",
26
+ "diagrams",
27
+ "flowcharts",
28
+ "graphs"
29
+ ],
30
+ "author": "walterra",
31
+ "license": "MIT",
32
+ "publishConfig": {
33
+ "access": "public"
34
+ },
35
+ "peerDependencies": {
36
+ "@mariozechner/pi-coding-agent": "*"
37
+ },
38
+ "dependencies": {
39
+ "@sinclair/typebox": "^0.32.0"
40
+ },
41
+ "devDependencies": {
42
+ "@types/node": "^20.0.0"
43
+ }
44
+ }