omegon 0.6.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.
Files changed (160) hide show
  1. package/.gitattributes +3 -0
  2. package/AGENTS.md +16 -0
  3. package/LICENSE +15 -0
  4. package/README.md +289 -0
  5. package/bin/pi.mjs +30 -0
  6. package/extensions/00-secrets/index.ts +1126 -0
  7. package/extensions/01-auth/auth.ts +401 -0
  8. package/extensions/01-auth/index.ts +289 -0
  9. package/extensions/auto-compact.ts +42 -0
  10. package/extensions/bootstrap/deps.ts +291 -0
  11. package/extensions/bootstrap/index.ts +811 -0
  12. package/extensions/chronos/chronos.sh +487 -0
  13. package/extensions/chronos/index.ts +148 -0
  14. package/extensions/cleave/assessment.ts +754 -0
  15. package/extensions/cleave/bridge.ts +31 -0
  16. package/extensions/cleave/conflicts.ts +250 -0
  17. package/extensions/cleave/dispatcher.ts +808 -0
  18. package/extensions/cleave/guardrails.ts +426 -0
  19. package/extensions/cleave/index.ts +3121 -0
  20. package/extensions/cleave/lifecycle-emitter.ts +20 -0
  21. package/extensions/cleave/openspec.ts +811 -0
  22. package/extensions/cleave/planner.ts +260 -0
  23. package/extensions/cleave/review.ts +579 -0
  24. package/extensions/cleave/skills.ts +355 -0
  25. package/extensions/cleave/types.ts +261 -0
  26. package/extensions/cleave/workspace.ts +861 -0
  27. package/extensions/cleave/worktree.ts +243 -0
  28. package/extensions/core-renderers.ts +253 -0
  29. package/extensions/dashboard/context-gauge.ts +58 -0
  30. package/extensions/dashboard/file-watch.ts +14 -0
  31. package/extensions/dashboard/footer.ts +1145 -0
  32. package/extensions/dashboard/git.ts +185 -0
  33. package/extensions/dashboard/index.ts +478 -0
  34. package/extensions/dashboard/memory-audit.ts +34 -0
  35. package/extensions/dashboard/overlay-data.ts +705 -0
  36. package/extensions/dashboard/overlay.ts +365 -0
  37. package/extensions/dashboard/render-utils.ts +54 -0
  38. package/extensions/dashboard/types.ts +191 -0
  39. package/extensions/dashboard/uri-helper.ts +45 -0
  40. package/extensions/debug.ts +69 -0
  41. package/extensions/defaults.ts +282 -0
  42. package/extensions/design-tree/dashboard-state.ts +161 -0
  43. package/extensions/design-tree/design-card.ts +362 -0
  44. package/extensions/design-tree/index.ts +2130 -0
  45. package/extensions/design-tree/lifecycle-emitter.ts +41 -0
  46. package/extensions/design-tree/tree.ts +1607 -0
  47. package/extensions/design-tree/types.ts +163 -0
  48. package/extensions/distill.ts +127 -0
  49. package/extensions/effort/index.ts +395 -0
  50. package/extensions/effort/tiers.ts +146 -0
  51. package/extensions/effort/types.ts +105 -0
  52. package/extensions/lib/git-state.ts +227 -0
  53. package/extensions/lib/local-models.ts +157 -0
  54. package/extensions/lib/model-preferences.ts +51 -0
  55. package/extensions/lib/model-routing.ts +720 -0
  56. package/extensions/lib/operator-fallback.ts +205 -0
  57. package/extensions/lib/operator-profile.ts +360 -0
  58. package/extensions/lib/slash-command-bridge.ts +253 -0
  59. package/extensions/lib/typebox-helpers.ts +16 -0
  60. package/extensions/local-inference/index.ts +727 -0
  61. package/extensions/mcp-bridge/README.md +220 -0
  62. package/extensions/mcp-bridge/index.ts +951 -0
  63. package/extensions/mcp-bridge/lib.ts +365 -0
  64. package/extensions/mcp-bridge/mcp.json +3 -0
  65. package/extensions/mcp-bridge/package.json +11 -0
  66. package/extensions/model-budget.ts +752 -0
  67. package/extensions/offline-driver.ts +403 -0
  68. package/extensions/openspec/archive-gate.ts +164 -0
  69. package/extensions/openspec/branch-cleanup.ts +64 -0
  70. package/extensions/openspec/dashboard-state.ts +50 -0
  71. package/extensions/openspec/index.ts +1917 -0
  72. package/extensions/openspec/lifecycle-emitter.ts +65 -0
  73. package/extensions/openspec/lifecycle-files.ts +70 -0
  74. package/extensions/openspec/lifecycle.ts +50 -0
  75. package/extensions/openspec/reconcile.ts +187 -0
  76. package/extensions/openspec/spec.ts +1385 -0
  77. package/extensions/openspec/types.ts +98 -0
  78. package/extensions/project-memory/DESIGN-global-mind.md +198 -0
  79. package/extensions/project-memory/README.md +202 -0
  80. package/extensions/project-memory/api-types.ts +382 -0
  81. package/extensions/project-memory/compaction-policy.ts +29 -0
  82. package/extensions/project-memory/core.ts +164 -0
  83. package/extensions/project-memory/embeddings.ts +230 -0
  84. package/extensions/project-memory/extraction-v2.ts +861 -0
  85. package/extensions/project-memory/factstore.ts +2177 -0
  86. package/extensions/project-memory/index.ts +3459 -0
  87. package/extensions/project-memory/injection-metrics.ts +91 -0
  88. package/extensions/project-memory/jsonl-io.ts +12 -0
  89. package/extensions/project-memory/lifecycle.ts +331 -0
  90. package/extensions/project-memory/migration.ts +293 -0
  91. package/extensions/project-memory/package.json +9 -0
  92. package/extensions/project-memory/sci-renderers.ts +7 -0
  93. package/extensions/project-memory/template.ts +103 -0
  94. package/extensions/project-memory/triggers.ts +52 -0
  95. package/extensions/project-memory/types.ts +102 -0
  96. package/extensions/render/composition/fonts/Inter-Bold.ttf +0 -0
  97. package/extensions/render/composition/fonts/Inter-Regular.ttf +0 -0
  98. package/extensions/render/composition/fonts/Tomorrow-Bold.ttf +0 -0
  99. package/extensions/render/composition/fonts/Tomorrow-Regular.ttf +0 -0
  100. package/extensions/render/composition/package-lock.json +534 -0
  101. package/extensions/render/composition/package.json +22 -0
  102. package/extensions/render/composition/render.mjs +246 -0
  103. package/extensions/render/composition/test-comp.tsx +87 -0
  104. package/extensions/render/composition/types.ts +24 -0
  105. package/extensions/render/excalidraw/UPSTREAM.md +81 -0
  106. package/extensions/render/excalidraw/elements.ts +764 -0
  107. package/extensions/render/excalidraw/index.ts +66 -0
  108. package/extensions/render/excalidraw/types.ts +223 -0
  109. package/extensions/render/excalidraw-renderer/pyproject.toml +8 -0
  110. package/extensions/render/excalidraw-renderer/render_excalidraw.py +182 -0
  111. package/extensions/render/excalidraw-renderer/render_template.html +59 -0
  112. package/extensions/render/index.ts +830 -0
  113. package/extensions/render/native-diagrams/index.ts +57 -0
  114. package/extensions/render/native-diagrams/motifs.ts +542 -0
  115. package/extensions/render/native-diagrams/raster.ts +8 -0
  116. package/extensions/render/native-diagrams/scene.ts +75 -0
  117. package/extensions/render/native-diagrams/spec.ts +204 -0
  118. package/extensions/render/native-diagrams/svg.ts +116 -0
  119. package/extensions/sci-ui.ts +304 -0
  120. package/extensions/session-log.ts +174 -0
  121. package/extensions/shared-state.ts +146 -0
  122. package/extensions/spinner-verbs.ts +91 -0
  123. package/extensions/style.ts +281 -0
  124. package/extensions/terminal-title.ts +191 -0
  125. package/extensions/tool-profile/index.ts +291 -0
  126. package/extensions/tool-profile/profiles.ts +290 -0
  127. package/extensions/types.d.ts +9 -0
  128. package/extensions/vault/index.ts +185 -0
  129. package/extensions/version-check.ts +90 -0
  130. package/extensions/view/index.ts +859 -0
  131. package/extensions/view/uri-resolver.ts +148 -0
  132. package/extensions/web-search/index.ts +182 -0
  133. package/extensions/web-search/providers.ts +121 -0
  134. package/extensions/web-ui/index.ts +110 -0
  135. package/extensions/web-ui/server.ts +265 -0
  136. package/extensions/web-ui/state.ts +462 -0
  137. package/extensions/web-ui/static/index.html +145 -0
  138. package/extensions/web-ui/types.ts +284 -0
  139. package/package.json +76 -0
  140. package/prompts/init.md +75 -0
  141. package/prompts/new-repo.md +54 -0
  142. package/prompts/oci-login.md +56 -0
  143. package/prompts/status.md +50 -0
  144. package/settings.json +4 -0
  145. package/skills/cleave/SKILL.md +218 -0
  146. package/skills/git/SKILL.md +209 -0
  147. package/skills/git/_reference/ci-validation.md +204 -0
  148. package/skills/oci/SKILL.md +338 -0
  149. package/skills/openspec/SKILL.md +346 -0
  150. package/skills/pi-extensions/SKILL.md +191 -0
  151. package/skills/pi-tui/SKILL.md +517 -0
  152. package/skills/python/SKILL.md +189 -0
  153. package/skills/rust/SKILL.md +268 -0
  154. package/skills/security/SKILL.md +206 -0
  155. package/skills/style/SKILL.md +264 -0
  156. package/skills/typescript/SKILL.md +225 -0
  157. package/skills/vault/SKILL.md +102 -0
  158. package/themes/alpharius-legacy.json +85 -0
  159. package/themes/alpharius.conf +59 -0
  160. package/themes/alpharius.json +88 -0
