instruckt 0.4.26 → 0.4.27

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
@@ -16,7 +16,101 @@ Or load via CDN:
16
16
  <script src="https://cdn.jsdelivr.net/npm/instruckt/dist/instruckt.iife.js"></script>
17
17
  ```
18
18
 
19
- ## Quick Start
19
+ ## Quick Start: Vite Plugin
20
+
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.
22
+
23
+ ```js
24
+ // vite.config.ts
25
+ import instruckt from 'instruckt/vite'
26
+
27
+ export default defineConfig({
28
+ plugins: [instruckt()],
29
+ })
30
+ ```
31
+
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:
37
+
38
+ ```js
39
+ // SvelteKit: src/routes/+layout.svelte
40
+ import 'virtual:instruckt'
41
+
42
+ // Nuxt: plugins/instruckt.client.ts
43
+ import 'virtual:instruckt'
44
+ ```
45
+
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:
51
+
52
+ ```js
53
+ // vite.config.ts
54
+ import laravel from 'laravel-vite-plugin'
55
+ import instruckt from 'instruckt/vite'
56
+
57
+ 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,
108
+ })
109
+ ```
110
+
111
+ ## Manual Setup
112
+
113
+ If you're not using Vite, you can initialize instruckt directly:
20
114
 
21
115
  ```js
22
116
  import { Instruckt } from 'instruckt'
@@ -35,6 +129,118 @@ Or with the IIFE build:
35
129
  </script>
