instruckt 0.4.27 → 0.4.29

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
@@ -10,15 +10,24 @@ Framework-agnostic JS core with adapters for Livewire, Vue, Svelte, and React.
10
10
  npm install instruckt
11
11
  ```
12
12
 
13
- Or load via CDN:
13
+ ## Quick Start
14
14
 
15
- ```html
16
- <script src="https://cdn.jsdelivr.net/npm/instruckt/dist/instruckt.iife.js"></script>
17
- ```
15
+ > **Pick your framework below** -- each section is self-contained.
16
+
17
+ | Framework | Setup |
18
+ |-----------|-------|
19
+ | [SPA (Vue, React, Svelte)](#spa-vue-react-svelte-with-vite) | Vite plugin only |
20
+ | [SvelteKit](#sveltekit) | Vite plugin + virtual import |
21
+ | [Nuxt](#nuxt) | Vite plugin + virtual import |
22
+ | [Next.js](#nextjs) | Client component |
23
+ | [Laravel](#laravel) | Composer package |
24
+ | [Astro](#astro) | Community integration |
25
+
26
+ ---
18
27
 
19
- ## Quick Start: Vite Plugin
28
+ ### SPA (Vue, React, Svelte with Vite)
20
29
 
21
- The easiest way to use instruckt is with the Vite plugin. It handles client injection and provides a built-in dev API server no backend required.
30
+ Add the Vite plugin it handles client injection and provides a built-in dev API server. No backend required.
22
31
 
23
32
  ```js
24
33
  // vite.config.ts
@@ -29,171 +38,59 @@ export default defineConfig({
29
38
  })
30
39
  ```
31
40
 
32
- That's it for SPA apps (Vue, React, Svelte with Vite). The plugin auto-injects the client via `transformIndexHtml`.
33
-
34
- ### SSR Frameworks (SvelteKit, Nuxt, etc.)
35
-
36
- For frameworks that don't use `index.html`, import the virtual module in your layout:
41
+ That's it. The plugin auto-injects the client via `transformIndexHtml`.
37
42
 
38
- ```js
39
- // SvelteKit: src/routes/+layout.svelte
40
- import 'virtual:instruckt'
43
+ ---
41
44
 
42
- // Nuxt: plugins/instruckt.client.ts
43
- import 'virtual:instruckt'
44
- ```
45
+ ### SvelteKit
45
46
 
46
- The virtual module is SSR-safe it only initializes in the browser.
47
-
48
- ### Laravel
49
-
50
- The Vite plugin works alongside the Laravel package. Set `server: false` so Laravel handles the backend:
47
+ Two steps add the Vite plugin, then import the virtual module in your layout:
51
48
 
52
49
  ```js
53
50
  // vite.config.ts
54
- import laravel from 'laravel-vite-plugin'
55
51
  import instruckt from 'instruckt/vite'
56
52
 
57
53
  export default defineConfig({
58
- plugins: [
59
- laravel({ input: ['resources/js/app.js'] }),
60
- instruckt({
61
- server: false,
62
- adapters: ['livewire', 'blade'],
63
- mcp: true,
64
- }),
65
- ],
66
- })
67
- ```
68
-
69
- Then in your app entry:
70
-
71
- ```js
72
- // resources/js/app.js
73
- import 'virtual:instruckt'
74
- ```
75
-
76
- ### Vite Plugin Options
77
-
78
- ```js
79
- instruckt({
80
- // Framework adapters to activate (default: auto-detect)
81
- adapters: ['svelte'],
82
-
83
- // Theme: 'light' | 'dark' | 'auto' (default: 'auto')
84
- theme: 'auto',
85
-
86
- // Toolbar position (default: 'bottom-right')
87
- position: 'bottom-right',
88
-
89
- // Customize marker pin colors
90
- colors: { default: '#6366f1', screenshot: '#22c55e', dismissed: '#71717a' },
91
-
92
- // Customize keyboard shortcuts
93
- keys: { annotate: 'a', freeze: 'f', screenshot: 'c', clearPage: 'x' },
94
-
95
- // Storage directory for annotations + screenshots (default: '.instruckt')
96
- dir: '.instruckt',
97
-
98
- // API endpoint prefix (default: '/instruckt')
99
- endpoint: '/instruckt',
100
-
101
- // Enable built-in dev API server (default: true)
102
- // Set to false when your framework provides its own backend (e.g. Laravel)
103
- server: true,
104
-
105
- // Show MCP tool instructions in clipboard markdown (default: false)
106
- // Set to true when using with a backend that registers MCP tools
107
- mcp: false,
54
+ plugins: [sveltekit(), instruckt()],
108
55
  })
