extractbrand
v0.1.0
Published
Design-system-grade brand extraction from any domain — the ExtractBrand engine.
Readme
extractbrand
The ExtractBrand extraction engine, as an installable package. Given a URL it returns design-system-grade brand data: colours with roles, typography with availability, logo variants, visual style, brand intelligence, copywriting tone, spacing and accessibility targets.
Closed source. Published from packages/extractbrand in the
extractbrand repo.
Install
Published to the public npm registry — no git access to this repo required:
npm install extractbrand
# or: yarn add extractbrand / bun add extractbranddist (ESM + .d.ts) ships pre-built in the package; consumers never compile
the engine themselves.
Use
import { createBrandOrchestrator, stripV2Fields } from "extractbrand";
const orchestrator = createBrandOrchestrator();
const result = await orchestrator.run({
url: "https://stripe.com",
ctx: { user_id, organization_id },
});ctx is the tenant the run is attributed to — the two fields are stamped on the
session row and repeated in the read filter, so a session written under one
organization cannot be read from another.
Entry points
| Subpath | What it is |
| ---------- | ----------------------------------------------------------------------- |
| . | The server barrel: orchestrator factory, adapters, schemas, error types |
| ./client | Types and Zod schemas only — safe in a browser bundle |
| ./colour | Colour maths (contrast, harmony, snapping). No imports, browser-safe |
| ./usage | Token/cost arithmetic and the model price table |
| ./ai-sdk | The generation facade the specialists call |
| ./* | Any module by its source path, e.g. ./domain/schema |
What the host supplies
The engine ships its own AI, Playwright, Mongo and Blob adapters, so
createBrandOrchestrator() works with no wiring. Two things are the host's:
Progress. createBrandOrchestrator({ lifecycle }) takes an
ILifecycleEmitter; omitted, progress goes nowhere. A host publishing over
Inngest Realtime builds its own publisher and wraps it — the package maps stages
to progress frames but never names a channel, because the channel and its topic
vocabulary belong to the application:
const rt = createRealtimePublisher(publish, userId); // yours
const lifecycle = new InngestLifecycleEmitter(rt, sessionId); // ours
createBrandOrchestrator({ lifecycle });Cost. costMeter is optional. Without one a run records unmeasured rather
than a misleading zero.
Peer dependencies
zod, ai, @ai-sdk/google, mongoose, playwright-core, sharp and
node-vibrant are peers so the host owns one copy of each — zod especially,
where two copies mean two sets of schema identities. @vercel/blob and
braintrust are optional: without them screenshot upload and eval recording are
the only things that degrade.
Runtime requirements
GOOGLE_GENERATIVE_AI_API_KEY and BROWSERLESS_API_KEY are the whole list.
LOGO_DEV_PUBLISHABLE_KEY is deliberately never set — see PLAN.md; with it
absent the provider is inert and a test pins that.
The engine is Node-only. internal/utils/log-context imports node:async_hooks
statically, so an accidental import from a client component is a build error
naming that module rather than a silently disabled logger. ./client and
./colour have no path to it.
Vendored, not shared
internal/ holds helpers that were shared/ modules in the origin repo — the
logger, colour maths, cost arithmetic, the AI facade. They are copied in rather
than imported so the package stands alone. The consequence to know: the logger,
log-context and date exist in both this package and the ExtractBrand app.
That duplication is deliberate (a package cannot import its host) and bounded to
infrastructure boilerplate that encodes no product rule. Anything that does
encode one — colour maths, the price table — has its single home here, and
the app imports it back through ./colour and ./usage.
Development
bun run build # tsc → dist, then rewrite relative specifiers for Node ESM
bun run typechecktsc emits ESM but never adds file extensions, which a bundler tolerates and
Node does not. scripts/package/fix-esm-specifiers.mjs closes that and fails the
build on any specifier it cannot resolve, so the artifact runs under plain
node and not only inside a bundler.
