@agent-native/core 0.77.16 → 0.77.17

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 (60) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/core/CHANGELOG.md +8 -0
  3. package/corpus/core/package.json +1 -1
  4. package/corpus/core/src/client/AgentPanel.tsx +4 -0
  5. package/corpus/core/src/client/org/OrgSwitcher.tsx +4 -0
  6. package/corpus/core/src/client/use-db-sync.ts +19 -2
  7. package/corpus/core/src/server/auth-marketing.ts +15 -0
  8. package/corpus/core/src/server/onboarding-html.ts +47 -1
  9. package/corpus/templates/analytics/.agents/skills/dashboard-management/SKILL.md +76 -0
  10. package/corpus/templates/analytics/actions/update-dashboard.ts +1 -0
  11. package/corpus/templates/analytics/app/components/dashboard/SqlChart.tsx +13 -2
  12. package/corpus/templates/analytics/app/i18n-data.ts +10 -0
  13. package/corpus/templates/analytics/app/lib/sql-query.ts +97 -21
  14. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/SqlChartCard.tsx +22 -1
  15. package/corpus/templates/analytics/app/root.tsx +15 -6
  16. package/corpus/templates/analytics/changelog/2026-06-25-dashboard-charts-no-longer-refresh-in-the-background-during-.md +6 -0
  17. package/corpus/templates/analytics/changelog/2026-06-25-first-party-dashboards-use-indexed-event-dates-for-faster-da.md +6 -0
  18. package/corpus/templates/analytics/changelog/2026-06-25-retention-and-active-user-dashboard-panels-now-count-account.md +6 -0
  19. package/corpus/templates/analytics/seeds/dashboards/agent-native-templates-first-party.json +58 -58
  20. package/corpus/templates/analytics/server/db/schema.ts +2 -0
  21. package/corpus/templates/analytics/server/handlers/sql-query.ts +1 -1
  22. package/corpus/templates/analytics/server/lib/dashboard-catalog.ts +1 -1
  23. package/corpus/templates/analytics/server/lib/first-party-analytics.ts +16 -6
  24. package/corpus/templates/analytics/server/lib/first-party-metric-catalog.ts +88 -56
  25. package/corpus/templates/analytics/server/plugins/db.ts +73 -0
  26. package/corpus/templates/clips/AGENTS.md +5 -0
  27. package/corpus/templates/clips/changelog/2026-06-25-github-issue-and-pull-request-pages-can-now-preview-playable.md +6 -0
  28. package/corpus/templates/clips/chrome-extension/PERMISSIONS.md +8 -2
  29. package/corpus/templates/clips/chrome-extension/public/manifest.json +14 -2
  30. package/corpus/templates/clips/chrome-extension/src/github-preview-content.ts +233 -0
  31. package/corpus/templates/clips/chrome-extension/src/github-preview.html +12 -0
  32. package/corpus/templates/clips/chrome-extension/src/github-preview.ts +414 -0
  33. package/corpus/templates/clips/chrome-extension/vite.config.ts +5 -0
  34. package/corpus/templates/mail/actions/archive-email.ts +13 -3
  35. package/corpus/templates/mail/app/hooks/use-emails.ts +5 -2
  36. package/corpus/templates/mail/changelog/2026-06-25-archive-failures-now-explain-when-gmail-needs-reconnecting-p.md +6 -0
  37. package/corpus/templates/mail/shared/archive-errors.ts +137 -0
  38. package/corpus/templates/plan/changelog/2026-06-25-hosted-signup-now-shows-how-to-switch-visual-plan-to-local-files.md +6 -0
  39. package/dist/client/AgentPanel.d.ts.map +1 -1
  40. package/dist/client/AgentPanel.js +2 -1
  41. package/dist/client/AgentPanel.js.map +1 -1
  42. package/dist/client/org/OrgSwitcher.d.ts.map +1 -1
  43. package/dist/client/org/OrgSwitcher.js +3 -1
  44. package/dist/client/org/OrgSwitcher.js.map +1 -1
  45. package/dist/client/use-db-sync.d.ts +9 -0
  46. package/dist/client/use-db-sync.d.ts.map +1 -1
  47. package/dist/client/use-db-sync.js +8 -1
  48. package/dist/client/use-db-sync.js.map +1 -1
  49. package/dist/collab/awareness.d.ts +2 -2
  50. package/dist/collab/awareness.d.ts.map +1 -1
  51. package/dist/observability/routes.d.ts +5 -5
  52. package/dist/server/agent-engine-api-key-route.d.ts +2 -2
  53. package/dist/server/auth-marketing.d.ts +4 -0
  54. package/dist/server/auth-marketing.d.ts.map +1 -1
  55. package/dist/server/auth-marketing.js +9 -0
  56. package/dist/server/auth-marketing.js.map +1 -1
  57. package/dist/server/onboarding-html.d.ts.map +1 -1
  58. package/dist/server/onboarding-html.js +46 -1
  59. package/dist/server/onboarding-html.js.map +1 -1
  60. package/package.json +1 -1