@@ -0,0 +1,66 @@
1
+ /**
2
+ * Excalidraw element factory — programmatic .excalidraw file generation.
3
+ *
4
+ * Usage in LLM tool calls:
5
+ * import { rect, diamond, ellipse, dot, text, arrow, line,
6
+ * bindArrow, createDocument, validateDocument,
7
+ * fanOut, timeline, grid,
8
+ * SEMANTIC_COLORS, TEXT_COLORS } from "./excalidraw/index.ts";
9
+ *
10
+ * See UPSTREAM.md for vendoring provenance and sync instructions.
11
+ */
12
+
13
+ export {
14
+ // Shape factories
15
+ rect,
16
+ diamond,
17
+ ellipse,
18
+ dot,
19
+ // Text
20
+ text,
21
+ // Connectors
22
+ arrow,
23
+ line,
24
+ // Binding
25
+ bindArrow,
26
+ // Document
27
+ createDocument,
28
+ validateDocument,
29
+ resetIndexCounter,
30
+ // Layout helpers
31
+ fanOut,
32
+ timeline,
33
+ grid,
34
+ } from "./elements.ts";
35
+
36
+ export type {
37
+ ExcalidrawElement,
38
+ ExcalidrawFile,
39
+ RectangleElement,
40
+ DiamondElement,
41
+ EllipseElement,
42
+ TextElement,
43
+ ArrowElement,
44
+ LineElement,
45
+ SemanticPurpose,
46
+ TextLevel,
47
+ Point,
48
+ ShapeOptions,
49
+ TextOptions,
50
+ ArrowOptions,
51
+ } from "./elements.ts";
52
+
53
+ export {
54
+ SEMANTIC_COLORS,
55
+ TEXT_COLORS,
56
+ FONT_FAMILIES,
57
+ DEFAULT_ELEMENT_STYLE,
58
+ DEFAULT_APP_STATE,
59
+ } from "./types.ts";
60
+
61
+ export type {
62
+ ColorPair,
63
+ AppState,
64
+ ArrowBinding,
65
+ BoundElement,
66
+ } from "./types.ts";
@@ -0,0 +1,223 @@
1
+ /**
2
+ * Excalidraw Element Type Definitions
3
+ *
4
+ * Vendored from @swiftlysingh/excalidraw-cli@1.1.0 (src/types/excalidraw.ts)
5
+ * with additions for semantic palette system. See UPSTREAM.md for sync guide.
6
+ *
7
+ * Based on Excalidraw JSON schema v2 (excalidraw.com)
8
+ */
9
+
10
+ // ---------------------------------------------------------------------------
11
+ // Primitives
12
+ // ---------------------------------------------------------------------------
13
+
14
+ export type ExcalidrawElementType =
15
+ | "rectangle"
16
+ | "diamond"
17
+ | "ellipse"
18
+ | "text"
19
+ | "arrow"
20
+ | "line"
21
+ | "freedraw";
22
+
23
+ export type FillStyle = "solid" | "hachure" | "cross-hatch";
24
+ export type StrokeStyle = "solid" | "dashed" | "dotted";
25
+ export type Arrowhead = "arrow" | "bar" | "dot" | "triangle" | null;
26
+ export type TextAlign = "left" | "center" | "right";
27
+ export type VerticalAlign = "top" | "middle" | "bottom";
28
+
29
+ export interface Roundness {
30
+ type: 1 | 2 | 3; // 1=legacy, 2=proportional, 3=adaptive
31
+ }
32
+
33
+ export interface BoundElement {
34
+ id: string;
35
+ type: "arrow" | "text";
36
+ }
37
+
38
+ export interface ArrowBinding {
39
+ elementId: string;
40
+ mode: "orbit" | "point";
41
+ fixedPoint: [number, number]; // Normalized [0-1] coordinates
42
+ }
43
+
44
+ // ---------------------------------------------------------------------------
45
+ // Element types
46
+ // ---------------------------------------------------------------------------
47
+
48
+ export interface ElementBase {
49
+ id: string;
50
+ type: ExcalidrawElementType;
51
+ x: number;
52
+ y: number;
53
+ width: number;
54
+ height: number;
55
+ angle: number;
56
+ strokeColor: string;
57
+ backgroundColor: string;
58
+ fillStyle: FillStyle;
59
+ strokeWidth: number;
60
+ strokeStyle: StrokeStyle;
61
+ roughness: number;
62
+ opacity: number;
63
+ groupIds: string[];
64
+ frameId: string | null;
65
+ index: string;
66
+ roundness: Roundness | null;
67
+ seed: number;
68
+ version: number;
69
+ versionNonce: number;
70
+ isDeleted: boolean;
71
+ boundElements: BoundElement[] | null;
72
+ updated: number;
73
+ link: string | null;
74
+ locked: boolean;
75
+ }
76
+
77
+ export interface RectangleElement extends ElementBase {
78
+ type: "rectangle";
79
+ }
80
+
81
+ export interface DiamondElement extends ElementBase {
82
+ type: "diamond";
83
+ }
84
+
85
+ export interface EllipseElement extends ElementBase {
86
+ type: "ellipse";
87
+ }
88
+
89
+ export interface TextElement extends ElementBase {
90
+ type: "text";
91
+ text: string;
92
+ fontSize: number;
93
+ fontFamily: number; // 1=Virgil, 2=Helvetica, 3=Cascadia, 5=Excalifont
94
+ textAlign: TextAlign;
95
+ verticalAlign: VerticalAlign;
96
+ containerId: string | null;
97
+ originalText: string;
98
+ autoResize: boolean;
99
+ lineHeight: number;
100
+ }
101
+
102
+ export interface ArrowElement extends ElementBase {
103
+ type: "arrow";
104
+ points: Array<[number, number]>;
105
+ lastCommittedPoint: [number, number] | null;
106
+ startBinding: ArrowBinding | null;
107
+ endBinding: ArrowBinding | null;
108
+ startArrowhead: Arrowhead;
109
+ endArrowhead: Arrowhead;
110
+ elbowed: boolean;
111
+ }
112
+
113
+ export interface LineElement extends ElementBase {
114
+ type: "line";
115
+ points: Array<[number, number]>;
116
+ lastCommittedPoint: [number, number] | null;
117
+ startBinding: ArrowBinding | null;
118
+ endBinding: ArrowBinding | null;
119
+ startArrowhead: Arrowhead;
120
+ endArrowhead: Arrowhead;
121
+ }
122
+
123
+ export type ExcalidrawElement =
124
+ | RectangleElement
125
+ | DiamondElement
126
+ | EllipseElement
127
+ | TextElement
128
+ | ArrowElement
129
+ | LineElement;
130
+
131
+ // ---------------------------------------------------------------------------
132
+ // File structure
133
+ // ---------------------------------------------------------------------------
134
+
135
+ export interface AppState {
136
+ gridSize: number;
137
+ gridStep: number;
138
+ gridModeEnabled: boolean;
139
+ viewBackgroundColor: string;
140
+ }
141
+
142
+ export interface ExcalidrawFile {
143
+ type: "excalidraw";
144
+ version: 2;
145
+ source: string;
146
+ elements: ExcalidrawElement[];
147
+ appState: AppState;
148
+ files: Record<string, unknown>;
149
+ }
150
+
151
+ // ---------------------------------------------------------------------------
152
+ // Defaults
153
+ // ---------------------------------------------------------------------------
154
+
155
+ export const DEFAULT_APP_STATE: AppState = {
156
+ gridSize: 20,
157
+ gridStep: 5,
158
+ gridModeEnabled: false,
159
+ viewBackgroundColor: "#06080e",
160
+ };
161
+
162
+ export const DEFAULT_ELEMENT_STYLE = {
163
+ strokeColor: "#1e1e1e",
164
+ backgroundColor: "transparent",
165
+ fillStyle: "solid" as FillStyle,
166
+ strokeWidth: 2,
167
+ strokeStyle: "solid" as StrokeStyle,
168
+ roughness: 0, // Clean/modern default (upstream uses 1)
169
+ opacity: 100,
170
+ };
171
+
172
+ export const FONT_FAMILIES = {
173
+ Virgil: 1,
174
+ Helvetica: 2,
175
+ Cascadia: 3,
176
+ Excalifont: 5,
177
+ } as const;
178
+
179
+ // ---------------------------------------------------------------------------
180
+ // Semantic palette (omegon addition, not from upstream)
181
+ // ---------------------------------------------------------------------------
182
+
183
+ export type SemanticPurpose =
184
+ | "primary"
185
+ | "secondary"
186
+ | "tertiary"
187
+ | "start"
188
+ | "end"
189
+ | "warning"
190
+ | "decision"
191
+ | "ai"
192
+ | "inactive"
193
+ | "error"
194
+ | "evidence";
195
+
196
+ export interface ColorPair {
197
+ fill: string;
198
+ stroke: string;
199
+ }
200
+
201
+ export const SEMANTIC_COLORS: Record<SemanticPurpose, ColorPair> = {
202
+ primary: { fill: "#1a4a6e", stroke: "#2ab4c8" },
203
+ secondary: { fill: "#1a3a5a", stroke: "#1a8898" },
204
+ tertiary: { fill: "#0e2a40", stroke: "#344858" },
205
+ start: { fill: "#0e2e20", stroke: "#1ab878" },
206
+ end: { fill: "#2e2010", stroke: "#b89020" },
207
+ warning: { fill: "#2a1808", stroke: "#c86418" },
208
+ decision: { fill: "#2a1010", stroke: "#c83030" },
209
+ ai: { fill: "#1a1040", stroke: "#6060c0" },
210
+ inactive: { fill: "#0e1622", stroke: "#344858" },
211
+ error: { fill: "#2e0e0e", stroke: "#c83030" },
212
+ evidence: { fill: "#06080e", stroke: "#1a3448" },
213
+ };
214
+
215
+ export const TEXT_COLORS = {
216
+ title: "#2ab4c8",
217
+ subtitle: "#1a8898",
218
+ body: "#607888",
219
+ onLight: "#c4d8e4",
220
+ onDark: "#c4d8e4",
221
+ } as const;
222
+
223
+ export type TextLevel = keyof typeof TEXT_COLORS;
@@ -0,0 +1,8 @@
1
+ [project]
2
+ name = "excalidraw-render"
3
+ version = "0.1.0"
4
+ description = "Render .excalidraw JSON files to PNG via Playwright + headless Chromium"
5
+ requires-python = ">=3.11"
6
+ dependencies = [
7
+ "playwright>=1.40.0",
8
+ ]
@@ -0,0 +1,182 @@
1
+ """Render Excalidraw JSON to PNG using Playwright + headless Chromium.
2
+
3
+ Vendored from coleam00/excalidraw-diagram-skill (references/render_excalidraw.py)
4
+ with minor changes: pinned esm.sh version in template, improved error messages.
5
+
6
+ Usage:
7
+ cd skills/visualize/references/excalidraw
8
+ uv run python render_excalidraw.py <path-to-file.excalidraw> [--output path.png] [--scale 2]
9
+
10
+ First-time setup:
11
+ cd skills/visualize/references/excalidraw
12
+ uv sync
13
+ uv run playwright install chromium
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ import argparse
19
+ import json
20
+ import sys
21
+ from pathlib import Path
22
+
23
+
24
+ def validate_excalidraw(data: dict) -> list[str]:
25
+ """Validate Excalidraw JSON structure. Returns list of errors (empty = valid)."""
26
+ errors: list[str] = []
27
+
28
+ if data.get("type") != "excalidraw":
29
+ errors.append(f"Expected type 'excalidraw', got '{data.get('type')}'")
30
+
31
+ if "elements" not in data:
32
+ errors.append("Missing 'elements' array")
33
+ elif not isinstance(data["elements"], list):
34
+ errors.append("'elements' must be an array")
35
+ elif len(data["elements"]) == 0:
36
+ errors.append("'elements' array is empty — nothing to render")
37
+
38
+ return errors
39
+
40
+
41
+ def compute_bounding_box(elements: list[dict]) -> tuple[float, float, float, float]:
42
+ """Compute bounding box (min_x, min_y, max_x, max_y) across all elements."""
43
+ min_x = float("inf")
44
+ min_y = float("inf")
45
+ max_x = float("-inf")
46
+ max_y = float("-inf")
47
+
48
+ for el in elements:
49
+ if el.get("isDeleted"):
50
+ continue
51
+ x = el.get("x", 0)
52
+ y = el.get("y", 0)
53
+ w = el.get("width", 0)
54
+ h = el.get("height", 0)
55
+
56
+ if el.get("type") in ("arrow", "line") and "points" in el:
57
+ for px, py in el["points"]:
58
+ min_x = min(min_x, x + px)
59
+ min_y = min(min_y, y + py)
60
+ max_x = max(max_x, x + px)
61
+ max_y = max(max_y, y + py)
62
+ else:
63
+ min_x = min(min_x, x)
64
+ min_y = min(min_y, y)
65
+ max_x = max(max_x, x + abs(w))
66
+ max_y = max(max_y, y + abs(h))
67
+
68
+ if min_x == float("inf"):
69
+ return (0, 0, 800, 600)
70
+
71
+ return (min_x, min_y, max_x, max_y)
72
+
73
+
74
+ def render(
75
+ excalidraw_path: Path,
76
+ output_path: Path | None = None,
77
+ scale: int = 2,
78
+ max_width: int = 1920,
79
+ ) -> Path:
80
+ """Render an .excalidraw file to PNG. Returns the output PNG path."""
81
+ try:
82
+ from playwright.sync_api import sync_playwright
83
+ except ImportError:
84
+ print("ERROR: playwright not installed.", file=sys.stderr)
85
+ print("Run: cd skills/visualize/references/excalidraw && uv sync && uv run playwright install chromium", file=sys.stderr)
86
+ sys.exit(1)
87
+
88
+ raw = excalidraw_path.read_text(encoding="utf-8")
89
+ try:
90
+ data = json.loads(raw)
91
+ except json.JSONDecodeError as e:
92
+ print(f"ERROR: Invalid JSON in {excalidraw_path}: {e}", file=sys.stderr)
93
+ sys.exit(1)
94
+
95
+ errors = validate_excalidraw(data)
96
+ if errors:
97
+ print(f"ERROR: Invalid Excalidraw file:", file=sys.stderr)
98
+ for err in errors:
99
+ print(f" - {err}", file=sys.stderr)
100
+ sys.exit(1)
101
+
102
+ elements = [e for e in data["elements"] if not e.get("isDeleted")]
103
+ min_x, min_y, max_x, max_y = compute_bounding_box(elements)
104
+ padding = 80
105
+ diagram_w = max_x - min_x + padding * 2
106
+ diagram_h = max_y - min_y + padding * 2
107
+
108
+ vp_width = min(int(diagram_w), max_width)
109
+ vp_height = max(int(diagram_h), 600)
110
+
111
+ if output_path is None:
112
+ output_path = excalidraw_path.with_suffix(".png")
113
+
114
+ template_path = Path(__file__).parent / "render_template.html"
115
+ if not template_path.exists():
116
+ print(f"ERROR: Template not found at {template_path}", file=sys.stderr)
117
+ sys.exit(1)
118
+
119
+ template_url = template_path.as_uri()
120
+
121
+ with sync_playwright() as p:
122
+ try:
123
+ browser = p.chromium.launch(headless=True)
124
+ except Exception as e:
125
+ if "Executable doesn't exist" in str(e) or "browserType.launch" in str(e):
126
+ print("ERROR: Chromium not installed for Playwright.", file=sys.stderr)
127
+ print("Run: cd skills/visualize/references/excalidraw && uv run playwright install chromium", file=sys.stderr)
128
+ sys.exit(1)
129
+ raise
130
+
131
+ try:
132
+ page = browser.new_page(
133
+ viewport={"width": vp_width, "height": vp_height},
134
+ device_scale_factor=scale,
135
+ )
136
+
137
+ page.goto(template_url)
138
+ page.wait_for_function("window.__moduleReady === true", timeout=30000)
139
+
140
+ # Use Playwright argument passing instead of string interpolation
141
+ # to avoid JS injection issues with special characters in JSON values.
142
+ result = page.evaluate("(data) => window.renderDiagram(data)", data)
143
+
144
+ if not result or not result.get("success"):
145
+ error_msg = result.get("error", "Unknown render error") if result else "renderDiagram returned null"
146
+ print(f"ERROR: Render failed: {error_msg}", file=sys.stderr)
147
+ sys.exit(1)
148
+
149
+ # Wait for render to complete (exportToSvg is async, renderDiagram
150
+ # sets __renderComplete after SVG is appended to DOM).
151
+ page.wait_for_function("window.__renderComplete === true", timeout=15000)
152
+
153
+ svg_el = page.query_selector("#root svg")
154
+ if svg_el is None:
155
+ print("ERROR: No SVG element found after render.", file=sys.stderr)
156
+ sys.exit(1)
157
+
158
+ svg_el.screenshot(path=str(output_path))
159
+ finally:
160
+ browser.close()
161
+
162
+ return output_path
163
+
164
+
165
+ def main() -> None:
166
+ parser = argparse.ArgumentParser(description="Render Excalidraw JSON to PNG")
167
+ parser.add_argument("input", type=Path, help="Path to .excalidraw JSON file")
168
+ parser.add_argument("--output", "-o", type=Path, default=None, help="Output PNG path")
169
+ parser.add_argument("--scale", "-s", type=int, default=2, help="Device scale factor (default: 2)")
170
+ parser.add_argument("--width", "-w", type=int, default=1920, help="Max viewport width (default: 1920)")
171
+ args = parser.parse_args()
172
+
173
+ if not args.input.exists():
174
+ print(f"ERROR: File not found: {args.input}", file=sys.stderr)
175
+ sys.exit(1)
176
+
177
+ png_path = render(args.input, args.output, args.scale, args.width)
178
+ print(str(png_path))
179
+
180
+
181
+ if __name__ == "__main__":
182
+ main()
@@ -0,0 +1,59 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <style>
6
+ * { margin: 0; padding: 0; box-sizing: border-box; }
7
+ body { background: #ffffff; overflow: hidden; }
8
+ #root { display: inline-block; }
9
+ #root svg { display: block; }
10
+ </style>
11
+ </head>
12
+ <body>
13
+ <div id="root"></div>
14
+
15
+ <!--
16
+ Pin @excalidraw/excalidraw to a specific version to avoid CDN updates breaking rendering.
17
+ Update this version intentionally when syncing with upstream (see UPSTREAM.md).
18
+ Current pin: 0.18.0 (pinned 2026-03-03)
19
+ -->
20
+ <script type="module">
21
+ import { exportToSvg } from "https://esm.sh/@excalidraw/excalidraw@0.18.0?bundle";
22
+
23
+ window.renderDiagram = async function(jsonData) {
24
+ try {
25
+ const data = typeof jsonData === "string" ? JSON.parse(jsonData) : jsonData;
26
+ const elements = data.elements || [];
27
+ const appState = data.appState || {};
28
+ const files = data.files || {};
29
+
30
+ appState.viewBackgroundColor = appState.viewBackgroundColor || "#ffffff";
31
+ appState.exportWithDarkMode = false;
32
+
33
+ const svg = await exportToSvg({
34
+ elements: elements,
35
+ appState: {
36
+ ...appState,
37
+ exportBackground: true,
38
+ },
39
+ files: files,
40
+ });
41
+
42
+ const root = document.getElementById("root");
43
+ root.innerHTML = "";
44
+ root.appendChild(svg);
45
+
46
+ window.__renderComplete = true;
47
+ window.__renderError = null;
48
+ return { success: true, width: svg.getAttribute("width"), height: svg.getAttribute("height") };
49
+ } catch (err) {
50
+ window.__renderComplete = true;
51
+ window.__renderError = err.message;
52
+ return { success: false, error: err.message };
53
+ }
54
+ };
55
+
56
+ window.__moduleReady = true;
57
+ </script>
58
+ </body>
59
+ </html>