36
130
  ```
37
131
 
132
+ ### Framework-Specific Manual Setup
133
+
134
+ instruckt is a browser-only library. In SSR frameworks without the Vite plugin, make sure it only loads on the client.
135
+
136
+ <details>
137
+ <summary>SvelteKit</summary>
138
+
139
+ ```svelte
140
+ <!-- src/lib/InstrucktProvider.svelte -->
141
+ <script>
142
+ import { onMount } from 'svelte';
143
+
144
+ onMount(async () => {
145
+ const { Instruckt } = await import('instruckt');
146
+ const instruckt = new Instruckt({
147
+ endpoint: '/api/annotations',
148
+ adapters: ['svelte'],
149
+ });
150
+
151
+ return () => instruckt.destroy();
152
+ });
153
+ </script>
154
+ ```
155
+
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()}
171
+ ```
172
+
173
+ </details>
174
+
175
+ <details>
176
+ <summary>Nuxt</summary>
177
+
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>
197
+
198
+ ```tsx
199
+ // components/InstrucktProvider.tsx
200
+ 'use client'
201
+
202
+ import { useEffect } from 'react'
203
+
204
+ export function InstrucktProvider() {
205
+ useEffect(() => {
206
+ let instruckt: any
207
+
208
+ import('instruckt').then(({ Instruckt }) => {
209
+ instruckt = new Instruckt({
210
+ endpoint: '/api/annotations',
211
+ adapters: ['react'],
212
+ })
213
+ })
214
+
215
+ return () => instruckt?.destroy()
216
+ }, [])
217
+
218
+ return null
219
+ }
220
+ ```
221
+
222
+ ```tsx
223
+ // app/layout.tsx
224
+ import { InstrucktProvider } from '@/components/InstrucktProvider'
225
+
226
+ export default function RootLayout({ children }) {
227
+ return (
228
+ <html>
229
+ <body>
230
+ {children}
231
+ <InstrucktProvider />
232
+ </body>
233
+ </html>
234
+ )
235
+ }
236
+ ```
237
+
238
+ </details>
239
+
240
+ ### Astro
241
+
242
+ See **[instruckt-astro](https://github.com/sgasser/instruckt-astro)** for a community-maintained Astro integration.
243
+
38
244
  ## How It Works
39
245
 
40
246
  1. A floating toolbar appears in your app
@@ -56,7 +262,7 @@ Or with the IIFE build:
56
262
  - Element: `button.btn-primary` in `pages::auth.login`
57
263
  - Classes: `btn btn-primary`
58
264
  - Text: "Submit Login"
59
- - Screenshot: `storage/app/_instruckt/screenshots/01JWXYZ.png`
265
+ - Screenshot: `.instruckt/screenshots/01JWXYZ.png`
60
266
 
61
267
  ## 2. Make the login card have rounded corners
62
268
  - Element: `div.bg-white` in `pages::auth.login`
@@ -67,7 +273,7 @@ Or with the IIFE build:
67
273
 
68
274
  ```js
69
275
  new Instruckt({
70
- // Required — URL to your instruckt API (provided by the Laravel package or your own backend)
276
+ // Required — URL to your instruckt API (provided by the Vite plugin, Laravel package, or your own backend)
71
277
  endpoint: '/instruckt',
72
278
 
73
279
  // Framework adapters to activate (default: all)
@@ -94,6 +300,10 @@ new Instruckt({
94
300
  clearPage: 'x', // clear annotations on this page
95
301
  },
96
302
 
303
+ // Whether MCP tools are available (default: false)
304
+ // Set to true when using with Laravel or another backend that registers MCP tools
305
+ mcp: false,
306
+
97
307
  // Callbacks
98
308
  onAnnotationAdd: (annotation) => {},
99
309
  })
@@ -118,7 +328,7 @@ Default shortcuts (customizable via `keys` config):
118
328
  - **Shadow DOM isolation** — all UI renders in shadow roots so it never conflicts with your styles
119
329
  - **Copy as markdown** — annotations auto-copy as structured markdown optimized for AI agents
120
330
  - **Freeze mode** — pause animations, freeze popovers/dropdowns, and block all navigation
121
- - **Annotation persistence** — annotations survive page reloads and Vite rebuilds via localStorage fallback; with a backend (Laravel), annotations are loaded from the API on init
331
+ - **Annotation persistence** — annotations survive page reloads via localStorage; with a backend (Vite plugin or Laravel), annotations are stored on disk as JSON
122
332
  - **Minimize** — collapse to a small floating button with annotation count badge
123
333
  - **Page-scoped markers** — annotation pins reposition on scroll/resize and only appear on the page where they were created
124
334
  - **Clear controls** — clear current page (`X` key or trash icon), or clear all pages via flyout
@@ -139,9 +349,23 @@ instruckt.destroy()
139
349
 
140
350
  ## Backend
141
351
 
142
- instruckt needs a backend to persist annotations. The official Laravel package provides this out of the box:
352
+ ### Vite Plugin (Built-in)
353
+
354
+ The Vite plugin includes a dev API server that saves annotations and screenshots to disk (`.instruckt/` directory). No external backend needed. Screenshots are saved as files instead of base64, keeping clipboard markdown small.
355
+
356
+ ### Laravel
143
357
 
144
- - **[instruckt-laravel](https://github.com/joshcirre/instruckt-laravel)** — Laravel package with JSON file storage, MCP tools, Blade component, and API routes
358
+ **[instruckt-laravel](https://github.com/joshcirre/instruckt-laravel)** — Laravel package with JSON file storage, MCP tools, Blade component, and API routes.
359
+
360
+ ### Custom Backend
361
+
362
+ instruckt expects these endpoints:
363
+
364
+ ```
365
+ GET {endpoint}/annotations → list annotations
366
+ POST {endpoint}/annotations → create annotation
367
+ PATCH {endpoint}/annotations/{id} → update annotation
368
+ ```
145
369
 
146
370
  ## License
147
371
 
@@ -3185,6 +3185,7 @@ var _Instruckt = class _Instruckt {
3185
3185
  this.highlightLocked = false;
3186
3186
  this.pollTimer = null;
3187
3187
  this.initialLoadDone = false;
3188
+ this.hasBackend = false;
3188
3189
  this.boundReposition = () => {
3189
3190
  var _a2;
3190
3191
  (_a2 = this.markers) == null ? void 0 : _a2.reposition(this.annotations);
@@ -3368,29 +3369,97 @@ var _Instruckt = class _Instruckt {
3368
3369
  (_e = this.markers) == null ? void 0 : _e.setVisible(true);
3369
3370
  }
3370
3371
  }
3372
+ // ── Persistence ─────────────────────────────────────────────────
3373
+ static get STORAGE_KEY() {
3374
+ return `instruckt:${window.location.origin}:annotations`;
3375
+ }
3371
3376
  async loadAnnotations() {
3372
3377
  this.loadFromStorage();
3373
3378
  try {
3374
3379
  const remote = await this.api.getAnnotations();
3380
+ this.hasBackend = true;
3375
3381
  const remoteIds = new Set(remote.map((a) => a.id));
3376
3382
  const localOnly = this.annotations.filter((a) => !remoteIds.has(a.id));
3377
3383
  this.annotations = [...remote, ...localOnly];
3378
3384
  this.saveToStorage();
3379
3385
  } catch (e) {
3386
+ this.hasBackend = false;
3380
3387
  }
3381
3388
  this.initialLoadDone = true;
3382
3389
  this.syncMarkers();
3383
3390
  }
3384
3391
  saveToStorage() {
3385
3392
  try {
3386
- localStorage.setItem(_Instruckt.STORAGE_KEY, JSON.stringify(this.annotations));
3393
+ const screenshotMap = /* @__PURE__ */ new Map();
3394
+ const stripped = this.annotations.map((a) => {
3395
+ var _a2;
3396
+ if ((_a2 = a.screenshot) == null ? void 0 : _a2.startsWith("data:")) {
3397
+ screenshotMap.set(a.id, a.screenshot);
3398
+ return __spreadProps(__spreadValues({}, a), { screenshot: `idb:${a.id}` });
3399
+ }
3400
+ return a;
3401
+ });
3402
+ localStorage.setItem(_Instruckt.STORAGE_KEY, JSON.stringify(stripped));
3403
+ if (screenshotMap.size > 0) this.saveScreenshotsToIdb(screenshotMap);
3387
3404
  } catch (e) {
3388
3405
  }
3389
3406
  }
3390
3407
  loadFromStorage() {
3391
3408
  try {
3392
3409
  const raw = localStorage.getItem(_Instruckt.STORAGE_KEY);
3393
- if (raw) this.annotations = JSON.parse(raw);
3410
+ if (raw) {
3411
+ this.annotations = JSON.parse(raw);
3412
+ const idbRefs = this.annotations.filter((a) => {
3413
+ var _a2;
3414
+ return (_a2 = a.screenshot) == null ? void 0 : _a2.startsWith("idb:");
3415
+ });
3416
+ if (idbRefs.length > 0) this.loadScreenshotsFromIdb(idbRefs);
3417
+ }
3418
+ } catch (e) {
3419
+ }
3420
+ }
3421
+ openIdb() {
3422
+ return new Promise((resolve, reject) => {
3423
+ const req = indexedDB.open(_Instruckt.IDB_NAME, 1);
3424
+ req.onupgradeneeded = () => {
3425
+ const db = req.result;
3426
+ if (!db.objectStoreNames.contains(_Instruckt.IDB_STORE)) {
3427
+ db.createObjectStore(_Instruckt.IDB_STORE);
3428
+ }
3429
+ };
3430
+ req.onsuccess = () => resolve(req.result);
3431
+ req.onerror = () => reject(req.error);
3432
+ });
3433
+ }
3434
+ async saveScreenshotsToIdb(screenshots) {
3435
+ try {
3436
+ const db = await this.openIdb();
3437
+ const tx = db.transaction(_Instruckt.IDB_STORE, "readwrite");
3438
+ const store = tx.objectStore(_Instruckt.IDB_STORE);
3439
+ for (const [id, dataUri] of screenshots) {
3440
+ store.put(dataUri, id);
3441
+ }
3442
+ db.close();
3443
+ } catch (e) {
3444
+ }
3445
+ }
3446
+ async loadScreenshotsFromIdb(annotations) {
3447
+ try {
3448
+ const db = await this.openIdb();
3449
+ const tx = db.transaction(_Instruckt.IDB_STORE, "readonly");
3450
+ const store = tx.objectStore(_Instruckt.IDB_STORE);
3451
+ for (const a of annotations) {
3452
+ const id = a.screenshot.replace("idb:", "");
3453
+ const req = store.get(id);
3454
+ req.onsuccess = () => {
3455
+ if (req.result) a.screenshot = req.result;
3456
+ };
3457
+ }
3458
+ await new Promise((resolve) => {
3459
+ tx.oncomplete = () => resolve();
3460
+ });
3461
+ db.close();
3462
+ this.syncMarkers();
3394
3463
  } catch (e) {
3395
3464
  }
3396
3465
  }
@@ -3863,19 +3932,21 @@ No open annotations.`;
3863
3932
  const screenshotPath = (_e = this.config.screenshotPath) != null ? _e : "storage/app/_instruckt/";
3864
3933
  lines.push(`- Screenshot: \`${screenshotPath}${a.screenshot}\``);
3865
3934
  } else {
3866
- lines.push(`- Screenshot: attached`);
3935
+ lines.push(`- Screenshot: ![Screenshot](${a.screenshot})`);
3867
3936
  }