@@ -134,8 +134,10 @@ export default runMigrations(
134
134
  event_name TEXT NOT NULL,
135
135
  user_id TEXT,
136
136
  anonymous_id TEXT,
137
+ user_key TEXT,
137
138
  session_id TEXT,
138
139
  timestamp TEXT NOT NULL,
140
+ event_date TEXT,
139
141
  received_at TEXT NOT NULL DEFAULT (datetime('now')),
140
142
  url TEXT,
141
143
  path TEXT,
@@ -219,6 +221,77 @@ export default runMigrations(
219
221
  version: 38,
220
222
  sql: `SELECT 1`,
221
223
  },
224
+ {
225
+ version: 39,
226
+ sql: `ALTER TABLE analytics_events ADD COLUMN IF NOT EXISTS event_date TEXT`,
227
+ },
228
+ {
229
+ version: 40,
230
+ sql: `ALTER TABLE analytics_events ADD COLUMN IF NOT EXISTS user_key TEXT`,
231
+ },
232
+ {
233
+ version: 41,
234
+ sql: {
235
+ postgres: `DROP INDEX CONCURRENTLY IF EXISTS analytics_events_org_event_date_idx; CREATE INDEX CONCURRENTLY analytics_events_org_event_date_idx ON analytics_events (org_id, event_date)`,
236
+ sqlite: `CREATE INDEX IF NOT EXISTS analytics_events_org_event_date_idx ON analytics_events (org_id, event_date)`,
237
+ },
238
+ },
239
+ {
240
+ version: 42,
241
+ sql: {
242
+ postgres: `DROP INDEX CONCURRENTLY IF EXISTS analytics_events_org_event_name_date_idx; CREATE INDEX CONCURRENTLY analytics_events_org_event_name_date_idx ON analytics_events (org_id, event_name, event_date)`,
243
+ sqlite: `CREATE INDEX IF NOT EXISTS analytics_events_org_event_name_date_idx ON analytics_events (org_id, event_name, event_date)`,
244
+ },
245
+ },
246
+ {
247
+ version: 43,
248
+ sql: {
249
+ postgres: `DROP INDEX CONCURRENTLY IF EXISTS analytics_events_org_date_user_idx; CREATE INDEX CONCURRENTLY analytics_events_org_date_user_idx ON analytics_events (org_id, event_date, user_key)`,
250
+ sqlite: `CREATE INDEX IF NOT EXISTS analytics_events_org_date_user_idx ON analytics_events (org_id, event_date, user_key)`,
251
+ },
252
+ },
253
+ {
254
+ version: 44,
255
+ sql: {
256
+ postgres: `DROP INDEX CONCURRENTLY IF EXISTS analytics_events_org_date_template_idx; CREATE INDEX CONCURRENTLY analytics_events_org_date_template_idx ON analytics_events (org_id, event_date, template)`,
257
+ sqlite: `CREATE INDEX IF NOT EXISTS analytics_events_org_date_template_idx ON analytics_events (org_id, event_date, template)`,
258
+ },
259
+ },
260
+ {
261
+ version: 45,
262
+ sql: {
263
+ postgres: `DROP INDEX CONCURRENTLY IF EXISTS analytics_events_owner_event_date_idx; CREATE INDEX CONCURRENTLY analytics_events_owner_event_date_idx ON analytics_events (owner_email, event_date) WHERE org_id IS NULL`,
264
+ sqlite: `CREATE INDEX IF NOT EXISTS analytics_events_owner_event_date_idx ON analytics_events (owner_email, event_date) WHERE org_id IS NULL`,
265
+ },
266
+ },
267
+ {
268
+ version: 46,
269
+ sql: {
270
+ postgres: `DROP INDEX CONCURRENTLY IF EXISTS analytics_events_owner_event_name_date_idx; CREATE INDEX CONCURRENTLY analytics_events_owner_event_name_date_idx ON analytics_events (owner_email, event_name, event_date) WHERE org_id IS NULL`,
271
+ sqlite: `CREATE INDEX IF NOT EXISTS analytics_events_owner_event_name_date_idx ON analytics_events (owner_email, event_name, event_date) WHERE org_id IS NULL`,
272
+ },
273
+ },
274
+ {
275
+ version: 47,
276
+ sql: {
277
+ postgres: `DROP INDEX CONCURRENTLY IF EXISTS analytics_events_owner_date_user_idx; CREATE INDEX CONCURRENTLY analytics_events_owner_date_user_idx ON analytics_events (owner_email, event_date, user_key) WHERE org_id IS NULL`,
278
+ sqlite: `CREATE INDEX IF NOT EXISTS analytics_events_owner_date_user_idx ON analytics_events (owner_email, event_date, user_key) WHERE org_id IS NULL`,
279
+ },
280
+ },
281
+ {
282
+ version: 48,
283
+ sql: {
284
+ postgres: `DROP INDEX CONCURRENTLY IF EXISTS analytics_events_owner_date_template_idx; CREATE INDEX CONCURRENTLY analytics_events_owner_date_template_idx ON analytics_events (owner_email, event_date, template) WHERE org_id IS NULL`,
285
+ sqlite: `CREATE INDEX IF NOT EXISTS analytics_events_owner_date_template_idx ON analytics_events (owner_email, event_date, template) WHERE org_id IS NULL`,
286
+ },
287
+ },
288
+ {
289
+ version: 49,
290
+ sql: {
291
+ postgres: `UPDATE analytics_events SET event_date = COALESCE(NULLIF(event_date, ''), substr(timestamp, 1, 10)), user_key = COALESCE(NULLIF(user_key, ''), NULLIF(user_id, ''), NULLIF(anonymous_id, '')) WHERE (event_date IS NULL OR event_date = '' OR user_key IS NULL OR user_key = '') AND timestamp >= to_char(CURRENT_DATE - INTERVAL '400 days', 'YYYY-MM-DD')`,
292
+ sqlite: `UPDATE analytics_events SET event_date = COALESCE(NULLIF(event_date, ''), substr(timestamp, 1, 10)), user_key = COALESCE(NULLIF(user_key, ''), NULLIF(user_id, ''), NULLIF(anonymous_id, '')) WHERE (event_date IS NULL OR event_date = '' OR user_key IS NULL OR user_key = '') AND timestamp >= date('now', '-400 days')`,
293
+ },
294
+ },
222
295
  ],
