AI Decision System
Replayable, offline-verifiable records for AI-assisted go/no-go decisions: experiment promotions, typed model answers and human-approved actions.
A dependency-free processing core, storage as an interface, and graceful degradation when a format is unavailable.
Developer toolsTypeScript · CSSGitHub @Sthabiso10OnlineOwner unconfirmed
Have you tried it? One tap is enough. No sign-in needed.
No updates detected yet. GitHub releases will appear here.
Owners of public repositories under a personal GitHub account can sign in to verify ownership and manage the introduction themselves. No administrator approval is needed.
Sign in, generate a verification file, and commit it to this repository’s default branch within 30 minutes. Files on other branches or forks do not count. Then check the file below to manage the project without administrator approval. Keep the file to retain your access.
If needed, you will go to sign-in. After signing in, return here and press your chosen verification button again.
Run /nomorevibe verify in your project folder to verify ownership and manage the introduction yourself.
Verify in one minuteAsk about other verification options
Composable image optimization for Node.js. Zero-dependency core, pluggable storage adapters and codecs.
Taken from the repository README
Loading the full README. The saved excerpt is shown below.
Image optimization you can actually audit.
Resize, compress and convert images, then hand them to whatever storage you already use.
A processing core with no dependencies, and everything heavy behind an interface you control.
Pre-1.0 and moving. The API will change before 1.0. Pin an exact version and read the
changelog before upgrading. AVIF output depends on how your sharp binary
was built, so treat it as best-effort rather than guaranteed.
Eight lines, six optimized files in S3, and a ready-to-paste srcset.Read the full README ↗[!WARNING] Pre-1.0 and moving. The API will change before 1.0. Pin an exact version and read the changelog before upgrading. AVIF output depends on how your
sharpbinary was built, so treat it as best-effort rather than guaranteed.
npm install @lens-image/core sharp @lens-image/adapter-s3 @aws-sdk/client-s3
import { ImageOptimizer } from '@lens-image/core';
import { S3Adapter } from '@lens-image/adapter-s3';
const optimizer = new ImageOptimizer({
adapter: new S3Adapter({ bucket: 'my-images', region: 'us-east-1' }),
});
const result = await optimizer.optimize({
source: './photo.jpg',
quality: 80,
formats: ['webp', 'jpg'],
sizes: [{ width: 1200 }, { width: 600 }, { width: 300 }],
});
result.formats.webp.srcset;
// 'https://...-300w.webp 300w, https://...-600w.webp 600w, https://...-1200w.webp 1200w'
Eight lines, six optimized files in S3, and a ready-to-paste srcset.
npm install
npm run dev
Drop an image in and watch the file size move as you drag the quality slider. The encoding is real, done by your browser's own WebP encoder, and the storage keys come from the same functions the server calls.
Most image libraries make you choose between "does everything, owns your stack" and "does one thing, you wire up the rest". Lens is the second kind, with the wiring included.
| Zero dependencies in the core | npm install @lens-image/core adds exactly one package to your lockfile. Not one plus forty. A CI check enforces it. |
| Storage is an interface | S3, filesystem, Cloudinary, or forty lines of your own for R2, GCS, Backblaze. |
| The codec is an interface too | sharp is an optional peer. Swap it, stub it in tests, or skip it entirely if you only validate and store. |
| Degrades instead of failing | AVIF unsupported on your platform? You get WebP and a warning, not a 500. |
| Errors you can branch on | Every failure is a LensError with a stable code and a suggested HTTP status. |
| Package | What it does | Dependencies |
|---|---|---|
@lens-image/core | Processing engine, adapter contract, upload handler | 0 |
@lens-image/adapter-local | Filesystem storage | 0 |
@lens-image/adapter-s3 | S3, R2, MinIO, Spaces, B2 | 0 (AWS SDK is a peer) |
@lens-image/adapter-cloudinary | Cloudinary | 0 |
@lens-image/react | Headless upload hooks | 0 (React is a peer) |
@lens-image/core also exposes @lens-image/core/browser,
a Node-free subset (sniffing, validation, key templating) that bundles for browsers and edge
runtimes. You can reject a bad file from its first 4 KB, before the upload starts.
Being precise, because this is the sort of claim that deserves it.
@lens-image/core has zero entries in dependencies. Installing it adds one package to
your lockfile and downloads no native binaries. That is enforced in CI by
scripts/release-check.mjs, not just promised here.
Actual pixel work needs sharp, which is a native module. Lens treats the codec as a
pluggable ImageEngine and declares sharp as an optional peer dependency, imported
dynamically only when you first encode something.
So:
ImageEngine is three methods. Point it at squoosh, a WASM codec,
or an ImageMagick shell-out.The claim is "core is dependency-free and the heavy thing is your explicit choice", not "image processing happens by magic".
The full adapter contract:
interface StorageAdapter {
readonly name: string;
upload(file: StorageFile, ctx: StorageContext): Promise<StorageObject>;
exists?(key: string): Promise<boolean>; // unlocks cache: 'storage'
remove?(key: string): Promise<void>; // unlocks optimizer.delete()
getUrl?(key: string): string;
dispose?(): Promise<void> | void;
}
One required method. A complete Google Cloud Storage adapter:
import { Storage } from '@google-cloud/storage';
import type { StorageAdapter } from '@lens-image/core';
export class GCSAdapter implements StorageAdapter {
readonly name = 'gcs';
#bucket = new Storage().bucket(this.bucketName);
constructor(private bucketName: string) {}
async upload(file) {
const object = this.#bucket.file(file.key);
await object.save(file.data, {
contentType: file.contentType,
metadata: { cacheControl: file.cacheControl },
});
return {
key: file.key,
url: `https://storage.googleapis.com/${this.bucketName}/${file.key}`,
size: file.data.byteLength,
};
}
async exists(key) {
const [exists] = await this.#bucket.file(key).exists();
return exists;
}
async remove(key) {
await this.#bucket.file(key).delete({ ignoreNotFound: true });
}
}
Lens handles retries, concurrency limiting and error wrapping around it.
@lens-image/core ships a Request to Response handler. It works in Next.js, Remix, Hono,
Bun and Deno without a framework adapter, because it only uses web standards.
Server, app/api/upload/route.ts:
import { ImageOptimizer, createUploadHandler } from '@lens-image/core';
import { S3Adapter } from '@lens-image/adapter-s3';
const optimizer = new ImageOptimizer({
adapter: new S3Adapter({ bucket: 'uploads', region: 'us-east-1' }),
});
export const POST = createUploadHandler({
optimizer,
maxFiles: 5,
async authorize(request) {
const session = await auth(request);
return session ? undefined : Response.json({ error: 'Unauthorized' }, { status: 401 });
},
options: { formats: ['webp', 'jpg'], sizes: [{ width: 1200 }, { width: 600 }], thumbnail: true },
});
Client:
import { useImageUpload } from '@lens-image/react';
function Uploader() {
const { upload, items, progress, isUploading, error } = useImageUpload({
endpoint: '/api/upload',
maxBytes: 10 * 1024 * 1024,
accept: ['image/*'],
});
return (
<>
<input type="file" multiple onChange={(e) => upload(e.target.files)} disabled={isUploading} />
{isUploading && <progress value={progress} max={100} />}
{error && <p role="alert">{error.message}</p>}
</>
);
}
Progress is real, from XMLHttpRequest.upload.onprogress, and weighted by file size. fetch
cannot report request-body progress in any shipping browser, so a fetch-based hook would have
to fake the bar.
Every failure is a LensError with a stable code and a suggested HTTP status:
import { LensError } from '@lens-image/core';
try {
await optimizer.optimize({ source, validate: { maxBytes: 5_000_000 } });
} catch (error) {
if (LensError.is(error, 'VALIDATION_FAILED')) {
return Response.json({ error: error.message }, { status: error.status }); // 422
}
throw error;
}
| Code | Status | Means |
|---|---|---|
INVALID_SOURCE | 400 | source was not a shape Lens understands |
SOURCE_UNREADABLE | 400 | The file could not be read |
UNSUPPORTED_INPUT | 415 | Bytes are not a recognisable image |
VALIDATION_FAILED | 422 | Failed a size, dimension or format rule |
INVALID_OPTIONS | 400 | Contradictory or out-of-range options |
ENGINE_UNAVAILABLE | 500 | No codec, usually sharp is not installed |
ENCODE_FAILED | 500 | The codec threw |
ALL_FORMATS_FAILED | 500 | Nothing could be produced, fallback included |
UPLOAD_FAILED | 502 | Storage failed after retries |
ADAPTER_REQUIRED | 500 | The operation needs an adapter |
ADAPTER_UNSUPPORTED | 501 | The adapter lacks an optional method |
ADAPTER_MISCONFIGURED | 500 | Bad adapter construction options |
ABORTED | 499 | Your AbortSignal fired |
A per-format failure is a warning, not an exception:
const result = await optimizer.optimize({ source, formats: ['avif', 'webp', 'jpg'] });
// AVIF unavailable on this platform? You still get WebP and JPEG.
result.warnings;
// [{ code: 'format_unsupported', format: 'avif', message: '...' }]
Only a total failure throws. This is not hypothetical: prebuilt sharp binaries do not all ship AV1.
Things that are on unless you turn them off, because getting them wrong is expensive:
.jpg that is actually an SVG is detected as SVG.LocalAdapter rejects any key that resolves outside its root.packages/
core/ the engine, adapter contract, upload handler 0 deps
adapter-local/ filesystem 0 deps
adapter-s3/ S3 and anything that speaks it SDK is a peer
adapter-cloudinary/ Cloudinary over plain fetch 0 deps
react/ headless upload hooks React is a peer
docs/ the documentation site private
The build is tsc twice per package, once for ESM and once for CJS, and nothing else. No
bundler, by design: a library whose selling point is a readable dependency graph should not
need a hundred dev packages to produce a dist.
Strict mode with noUncheckedIndexedAccess on. Tests run on Node's built-in runner with no
framework.
npm install
npm run dev # the docs site on :3000
npm run build # all five packages, dual ESM and CJS
npm test # 213 tests
npm run typecheck
npm run release:check # enforces the promises made above
| Command | |
|---|---|
npm run dev | Docs site on :3000 |
npm test core | One package |
npm run test:watch | |
npm run docs:build | Static export to packages/docs/out |
npm run hero:build | Regenerate the docs imagery through Lens itself |
npm run set-scope @you | Rename the npm scope across the repo |
npm run example | Run examples/basic.mjs |
Requires Node 18.19 or newer.
Every app that accepts an image eventually grows the same three hundred lines: a resize loop, a format matrix, a naming scheme, an S3 client, a retry, and a nagging suspicion that none of it validates the input properly. I had written that file more than once and did not want to write it again.
So the opinions here are the ones you arrive at after getting it wrong. Keys are content-addressed because cache invalidation was the recurring bug. Formats fail independently because one missing codec taking down an upload is infuriating. The pixel-count guard exists because a byte-size limit does not stop a decompression bomb. sharp is a peer dependency because a native binary appearing in a lockfile without being asked for is how you lose an afternoon.
The docs hero is not stock art either. It is built by running photographs through Lens, and the figure printed under it is what that run measured.
Issues and PRs welcome. CONTRIBUTING.md covers the rules that are not
negotiable, which are the ones the whole project exists for: zero dependencies in the core,
SDKs as peers, a Node-free browser entry point, and errors with stable codes.
MIT