instruckt 0.4.26 → 0.4.28

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,13 +10,106 @@ 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
+
15
+ ### Vite Plugin
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.
14
18
 
15
- ```html
16
- <script src="https://cdn.jsdelivr.net/npm/instruckt/dist/instruckt.iife.js"></script>
19
+ ```js
20
+ // vite.config.ts
21
+ import instruckt from 'instruckt/vite'
22
+
23
+ export default defineConfig({
24
+ plugins: [instruckt()],
25
+ })
17
26
  ```
18
27
 
19
- ## Quick Start
28
+ That's it for SPA apps (Vue, React, Svelte with Vite). The plugin auto-injects the client via `transformIndexHtml`.
29
+
30
+ ### Laravel
31
+
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:
33
+
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.
40
+
41
+ ```js
42
+ // vite.config.js (added automatically by install command)
43
+ import instruckt from 'instruckt/vite'
44
+
45
+ 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
+ })
55
+ ```
56
+
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'
67
+ ```
68
+
69
+ The virtual module is SSR-safe — it only initializes in the browser.
70
+
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',
84
+
85
+ // Toolbar position (default: 'bottom-right')
86
+ position: 'bottom-right',
87
+
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:
20
113
 
21
114
  ```js
22
115
  import { Instruckt } from 'instruckt'
@@ -26,15 +119,114 @@ const instruckt = new Instruckt({
26
119
  })
27
120
  ```
28
121
 
29
- Or with the IIFE build:
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
+ ```
30
145
 
31
- ```html
32
- <script src="/path/to/instruckt.iife.js"></script>
146
+ ```svelte
147
+ <!-- src/routes/+layout.svelte -->
33
148
  <script>
34
- Instruckt.init({ endpoint: '/instruckt' })
149
+ import { browser } from '$app/environment';
150
+
151
+ let { children } = $props();
35
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()}
36
161
  ```
37
162
 
163
+ </details>
164
+
165
+ <details>
166
+ <summary>Nuxt</summary>
167
+
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>
187
+
188
+ ```tsx
189
+ // components/InstrucktProvider.tsx
190
+ 'use client'
191
+
192
+ import { useEffect } from 'react'
193
+
194
+ export function InstrucktProvider() {
195
+ useEffect(() => {
196
+ let instruckt: any
197
+
198
+ import('instruckt').then(({ Instruckt }) => {
199
+ instruckt = new Instruckt({
200
+ endpoint: '/api/annotations',
201
+ adapters: ['react'],
202
+ })
203
+ })
204
+
205
+ return () => instruckt?.destroy()
206
+ }, [])
207
+
208
+ return null
209
+ }
210
+ ```
211
+
212
+ ```tsx
213
+ // app/layout.tsx
214
+ import { InstrucktProvider } from '@/components/InstrucktProvider'
215
+
216
+ export default function RootLayout({ children }) {
217
+ return (
218
+ <html>
219
+ <body>
220
+ {children}
221
+ <InstrucktProvider />
222
+ </body>
223
+ </html>
224
+ )
225
+ }
226
+ ```
227
+
228
+ </details>
229
+
38
230
  ## How It Works
39
231
 
40
232
  1. A floating toolbar appears in your app
@@ -56,7 +248,7 @@ Or with the IIFE build:
56
248
  - Element: `button.btn-primary` in `pages::auth.login`
57
249
  - Classes: `btn btn-primary`
58
250
  - Text: "Submit Login"
59
- - Screenshot: `storage/app/_instruckt/screenshots/01JWXYZ.png`
251
+ - Screenshot: `.instruckt/screenshots/01JWXYZ.png`
60
252
 
61
253
  ## 2. Make the login card have rounded corners
62
254
  - Element: `div.bg-white` in `pages::auth.login`
@@ -67,7 +259,7 @@ Or with the IIFE build:
67
259
 