223
296
  { table: "analytics_migrations" },
224
297
  );
@@ -79,6 +79,11 @@ Detailed media, meeting, dictation, editing, and sharing rules live in
79
79
  the Chrome debugger API only on the tab the user launched from, only while a
80
80
  recording is active, and returns the same redacted diagnostics shape saved by
81
81
  `save-browser-diagnostics`.
82
+ - The Chrome extension also enhances GitHub issue and PR markdown: a narrow
83
+ `github.com` content script detects Clips `/r/`, `/share/`, and `/embed/`
84
+ links, then renders the existing `/embed/:id` player in an extension-owned
85
+ preview iframe so the video is playable without leaving GitHub. Keep this
86
+ scoped to GitHub unless there is a deliberate permission review.
82
87
  - After mutations, rely on the app refresh/polling path; do not invent a second
83
88
  sync mechanism.
84
89
 
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: added
3
+ date: 2026-06-25
4
+ ---
5
+
6
+ GitHub issue and pull request pages can now preview playable Clips links when the Chrome extension is installed.
@@ -1,7 +1,7 @@
1
1
  # Permissions model
2
2
 
3
- This extension ships **activeTab-only**: it deliberately does **not** request the
4
- broad `<all_urls>` host permission, and it has **no declarative `content_scripts`**.
3
+ This extension ships **activeTab-only for recording overlays**: it deliberately
4
+ does **not** request the broad `<all_urls>` host permission for recording UI.
5
5
 
