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 +230 -6
- package/dist/instruckt.cjs.js +83 -12
- package/dist/instruckt.cjs.js.map +1 -1
- package/dist/instruckt.d.cts +184 -0
- package/dist/instruckt.d.mts +7 -1
- package/dist/instruckt.d.ts +9 -1
- package/dist/instruckt.esm.js +83 -12
- package/dist/instruckt.esm.js.map +1 -1
- package/dist/instruckt.iife.js +17 -17
- package/dist/instruckt.iife.js.map +1 -1
- package/dist/vite.cjs.js +218 -0
- package/dist/vite.cjs.js.map +1 -0
- package/dist/vite.d.cts +45 -0
- package/dist/vite.d.mts +28 -0
- package/dist/vite.d.ts +45 -0
- package/dist/vite.esm.js +197 -0
- package/dist/vite.esm.js.map +1 -0
- package/package.json +18 -2
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:
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
package/dist/instruckt.cjs.js
CHANGED
|
@@ -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
|
-
|
|
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)
|
|
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:
|
|
3935
|
+
lines.push(`- Screenshot: `);
|
|
3867
3936
|
}
|
|
3868
3937
|
}
|
|
3869
3938
|
lines.push("");
|
|
3870
3939
|
});
|
|
3871
3940
|
}
|
|
3872
|
-
|
|
3873
|
-
|
|
3874
|
-
|
|
3875
|
-
|
|
3876
|
-
|
|
3877
|
-
|
|
3878
|
-
|
|
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
|
-
|
|
3902
|
-
_Instruckt.
|
|
3972
|
+
_Instruckt.IDB_NAME = "instruckt";
|
|
3973
|
+
_Instruckt.IDB_STORE = "screenshots";
|
|
3903
3974
|
var Instruckt = _Instruckt;
|
|
3904
3975
|
|
|
3905
3976
|
// src/index.ts
|