109
56
  ```
110
57
 
111
- ## Manual Setup
112
-
113
- If you're not using Vite, you can initialize instruckt directly:
114
-
115
- ```js
116
- import { Instruckt } from 'instruckt'
117
-
118
- const instruckt = new Instruckt({
119
- endpoint: '/instruckt',
120
- })
121
- ```
122
-
123
- Or with the IIFE build:
124
-
125
- ```html
126
- <script src="/path/to/instruckt.iife.js"></script>
58
+ ```svelte
59
+ <!-- src/routes/+layout.svelte -->
127
60
  <script>
128
- Instruckt.init({ endpoint: '/instruckt' })
61
+ import 'virtual:instruckt'
129
62
  </script>
130
63
  ```
131
64
 
132
- ### Framework-Specific Manual Setup
65
+ The virtual module is SSR-safe it only initializes in the browser.
133
66
 
134
- instruckt is a browser-only library. In SSR frameworks without the Vite plugin, make sure it only loads on the client.
67
+ ---
135
68
 
136
- <details>
137
- <summary>SvelteKit</summary>
69
+ ### Nuxt
138
70
 
139
- ```svelte
140
- <!-- src/lib/InstrucktProvider.svelte -->
141
- <script>
142
- import { onMount } from 'svelte';
71
+ Same idea — add the Vite plugin, then import the virtual module in a client plugin:
143
72
 
144
- onMount(async () => {
145
- const { Instruckt } = await import('instruckt');
146
- const instruckt = new Instruckt({
147
- endpoint: '/api/annotations',
148
- adapters: ['svelte'],
149
- });
73
+ ```js
74
+ // nuxt.config.ts add the Vite plugin
75
+ import instruckt from 'instruckt/vite'
150
76
 
