instruckt 0.4.28 → 0.4.30

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
@@ -12,9 +12,22 @@ npm install instruckt
12
12
 
13
13
  ## Quick Start
14
14
 
15
- ### Vite Plugin
15
+ > **Pick your framework below** -- each section is self-contained.
16
16
 
17
- 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.
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
+ ---
27
+
28
+ ### SPA (Vue, React, Svelte with Vite)
29
+
30
+ Add the Vite plugin — it handles client injection and provides a built-in dev API server. No backend required.
18
31
 
19
32
  ```js
20
33
  // vite.config.ts
@@ -25,165 +38,59 @@ export default defineConfig({
25
38
  })
26
39
  ```
27
40
 
28
- That's it for SPA apps (Vue, React, Svelte with Vite). The plugin auto-injects the client via `transformIndexHtml`.
41
+ That's it. The plugin auto-injects the client via `transformIndexHtml`.
29
42
 
30
- ### Laravel
43
+ ---
31
44
 
32
- 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:
45
+ ### SvelteKit
33
46
 
34
- ```bash
35
- composer require joshcirre/instruckt-laravel --dev
36
- php artisan instruckt:install
37
- ```
38
-
39
- 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.
47
+ Two steps — add the Vite plugin, then import the virtual module in your layout:
40
48
 
41
49
  ```js
42
- // vite.config.js (added automatically by install command)
50
+ // vite.config.ts
43
51
  import instruckt from 'instruckt/vite'
44
52
 
45
53
  export default defineConfig({
46
- plugins: [
47
- laravel({ input: ['resources/js/app.js'] }),
48
- instruckt({
49
- server: false,
50
- adapters: ['livewire', 'blade'],
51
- mcp: true,
52
- }),
53
- ],
54
+ plugins: [sveltekit(), instruckt()],
54
55
  })
55
56
  ```
56
57
 
57
- ### SSR Frameworks (SvelteKit, Nuxt, etc.)
58
-
59
- For frameworks that don't use `index.html`, import the virtual module in your layout:
60
-
61
- ```js
62
- // SvelteKit: src/routes/+layout.svelte
63
- import 'virtual:instruckt'
64
-
65
- // Nuxt: plugins/instruckt.client.ts
66
- import 'virtual:instruckt'
58
+ ```svelte
59
+ <!-- src/routes/+layout.svelte -->
60
+ <script>
61
+ import 'virtual:instruckt'
62
+ </script>
67
63
  ```
68
64
 
69
65
  The virtual module is SSR-safe — it only initializes in the browser.
70
66
 
71
- ### Astro
72
-
73
- See **[instruckt-astro](https://github.com/sgasser/instruckt-astro)** for a community-maintained Astro integration.
74
-
75
- ## Vite Plugin Options
76
-
77
- ```js
78
- instruckt({
79
- // Framework adapters to activate (default: auto-detect)
80
- adapters: ['svelte'],
81
-
82
- // Theme: 'light' | 'dark' | 'auto' (default: 'auto')
83
- theme: 'auto',
67
+ ---
84
68
 
85
- // Toolbar position (default: 'bottom-right')
86
- position: 'bottom-right',
69
+ ### Nuxt
87
70
 
88
- // Customize marker pin colors
89
- colors: { default: '#6366f1', screenshot: '#22c55e', dismissed: '#71717a' },
90
-
91
- // Customize keyboard shortcuts
92
- keys: { annotate: 'a', freeze: 'f', screenshot: 'c', clearPage: 'x' },
93
-
94
- // Storage directory for annotations + screenshots (default: '.instruckt')
95
- dir: '.instruckt',
96
-
97
- // API endpoint prefix (default: '/instruckt')
98
- endpoint: '/instruckt',
99
-
100
- // Enable built-in dev API server (default: true)
101
- // Set to false when your framework provides its own backend (e.g. Laravel)
102
- server: true,
103
-
104
- // Show MCP tool instructions in clipboard markdown (default: false)
105
- // Set to true when using with a backend that registers MCP tools
106
- mcp: false,
107
- })
108
- ```
109
-
110
- ## Manual Setup
111
-
112
- If you're not using Vite, you can initialize instruckt directly:
71
+ Same idea add the Vite plugin, then import the virtual module in a client plugin:
113
72
 
114
73
  ```js
115
- import { Instruckt } from 'instruckt'
74
+ // nuxt.config.ts add the Vite plugin
75
+ import instruckt from 'instruckt/vite'
116
76
 