6
6
  ## Why
7
7
 
@@ -9,6 +9,11 @@ A declarative content script on `<all_urls>` (and the matching `<all_urls>` host
9
9
  permission) triggers Chrome Web Store's **broad host permission in-depth review**,
10
10
  which significantly delays publishing and updates. We avoid that.
11
11
 
12
+ The only declarative content script is scoped to `https://github.com/*`. It
13
+ looks for Clips links in GitHub issue/PR markdown and replaces them with a
14
+ playable preview iframe owned by the extension. That adds a narrow GitHub host
15
+ disclosure, but does not grant broad access to every page.
16
+
12
17
  ## How the overlay gets on the page
13
18
 
14
19
  When the user clicks the extension and starts a recording, the background service
@@ -24,6 +29,7 @@ Declared permissions: `activeTab`, `debugger`, `offscreen`, `scripting`,
24
29
  `storage`. Host permissions: only the configured Clips app + `forms.agent-native.com`
25
30
 
26
31
  - `localhost`/`127.0.0.1`.
32
+ - `https://github.com/*` is content-script scoped for link previews only.
27
33
 
28
34
  ## What this costs
29
35
 
@@ -2,8 +2,8 @@
2
2
  "manifest_version": 3,
3
3
  "name": "Agent-Native Clips",
4
4
  "short_name": "Clips",
5
- "version": "0.1.3",
6
- "description": "Start Clips recordings from Chrome with optional redacted console and network diagnostics.",
5
+ "version": "0.1.4",
6
+ "description": "Start Clips recordings from Chrome, capture optional diagnostics, and preview Clips links on GitHub.",
7
7
  "minimum_chrome_version": "116",
8
8
  "action": {
9
9
  "default_title": "Record with Clips",
@@ -25,10 +25,22 @@
25
25
  "http://localhost/*",
26
26
  "http://127.0.0.1/*"
27
27
  ],
28
+ "content_scripts": [
29
+ {
30
+ "matches": ["https://github.com/*"],
31
+ "js": ["assets/github-preview-content.js"],
32
+ "run_at": "document_idle",
33
+ "all_frames": false
34
+ }
35
+ ],
28
36
  "web_accessible_resources": [
29
37
  {
30
38
  "resources": ["src/overlay.html", "assets/*", "icons/*"],
31
39
  "matches": ["<all_urls>"]
40
+ },
41
+ {
42
+ "resources": ["src/github-preview.html"],
43
+ "matches": ["https://github.com/*"]
32
44
  }
33
45
  ],
