Loupe v0.5.2

Documentation · v0.5.2

Loupe

Pin feedback to the live UI — inspect any element, drop a comment, capture a screenshot — then hand it to Claude Code as an actionable backlog. Comments persist and re-anchor across redeploys.

Version
0.5.2
Packages
7
Runtime
Node 24
Also
PHP 8.4+
License
MIT

What Loupe is

An embeddable visual-feedback platform. A product manager inspects any element on a running product, pins a comment to it, and captures a screenshot. Comments persist and re-anchor across redeploys, and are handed to Claude Code via MCP as an actionable, fully-contextualized backlog. A browser extension does the same on any site without an SDK install, and a Laravel package embeds the whole loop into a PHP app.

It's built like a product, not a prototype: an SDK, a backend, a triage board, an MCP server, and a browser extension — all open source, all TypeScript — plus a first-party Laravel package. Everything runs locally with no external services (embedded Postgres via PGlite).

The packages

shared

Contract layer

Canonical types + normalizeUrl(). Zero runtime deps; everyone imports from here.

sdk

The widget

Embeddable Shadow-DOM widget: inspect, comment, capture, re-anchor.

server

The API

node:http + Postgres + object storage + HMAC auth + static hosting.

dashboard

Triage board

Kanban board for humans — status columns, filters, Copy for Claude.

mcp

For Claude

MCP server exposing the backlog to Claude Code as three tools.

extension

Any site

MV3 extension reusing the SDK core with pixel-perfect capture.

laravel

Loupe for Laravel

Composer package: widget + your DB + gating + dashboard + MCP.