68
260
  ```js
69
261
  new Instruckt({
70
- // Required — URL to your instruckt API (provided by the Laravel package or your own backend)
262
+ // Required — URL to your instruckt API (provided by the Vite plugin, Laravel package, or your own backend)
71
263
  endpoint: '/instruckt',
72
264
 
73
265
  // Framework adapters to activate (default: all)
@@ -94,6 +286,10 @@ new Instruckt({
94
286
  clearPage: 'x', // clear annotations on this page
95
287
  },
96
288
 
289
+ // Whether MCP tools are available (default: false)
290
+ // Set to true when using with Laravel or another backend that registers MCP tools
291
+ mcp: false,
292
+
97
293
  // Callbacks
98
294
  onAnnotationAdd: (annotation) => {},
99
295
  })
@@ -118,7 +314,7 @@ Default shortcuts (customizable via `keys` config):
118
314
  - **Shadow DOM isolation** — all UI renders in shadow roots so it never conflicts with your styles
119
315
  - **Copy as markdown** — annotations auto-copy as structured markdown optimized for AI agents
120
316
  - **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
317
+ - **Annotation persistence** — annotations survive page reloads via localStorage; with a backend (Vite plugin or Laravel), annotations are stored on disk as JSON
122
318
  - **Minimize** — collapse to a small floating button with annotation count badge
123
319
  - **Page-scoped markers** — annotation pins reposition on scroll/resize and only appear on the page where they were created
124
320
  - **Clear controls** — clear current page (`X` key or trash icon), or clear all pages via flyout
@@ -139,9 +335,23 @@ instruckt.destroy()
139
335
 
140
336
  ## Backend
141
337
 
142
- instruckt needs a backend to persist annotations. The official Laravel package provides this out of the box:
338
+ ### Vite Plugin (Built-in)
339
+
340
+ 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.
341
+
342
+ ### Laravel
143
343
 
144
- - **[instruckt-laravel](https://github.com/joshcirre/instruckt-laravel)** — Laravel package with JSON file storage, MCP tools, Blade component, and API routes
344
+ **[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.
345
+
346
+ ### Custom Backend
347
+
348
+ instruckt expects these endpoints:
349
+
350
+ ```
351
+ GET {endpoint}/annotations → list annotations
352
+ POST {endpoint}/annotations → create annotation
353
+ PATCH {endpoint}/annotations/{id} → update annotation
354
+ ```
145
355
 
146
356
  ## License
147
357
 
@@ -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";
@@ -3185,6 +3192,7 @@ var _Instruckt = class _Instruckt {
3185
3192
  this.highlightLocked = false;
3186
3193
  this.pollTimer = null;
3187
3194
  this.initialLoadDone = false;
3195
+ this.hasBackend = false;
3188
3196
  this.boundReposition = () => {
3189
3197
  var _a2;
3190
3198
  (_a2 = this.markers) == null ? void 0 : _a2.reposition(this.annotations);
@@ -3304,7 +3312,7 @@ var _Instruckt = class _Instruckt {
3304
3312
  onClearPage: () => this.clearPage(),
3305
3313
  onClearAll: () => this.clearEverything(),
3306
3314
  onMinimize: (min) => this.onMinimize(min)
3307
- }, this.config.keys);
3315
+ }, this.config.keys, this.config.tools);
3308
3316
  this.highlight = new ElementHighlight();
3309
3317
  this.popup = new AnnotationPopup();
3310
3318
  this.markers = new AnnotationMarkers((annotation) => this.onMarkerClick(annotation));
@@ -3343,7 +3351,7 @@ var _Instruckt = class _Instruckt {
3343
3351
  this.isAnnotating = false;
3344
3352
  this.isFrozen = false;
3345
3353
  document.querySelectorAll("[data-instruckt]").forEach((el) => el.remove());
3346
- this.toolbar = new Toolbar(this.config.position, this.makeToolbarCallbacks());
3354
+ this.toolbar = new Toolbar(this.config.position, this.makeToolbarCallbacks(), this.config.keys, this.config.tools);
3347
3355
  if (wasMinimized) this.toolbar.minimize();
3348
3356
  this.markers = new AnnotationMarkers((annotation) => this.onMarkerClick(annotation));
3349
3357
  this.highlight = new ElementHighlight();
@@ -3368,29 +3376,97 @@ var _Instruckt = class _Instruckt {
3368
3376
  (_e = this.markers) == null ? void 0 : _e.setVisible(true);
3369
3377
  }
3370
3378
  }
3379
+ // ── Persistence ─────────────────────────────────────────────────
3380
+ static get STORAGE_KEY() {
3381
+ return `instruckt:${window.location.origin}:annotations`;
3382
+ }
3371
3383
  async loadAnnotations() {
3372
3384
  this.loadFromStorage();
3373
3385
  try {
3374
3386
  const remote = await this.api.getAnnotations();
3387
+ this.hasBackend = true;
3375
3388
  const remoteIds = new Set(remote.map((a) => a.id));
3376
3389
  const localOnly = this.annotations.filter((a) => !remoteIds.has(a.id));
3377
3390
  this.annotations = [...remote, ...localOnly];
3378
3391
  this.saveToStorage();
3379
3392
  } catch (e) {
3393
+ this.hasBackend = false;
3380
3394
  }
3381
3395
  this.initialLoadDone = true;
3382
3396
  this.syncMarkers();
3383
3397
  }
3384
3398
  saveToStorage() {
3385
3399
  try {
3386
- localStorage.setItem(_Instruckt.STORAGE_KEY, JSON.stringify(this.annotations));
3400
+ const screenshotMap = /* @__PURE__ */ new Map();
3401
+ const stripped = this.annotations.map((a) => {
3402
+ var _a2;
3403
+ if ((_a2 = a.screenshot) == null ? void 0 : _a2.startsWith("data:")) {
3404
+ screenshotMap.set(a.id, a.screenshot);
3405
+ return __spreadProps(__spreadValues({}, a), { screenshot: `idb:${a.id}` });
3406
+ }
3407
+ return a;
3408
+ });
3409
+ localStorage.setItem(_Instruckt.STORAGE_KEY, JSON.stringify(stripped));
3410
+ if (screenshotMap.size > 0) this.saveScreenshotsToIdb(screenshotMap);
3387
3411
  } catch (e) {
3388
3412
  }
3389
3413
  }
3390
3414
  loadFromStorage() {
3391
3415
  try {
3392
3416
  const raw = localStorage.getItem(_Instruckt.STORAGE_KEY);
3393
- if (raw) this.annotations = JSON.parse(raw);
3417
+ if (raw) {
3418
+ this.annotations = JSON.parse(raw);
3419
+ const idbRefs = this.annotations.filter((a) => {
3420
+ var _a2;
3421
+ return (_a2 = a.screenshot) == null ? void 0 : _a2.startsWith("idb:");
3422
+ });
3423
+ if (idbRefs.length > 0) this.loadScreenshotsFromIdb(idbRefs);
3424
+ }
3425
+ } catch (e) {
3426
+ }
3427
+ }
3428
+ openIdb() {
3429
+ return new Promise((resolve, reject) => {
3430
+ const req = indexedDB.open(_Instruckt.IDB_NAME, 1);
3431
+ req.onupgradeneeded = () => {
3432
+ const db = req.result;
3433
+ if (!db.objectStoreNames.contains(_Instruckt.IDB_STORE)) {
3434
+ db.createObjectStore(_Instruckt.IDB_STORE);
3435
+ }
3436
+ };
3437
+ req.onsuccess = () => resolve(req.result);
3438
+ req.onerror = () => reject(req.error);
3439
+ });
3440
+ }
3441
+ async saveScreenshotsToIdb(screenshots) {
3442
+ try {
3443
+ const db = await this.openIdb();
3444
+ const tx = db.transaction(_Instruckt.IDB_STORE, "readwrite");
3445
+ const store = tx.objectStore(_Instruckt.IDB_STORE);
3446
+ for (const [id, dataUri] of screenshots) {
3447
+ store.put(dataUri, id);
3448
+ }
3449
+ db.close();
3450
+ } catch (e) {
3451
+ }
3452
+ }
3453
+ async loadScreenshotsFromIdb(annotations) {
3454
+ try {
3455
+ const db = await this.openIdb();
3456
+ const tx = db.transaction(_Instruckt.IDB_STORE, "readonly");
3457
+ const store = tx.objectStore(_Instruckt.IDB_STORE);
3458
+ for (const a of annotations) {
3459
+ const id = a.screenshot.replace("idb:", "");
3460
+ const req = store.get(id);
3461
+ req.onsuccess = () => {
3462
+ if (req.result) a.screenshot = req.result;
3463
+ };
3464
+ }
3465
+ await new Promise((resolve) => {
3466
+ tx.oncomplete = () => resolve();
3467
+ });
3468
+ db.close();
3469
+ this.syncMarkers();
3394
3470
  } catch (e) {
3395
3471
  }
3396
3472
  }
@@ -3863,19 +3939,21 @@ No open annotations.`;
3863
3939
  const screenshotPath = (_e = this.config.screenshotPath) != null ? _e : "storage/app/_instruckt/";
