klar-cli
v3.0.0
Published
klar — color accessibility tools (OKCA contrast algorithm) for AI coding agents and the terminal
Downloads
39
Maintainers
Readme
klar
Color accessibility tools for AI coding agents and the terminal — powered by OKCA.
(Installed as the klar-cli package; the command is klar.)
All commands run locally with zero network calls.
What this is
klar ships with OKCA (OK Contrast Algorithm) — a WCAG-compatible contrast
algorithm on the familiar WCAG scale, with zero false passes against the
WCAG 2.x threshold.
Since okca 2.0.0 that guarantee holds by construction, not just by sampling:
OKCA ≤ WCAG is proven across the sRGB gamut via an exact identity plus
interval-verified lemmas (docs/FP0_PROOF.md),
with a gamut-wide invariant enforced in okca's CI. The empirical battery — 1,249
pairs across Tailwind, the GOV.UK Design System, and USWDS v3.x — remains at zero
false passes and is now a cross-check on the proof rather than the basis for it.
The problem OKCA solves:
Hot pink (
#ff69b4) on near-black (#1a1a1a) scores 6.6:1 under WCAG and passes AA. It's also illegible. A same-luminance neutral grey scores identically — the WCAG formula is blind to saturation.Full write-up: Hot Pink Passes WCAG. That's the Problem.
Try it in 10 seconds
npx klar-cli contrast "#ff69b4" "#1a1a1a"Install
# Global CLI
npm install -g klar-cli
klar contrast "#fff" "#000"
# Or run without installing
npx klar-cli contrast "#fff" "#000"Contrast algorithms
Built-in (no plugin required): okca (default), wcag2, deltaE.
Additional contrast algorithms are available as optional, separately-installed
plugins discovered by the klar-plugin-* naming convention. Each plugin is
maintained and licensed independently — see PLUGINS.md.
Global Options
| Flag | Description |
|------|-------------|
| -v, --version | Print version |
| -h, --help | Print help |
| --no-plugins | Disable discovery of contrast-algorithm plugins for this run; use built-in algorithms only |
Plugin loading can also be restricted via the KLAR_PLUGINS allowlist,
KLAR_NO_PLUGINS, or a package.json "klar": { "plugins": [...] } entry — see
SECURITY.md.
Output Modes
Every command supports three output modes (except variants which lacks -q):
| Flag | Behavior | Stdout format |
|------|----------|---------------|
| (none) | Human-readable with ANSI color swatches | Multi-line text |
| --json | Machine-readable structured data | Pretty-printed JSON |
| -q, --quiet | Single value only, no labels | Plain text, one line |
--json and -q write to stdout only, with no ANSI codes, making them safe for piping.
Command Reference
contrast — Calculate contrast between two colors
klar contrast <color1> <color2> [options]Arguments:
| Arg | Description |
|-----|-------------|
| color1 | First color — hex, rgb(), oklch() |
| color2 | Second color — hex, rgb(), oklch() |
Options:
| Flag | Default | Description |
|------|---------|-------------|
| -t, --type <type> | okca | Algorithm: okca, wcag2, deltaE (built-in), plus any installed plugins |
| --gamut-map <method> | css | Out-of-gamut resolution — see Out-of-gamut colors |
| --allow-out-of-gamut | off | Exit 0 instead of 1 when an input is outside the gamut |
| --json | | JSON output |
| -q, --quiet | | Print only the numeric contrast value |
Built-in contrast algorithms:
| Type | Range | Meaning |
|------|-------|---------|
| okca | 1 to 21 (20.9 reachable) | OKCA: OKLCH-native, WCAG-compatible ratio. 0 false passes vs WCAG, proven across the sRGB gamut. Polarity-aware — light-on-dark caps at 20.9, dark-on-light at 20. |
| wcag2 | 1 to 21 | Traditional WCAG 2.x luminance ratio |
| deltaE | 0 to 100 | Delta E 2000 perceptual color difference |
Additional algorithms are available as optional plugins — see PLUGINS.md.
JSON schema:
{
"contrast": 20.9, // number — the calculated value
"type": "okca", // string — algorithm used
"colorOne": "#ffffff", // string — first color as provided
"colorTwo": "#000000", // string — second color as provided
"gamut": {
"map": "css", // string — the mapping applied
"outOfGamut": false // boolean — true if either color is outside sRGB
}
}When a color is outside the gamut, a measured object is added listing only
the colors that were mapped, and the command exits 1:
{
"contrast": 6,
"type": "okca",
"colorOne": "oklch(0.79 0.22 25)",
"colorTwo": "#070e16",
"gamut": {
"map": "css",
"outOfGamut": true,
"measured": { "colorOne": "#ff938b" } // what the figure was computed on
}
}Quiet output: single number, e.g. 20.9
Out-of-gamut colors
A color authored in OKLCH can sit outside the sRGB gamut. It has to be resolved to a displayable color before it can be measured, so the number describes that mapped equivalent — not the color in your token file. That is a real answer to a different question than the one asked, so the command exits 1 rather than passing silently.
klar contrast "oklch(0.79 0.22 25)" "#070e16" -q
# 6 exit 1 — measures #ff938b, the mapped equivalent
klar contrast "oklch(0.79 0.22 25)" "#070e16" -q --allow-out-of-gamut
# 6 exit 0 — same number, failure waived
klar contrast "oklch(0.79 0.22 25)" "#070e16" -q --type wcag2 --gamut-map none
# 9.2 the colorimetric value of the color as authoredklar resolves out-of-gamut colors with CSS Color 4 gamut mapping — chroma reduction until the color fits — everywhere it maps a color. klar takes no position on what any particular browser does: engines vary and change, and tracking them would mean maintaining an engine-and-version matrix indefinitely. The spec is a stable reference; an implementation is not.
| --gamut-map | Behavior |
|------|----------|
| css (default) | CSS Color 4 chroma reduction |
| clip | Clamp each channel into range — a well-defined operation, no claim about who performs it |
| none | Measure the authored color, unmapped. Refused for okca and plugins, whose guarantees are established across the sRGB gamut |
Two things worth holding onto:
- Reducing chroma will not move the number until the color is in gamut.
Chroma reduction maps every out-of-gamut chroma at a given lightness and hue
onto the same boundary color, so
0.22 → 0.15changes nothing. Reduce untiloutOfGamutis false; only then does further reduction help. - Two different tokens can produce the same figure. Because of the above,
gamut.outOfGamutis the only thing distinguishing them in the output.
KLAR_GAMUT_MAP sets the mapping process-wide — useful for pinning the previous
behavior while migrating. There is deliberately no environment variable that
waives the failure: a global mute gets set once in CI and never removed.
Scripts using -q get the value on stdout, whether to trust it from the exit
code, and which color was mapped from stderr. --json carries all three.
Examples:
klar contrast "#ffffff" "#000000"
klar contrast "#fff" "#000" --type wcag2
klar contrast "#fff" "#000" --type okca --json
klar contrast "oklch(50% 0.2 240)" "#000" -q
klar contrast "rgb(59,130,246)" "#ffffff" --type deltaE
klar contrast "oklch(0.79 0.22 25)" "#070e16" --allow-out-of-gamutpair — Generate a random accessible color pair
klar pair [options]Options:
| Flag | Default | Description |
|------|---------|-------------|
| --json | | JSON output |
| -q, --quiet | | Print only two hex values space-separated |
The generated pair is always a dark/light combination chosen for high contrast; its lightness range is not configurable.
JSON schema:
{
"colorOne": "#1c2333", // string — foreground hex
"colorTwo": "#ffe3e8", // string — background hex
"contrast": 12 // integer — OKCA score, rounded to a whole number
// (unlike `contrast`, which reports one decimal)
}Quiet output: two hex values space-separated, e.g. #1c2333 #ffe3e8
Examples:
klar pair
klar pair --json
klar pair -qmeta — Get OKLCH metadata for a color
klar meta <color> [options]Arguments:
| Arg | Description |
|-----|-------------|
| color | Color to analyze — hex, rgb(), oklch() |
Options:
| Flag | Description |
|------|-------------|
| --json | JSON output |
| -q, --quiet | Print L C H space-separated |
JSON schema:
{
"color": "#3b82f6", // string — input color
"lightness": 0.62, // number — OKLCH L (0–1 scale)
"chroma": 0.19, // number — OKLCH C (0–~0.4)
"hue": 259.81, // number — OKLCH H (0–360 degrees)
"saturation": 30.17 // number — saturation percentage
}Quiet output: L C H space-separated, e.g. 0.62 0.19 259.81
Examples:
klar meta "#3b82f6"
klar meta "rgb(59, 130, 246)" --json
klar meta "#000000" -qvariants — Generate a perceptually-spaced color variant grid
Two modes: adaptive (default) and fixed-step.
Adaptive mode (default)
The grid walks chroma first (at the base lightness), then lightness
independently per chroma level — so each column explores the full gamut
range at that chroma. Within each column, adjacent cells differ by at
least 11 Delta E 2000 (configurable via --min-delta). Only in-gamut
cells are emitted — no wasted tokens on out-of-gamut placeholders.
klar variants <color> [--min-delta <n>] [--json]The grid size depends on the input color's sRGB gamut:
- Neutral colors (e.g.
#808080) produce larger grids because they have wide gamut boundaries in both lightness and chroma. - High-chroma colors (e.g.
#3b82f6,#ff6600) produce smaller grids because their gamut is narrow — there are fewer perceptually distinct variants.
Each column has constant chroma with lightness decreasing top-to-bottom. This is correct behavior, not a limitation — every cell in the output is a meaningfully different color that an agent can act on.
Fixed-step mode
Activated by passing --light-steps and/or --chroma-steps, or by using --color-space hsl.
Divides the space into uniform intervals and is not gamut-aware: cells outside
sRGB are emitted as "color": "", and they are frequently the majority —
--light-steps 10 --chroma-steps 5 on #3b82f6 returns 50 cells, 36 of them empty.
Filter before use, since passing "" to another klar command is a usage error:
klar variants "#3b82f6" --light-steps 4 --chroma-steps 3 --json \
| jq -r '.[][] | select(.color != "") | .color'klar variants <color> --light-steps <n> --chroma-steps <n> [--color-space <space>] [--json]Arguments:
| Arg | Description |
|-----|-------------|
| color | Base color — hex, rgb(), oklch() |
Options:
| Flag | Default | Description |
|------|---------|-------------|
| --min-delta <n> | 11 | Minimum Delta E 2000 between vertically adjacent cells in a column (adaptive mode) |
| --light-steps <n> | | Fixed lightness steps — activates fixed-step mode |
| --chroma-steps <n> | | Fixed chroma steps — activates fixed-step mode |
| --color-space <space> | oklch | oklch or hsl (hsl forces fixed-step mode) |
| --json | | JSON output |
No -q mode (grid data doesn't reduce to a single value).
JSON schema: 2D array [row][col], each cell:
{
"color": "#7f8895", // string — hex (empty "" only in fixed-step mode)
"lightness": 0.623, // number — OKLCH L
"chroma": 0.023, // number — OKLCH C
"hue": 259.81, // number — OKLCH H
"deltaE": 18, // number — Delta E 2000 from base color
"deltaChroma": -88, // number — % chroma change from base
"deltaLightness": 0 // number — % lightness change from base
}In adaptive mode, every cell has a valid hex color — no empty strings.
In fixed-step mode, cells with "color": "" are out-of-gamut and should be skipped.
Examples:
# Adaptive (default) — perceptually-spaced, zero waste
klar variants "#3b82f6"
klar variants "#ff6600" --min-delta 15
klar variants "#3b82f6" --json
# Fixed-step — uniform intervals, may have out-of-gamut cells
klar variants "#3b82f6" --light-steps 10 --chroma-steps 5
klar variants "#3b82f6" --color-space hsl --light-steps 5 --chroma-steps 3match — Match chroma between two colors
Adjusts the first color's chroma to match the second color's chroma in OKLCH space.
klar match <color1> <color2> [options]Arguments:
| Arg | Description |
|-----|-------------|
| color1 | First color of the pair |
| color2 | Second color of the pair |
Neither argument is fixed: klar rebuilds whichever color can adopt the other's
chroma while staying inside the sRGB gamut, and returns the other untouched.
When both directions are in gamut it converges on the higher of the two
chromas. So color1 may well be the one that comes back changed — read
colors rather than assuming a direction. chroma is the resulting shared
value.
Options:
| Flag | Description |
|------|-------------|
| --json | JSON output |
| -q, --quiet | Print two adjusted hex values space-separated |
JSON schema:
{
"success": true, // boolean — false when no in-gamut match exists
"colors": ["#f86d25", "#3b82f6"], // [string, string] | null
"chroma": 0.188 // number — matched chroma value
}When success is false, neither color can adopt the other's chroma within the
sRGB gamut: colors is null and the command exits 1 (see Exit Codes).
Quiet output: two hex values space-separated, e.g. #f86d25 #3b82f6
Examples:
klar match "#ff6600" "#3b82f6"
klar match "#ff6600" "#3b82f6" --json
klar match "#ff6600" "#3b82f6" -qlightness — Get min/max lightness for a color in sRGB gamut
Returns the OKLCH lightness boundaries where the color's hue + chroma remain displayable in sRGB.
klar lightness <color> [options]Arguments:
| Arg | Description |
|-----|-------------|
| color | Color to analyze — hex, rgb(), oklch() |
Options:
| Flag | Description |
|------|-------------|
| --json | JSON output |
| -q, --quiet | Print min and max space-separated |
Exits 1 when the color's chroma is not renderable at any lightness.
JSON schema:
{
"originalCoords": [0.623, 0.188, 259.81], // [L, C, H] in OKLCH
"lightMin": 0.466, // number|null — minimum L in gamut
"lightMax": 0.648, // number|null — maximum L in gamut
"gamut": { "outOfGamut": false } // was the input renderable as given?
}When no lightness renders at all, the bounds are null and the command
exits 1. oklch(0.5 0.9 25) is not displayable at any lightness, so there is
no range to report — and reporting one would be a confident, specific, false
answer to the question the command exists to answer:
{ "originalCoords": [0.5, 0.9, 25], "lightMin": null, "lightMax": null,
"gamut": { "outOfGamut": true } }The reported range can legitimately exclude the input's own lightness:
oklch(0.45 0.22 25) is not renderable at L=0.45, but the same chroma and hue do
render between 0.543 and 0.667. That band is the honest answer for a color whose
authored lightness does not work.
Quiet output: min max space-separated (4 decimal places), e.g. 0.4660 0.6480.
Nothing is printed when there is no range; the exit code and stderr carry it.
Examples:
klar lightness "#3b82f6"
klar lightness "#3b82f6" --json
klar lightness "#3b82f6" -qfind — Find a color meeting a target contrast
Binary-searches OKLCH lightness to find a variation of the reference color that meets the target contrast against the base color. The target is a floor (a minimum): the result is guaranteed to be at or above it, returning the color closest to the target from above.
klar find <base-color> <reference-color> [options]Arguments:
| Arg | Description |
|-----|-------------|
| base-color | Color to keep fixed (e.g. background) |
| reference-color | Color to adjust (e.g. text) |
Options:
| Flag | Default | Description |
|------|---------|-------------|
| --target <n> | (required) | Target contrast value |
| -t, --type <type> | okca | Algorithm: okca, wcag2, deltaE (built-in), plus any installed plugins |
| --tolerance <n> | 0.5 | Acceptable overshoot above the target — the band within which the search stops early. Never accepts a result below the target. |
| --allow-desaturation | off | Permit reducing chroma to reach the target |
| --json | | JSON output |
| -q, --quiet | | Print only the adjusted hex color |
JSON schema:
{
"adjustedColor": "#2563eb", // string — result hex, always renderable
"actualContrast": 4.6, // number — achieved contrast (of adjustedColor)
"iterations": 10, // number — binary search steps
"success": true, // boolean — true when actualContrast >= target
"reason": "ok", // string — branch on this, never on `message`
"axesAdjusted": ["lightness"], // string[] — what moved to reach the target
"deltaE": 3, // number — perceptual drift from the reference
"message": "", // string? — human prose; wording is not stable
"oklch": { // object? — OKLCH of adjusted color
"l": 0.523,
"c": 0.188,
"h": 259.8
},
"gamut": { // object — was the reference renderable as given?
"outOfGamut": false
}
}reason is the machine-readable outcome:
| Value | Meaning |
|-------|---------|
| ok | Target met |
| lightness-exhausted | Desaturation was not permitted and lightness alone fell short. A fix exists — see resolvableBy |
| unreachable | The target is above the contrast ceiling for this base. No color meets it; the base has to change |
| chroma-exhausted | Defensive: no chroma at any lightness reached a target that should have been reachable |
When success is false the command exits 1 (see Exit Codes)
and adjustedColor holds the closest reachable color — always renderable,
never the input echoed back. On lightness-exhausted the payload also carries
resolvableBy: what would solve it, reported without being applied.
{
"success": false,
"reason": "lightness-exhausted",
"adjustedColor": "#a9000b", // closest reachable, still renderable
"actualContrast": 6,
"resolvableBy": {
"chroma": 0.149, // the least saturation sacrifice that works
"lightness": 0.365, // the lightness it needs
"deltaE": 10 // what it costs, perceptually
}
}Chroma: normalized always, traded never (unless you say so)
Two chroma operations are involved, and only one is optional.
Gamut normalization is mandatory. An out-of-gamut reference is chroma-reduced
onto the gamut boundary before the search begins, because otherwise there is no
renderable color to return. It is reported under gamut and on stderr, never as
an entry in axesAdjusted — it is not something you chose to trade.
Target-reaching desaturation is off by default. Once the color is renderable,
only lightness moves. Trading brand saturation for contrast is a design decision,
not a computation, so when lightness runs out find exits 1 and quotes the fix
rather than applying it:
klar find "oklch(1 0 0)" "oklch(0.45 0.22 25)" --target 9.5
# → chroma 0.149 at lightness 0.365 would reach the target (10 ΔE from the reference)
# Apply with --allow-desaturation, or take it to a human.
klar find "oklch(1 0 0)" "oklch(0.45 0.22 25)" --target 9.5 -q --allow-desaturation
# #7a0005 exit 0--allow-desaturation runs the identical search, so the quote and the fix cannot
disagree. The search keeps as much chroma as possible first, then moves lightness
as little as possible — lightness is free to move because adjusting it is what
find does; the flag grants permission for chroma.
Quiet output: adjusted hex, e.g. #2563eb. On a soft failure the closest
color is still printed, but the command exits 1 — so $(klar find … -q)
inside && won't apply a non-compliant color.
Examples:
klar find "#ffffff" "#3b82f6" --target 4.5
klar find "#000000" "#cccccc" --target 4.5 --type wcag2
klar find "#ffffff" "#3b82f6" --target 4.5 --json
klar find "#ffffff" "#3b82f6" --target 4.5 -q
klar find "#ffffff" "#3b82f6" --target 5.5 --type okca --tolerance 0.1
# This blue tops out at 5.9 on white, so a target of 7 exits 1
klar find "#ffffff" "#3b82f6" --target 7plugins — List registered contrast algorithm plugins
klar plugins list [options]Lists every contrast-algorithm plugin klar discovered, with its source, package
name, version, and resolved path — so you can see exactly what third-party code
klar will run. Built-in algorithms (okca, wcag2, deltaE) are always
available and are not plugins. See PLUGINS.md and SECURITY.md.
Options:
| Flag | Description |
|------|-------------|
| --json | JSON output |
| -q, --quiet | Print only plugin ids, one per line |
JSON schema: array of objects, one per registered plugin:
[
{
"id": "cvd-brettel", // string — the --type value
"displayName": "CVD (Brettel)", // string — human-readable name
"description": "…", // string — one-line summary
"source": "global", // "project" | "global" | null
"packageName": "klar-plugin-cvd-brettel", // string | null
"version": "1.0.0", // string | null
"resolvedPath": "/abs/path/to/plugin" // string | null
}
]Quiet output: plugin ids, one per line.
Examples:
klar plugins list
klar plugins list --json
klar plugins list -qColor Input Formats
All commands accept colors in these formats:
| Format | Example |
|--------|---------|
| Hex (6 digit) | "#3b82f6" |
| Hex (3 digit) | "#fff" |
| RGB function | "rgb(59, 130, 246)" |
| OKLCH function | "oklch(62% 0.19 260)" |
Shell quoting: Always quote colors containing # or parentheses to prevent shell interpretation:
klar contrast "#fff" "#000" # Hex — must quote #
klar meta "rgb(59, 130, 246)" # RGB — must quote parens
klar meta "oklch(62% 0.19 260)" # OKLCH — must quote parensAdvanced Workflows
For AI agents: See AGENT_PLAYBOOK.md for end-to-end art direction workflows — palette building, dark mode translation, accessibility auditing, and more.
Scripting with --json and jq
# Extract just the contrast score from a check
klar contrast "#1a1a2e" "#e94560" --json | jq '.contrast'
# Compare the built-in algorithms for the same pair
for algo in okca wcag2 deltaE; do
echo -n "$algo: "
klar contrast "#1a1a2e" "#e94560" --type $algo -q
done
# Generate a pair and immediately check all algorithms
PAIR=$(klar pair --json)
FG=$(echo "$PAIR" | jq -r '.colorOne')
BG=$(echo "$PAIR" | jq -r '.colorTwo')
klar contrast "$FG" "$BG" --type wcag2 --jsonFinding accessible color variations
# Given a brand blue, find a shade that passes WCAG AA on white
klar find "#ffffff" "#3b82f6" --target 4.5 --type wcag2 -q
# Find accessible text for dark background
klar find "#1a1a2e" "#22c55e" --target 4.5 --type okca --json
# A saturated color may have no passing shade — find only moves lightness,
# so check the exit code rather than applying the output blindly.
klar find "#1a1a2e" "#e94560" --target 4.5 --type okca --json # exits 1
# Check if the result actually meets the target
ADJUSTED=$(klar find "#ffffff" "#3b82f6" --target 4.5 --type wcag2 -q)
klar contrast "#ffffff" "$ADJUSTED" --type wcag2 -qAnalyzing gamut boundaries
# Get the lightness range for a color
klar lightness "#3b82f6" --json | jq '{min: .lightMin, max: .lightMax}'
# Check how much room to darken or lighten
META=$(klar meta "#3b82f6" --json)
RANGE=$(klar lightness "#3b82f6" --json)
echo "Current L: $(echo $META | jq '.lightness')"
echo "Range: $(echo $RANGE | jq '.lightMin') to $(echo $RANGE | jq '.lightMax')"Variant exploration
# Adaptive grid (default) — all cells are in-gamut and perceptually distinct
klar variants "#3b82f6" --json | jq '[.[][] | {color, lightness, deltaE}]'
# List all variant hexes for downstream use
klar variants "#3b82f6" --json | jq '[.[][] | .color]'
# Find the variant most different from the base
klar variants "#3b82f6" --json \
| jq '[.[][]] | sort_by(-.deltaE) | .[0]'
# Use a tighter grid for more choices (minDelta=8)
klar variants "#808080" --min-delta 8 --json | jq '[.[][]] | length'
# Fixed-step mode when you need a specific grid size
klar variants "#ff6600" --light-steps 5 --chroma-steps 3 --json \
| jq '[.[][] | select(.color != "") | {color, lightness, deltaE}]'Chroma matching for brand palettes
# Match chroma between brand primary and secondary
klar match "#ff6600" "#3b82f6" --json | jq '.chroma'
# Verify both colors now share the same chroma
MATCHED=$(klar match "#ff6600" "#3b82f6" --json)
C1=$(echo "$MATCHED" | jq -r '.colors[0]')
C2=$(echo "$MATCHED" | jq -r '.colors[1]')
echo "Color 1 chroma: $(klar meta "$C1" --json | jq '.chroma')"
echo "Color 2 chroma: $(klar meta "$C2" --json | jq '.chroma')"Combining commands in pipelines
# Full accessibility audit for a color pair
FG="#1a1a2e"
BG="#e94560"
echo "=== Color Pair Audit ==="
echo "Foreground: $FG"
klar meta "$FG" --json | jq '{L: .lightness, C: .chroma, H: .hue}'
echo "Background: $BG"
klar meta "$BG" --json | jq '{L: .lightness, C: .chroma, H: .hue}'
echo "---"
echo "OKCA: $(klar contrast "$FG" "$BG" --type okca -q)"
echo "WCAG: $(klar contrast "$FG" "$BG" --type wcag2 -q)"
echo "DeltaE: $(klar contrast "$FG" "$BG" --type deltaE -q)"Exit Codes
klar follows a grep-style convention so scripts can branch on the outcome:
| Code | Meaning |
|------|---------|
| 0 | Success — the operation produced a satisfying result (also --help and --version) |
| 1 | Soft failure — a valid operation whose answer is negative |
| 2 | Usage error — invalid input, unknown flag or command, or a missing argument |
Soft failures (1) are:
| Command | Condition |
|---------|-----------|
| contrast | An input is outside the gamut, so the figure describes a mapped equivalent rather than the color you asked about. Waive with --allow-out-of-gamut |
| find | Target unachievable under the constraints given — see reason |
| lightness | The color's chroma is not renderable at any lightness, so there is no range |
| match | No in-gamut chroma match exists |
2 covers both an unusable value (klar contrast notacolor "#000") and an
unusable invocation (klar find "#fff" "#000" with no --target, a
misspelled flag, an unknown command, or no command at all).
Every usage error writes to stderr and leaves stdout empty, so 1
always means "klar understood you, and the answer is no." Most carry an
Error: prefix; the exception is klar with no command, which prints the
help text to stderr instead. Match on the exit code, not the message.
On a soft failure (1) the result payload is still written to stdout —
find prints the closest reachable color, contrast prints the number it
measured, --json reports "success": false — so the exit code guards a
pipeline while the data remains inspectable:
# Apply only when a compliant color was actually found
ADJUSTED=$(klar find "$BG" "$FG" --target 4.5 -q) && apply "$ADJUSTED" \
|| echo "no color meets the target on this background"
# Gate on trustworthiness as well as value: exit 1 here means the number
# describes a mapped equivalent, not the token as authored
OKCA=$(klar contrast "$FG" "$BG" -q) || echo "warning: $FG is outside sRGB"The one exception to "stdout carries the payload" is lightness with -q when
no range exists — there are no numbers to print, and printing a pair would be
the defect this replaced. The exit code and stderr carry it.
set -e and out-of-gamut input
Under set -e, a bare assignment aborts the script the first time contrast
meets an out-of-gamut color — leaving a partial result on stdout that reads
like a complete short run. Guard the assignment when auditing colors you
supply:
for T in "${TOKENS[@]}"; do
if OKCA=$(klar contrast "$T" "$BG" -q); then
echo "$T -> $OKCA"
else
echo "$T -> $OKCA (outside sRGB — figure describes the mapped color)"
fi
doneColors klar produces — variants cells, find results, match output — are
in gamut by construction and never trigger this.
Architecture
The CLI uses standalone service classes (no framework DI). Contrast algorithms are loaded via a plugin registry — built-in types (okca, wcag2, deltaE) plus any optional contrast-algorithm plugins discovered by the klar-plugin-* convention at runtime (see PLUGINS.md).
Development
npm install
npm run build # Compile TypeScript to dist/
npm test # Run tests
npm run dev # Watch mode (tsc --watch)License
MIT