Author: Mohamed Ashraf Elsaed. Published to npm (@loupekit/*), GitHub Packages, and Packagist (loupekit/laravel).


Quick start

Everything is orchestrated from the repo root. Build once, seed the demo project, and start the server — a single process hosts the API, dashboard, demo product, and SDK bundle on one port.

bash
# install workspace deps
npm install

# build shared → sdk → dashboard → extension
npm run build

# create the demo project; prints its admin key + demo HMAC
npm run seed

# one process on http://localhost:8787 → API + dashboard + demo + SDK
npm start

Open these

  • Demo product — /demo/
  • Triage board — /dashboard/?key=<admin key>
  • Health — GET /v1/health

Seeded demo credentials

  • Project — pk_demo_acme
  • Admin key — sk_demo_acme_0f3b9c
  • Demo user — u_92

Embed the SDK in your product

app.ts
import { init } from "@loupekit/sdk";

init({
  projectKey: "pk_demo_acme",
  user: { id: "u_92", name: "Ada Lovelace" },
  userHmac: "<HMAC-SHA256(user.id, PROJECT_SECRET)>", // from your server
  apiBase: "http://localhost:8787", // omit → offline localStorage mode
});

Connect Claude Code over MCP

.mcp.json
{
  "mcpServers": {
    "loupe": {
      "command": "node",
      "args": ["/abs/path/to/loupe/packages/mcp/index.ts"],
      "env": {
        "LOUPE_API": "http://localhost:8787",
        "LOUPE_PROJECT_KEY": "pk_demo_acme",
        "LOUPE_ADMIN_KEY": "<admin key>"
      }
    }
  }
}
The loop Claude calls list_commentsget_comment (request + element HTML + computed styles + screenshot URL) → makes the change → update_status(done).

Verify everything with npm test (Vitest) and npm run test:coverage for a v8 coverage report.


The feedback loop

SDK (in the product) → backend API → Postgres + object storage → dashboard (human triage) and MCP server (Claude reads it) → status flows back.

  1. Inspect & comment. A PM clicks an element or drags a region. The SDK records a multi-signal anchor, the element's HTML and computed styles, and a screenshot.
  2. Store. The screenshot uploads to POST /v1/blobs; the comment (with the returned URL) is written to Postgres against the normalized page URL.
  3. Triage. A human works the Kanban board — open → in progress → done — with screenshots and the exact selector.
  4. Ship with Claude. Claude Code reads the same backlog over MCP, makes the change, and marks it done. Status flows back either way.

Both capture surfaces (SDK and extension) and both triage surfaces (dashboard and MCP) share the same data and contracts — see Seams for the extension points.


Working in the repo

Loupe is an npm workspaces monorepo. Root scripts orchestrate the packages.

Node-native packages

server/ and mcp/ run TypeScript directly on Node 24 (type-stripping) in dev — node index.ts, .ts imports. mcp is additionally bundled (tsup → dist) for publishing, since Node won't strip types under node_modules.

Bundled browser packages

sdk/, dashboard/, and extension/ are bundled by tsup to dist/; imports use .js (ESM) extensions.

@loupekit/shared is a built package (tscdist) and must be built before the others. Keep changes behind the seams (StorageAdapter, db.ts, blobs.ts, the captureScreenshot hook) — change implementations, not contracts.

Versioning As of 0.5.2 every package is aligned on one version — the previously stale @loupekit/server, the extension manifest.json, and the Node McpServer (all 0.2.0) are now 0.5.0. The Laravel LoupeServer reports its own 1.0.0 — an independent MCP-server identity, not the package version.

Re-anchoring — the crown jewel

The hardest problem Loupe solves: a comment pinned to an element must find that same element again after the product is redeployed and the markup has changed. fingerprint.ts records a multi-signal fingerprint with captureAnchor(el): Anchor, then re-locates it with resolveAnchor(anchor, root?): ResolveResult | null in tiers.

What the anchor records

Stable id / testid, a CSS path rooted at the nearest stable ancestor, XPath, normalized text, identifying attributes, nth-of-type, and the bounding rect + viewport.

Resolution tiers

TierMatchScore
1Unique testid / id0.98
1.5cssPath rooted at a stable id, single match, right tag0.90 — survives changed content
2Weighted similarity scanaccepted at ≥ 0.50

The similarity scan weights are: text .34 · attrs .22 · testid .22 · cssPath .20 · tag .12 · position .10. If the best candidate scores below 0.5, the pin detaches rather than anchoring to the wrong element.

Why detach A wrong pin is worse than a missing one — it sends Claude or a developer to the wrong place. Loupe would rather show a detached pin than lie about where the feedback belongs.

Authentication

Every project has a secret. There are two identities, and the browser never sees the secret.

Writes — user identity (HMAC)

Your server computes signUser(userId, secret) = HMAC-SHA256(userId, secret) and injects it into the page. The SDK sends X-Loupe-User + X-Loupe-Hmac; the server verifies with a timing-safe compare. A user may only post as themselves.

Read / triage — admin identity

The dashboard and MCP authenticate as admin with X-Loupe-Admin = the project secret. authenticate(projectKey, req) resolves to admin, user, or a failure (400 missing key · 401 bad creds · 404 unknown project).

The Laravel package swaps this whole scheme for Laravel's own session + CSRF + guard + Gate authorization — no shared secret. See Authorization.


URL normalization

Comments are keyed by normalized URL, so tracking params never fragment a page's feedback. normalizeUrl(input) strips utm_*, click ids (gclid, fbclid, msclkid, …), and Loupe dev params (api, key), sorts the remaining query pairs, and drops trailing slashes. It's applied server-side on both write and the list filter.

example
normalizeUrl("/checkout?utm_source=x&gclid=123&step=2")
// → "/checkout?step=2"
Debugging tip If comments look "missing" across query-string variants, run normalizeUrl on both URLs first — they almost certainly collapse to the same key.

Screenshots & storage

The SDK uploads a PNG to POST /v1/blobs and stores only the returned URL on the comment. Two things are excluded before pixels ever leave the browser: any [data-loupe-redact] region and Loupe's own UI.

  • Capture engine. The SDK uses modern-screenshot (DOM-based). The extension overrides this with chrome.tabs.captureVisibleTab for real pixels, then crops to the element/region and paints over redacted areas (accounting for devicePixelRatio).
  • Font embedding. Capture awaits document.fonts.ready (capped at ~0.8s) with a 6s capture timeout and a hard outer timeout, so it renders with the right fonts but can never hang the widget. CORS-less fonts fall back to the pixel-perfect extension.
  • Blobs seam. blobs.ts stores to local disk today (public by unguessable id); production swaps in S3/R2 with signed URLs.

Seams (extension points)

Four seams isolate the swappable parts. Change what's behind them, not their contracts.

SeamWhereLocalProduction
Storage adapterSDK store.ts / http-adapter.tslocalStorage / HTTPHTTP
Databaseserver db.tsPGlitehosted Postgres via DATABASE_URL
Object storageserver blobs.tslocal diskS3/R2 + signed URLs
Screenshot captureSDK captureScreenshot configmodern-screenshotextension captureVisibleTab

Package

@loupekit/shared
publishablev0.5.2no runtime depstsc → distESM-only

The contract layer — canonical data types and pure helpers. Everyone imports types from here; Node packages import the compiled JS.

Exported types

TypeShape
CommentStatus"open" | "in_progress" | "done"
CommentKind"element" | "region" | "free"
DeviceType"mobile" | "tablet" | "desktop"
LoupeUser{ id, name, email? }
Anchorfingerprint: tag, cssPath, xpath, testid, text, attrs, nthOfType, rect, viewport
ElementContext{ html, styles } — outerHTML + curated computed styles
RegionRect{ x, y, w, h, rel? } — document coords; rel = element-relative fractions
Commentthe canonical record — see the data model

Helpers

  • deviceType(width): DeviceType — mobile <768, tablet 768–1023, desktop ≥1024.
  • normalizeUrl(input): string — see URL normalization.

Package

@loupekit/sdk
publishablev0.5.2tsup bundledep: modern-screenshot

The embeddable browser widget — the thing a developer installs. Builds a Shadow-DOM host (#loupe-root) so its CSS never leaks into the host page or vice-versa.

Public API

  • init(config: LoupeConfig): void — idempotent; requires projectKey and user.id. Starts on DOMContentLoaded. apiBase set → HttpAdapter; omitted → LocalStorageAdapter (offline mode).
  • destroy(): void — tears down the toolbar and all listeners.

Toolbar modes: Inspect & comment, Note (a free page-level comment), Region shot, Comments. Drag the ◎ Loupe logo to move the whole bar anywhere on screen; click it to collapse. A moved bar expands edge-aware so it's always on-screen — vertical against a left/right edge, horizontal along the top/bottom.

LoupeConfig

FieldTypePurpose
projectKeystringRequired. Project identifier.
userLoupeUserRequired. Who is commenting.
userHmacstring?HMAC-SHA256(user.id, secret), computed server-side.
apiBasestring?Set → HTTP mode; omitted → offline localStorage.
autoOpenboolean?Open the toolbar immediately.
captureScreenshotfn?Override (el) => Promise<string?> (the extension uses this).
captureRegionfn?Region-shot override (rect) => Promise<string?>.
headersobject?Extra request headers (e.g. a CSRF token).
credentialsRequestCredentials?Fetch credentials mode (include for cross-origin cookies).

Internal modules

fingerprint.ts

captureAnchor / resolveAnchor — the re-anchoring engine.

capture.ts

captureElementContext + captureScreenshot / captureRegionScreenshot.

http-adapter.ts

HttpAdapter — uploads the blob first, stores only the URL.

store.ts

LocalStorageAdapter — the offline fallback.

app.ts

LoupeApp — Shadow-DOM host: toolbar, inspector, composer, pins, panel; repositions on scroll/resize + MutationObserver.

styles.ts

The Shadow-DOM stylesheet (theme vars on :host so they inherit through the shadow tree).

The StorageAdapter contract: list(projectKey, url), save(comment), update(id, patch), remove(id). A demo product ("Acme Analytics") lives in demo/, served at /demo/.


Package

@loupekit/server
private (app)v0.5.2deps: pglite, pgNode 24 native TS

The API — node:http, Postgres, object storage, auth, and static hosting. index.ts exports handler and start(port = PORT), and only listens when run as the entry point (so tests mount handler on an ephemeral port).

HTTP routes

MethodPathAuthNotes
GET/none302 → /dashboard/
GET/v1/healthnone{ ok: true }
GET/v1/blobs/:idpublicPNG, immutable cache
POST/v1/blobsauthed{ projectKey, data }{ url }
GET/v1/commentsauthed?projectKey=&url=
POST/v1/commentsauthedupsert; user may post only as self (else 403)
GET/v1/comments/:idauthedauth from the comment's project
PATCH/v1/comments/:idauthedpatch status / body
DELETE/v1/comments/:idauthed204
GET/dashboard/* · /demo/* · /sdk/*staticserved from disk

CORS echoes the request Origin; allows GET, POST, PATCH, DELETE, OPTIONS and headers Content-Type, X-Loupe-User, X-Loupe-Hmac, X-Loupe-Admin, X-Loupe-Project. Body limit 12 MB.

Environment variables

VariableDefaultEffect
PORT8787Listen port.
DATABASE_URLSet → node-pg; unset → embedded PGlite.
LOUPE_PG_DIR./data/pgPGlite dir; memory:// → in-memory (tests).
LOUPE_BLOB_DIR./data/blobsScreenshot disk directory.
LOUPE_PUBLIC_URLlocalhost:8787Public base for blob URLs.
LOUPE_DEMO_SECRETsk_demo_acme_0f3b9cSeed secret.

Package

@loupekit/dashboard
private (app)v0.5.2tsup bundle

The Kanban triage board — vanilla TypeScript, tsup-bundled. Three columns (Open · In progress · Done) with a page filter, screenshot thumbnails, status moves (PATCH), two-step delete, Copy for Claude, and a ~4s live refresh.

Admin mode

Query params ?api / ?project / ?key. The key is sent as X-Loupe-Admin and persisted to localStorage["loupe_admin"].

Session mode

Reads injected window.__LOUPE__ { api, project, csrf } (used by the Laravel package) and sends X-CSRF-TOKEN instead of an admin key.


Package

@loupekit/mcp
publishablev0.5.2bin: loupe-mcpdeps: @modelcontextprotocol/sdk, zod

The MCP stdio server that exposes the backlog to Claude Code. Authenticates to the API as admin. Install globally with npm i -g @loupekit/mcp (binary loupe-mcp), or point Claude Code at the source entry directly.

What Claude gets

The natural-language request, the page URL, the target element (stable id or CSS path), its outerHTML, a curated slice of computed styles, and the screenshot URL.

Tools

ToolParametersReturns
list_commentsstatus?, url?The backlog as a bulleted list.
get_commentidThe Claude-ready package (request + HTML + styles + screenshot URL).
update_statusid, statusMoves the comment (PATCH).

Environment

VariableDefault
LOUPE_APIhttp://localhost:8787
LOUPE_PROJECT_KEYpk_demo_acme
LOUPE_ADMIN_KEY"" — sent as X-Loupe-Admin; required vs a real backend

Transport is stdio via @modelcontextprotocol/sdk. The published package ships compiled JS (dist/index.js via tsup), so npm i -g @loupekit/mcp gives a working loupe-mcp binary; from a source checkout it runs directly with node index.ts. See Quick start for the .mcp.json.


Package

@loupekit/extension
private (app)v0.5.2Manifest V3dep: @loupekit/sdk

An MV3 browser extension that reuses the exact SDK core — Loupe on any site, no install on the target. The only difference from the SDK is the screenshot source: chrome.tabs.captureVisibleTab gives real pixels, cropped to the element/region with redaction, via the SDK's captureScreenshot override.

Parts

  • content.js — the injected widget (IIFE build artifact, gitignored)
  • background.js — the capture worker
  • popup.html / popup.js — config + "Start Loupe on this tab"

Load it

  • Permissions: activeTab, scripting, storage
  • Build: npm run build:extension
  • Load unpacked at chrome://extensions (Developer mode)
Privacy The extension only runs after you click its icon and "Start Loupe on this tab" — no background collection, no browsing tracking, no third-party analytics. Feedback goes only to your configured backend; with none set, it stays in local storage.

Loupe for Laravel

loupekit/laravel

PackagistPHP ^8.4Laravel 11 · 12 · 13LoupeServer v1.0.0100% coverage

A Composer package that installs the entire loop into any Laravel app: an in-app widget, comments in your database, per-user access control, a Laravel-served triage dashboard, and a first-party MCP server — session-authenticated, no HMAC wiring, no secrets in the browser.

Requirements

  • PHP 8.4+; Laravel 11 / 12 / 13 (9/10 intentionally unsupported).
  • Any Eloquent-supported database — MySQL, PostgreSQL, SQLite, SQL Server.
  • MCP is optional (laravel/mcp ^0.8), guarded by class_exists.

Install

bash
composer require loupekit/laravel
php artisan loupe:install   # config, migration, assets, provider stub
php artisan migrate
# add @loupeWidget before </body>, then open /loupe/dashboard
php artisan mcp:start loupe  # expose the backlog to Claude (optional)

loupe:install publishes the config, the migration, the browser assets to public/vendor/loupe, and app/Providers/LoupeServiceProvider.php (auto-registered in bootstrap/providers.php). The @loupeWidget directive injects the SDK <script> + Loupe.init({...}) — but only for users who pass loupe:use.

Publish individual pieces with the vendor:publish tags: loupe-config, loupe-migrations, loupe-assets, loupe-provider, loupe-views.

Routes

MethodPath (under /{path})AuthorizeName
GETv1/blobs/{id}publicloupe.blobs.show
GETv1/commentsuseloupe.comments.index
POSTv1/commentsuseloupe.comments.store
PATCHv1/comments/{id}useloupe.comments.update
DELETEv1/comments/{id}useloupe.comments.destroy
POSTv1/blobsuseloupe.blobs.store
GETdashboardadminloupe.dashboard

Authorization

Two abilities gate everything: loupe:use (see the widget, create/update comments) and loupe:admin (open the dashboard). Denied users never receive widget markup; API/dashboard return 403.

Authorization is decided in this order:

  1. Loupe::useWhen() / Loupe::adminWhen() closures (registered in code).
  2. Config closures authorize.use / authorize.dashboard.
  3. Frictionless access in the local environment, if allow_in_local (default true).
  4. Gate abilities loupe:use / loupe:admindeny by default (secure in production).
config:cache Config closures can't be used with config:cache. If you cache config in production, grant access via Gate abilities instead.

Configuration

Config · envDefaultPurpose
enabled · LOUPE_ENABLEDtrueMaster switch.
path · LOUPE_PATHloupeRoute prefix.
domain · LOUPE_DOMAINRoute domain.
project_key · LOUPE_PROJECT_KEYappProject identifier.
middleware.api / .dashboard['web','loupe.auth']Route middleware stacks.
guards · LOUPE_GUARDSdefault guardOrdered CSV of auth guards to try (e.g. web,admin).
authorize.use / .dashboardGate abilitiesClosures; null → the Gate abilities.
allow_in_localtrueFrictionless auth in local.
comment_modelComment::classBring your own Eloquent model.
user_resolverClosure → {id, name, email?}.
asset_url · LOUPE_ASSET_URLapp URLOrigin for bundled assets (bypasses ASSET_URL/CDN).
disk · LOUPE_DISKpublicFilesystem disk for screenshots (loupe/screenshots).

The dashboard is the exact @loupekit/dashboard bundle, vendored and configured via an injected window.__LOUPE__ (API base, project key, CSRF) — no secret in the browser. The data lands in a loupe_comments table (see the data model).

Multi-guard & CDN

Multiple auth guards

Apps using separate guards (e.g. web for users, admin for admins) could render the widget under one guard and authenticate the API under another, mismatching ids and producing a 403 "cannot post as another user". Set an ordered guard list and Loupe resolves identity the same way everywhere — widget author, gates, API middleware (loupe.auth), and the anti-spoof check.

.env
LOUPE_GUARDS=web,admin   # unset = the app's default guard (backward compatible)

Behind a CDN / ASSET_URL

Loupe resolves its published assets from the app URL via Url::asset(), bypassing ASSET_URL — so apps pointing ASSET_URL at a CDN (with only their Vite build uploaded) don't get 404s and a silently missing widget. Serve public/vendor/loupe elsewhere? Point LOUPE_ASSET_URL at it and re-run vendor:publish --tag=loupe-assets --force.

MCP server (Laravel)

With laravel/mcp installed, a server named loupe is registered automatically. Start it with php artisan mcp:start loupe. Unlike the Node MCP server, the tools read the app's own database directly — no HTTP hop, no admin key.

  • list_comments(status?, url?) — the backlog, newest first.
  • get_comment(id) — request + element HTML + computed styles + screenshot URL (or region rect).
  • update_status(id, status) — validates open / in_progress / done.

For different-subdomain SPA frontends (Sanctum), add EnsureFrontendRequestsAreStateful + auth:sanctum to middleware.api and use credentials: 'include'.

Troubleshooting

SymptomCause & fix
Widget missingUser fails loupe:use, @loupeWidget not in layout, or LOUPE_ENABLED=false.
419 on saveCSRF token missing — ensure the widget view sends X-CSRF-TOKEN.
403 on dashboardUser fails loupe:admin.
Screenshots 404Disk not public — run php artisan storage:link.
403 "cannot post as another user"Multi-guard app — set LOUPE_GUARDS=web,admin.
Widget missing only behind a CDNSet LOUPE_ASSET_URL; re-publish assets with --force.
mcp:start unknownInstall laravel/mcp:^0.8.

Feature guide

Cross-cutting SDK features ship simultaneously to the standalone SDK, the browser extension, and the Laravel widget — all three share the @loupekit/sdk core.

FeatureWhat it doesSince
Free commentsDrop a page-level note anywhere via the Note mode — no element, no screenshot (kind: "free").0.5.0
Draggable toolbarDrag the logo anywhere; position persists and the bar expands edge-aware (vertical on a side, horizontal on top/bottom).0.5.0
Re-anchoringMulti-signal fingerprint survives redeploys; detaches rather than mis-pin.0.1.0
Region shotDrag a free box (no element); anchored to element under center as fractional coords — tracks reflow, viewport & scroll.0.2.0
Offline modeOmit apiBase → localStorage adapter.0.1.0
Session auth (SDK)headers + credentials config for CSRF / cookie auth.0.3.0
Collapsible toolbarClick ◎ Loupe to collapse to just the logo.0.3.3
Font embeddingAwaits document.fonts.ready (capped) so screenshots use the right fonts, never hanging.0.3.4 / 0.4.1
Device / viewportDesktop/tablet/mobile badge end-to-end; deviceType() helper + DB column.0.4.0
SPA per-page commentsPatches pushState/replaceState + popstate to reload comments on client-side navigation.0.4.0
Mobile toolbarCompact, icon-only, pinned left; consistent icon+label layout.0.4.0
Instant region openComposer opens immediately; capture runs in background, attached on submit.0.4.1
Copy for ClaudeDashboard action copies a comment's full context to paste into Claude.
Redaction[data-loupe-redact] regions painted over before pixels leave the browser.0.1.0
Canary channelInstall prereleases with npm i @loupekit/sdk@next.0.2.0

Data model

Two tables in Postgres. Comments are keyed by normalized URL.

schema.sql
CREATE TABLE projects (
  project_key     TEXT PRIMARY KEY,
  name            TEXT,
  secret          TEXT,
  allowed_origins TEXT[] DEFAULT '{}',
  created_at      TIMESTAMPTZ DEFAULT now()
);

CREATE TABLE comments (
  id             TEXT PRIMARY KEY,
  project_key    TEXT REFERENCES projects ON DELETE CASCADE,
  url            TEXT,
  status         TEXT DEFAULT 'open',
  body           TEXT,
  author  JSONB,  anchor  JSONB,  context JSONB,  "offset" JSONB,
  kind    TEXT DEFAULT 'element',           -- additive
  region  JSONB,  viewport JSONB,             -- additive
  screenshot_url TEXT,
  created_at     TIMESTAMPTZ DEFAULT now()
);
CREATE INDEX comments_project_url ON comments (project_key, url);
Laravel table The Laravel package mirrors this as loupe_comments with a UUID id, a timestamps pair, and an indexed denormalized author_id for ownership checks. url is VARCHAR(500) and project_key VARCHAR(191) so the composite index fits MySQL's key-length limit.

Testing

Runner is Vitest + v8 coverage. DOM code runs under happy-dom (opt-in per file); the rest runs in Node.

bash
npm test              # run the suite once
npm run test:watch    # watch mode
npm run test:coverage # text + HTML report in coverage/

Coverage is ~91% lines / ~85% statements. Pure-logic modules sit at or near 100% (server/store, server/blobs). Deliberately under-covered: db.ts's pg branch (needs a live Postgres), the modern-screenshot redaction filter, and the rAF/MutationObserver-driven app code that needs a real layout engine. The extension's content.src.ts is validated by the manifest test + build rather than executed (it needs Chrome APIs + canvas).

The Laravel package runs an Orchestra Testbench suite across every PHP × Laravel combination in CI, under a 100% line-coverage gate (composer test:coverage-100).


Releasing & publishing

Loupe uses Semantic Versioning and ships through GitHub Releases backed by annotated tags (vX.Y.Z). MAJOR = breaking change to a public contract (SDK init, HTTP routes, MCP tool shapes, DB schema); MINOR = compatible features; PATCH = compatible fixes.

The release rule A release must update the docs: (1) CHANGELOG.md, (2) the landing-page version, (3) the wiki, (4) each published package's version. CI fails the release if CHANGELOG.md has no entry for the tag.

Two channels, two registries

ChannelTriggerdist-tagnpmGitHub Packages
Canaryevery push to mainnext@loupekit/*@mohamed-ashraf-elsaed/*
Stabletag vX.Y.Zlatest@loupekit/*@mohamed-ashraf-elsaed/*

Publishable to npm: @loupekit/shared, @loupekit/sdk, @loupekit/mcp. Publish shared first — mcp imports it at runtime. The Laravel package is split to its own repo by CI and picked up by Packagist; one vX.Y.Z tag ships npm + Packagist together. The extension ships to the Chrome Web Store.


Changelog

Follows Keep a Changelog + Semantic Versioning. Current release: 0.5.2 (2026-07-15).

0.5.22026-07-15Fixed
  • loupe-mcp binary now actually starts. After 0.5.1 shipped compiled JS, the installed binary still exited without serving: the entrypoint guard compared import.meta.url to argv[1], but the npm bin is a symlink — Node resolves it for import.meta.url while argv[1] stays the symlink path. The check now resolves symlinks before comparing, so loupe-mcp (and Glama-style introspection) works.
0.5.12026-07-15Fixed
  • @loupekit/mcp now installs and runs from npm. It shipped its raw index.ts and relied on runtime type-stripping, but Node won't strip types under node_modules — so npm i -g @loupekit/mcp + loupe-mcp failed for every consumer (and blocked containerized introspection on directories like Glama). It now compiles to dist/index.js (tsup) with bin/main pointing there. Running from a checkout (node packages/mcp/index.ts) is unchanged.
0.5.02026-07-14AddedChanged
  • Free comments. A new Note toolbar mode drops a page-level comment anywhere — no element, no screenshot. New kind: "free", positioned via the existing offset field as a document fraction, so no DB migration. Renders as a page-level item in the panel, dashboard, and both MCP servers.
  • Draggable, edge-aware toolbar. Drag the logo anywhere; the position persists and the bar expands inward so it's always on-screen — vertical on a left/right edge, horizontal along the top/bottom.
  • All packages aligned to 0.5.0@loupekit/server, the extension manifest.json, and the Node McpServer were stale at 0.2.0.
0.4.32026-07-13Fixed
  • Laravel: multi-guard identity resolution. Multi-guard apps no longer 403 with "cannot post as another user". Identity resolves through an ordered guard list (LOUPE_GUARDS) used everywhere — widget author, gates, API middleware (loupe.auth), anti-spoof. Backward compatible.
0.4.22026-07-12Fixed
  • Laravel: assets load behind a CDN. Loupe resolves published assets from the app URL via Url::asset(), bypassing ASSET_URL. New LOUPE_ASSET_URL override.
0.4.12026-07-12Fixed
  • Captures never hang. Font wait capped (~0.8s), capture timeout lowered to 6s, every capture wrapped in a hard outer timeout.
  • Region shot opens instantly. Composer appears immediately; screenshot captured in the background and attached on submit.
0.4.02026-07-12AddedChanged
  • Device / viewport tracking (end to end) — desktop/tablet/mobile badge in the panel, dashboard, and MCP payload; deviceType(width) helper + DB column.
  • Per-page comments on SPA navigation — reloads comments on client-side route changes.
  • Mobile toolbar — compact, icon-only, pinned left.
  • Region-shot renders the smallest covering element (faster, reliable font embedding).
0.3.42026-07-12Fixed
  • Screenshots no longer fall back to system fonts. Capture awaits document.fonts.ready (30s timeout) before rasterizing.
0.3.32026-07-12AddedFixed
  • Collapsible toolbar across all SDK surfaces.
  • Laravel: frictionless in local (allow_in_local, default true); baseline gates now deny by default.
0.3.22026-07-12Fixed
  • Laravel migration on MySQL. urlVARCHAR(500), project_keyVARCHAR(191) so the composite index fits MySQL's key-length limit.
0.3.12026-07-12ChangedFixed
  • SEO / GEO — schema.org author & entity metadata, AI-crawler allow-list, llms.txt.
  • Release/CI — synced package-lock.json so npm ci passes on tags.
0.3.02026-07-12AddedChanged
  • Loupe for Laravel (loupekit/laravel) — the whole loop as a Composer package; PHP 8.4+, Laravel 11/12/13, 100% line-coverage gate.
  • SDK gained headers + credentials config; dashboard reads window.__LOUPE__ for session auth.
0.2.12026-07-12Changed
  • Professional npm READMEs for sdk, mcp, shared. No code changes.
0.2.02026-07-12AddedChangedFixed
  • Free-region screenshots ("Region shot") — anchored to the element under its center as fractional coords; tracks reflow, a different viewport, and scrolling. New captureRegion hook.
  • Automated dual-registry publishing (next canary + latest stable).
  • Fixed SDK Shadow-DOM theming — vars moved to :host so they inherit.
0.1.02026-07-09Added
  • The first release — the full loop, end to end: sdk, server, dashboard, mcp, extension, shared. Vitest suite (~91%), Mermaid architecture docs, a GitHub Wiki, and an SEO/GEO landing page.

Loupe v0.5.2 · MIT © Mohamed Ashraf Elsaed · Pin feedback to the live UI. Hand it to Claude.

Generated from the repository source — README.md, CHANGELOG.md, docs/ARCHITECTURE.md, docs/TESTING.md, docs/LARAVEL.md, RELEASING.md, and each package's README.