34
46
  "externally_connectable": {
@@ -0,0 +1,233 @@
1
+ // GitHub Clips link previews. This is a narrow declarative content script for
2
+ // github.com only; the recording overlay remains activeTab-injected.
3
+ (function clipsGithubPreviewContent() {
4
+ const ROOT_CLASS = "clips-github-preview";
5
+ const PROCESSED_ATTR = "data-clips-github-preview";
6
+ const SOURCE_ATTR = "data-clips-source-url";
7
+ const EXTENSION_ORIGIN = chrome.runtime.getURL("").replace(/\/$/, "");
8
+ const CLIPS_HOSTS = new Set(["clips.agent-native.com"]);
9
+ const LOCAL_HOSTS = new Set(["localhost", "127.0.0.1"]);
10
+ const ROUTE_SEGMENTS = new Set(["r", "share", "embed"]);
11
+ const MIN_HEIGHT = 120;
12
+ const MAX_HEIGHT = 720;
13
+
14
+ type ClipsLink = {
15
+ sourceUrl: string;
16
+ embedUrl: string;
17
+ metadataUrl: string;
18
+ clipId: string;
19
+ host: string;
20
+ };
21
+
22
+ let scanTimer: ReturnType<typeof setTimeout> | undefined;
23
+ let frameCounter = 0;
24
+
25
+ function isAllowedHost(url: URL): boolean {
26
+ if (CLIPS_HOSTS.has(url.hostname)) return true;
27
+ if (LOCAL_HOSTS.has(url.hostname)) return true;
28
+ return false;
29
+ }
30
+
31
+ function safeTimeParam(value: string | null): string | null {
32
+ const trimmed = value?.trim();
33
+ if (!trimmed || trimmed.length > 32) return null;
34
+ if (/^\d{1,6}(\.\d{1,3})?$/.test(trimmed)) return trimmed;
35
+ if (/^\d{1,2}:\d{1,2}(:\d{1,2})?$/.test(trimmed)) return trimmed;
36
+ if (/^(?:\d{1,4}h)?(?:\d{1,4}m)?(?:\d{1,4}s)?$/i.test(trimmed)) {
37
+ return /\d/.test(trimmed) ? trimmed : null;
38
+ }
39
+ return null;
40
+ }
41
+
42
+ function parseClipsLink(raw: string): ClipsLink | null {
43
+ let parsed: URL;
44
+ try {
45
+ parsed = new URL(raw);
46
+ } catch {
47
+ return null;
48
+ }
49
+ if (parsed.protocol !== "https:" && parsed.protocol !== "http:") {
50
+ return null;
51
+ }
52
+ if (!isAllowedHost(parsed)) return null;
53
+
54
+ const segments = parsed.pathname.split("/").filter(Boolean);
55
+ const routeIndex = segments.findIndex((segment) =>
56
+ ROUTE_SEGMENTS.has(segment),
57
+ );
58
+ if (routeIndex === -1) return null;
59
+
60
+ const route = segments[routeIndex];
61
+ const clipId = segments[routeIndex + 1];
62
+ if (!clipId || clipId === "meeting" || clipId.length > 240) return null;
63
+ if (route === "share" && clipId === "meeting") return null;
64
+
65
+ let decodedClipId = clipId;
66
+ try {
67
+ decodedClipId = decodeURIComponent(clipId);
68
+ } catch {
69
+ // Keep the raw segment; malformed escaping should not break the scanner.
70
+ }
71
+
72
+ const baseSegments = segments.slice(0, routeIndex);
73
+ const sourceUrl = new URL(parsed.pathname, parsed.origin);
74
+ const embedPath = [
75
+ "",
76
+ ...baseSegments,
77
+ "embed",
78
+ encodeURIComponent(decodedClipId),
79
+ ].join("/");
80
+ const embedUrl = new URL(embedPath, parsed.origin);
81
+ const metadataUrl = new URL(
82
+ ["", ...baseSegments, "api", "public-recording"].join("/"),
83
+ parsed.origin,
84
+ );
85
+ metadataUrl.searchParams.set("id", decodedClipId);
86
+
87
+ const time = safeTimeParam(parsed.searchParams.get("t"));
88
+ if (time) {
89
+ sourceUrl.searchParams.set("t", time);
90
+ embedUrl.searchParams.set("t", time);
91
+ }
92
+
93
+ return {
94
+ sourceUrl: sourceUrl.toString(),
95
+ embedUrl: embedUrl.toString(),
96
+ metadataUrl: metadataUrl.toString(),
97
+ clipId,
98
+ host: parsed.host,
99
+ };
100
+ }
101
+
102
+ function isGitHubMarkdownAnchor(anchor: HTMLAnchorElement): boolean {
103
+ if (anchor.closest(`.${ROOT_CLASS}`)) return false;
104
+ return Boolean(
105
+ anchor.closest(
106
+ [
107
+ ".markdown-body",
108
+ ".comment-body",
109
+ ".js-comment-body",
110
+ ".timeline-comment",
111
+ ".js-issue-body",
112
+ ".js-pull-request-body",
113
+ ].join(","),
114
+ ),
115
+ );
116
+ }
117
+
118
+ function previewInsertionTarget(anchor: HTMLAnchorElement): HTMLElement {
119
+ const block = anchor.closest(
120
+ [
121
+ "p",
122
+ "li",
123
+ "td",
124
+ "blockquote",
125
+ "pre",
126
+ ".markdown-body > div",
127
+ ".comment-body",
128
+ ].join(","),
129
+ );
130
+ return block instanceof HTMLElement ? block : anchor;
131
+ }
132
+
133
+ function existingPreview(sourceUrl: string): HTMLElement | null {
134
+ for (const element of document.querySelectorAll<HTMLElement>(
135
+ `.${ROOT_CLASS}`,
136
+ )) {
137
+ if (element.getAttribute(SOURCE_ATTR) === sourceUrl) return element;
138
+ }
139
+ return null;
140
+ }
141
+
142
+ function mountPreview(anchor: HTMLAnchorElement, link: ClipsLink): void {
143
+ if (existingPreview(link.sourceUrl)) {
144
+ anchor.setAttribute(PROCESSED_ATTR, "mounted");
145
+ return;
146
+ }
147
+
148
+ const frameId = `clips-github-preview-${++frameCounter}`;
149
+ const wrapper = document.createElement("div");
150
+ wrapper.className = ROOT_CLASS;
151
+ wrapper.id = `${frameId}-wrapper`;
152
+ wrapper.setAttribute(SOURCE_ATTR, link.sourceUrl);
153
+ Object.assign(wrapper.style, {
154
+ display: "block",
155
+ width: "min(720px, 100%)",
156
+ maxWidth: "100%",
157
+ margin: "8px 0 16px",
158
+ clear: "both",
159
+ });
160
+
161
+ const frame = document.createElement("iframe");
162
+ frame.id = frameId;
163
+ frame.title = "Clips preview";
164
+ frame.allow = "autoplay; fullscreen; picture-in-picture";
165
+ frame.setAttribute("allowfullscreen", "true");
166
+ frame.setAttribute("scrolling", "no");
167
+ frame.referrerPolicy = "no-referrer";
168
+ Object.assign(frame.style, {
169
+ display: "block",
170
+ width: "100%",
171
+ height: "430px",
172
+ border: "0",
173
+ borderRadius: "8px",
174
+ background: "transparent",
175
+ overflow: "hidden",
176
+ });
177
+
178
+ const previewUrl = new URL(
179
+ chrome.runtime.getURL("src/github-preview.html"),
180
+ );
181
+ previewUrl.searchParams.set("frameId", frameId);
182
+ previewUrl.searchParams.set("embedUrl", link.embedUrl);
183
+ previewUrl.searchParams.set("metadataUrl", link.metadataUrl);
184
+ previewUrl.searchParams.set("sourceUrl", link.sourceUrl);
185
+ previewUrl.searchParams.set("clipId", link.clipId);
186
+ previewUrl.searchParams.set("host", link.host);
187
+ frame.src = previewUrl.toString();
188
+
189
+ wrapper.appendChild(frame);
190
+ previewInsertionTarget(anchor).after(wrapper);
191
+ anchor.setAttribute(PROCESSED_ATTR, "mounted");
192
+ }
193
+
194
+ function scan(): void {
195
+ for (const anchor of document.querySelectorAll<HTMLAnchorElement>(
196
+ `a[href]:not([${PROCESSED_ATTR}])`,
197
+ )) {
198
+ if (!isGitHubMarkdownAnchor(anchor)) continue;
199
+ const link = parseClipsLink(anchor.href);
200
+ if (!link) continue;
201
+ mountPreview(anchor, link);
202
+ }
203
+ }
204
+
205
+ function scheduleScan(): void {
206
+ clearTimeout(scanTimer);
207
+ scanTimer = setTimeout(scan, 120);
208
+ }
209
+
210
+ window.addEventListener("message", (event) => {
211
+ if (event.origin !== EXTENSION_ORIGIN) return;
212
+ const data = event.data as
213
+ | { source?: string; frameId?: string; height?: number }
214
+ | undefined;
215
+ if (data?.source !== "clips-github-preview") return;
216
+ if (!data.frameId || typeof data.height !== "number") return;
217
+ const frame = document.getElementById(data.frameId);
218
+ if (!(frame instanceof HTMLIFrameElement)) return;
219
+ const height = Math.max(
220
+ MIN_HEIGHT,
221
+ Math.min(MAX_HEIGHT, Math.ceil(data.height)),
222
+ );
223
+ frame.style.height = `${height}px`;
224
+ });
225
+
226
+ const observer = new MutationObserver(scheduleScan);
227
+ observer.observe(document.documentElement, {
228
+ childList: true,
229
+ subtree: true,
230
+ });
231
+
232
+ scan();
233
+ })();
@@ -0,0 +1,12 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>Clips preview</title>
7
+ <script type="module" src="./github-preview.ts"></script>
8
+ </head>
9
+ <body>
10
+ <main id="root"></main>
11
+ </body>
12
+ </html>