{"_id":"@broberg/lens-client","name":"@broberg/lens-client","dist-tags":{"latest":"0.1.0"},"versions":{"0.1.0":{"name":"@broberg/lens-client","version":"0.1.0","description":"Thin client for the HOSTED Lens (lens.cardmem.com) — createLensClient({baseUrl,token}).capture()/.runFlow() with cold-start retry + self-healing LocateSpec, and an optional mountable Hono proxy (createLensProxy) so a product frontend hits /api/lens/* same","type":"module","license":"MIT","sideEffects":false,"main":"./dist/index.cjs","module":"./dist/index.js","types":"./dist/index.d.ts","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js","require":"./dist/index.cjs"},"./hono":{"types":"./dist/proxy.d.ts","import":"./dist/proxy.js","require":"./dist/proxy.cjs"}},"scripts":{"build":"tsup","test":"vitest run","typecheck":"tsc --noEmit"},"peerDependencies":{"hono":">=4"},"peerDependenciesMeta":{"hono":{"optional":true}},"devDependencies":{"@types/node":"^22","hono":"^4.6.0","tsup":"^8.3.0","typescript":"^5.6.0","vitest":"^2.1.0"},"keywords":["lens","screenshot","browser-automation","self-healing-locators","e2e","hosted","client","proxy","hono","broberg"],"repository":{"type":"git","url":"git+https://github.com/broberg-ai/components.git","directory":"packages/lens-client"},"publishConfig":{"access":"public"},"gitHead":"a9f10a2195c27039f30072255b01a8b1ef717485","_id":"@broberg/lens-client@0.1.0","bugs":{"url":"https://github.com/broberg-ai/components/issues"},"homepage":"https://github.com/broberg-ai/components#readme","_nodeVersion":"25.7.0","_npmVersion":"11.10.1","dist":{"integrity":"sha512-EsxPKEUXQuOJoe+CAeQgSdz9f/wP2hv7bVtdBs7Wb7Y3hLbuWORVwxLQXaRcuOMyMBtIgADUgHiEC00Yr2aY9g==","shasum":"6f871955ae49fd861e06924b1451c55f72fdf5e9","tarball":"https://registry.npmjs.org/@broberg/lens-client/-/lens-client-0.1.0.tgz","fileCount":14,"unpackedSize":70174,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEQCIQDB+53DtrMNJ6PVZ2yqYj/TiieEsjxEihecWc1XK28mLgIfVldDIHY8iFHRPsCz8s4PG0r0JR44l2JOO+Wf1MV+Hg=="}]},"_npmUser":{"name":"cbroberg","email":"cb@webhouse.dk"},"directories":{},"maintainers":[{"name":"cbroberg","email":"cb@webhouse.dk"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/lens-client_0.1.0_1783115501214_0.6611243319187954"},"_hasShrinkwrap":false}},"time":{"created":"2026-07-03T21:51:41.063Z","0.1.0":"2026-07-03T21:51:41.333Z","modified":"2026-07-03T21:51:41.529Z"},"maintainers":[{"name":"cbroberg","email":"cb@webhouse.dk"}],"description":"Thin client for the HOSTED Lens (lens.cardmem.com) — createLensClient({baseUrl,token}).capture()/.runFlow() with cold-start retry + self-healing LocateSpec, and an optional mountable Hono proxy (createLensProxy) so a product frontend hits /api/lens/* same","homepage":"https://github.com/broberg-ai/components#readme","keywords":["lens","screenshot","browser-automation","self-healing-locators","e2e","hosted","client","proxy","hono","broberg"],"repository":{"type":"git","url":"git+https://github.com/broberg-ai/components.git","directory":"packages/lens-client"},"bugs":{"url":"https://github.com/broberg-ai/components/issues"},"license":"MIT","readme":"# @broberg/lens-client\n\nA **thin client for the hosted Lens** (`lens.cardmem.com`). Screenshot pages and\ndrive self-healing flows over HTTP — **no Playwright, no browser** on your side.\n\n```bash\nnpm i @broberg/lens-client\n```\n\n## The three-package split\n\n| You need to… | Use |\n| --- | --- |\n| Call the **hosted** Lens over HTTP (no browser) | **`@broberg/lens-client`** |\n| Run a real browser yourself (capture + flow) | `@broberg/lens-engine` (Playwright) |\n| Mint / validate a Lens session (auth/compliance) | `@broberg/lens` (dep-free) |\n\n## Usage (server-side)\n\n```ts\nimport { createLensClient } from \"@broberg/lens-client\";\n\nconst lens = createLensClient({\n  baseUrl: process.env.LENS_CLOUD_URL,   // default https://lens.cardmem.com\n  token: process.env.LENS_CLOUD_TOKEN,   // Bearer service token — server-side only\n});\n\nconst shot = await lens.capture({ url: \"https://example.com\", mode: \"fullPage\" });\n// → { run_id, screenshot_url, dom_hash, status: \"ok\", width, height, final_url, title }\nconst png = shot.screenshot_url ? await lens.fetchArtifact(shot.screenshot_url) : null;\n\nconst run = await lens.runFlow({\n  base_url: \"https://appstoreconnect.apple.com\",\n  steps: [\n    { action: \"goto\", url: \"/apps\" },\n    { action: \"click\", target: { role: \"button\", name: \"New Version\" } },  // self-healing LocateSpec\n    { action: \"fill\",  target: { label: \"Version Number\" }, value: \"1.2.0\" },\n  ],\n});\n```\n\n`baseUrl`/`token` default to `LENS_CLOUD_URL` / `LENS_CLOUD_TOKEN`.\n\n## Self-healing locators\n\nA step `target` is a plain string (CSS selector / bare `data-testid`) or a\n`LocateSpec` — `{ testid?, css?, role?, name?, label?, placeholder?, text?, exact?, nth?, vision? }` —\ntried in fixed priority: `testid → css → role → label → placeholder → text → vision`.\n\nEach step result carries **`resolved_via`** (which layer matched), so you can log a\n*degraded-match* alert when a flow drifted off `testid` onto a fuzzier layer.\n\n## Errors — a failed flow is data, not an exception\n\nA **failing step stops the flow and pins a screenshot**; the flow comes back as a\nnormal result with `status: \"failed\"` — read `steps` to see *which* step failed and\n*why* (never a thrown exception that loses context):\n\n```ts\nconst run = await lens.runFlow(manuscript);\nif (run.status === \"failed\") {\n  const bad = run.steps.find((s) => s.status === \"failed\");\n  // bad.index, bad.action, bad.error, bad.screenshot_url\n}\n```\n\nOnly **transport/auth failures throw** a `LensClientError` (`.kind` = `auth` on 401,\n`unavailable` on 503, `network` after retries, `http` otherwise).\n\n## Cold start\n\nThe hosted Lens auto-stops when idle. The client **pre-warms** with `GET /health`\nand retries a `502` / network error (1–2 tries, backoff). It **never** retries a\n`401` (bad token) or `503` (ship-dark) — those are terminal. Tune with\n`{ retries, retryBackoffMs, prewarm }`.\n\n## Artifact token gotcha\n\n`screenshot_url` points at `/artifact?key=…` and needs the **same Bearer** to fetch.\n`fetchArtifact(url)` attaches it **only** when the URL is same-origin as `baseUrl` —\nnever leaking the token to a foreign host.\n\n## Browser proxy — `@broberg/lens-client/hono`\n\nSo a product's **frontend** can call Lens without ever seeing the token, mount the\nproxy on your own server (same-origin `/api/lens/*`, token stays server-side):\n\n```ts\nimport { createLensProxy } from \"@broberg/lens-client/hono\";\n\napp.route(\"/api/lens\", createLensProxy());   // token from LENS_CLOUD_TOKEN\n\n// browser:\nconst lens = createLensClient({ baseUrl: \"/api/lens\" });   // no token in the browser\n```\n\n`hono` is an **optional peer** — only needed for the proxy. The core client (`.`) has\n**zero runtime dependencies**.\n\n## API\n\n```ts\nfunction createLensClient(opts?: LensClientOptions): LensClient;\n//   .capture(body) → CaptureResult   .runFlow(body) → FlowResult\n//   .health() → boolean              .fetchArtifact(url) → Uint8Array\nfunction createLensProxy(opts?: LensProxyOptions): Hono;   // \"@broberg/lens-client/hono\"\nclass LensClientError extends Error;   // .kind, .status\n```\n\nMIT · part of the [`@broberg/*`](https://github.com/broberg-ai/components) shared-library family.\n","readmeFilename":"README.md","_rev":"1-16dae33e8b5c63d0e2b64b316ad5f8b9"}