151
- return () => instruckt.destroy();
152
- });
153
- </script>
77
+ export default defineNuxtConfig({
78
+ vite: {
79
+ plugins: [instruckt()],
80
+ },
81
+ })
154
82
  ```
155
83
 
156
- ```svelte
157
- <!-- src/routes/+layout.svelte -->
158
- <script>
159
- import { browser } from '$app/environment';
160
-
161
- let { children } = $props();
162
- </script>
163
-
164
- {#if browser}
165
- {#await import('$lib/InstrucktProvider.svelte') then { default: InstrucktProvider }}
166
- <InstrucktProvider />
167
- {/await}
168
- {/if}
169
-
170
- {@render children()}
84
+ ```ts
85
+ // plugins/instruckt.client.ts
86
+ import 'virtual:instruckt'
171
87
  ```
172
88
 
173
- </details>
89
+ ---
174
90
 
175
- <details>
176
- <summary>Nuxt</summary>
91
+ ### Next.js
177
92
 
178
- ```vue
179
- <!-- plugins/instruckt.client.ts -->
180
- <script>
181
- // The .client.ts suffix ensures Nuxt only runs this in the browser
182
- export default defineNuxtPlugin(async () => {
183
- const { Instruckt } = await import('instruckt')
184
-
185
- const instruckt = new Instruckt({
186
- endpoint: '/api/annotations',
187
- adapters: ['vue'],
188
- })
189
- })
190
- </script>
191
- ```
192
-
193
- </details>
194
-
195
- <details>
196
- <summary>Next.js (App Router)</summary>
93
+ Next.js doesn't use Vite, so initialize instruckt directly in a client component:
197
94
 
198
95
  ```tsx
199
96
  // components/InstrucktProvider.tsx
@@ -235,12 +132,78 @@ export default function RootLayout({ children }) {
235
132
  }
236
133
  ```
237
134
 
238
- </details>
135
+ ---
136
+
137
+ ### Laravel
138
+
139
+ Use the **[instruckt-laravel](https://github.com/joshcirre/instruckt-laravel)** package — it provides the backend API, MCP tools, JSON file storage, and handles install/uninstall automatically:
140
+
141
+ ```bash
142
+ composer require joshcirre/instruckt-laravel --dev
143
+ php artisan instruckt:install
144
+ ```
145
+
146
+ The install command adds the Vite plugin to your `vite.config.js` with `server: false` (Laravel owns the backend), configures MCP for your AI agent, and adds the virtual import to your JS entry point.
147
+
148
+ ```js
149
+ // vite.config.js (added automatically by install command)
150
+ import instruckt from 'instruckt/vite'
151
+
152
+ export default defineConfig({
153
+ plugins: [
154
+ laravel({ input: ['resources/js/app.js'] }),
155
+ instruckt({
156
+ server: false,
157
+ adapters: ['livewire', 'blade'],
158
+ mcp: true,
159
+ }),
160
+ ],
161
+ })
162
+ ```
163
+
164
+ ---
239
165
 
240
166
  ### Astro
241
167
 
242
168
  See **[instruckt-astro](https://github.com/sgasser/instruckt-astro)** for a community-maintained Astro integration.
243
169
 
170
+ ---
171
+
172
+ ## Vite Plugin Options
173
+
174
+ ```js
175
+ instruckt({
176
+ // Framework adapters to activate (default: auto-detect)
177
+ adapters: ['svelte'],
178
+
179
+ // Theme: 'light' | 'dark' | 'auto' (default: 'auto')
180
+ theme: 'auto',
181
+
182
+ // Toolbar position (default: 'bottom-right')
183
+ position: 'bottom-right',
184
+
185
+ // Customize marker pin colors
186
+ colors: { default: '#6366f1', screenshot: '#22c55e', dismissed: '#71717a' },
187
+
188
+ // Customize keyboard shortcuts
189
+ keys: { annotate: 'a', freeze: 'f', screenshot: 'c', clearPage: 'x' },
190
+
191
+ // Storage directory for annotations + screenshots (default: '.instruckt')
192
+ dir: '.instruckt',
193
+
194
+ // API endpoint prefix (default: '/instruckt')
195
+ endpoint: '/instruckt',
196
+
197
+ // Enable built-in dev API server (default: true)
198
+ // Set to false when your framework provides its own backend (e.g. Laravel)
199
+ server: true,
200
+
201
+ // Show MCP tool instructions in clipboard markdown (default: false)
202
+ // Set to true when using with a backend that registers MCP tools
203
+ mcp: false,
204
+ })
205
+ ```
206
+
244
207
  ## How It Works
245
208
 
246
209
  1. A floating toolbar appears in your app
@@ -355,7 +318,7 @@ The Vite plugin includes a dev API server that saves annotations and screenshots
355
318
 
356
319
  ### Laravel
357
320
 
358
- **[instruckt-laravel](https://github.com/joshcirre/instruckt-laravel)** — Laravel package with JSON file storage, MCP tools, Blade component, and API routes.
321
+ **[instruckt-laravel](https://github.com/joshcirre/instruckt-laravel)** — Laravel package with JSON file storage, MCP tools, Blade component, and API routes. Includes `artisan instruckt:install` which auto-configures the Vite plugin, MCP, and agent skills.
359
322
 
360
323
  ### Custom Backend
361
324
 
@@ -549,7 +549,7 @@ var ICONS = {
549
549
  logo: `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 20h9"/><path d="M16.5 3.5a2.121 2.121 0 0 1 3 3L7 19l-4 1 1-4 12.5-12.5z"/></svg>`
550
550
  };
551
551
  var Toolbar = class {
552
- constructor(position, callbacks, keys) {
552
+ constructor(position, callbacks, keys, tools) {
553
553
  this.position = position;
554
554
  this.callbacks = callbacks;
555
555
  this.fabBadge = null;
@@ -560,9 +560,15 @@ var Toolbar = class {
560
560
  this.dragging = false;
561
561
  this.dragOffset = { x: 0, y: 0 };
562
562
  this.keys = keys != null ? keys : {};
563
+ this.tools = tools != null ? tools : {};
563
564
  this.build();
564
565
  this.setupDrag();
565
566
  }
567
+ /** Whether a built-in tool should be shown (default true if not specified). */
568
+ show(id) {
569
+ const v = this.tools[id];
570
+ return v !== false;
571
+ }
566
572
  build() {
567
573
  var _a2, _b, _c, _d, _e;
568
574
  this.host = document.createElement("div");
@@ -621,17 +627,18 @@ var Toolbar = class {
621
627
  d.className = "divider";
622
628
  return d;
623
629
  };
624
- this.toolbarEl.append(
625
- this.annotateBtn,
626
- screenshotBtn,
627
- mkDiv(),
628
- this.freezeBtn,
629
- mkDiv(),
630
- this.copyBtn,
631
- clearWrap,
632
- mkDiv(),
633
- minimizeBtn
634
- );
630
+ const toAppend = [];
631
+ const add = (el) => {
632
+ if (toAppend.length > 0) toAppend.push(mkDiv());
633
+ toAppend.push(el);
634
+ };
635
+ if (this.show("annotate")) add(this.annotateBtn);
636
+ if (this.show("screenshot")) add(screenshotBtn);
637
+ if (this.show("freeze")) add(this.freezeBtn);
638
+ if (this.show("copy")) add(this.copyBtn);
639
+ if (this.show("clear_page") || this.show("clear_all")) add(clearWrap);
640
+ if (this.show("minimize")) add(minimizeBtn);
641
+ this.toolbarEl.append(...toAppend);
635
642
  this.shadow.appendChild(this.toolbarEl);
636
643
  this.fab = document.createElement("button");
637
644
  this.fab.className = "fab";
@@ -2944,6 +2951,9 @@ function getPageBoundingBox(el) {
2944
2951
  };
2945
2952
  }
2946
2953
 
2954
+ // src/instruckt.ts
2955
+ var import_element_source = require("element-source");
2956
+
2947
2957
  // src/adapters/livewire.ts
2948
2958
  function isAvailable() {
2949
2959
  return typeof window.Livewire !== "undefined";
@@ -3243,7 +3253,7 @@ var _Instruckt = class _Instruckt {
3243
3253
  e.stopImmediatePropagation();
3244
3254
  };
3245
3255
  this.boundClick = (e) => {
3246
- var _a2, _b, _c;
3256
+ var _a2, _b;
3247
3257
  const target = e.target;
3248
3258
  if (this.isInstruckt(target)) return;
3249
3259
  e.preventDefault();
@@ -3256,23 +3266,24 @@ var _Instruckt = class _Instruckt {
3256
3266
  const cssClasses = getCssClasses(target);
3257
3267
  const nearbyText = getNearbyText(target) || void 0;
3258
3268
  const boundingBox = getPageBoundingBox(target);
3259
- const framework = (_b = this.detectFramework(target)) != null ? _b : void 0;
3260
- (_c = this.highlight) == null ? void 0 : _c.show(target);
3269
+ (_b = this.highlight) == null ? void 0 : _b.show(target);
3261
3270
  this.highlightLocked = true;
3262
- const pending = {
3263
- element: target,
3264
- elementPath,
3265
- elementName,
3266
- elementLabel,
3267
- cssClasses,
3268
- boundingBox,
3269
- x: e.clientX,
3270
- y: e.clientY,
3271
- selectedText,
3272
- nearbyText,
3273
- framework
3274
- };
3275
- this.showAnnotationPopup(pending);
3271
+ this.detectFramework(target).then((framework) => {
3272
+ const pending = {
3273
+ element: target,
3274
+ elementPath,
3275
+ elementName,
3276
+ elementLabel,
3277
+ cssClasses,
3278
+ boundingBox,
3279
+ x: e.clientX,
3280
+ y: e.clientY,
3281
+ selectedText,
3282
+ nearbyText,
3283
+ framework: framework != null ? framework : void 0
3284
+ };
3285
+ this.showAnnotationPopup(pending);
3286
+ });
3276
3287
  };
3277
3288
  this.config = __spreadValues({
3278
3289
  adapters: ["livewire", "vue", "svelte", "react", "blade"],
@@ -3305,7 +3316,7 @@ var _Instruckt = class _Instruckt {
3305
3316
  onClearPage: () => this.clearPage(),
3306
3317
  onClearAll: () => this.clearEverything(),
3307
3318
  onMinimize: (min) => this.onMinimize(min)
3308
- }, this.config.keys);
3319
+ }, this.config.keys, this.config.tools);
3309
3320
  this.highlight = new ElementHighlight();
3310
3321
  this.popup = new AnnotationPopup();
3311
3322
  this.markers = new AnnotationMarkers((annotation) => this.onMarkerClick(annotation));
@@ -3344,7 +3355,7 @@ var _Instruckt = class _Instruckt {
3344
3355
  this.isAnnotating = false;
3345
3356
  this.isFrozen = false;
3346
3357
  document.querySelectorAll("[data-instruckt]").forEach((el) => el.remove());
3347
- this.toolbar = new Toolbar(this.config.position, this.makeToolbarCallbacks());
3358
+ this.toolbar = new Toolbar(this.config.position, this.makeToolbarCallbacks(), this.config.keys, this.config.tools);
3348
3359
  if (wasMinimized) this.toolbar.minimize();
3349
3360
  this.markers = new AnnotationMarkers((annotation) => this.onMarkerClick(annotation));
3350
3361
  this.highlight = new ElementHighlight();
@@ -3678,7 +3689,7 @@ var _Instruckt = class _Instruckt {
3678
3689
  }
3679
3690
  // ── Region screenshot ────────────────────────────────────────
3680
3691
  async startRegionCapture() {
3681
- var _a2, _b;
3692
+ var _a2;
3682
3693
  const wasAnnotating = this.isAnnotating;
3683
3694
  if (wasAnnotating) this.setAnnotating(false);
3684
3695
  const rect = await selectRegion();
@@ -3694,6 +3705,7 @@ var _Instruckt = class _Instruckt {
3694
3705
  const centerX = rect.x + rect.width / 2;
3695
3706
  const centerY = rect.y + rect.height / 2;
3696
3707
  const target = (_a2 = document.elementFromPoint(centerX, centerY)) != null ? _a2 : document.body;
3708
+ const framework = await this.detectFramework(target);
3697
3709
  const pending = {
3698
3710
  element: target,
3699
3711
  elementPath: getElementSelector(target),
@@ -3705,18 +3717,60 @@ var _Instruckt = class _Instruckt {
3705
3717
  y: centerY,
3706
3718
  nearbyText: getNearbyText(target) || void 0,
3707
3719
  screenshot,
3708
- framework: (_b = this.detectFramework(target)) != null ? _b : void 0
3720
+ framework: framework != null ? framework : void 0
3709
3721
  };
3710
3722
  this.showAnnotationPopup(pending);
3711
3723
  }
3712
3724
  // ── Framework detection ───────────────────────────────────────
3713
- detectFramework(el) {
3714
- var _a2;
3725
+ /**
3726
+ * Use element-source as the primary resolver for React/Vue/Svelte,
3727
+ * then layer our adapters on top for props/state/framework-specific metadata.
3728
+ * Livewire and Blade are handled by our adapters only (element-source doesn't support them).
3729
+ */
3730
+ async detectFramework(el) {
3731
+ var _a2, _b, _c, _d, _e, _f, _g;
3715
3732
  const adapters = (_a2 = this.config.adapters) != null ? _a2 : [];
3716
3733
  if (adapters.includes("livewire")) {
3717
3734
  const ctx = getContext(el);
3718
3735
  if (ctx) return ctx;
3719
3736
  }
3737
+ const esAdapters = ["vue", "svelte", "react"];
3738
+ if (esAdapters.some((a) => adapters.includes(a))) {
3739
+ try {
3740
+ const info = await (0, import_element_source.resolveElementInfo)(el);
3741
+ if (info.source) {
3742
+ const stack = info.stack.map((f) => ({
3743
+ filePath: f.filePath,
3744
+ lineNumber: f.lineNumber,
3745
+ columnNumber: f.columnNumber,
3746
+ componentName: f.componentName
3747
+ }));
3748
+ let adapterCtx = null;
3749
+ if (adapters.includes("vue")) adapterCtx = getContext2(el);
3750
+ if (!adapterCtx && adapters.includes("svelte")) adapterCtx = getContext3(el);
3751
+ if (!adapterCtx && adapters.includes("react")) adapterCtx = getContext4(el);
3752
+ if (adapterCtx) {
3753
+ return __spreadProps(__spreadValues({}, adapterCtx), {
3754
+ component: (_b = info.componentName) != null ? _b : adapterCtx.component,
3755
+ source_file: info.source.filePath,
3756
+ source_line: (_c = info.source.lineNumber) != null ? _c : adapterCtx.source_line,
3757
+ source_column: (_d = info.source.columnNumber) != null ? _d : void 0,
3758
+ component_stack: stack.length > 0 ? stack : void 0
3759
+ });
3760
+ }
3761
+ return {
3762
+ framework: "react",
3763
+ // element-source defaults to React resolver
3764
+ component: (_e = info.componentName) != null ? _e : "Component",
3765
+ source_file: info.source.filePath,
3766
+ source_line: (_f = info.source.lineNumber) != null ? _f : void 0,
3767
+ source_column: (_g = info.source.columnNumber) != null ? _g : void 0,
3768
+ component_stack: stack.length > 0 ? stack : void 0
3769
+ };
3770
+ }
3771
+ } catch (e) {
3772
+ }
3773
+ }
3720
3774
  if (adapters.includes("vue")) {
3721
3775
  const ctx = getContext2(el);
3722
3776
  if (ctx) return ctx;
@@ -3908,17 +3962,33 @@ No open annotations.`;
3908
3962
  lines.push("");
3909
3963
  const hPrefix = multiPage ? "###" : "##";
3910
3964
  annotations.forEach((a, i) => {
3911
- var _a2, _b, _c, _d, _e;
3965
+ var _a2, _b, _c, _d, _e, _f;
3912
3966
  const componentSuffix = ((_a2 = a.framework) == null ? void 0 : _a2.component) ? ` in \`${a.framework.component}\`` : "";
3913
3967
  lines.push(`${hPrefix} ${i + 1}. ${a.comment}`);
3914
3968
  lines.push(`- ID: \`${a.id}\``);
3915
3969
  lines.push(`- Element: \`${a.element}\`${componentSuffix}`);
3916
3970
  if ((_b = a.framework) == null ? void 0 : _b.source_file) {
3917
- const loc = a.framework.source_line ? `${a.framework.source_file}:${a.framework.source_line}` : a.framework.source_file;
3971
+ let loc = a.framework.source_file;
3972
+ if (a.framework.source_line) {
3973
+ loc += `:${a.framework.source_line}`;
3974
+ if (a.framework.source_column) loc += `:${a.framework.source_column}`;
3975
+ }
3918
3976
  lines.push(`- Source: \`${loc}\``);
3919
3977
  } else if ((_d = (_c = a.framework) == null ? void 0 : _c.data) == null ? void 0 : _d.file) {
3920
3978
  lines.push(`- File: \`${a.framework.data.file}\``);
3921
3979
  }
3980
+ if (((_e = a.framework) == null ? void 0 : _e.component_stack) && a.framework.component_stack.length > 1) {
3981
+ lines.push(`- Component stack:`);
3982
+ for (const frame of a.framework.component_stack) {
3983
+ let frameLoc = frame.filePath;
3984
+ if (frame.lineNumber) {
3985
+ frameLoc += `:${frame.lineNumber}`;
3986
+ if (frame.columnNumber) frameLoc += `:${frame.columnNumber}`;
3987
+ }
3988
+ const name = frame.componentName ? `${frame.componentName} ` : "";
3989
+ lines.push(` - ${name}\`${frameLoc}\``);
3990
+ }
3991
+ }
3922
3992
  if (a.cssClasses) {
3923
3993
  lines.push(`- Classes: \`${a.cssClasses}\``);
3924
3994
  }
@@ -3929,7 +3999,7 @@ No open annotations.`;
3929
3999
  }
3930
4000
  if (a.screenshot) {
3931
4001
  if (!a.screenshot.startsWith("data:")) {
3932
- const screenshotPath = (_e = this.config.screenshotPath) != null ? _e : "storage/app/_instruckt/";
4002
+ const screenshotPath = (_f = this.config.screenshotPath) != null ? _f : "storage/app/_instruckt/";
3933
4003
  lines.push(`- Screenshot: \`${screenshotPath}${a.screenshot}\``);
3934
4004
  } else {
3935
4005
  lines.push(`- Screenshot: ![Screenshot](${a.screenshot})`);