3864
3940
  lines.push(`- Screenshot: \`${screenshotPath}${a.screenshot}\``);
3865
3941
  } else {
3866
- lines.push(`- Screenshot: attached`);
3942
+ lines.push(`- Screenshot: ![Screenshot](${a.screenshot})`);
3867
3943
  }
3868
3944
  }
3869
3945
  lines.push("");
3870
3946
  });
3871
3947
  }
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.");
3948
+ if (this.config.mcp) {
3949
+ const hasScreenshots = pending.some((a) => a.screenshot && !a.screenshot.startsWith("data:"));
3950
+ lines.push("---");
3951
+ lines.push("");
3952
+ if (hasScreenshots) {
3953
+ lines.push("Use the `instruckt.get_screenshot` MCP tool to view screenshots. After making changes, use `instruckt.resolve` to mark each annotation as resolved.");
3954
+ } else {
3955
+ lines.push("After making changes, use the `instruckt.resolve` MCP tool to mark each annotation as resolved.");
3956
+ }
3879
3957
  }
3880
3958
  return lines.join("\n").trim();
3881
3959
  }
@@ -3898,8 +3976,8 @@ No open annotations.`;
3898
3976
  if (this.pollTimer !== null) clearInterval(this.pollTimer);
3899
3977
  }
3900
3978
  };
3901
- // ── Persistence ─────────────────────────────────────────────────
3902
- _Instruckt.STORAGE_KEY = `instruckt:${window.location.origin}:annotations`;
3979
+ _Instruckt.IDB_NAME = "instruckt";
3980
+ _Instruckt.IDB_STORE = "screenshots";
3903
3981
  var Instruckt = _Instruckt;
3904
3982
 
3905
3983
  // src/index.ts