117
- const instruckt = new Instruckt({
118
- endpoint: '/instruckt',
77
+ export default defineNuxtConfig({
78
+ vite: {
79
+ plugins: [instruckt()],
80
+ },
119
81
  })
120
82
  ```
121
83
 
122
- ### Framework-Specific Manual Setup
123
-
124
- instruckt is a browser-only library. In SSR frameworks without the Vite plugin, make sure it only loads on the client.
125
-
126
- <details>
127
- <summary>SvelteKit</summary>
128
-
129
- ```svelte
130
- <!-- src/lib/InstrucktProvider.svelte -->
131
- <script>
132
- import { onMount } from 'svelte';
133
-
134
- onMount(async () => {
135
- const { Instruckt } = await import('instruckt');
136
- const instruckt = new Instruckt({
137
- endpoint: '/api/annotations',
138
- adapters: ['svelte'],
139
- });
140
-
141
- return () => instruckt.destroy();
142
- });
143
- </script>
144
- ```
145
-
146
- ```svelte
147
- <!-- src/routes/+layout.svelte -->
148
- <script>
149
- import { browser } from '$app/environment';
150
-
151
- let { children } = $props();
152
- </script>
153
-
154
- {#if browser}
155
- {#await import('$lib/InstrucktProvider.svelte') then { default: InstrucktProvider }}
156
- <InstrucktProvider />
157
- {/await}
158
- {/if}
159
-
160
- {@render children()}
84
+ ```ts
85
+ // plugins/instruckt.client.ts
86
+ import 'virtual:instruckt'
161
87
  ```
162
88
 
163
- </details>
89
+ ---
164
90
 
165
- <details>
166
- <summary>Nuxt</summary>
91
+ ### Next.js
167
92
 
168
- ```vue
169
- <!-- plugins/instruckt.client.ts -->
170
- <script>
171
- // The .client.ts suffix ensures Nuxt only runs this in the browser
172
- export default defineNuxtPlugin(async () => {
173
- const { Instruckt } = await import('instruckt')
174
-
175
- const instruckt = new Instruckt({
176
- endpoint: '/api/annotations',
177
- adapters: ['vue'],
178
- })
179
- })
180
- </script>
181
- ```
182
-
183
- </details>
184
-
185
- <details>
186
- <summary>Next.js (App Router)</summary>
93
+ Next.js doesn't use Vite, so initialize instruckt directly in a client component:
187
94
 
188
95
  ```tsx
189
96
  // components/InstrucktProvider.tsx
@@ -225,7 +132,77 @@ export default function RootLayout({ children }) {
225
132
  }
226
133
  ```
227
134
 
228
- </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
+ ---
165
+
166
+ ### Astro
167
+
168
+ See **[instruckt-astro](https://github.com/sgasser/instruckt-astro)** for a community-maintained Astro integration.
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
+ ```
229
206
 
230
207
  ## How It Works
231
208
 
@@ -242,16 +219,22 @@ export default function RootLayout({ children }) {
242
219
  ### Example Output
243
220
 
244
221
  ```markdown
245
- # UI Feedback: /auth/login
222
+ # UI Feedback: /dashboard
246
223
 
247
224
  ## 1. Change the submit button color to green
248
- - Element: `button.btn-primary` in `pages::auth.login`
225
+ - Element: `button.btn-primary` in `LoginForm`
226
+ - Source: `src/components/LoginForm.tsx:42:5`
227
+ - Component stack:
228
+ - LoginForm `src/components/LoginForm.tsx:42:5`
229
+ - AuthPage `src/pages/AuthPage.tsx:18:3`
230
+ - App `src/App.tsx:8:7`
249
231
  - Classes: `btn btn-primary`
250
232
  - Text: "Submit Login"
251
233
  - Screenshot: `.instruckt/screenshots/01JWXYZ.png`
252
234
 
253
235
  ## 2. Make the login card have rounded corners
254
- - Element: `div.bg-white` in `pages::auth.login`
236
+ - Element: `div.bg-white` in `LoginCard`
237
+ - Source: `src/components/LoginCard.tsx:15:3`
255
238
  - Classes: `bg-white dark:bg-white/10 border`
256
239
  ```
257
240
 
@@ -309,7 +292,7 @@ Default shortcuts (customizable via `keys` config):
309
292
 
310
293
  ## Features
311
294
 
312
- - **Framework detection** — automatically identifies Livewire, Vue, Svelte, and React components
295
+ - **Framework detection** — automatically identifies Livewire, Vue, Svelte, and React components with full component stacks and precise source locations (file:line:column) via [element-source](https://github.com/aidenybai/element-source)
313
296
  - **Screenshots** — capture element or region screenshots; uses DOM-to-image on standard apps, automatically falls back to Screen Capture API on shadow DOM frameworks (Flux UI, etc.)
314
297
  - **Shadow DOM isolation** — all UI renders in shadow roots so it never conflicts with your styles
315
298
  - **Copy as markdown** — annotations auto-copy as structured markdown optimized for AI agents
@@ -2951,6 +2951,9 @@ function getPageBoundingBox(el) {
2951
2951
  };
2952
2952
  }
2953
2953
 
2954
+ // src/instruckt.ts
2955
+ var import_element_source = require("element-source");
2956
+
2954
2957
  // src/adapters/livewire.ts
2955
2958
  function isAvailable() {
2956
2959
  return typeof window.Livewire !== "undefined";
@@ -3250,7 +3253,7 @@ var _Instruckt = class _Instruckt {
3250
3253
  e.stopImmediatePropagation();
3251
3254
  };
3252
3255
  this.boundClick = (e) => {
3253
- var _a2, _b, _c;
3256
+ var _a2, _b;
3254
3257
  const target = e.target;
3255
3258
  if (this.isInstruckt(target)) return;
3256
3259
  e.preventDefault();
@@ -3263,23 +3266,24 @@ var _Instruckt = class _Instruckt {
3263
3266
  const cssClasses = getCssClasses(target);
3264
3267
  const nearbyText = getNearbyText(target) || void 0;
3265
3268
  const boundingBox = getPageBoundingBox(target);
3266
- const framework = (_b = this.detectFramework(target)) != null ? _b : void 0;
3267
- (_c = this.highlight) == null ? void 0 : _c.show(target);
3269
+ (_b = this.highlight) == null ? void 0 : _b.show(target);
3268
3270
  this.highlightLocked = true;
3269
- const pending = {
3270
- element: target,
3271
- elementPath,
3272
- elementName,
3273
- elementLabel,
3274
- cssClasses,
3275
- boundingBox,
3276
- x: e.clientX,
3277
- y: e.clientY,
3278
- selectedText,
3279
- nearbyText,
3280
- framework
3281
- };
3282
- 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
+ });
3283
3287
  };
3284
3288
  this.config = __spreadValues({
3285
3289
  adapters: ["livewire", "vue", "svelte", "react", "blade"],
@@ -3685,7 +3689,7 @@ var _Instruckt = class _Instruckt {
3685
3689
  }
3686
3690
  // ── Region screenshot ────────────────────────────────────────
3687
3691
  async startRegionCapture() {
3688
- var _a2, _b;
3692
+ var _a2;
3689
3693
  const wasAnnotating = this.isAnnotating;
3690
3694
  if (wasAnnotating) this.setAnnotating(false);
3691
3695
  const rect = await selectRegion();
@@ -3701,6 +3705,7 @@ var _Instruckt = class _Instruckt {
3701
3705
  const centerX = rect.x + rect.width / 2;
3702
3706
  const centerY = rect.y + rect.height / 2;
3703
3707
  const target = (_a2 = document.elementFromPoint(centerX, centerY)) != null ? _a2 : document.body;
3708
+ const framework = await this.detectFramework(target);
3704
3709
  const pending = {
3705
3710
  element: target,
3706
3711
  elementPath: getElementSelector(target),
@@ -3712,18 +3717,60 @@ var _Instruckt = class _Instruckt {
3712
3717
  y: centerY,
3713
3718
  nearbyText: getNearbyText(target) || void 0,
3714
3719
  screenshot,
3715
- framework: (_b = this.detectFramework(target)) != null ? _b : void 0
3720
+ framework: framework != null ? framework : void 0
3716
3721
  };
3717
3722
  this.showAnnotationPopup(pending);
3718
3723
  }
3719
3724
  // ── Framework detection ───────────────────────────────────────
3720
- detectFramework(el) {
3721
- 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;
3722
3732
  const adapters = (_a2 = this.config.adapters) != null ? _a2 : [];
3723
3733
  if (adapters.includes("livewire")) {
3724
3734
  const ctx = getContext(el);
3725
3735
  if (ctx) return ctx;
3726
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
+ }
3727
3774
  if (adapters.includes("vue")) {
3728
3775
  const ctx = getContext2(el);
3729
3776
  if (ctx) return ctx;
@@ -3915,17 +3962,33 @@ No open annotations.`;
3915
3962
  lines.push("");
3916
3963
  const hPrefix = multiPage ? "###" : "##";
3917
3964
  annotations.forEach((a, i) => {
3918
- var _a2, _b, _c, _d, _e;
3965
+ var _a2, _b, _c, _d, _e, _f;
3919
3966
  const componentSuffix = ((_a2 = a.framework) == null ? void 0 : _a2.component) ? ` in \`${a.framework.component}\`` : "";
3920
3967
  lines.push(`${hPrefix} ${i + 1}. ${a.comment}`);
3921
3968
  lines.push(`- ID: \`${a.id}\``);
3922
3969
  lines.push(`- Element: \`${a.element}\`${componentSuffix}`);
3923
3970
  if ((_b = a.framework) == null ? void 0 : _b.source_file) {
3924
- 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
+ }
3925
3976
  lines.push(`- Source: \`${loc}\``);
3926
3977
  } else if ((_d = (_c = a.framework) == null ? void 0 : _c.data) == null ? void 0 : _d.file) {
3927
3978
  lines.push(`- File: \`${a.framework.data.file}\``);
3928
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
+ }
3929
3992
  if (a.cssClasses) {
3930
3993
  lines.push(`- Classes: \`${a.cssClasses}\``);
3931
3994
  }
@@ -3936,7 +3999,7 @@ No open annotations.`;
3936
3999
  }
3937
4000
  if (a.screenshot) {
3938
4001
  if (!a.screenshot.startsWith("data:")) {
3939
- const screenshotPath = (_e = this.config.screenshotPath) != null ? _e : "storage/app/_instruckt/";
4002
+ const screenshotPath = (_f = this.config.screenshotPath) != null ? _f : "storage/app/_instruckt/";
3940
4003
  lines.push(`- Screenshot: \`${screenshotPath}${a.screenshot}\``);
3941
4004
  } else {
3942
4005
  lines.push(`- Screenshot: ![Screenshot](${a.screenshot})`);