3868
3937
  }
3869
3938
  lines.push("");
3870
3939
  });
3871
3940
  }
3872
- const hasScreenshots = pending.some((a) => a.screenshot && !a.screenshot.startsWith("data:"));
3873
- lines.push("---");
3874
- lines.push("");
3875
- if (hasScreenshots) {
3876
- lines.push("Use the `instruckt.get_screenshot` MCP tool to view screenshots. After making changes, use `instruckt.resolve` to mark each annotation as resolved.");
3877
- } else {
3878
- lines.push("After making changes, use the `instruckt.resolve` MCP tool to mark each annotation as resolved.");
3941
+ if (this.config.mcp) {
3942
+ const hasScreenshots = pending.some((a) => a.screenshot && !a.screenshot.startsWith("data:"));
3943
+ lines.push("---");
3944
+ lines.push("");
3945
+ if (hasScreenshots) {
3946
+ lines.push("Use the `instruckt.get_screenshot` MCP tool to view screenshots. After making changes, use `instruckt.resolve` to mark each annotation as resolved.");
3947
+ } else {
3948
+ lines.push("After making changes, use the `instruckt.resolve` MCP tool to mark each annotation as resolved.");
3949
+ }
3879
3950
  }
3880
3951
  return lines.join("\n").trim();
3881
3952
  }
@@ -3898,8 +3969,8 @@ No open annotations.`;
3898
3969
  if (this.pollTimer !== null) clearInterval(this.pollTimer);
3899
3970
  }
3900
3971
  };
3901
- // ── Persistence ─────────────────────────────────────────────────
3902
- _Instruckt.STORAGE_KEY = `instruckt:${window.location.origin}:annotations`;
3972
+ _Instruckt.IDB_NAME = "instruckt";
3973
+ _Instruckt.IDB_STORE = "screenshots";
3903
3974
  var Instruckt = _Instruckt;
3904
3975
 
3905
3976
  // src/index.ts