Units-LE

io.github.nolindnaidoov1.0.0Updated Oct 4, 2026

Extract durations, sizes, percentages and frequencies in one base unit, refusing ambiguous ones.

VerifiedSTDIODesktop onlyDeveloper ToolsData & Analytics

Overview

AI-generated overview

Extracts durations, sizes, percentages and frequencies from documents and reports each in one base unit, refusing ambiguous quantities by name.

What it does
Exposes an extract_units tool that scans supplied content for quantities — a number welded to a unit — and returns each with its source text, base value, line, column and key path. Durations normalize to milliseconds, bytes to bytes, percentages to ratios and frequencies to hertz. Ambiguous or malformed quantities such as 500m, 1.5KB or 1,5s keep their row but return a named reason instead of a guessed value. It never rewrites the document and performs no arithmetic or unit conversion beyond normalization.
When to use it
Useful when reviewing configuration files, values files, runbooks or retention policies where timeouts, memory limits, percentages or frequencies appear in mixed notations. It helps compare quantities written differently across services and flags entries a reader could misread, such as 500m or 1MB.
Requirements
Runs locally over stdio via npx units-le-mcp, so Node.js is needed. No environment variables, API key or configuration are required. It takes content as an argument and reads no files and makes no network requests.
Before you install
It only reads the content passed to it and never writes or modifies documents, and it makes no network calls. Findings are capped at 500 by default with a truncation flag. Its text scan can produce occasional false positives inside opaque blobs such as hashes or UUIDs, so each finding should be checked by its line and column.

Installation

In SourceWeft

  1. Open Units-LE in the dashboard and add it to a workspace.
  2. Enable the server for the chats that should use its tools.

Desktop only via STDIO. STDIO servers start a local process, so they need the SourceWeft desktop host.

Other MCP clients

Follow the launch instructions in the repository.

README

[Units-LE Logo]

Units-LE: One Quantity, One Unit

Find every quantity in a document — durations, sizes, percentages, frequencies — with its value in one base unit, and refuse the ambiguous ones by name
YAML · TOML · JSON · INI · dotenv · CSV · and anything else, scanned as text

[Install from VS Code Marketplace] [Open VSX downloads] [units-le-mcp on npm] [units-le on crates.io] [LE Tools]


Useful? A star or rating is how other developers find it — ★ GitHub · ★ Open VSX · ★ Marketplace

What it does

timeout: 30s in one service and timeout: 30000 in another are the same number of milliseconds, and nothing about the text says so. memory: 1GB and memory: 1GiB differ by 7% and look identical at a glance.

Press Ctrl+Alt+Q (Cmd+Alt+Q on Mac) and every quantity in the active document — a number welded to a unit — opens in a report beside the editor: as the document wrote it, in one base unit so two of them can be compared, with its line, column and key path. A quantity it cannot read unambiguously keeps its row and gets a reason instead of a guess. Works in VS Code and in VS Code–based editors like Cursor and VSCodium (installable from Open VSX).

  • Before a migration — every timeout and memory limit in a values file, in milliseconds and bytes
  • Reviewing a config change — the 500m that means minutes to one reader and millicores to another
  • Against a runbook or a retention policy — the limit the document states, checked against the one the config sets

It never rewrites a document, and never guesses.

Install

WhereWhat you getInstall
VS CodeThe report, in your editor, on a keystrokeMarketplace
Cursor, VSCodium, WindsurfThe same extensionOpen VSX
A terminal or a CI stepA whole tree, with exit codescargo install units-le · crates.io
Any MCP agent, via Nodeextract_units over stdionpx units-le-mcp · npm
ZedThe MCP server as a context serveradd it by hand (no listing yet)

A refusal is a finding

This is the whole product. Any tool can multiply 30s by a thousand; what makes this one usable in a review is that it will not pretend.

It seesReasonWhat comes back
500m, 10M, 5k, 1y, 100Mbambiguous_unitno base value — m is minutes, milliseconds or Kubernetes millicores; M is mega- or minutes; k/K/G/T/P are prefixes with no unit after them; a calendar year has no fixed length; a lowercase b is bits by the standard and bytes in most software that writes it
1.5KB, 1.5KiBfractional_bytesno base value — a byte count has no fractional part. 1.5KiB is exactly 1536 bytes and is still refused: the refusal is about the category, not the arithmetic
1,5s, 1.000s, 1.2.3slocale_separatorno base value — one and a half, or fifteen hundred? This tool does not infer a locale. Narrowed so 0.825s and 2.5s read normally
1h + 30m, 30s*2compound_arithmeticno base value — an expression, not a quantity, reported as one finding rather than two
1MB, 256KBsi_iec_hazardthe base value, plus the reason — see below
99999…PiBout_of_rangeno base value — it does not fit in 128 bits, and is refused rather than wrapped

Every one of them keeps its row, its source text, its position and a sentence a person can act on. A refusal is never a dropped row and never a guess.

si_iec_hazard annotates; it does not withhold

It is the one reason that still answers. MB is 10^6 by the standard, so 1MB comes back as 1000000 bytes and carries reason: "si_iec_hazard" — because a great deal of software writes MB and means 2^20. Reporting nothing would be less useful than reporting the standard answer with the flag attached, and picking 2^20 would be the guess this tool exists not to make.

So it is not counted as refused. A row with si_iec_hazard is the only row that carries both a base and a reason; every other reason means the base is null.

A bare number with no unit is not a finding at all. timeout: 30 yields nothing here — that is numbers-le's question, and the boundary is what keeps the two tools distinct.

Four dimensions

DimensionBase unitUnits read
durationmillisecondsns us µs μs ms s sec secs min mins h hr hrs d day days w · compounds 1h30m · ISO-8601 PT1H30M P1DT2H P2W PT0.5S
bytesbytesB · SI kB KB MB GB TB PB · IEC KiB MiB GiB TiB PiB · Kubernetes Ki Mi Gi Ti Pi
percentratio% — 15% is 0.15
frequencyhertzHz kHz KHz MHz GHz THz

Case is part of the symbol. MB is a megabyte and Mb is a megabit; folding them would silently multiply by eight, so Mb is refused rather than treated as a synonym.

1h30m is a grammar and 1h + 30m is arithmetic. The first is one quantity written in two parts. Inside a compound the parts disambiguate each other — m alone is refused, and m between h and s can only be minutes, because a compound is written largest-first. That ordering is required rather than assumed, so 30s1h is not a quantity at all.

Physical units are a non-goal, not a gap. Length, mass, temperature belong to a different question, and reading them would mean unit algebra (m/s²), which is what uom is for. See crate/SPEC.md.

Formats

JSON, YAML, CSV, TOML, INI and dotenv are parsed. Everything else is scanned as text — a Kubernetes manifest, a Terraform file, a Markdown table of limits, a log — so the files where quantities actually live yield them rather than nothing.

Unlike numbers-le's text scan, this one has a shape to look for, so v1.2.3 yields nothing rather than two numbers. Every format with a shape carries a key path — cache.ttl, limits[0], server.timeout, TIMEOUT — and every finding carries a 1-based line and column, the column in UTF-16 units, which is the number your editor shows.

Its false-positive class is measured rather than imagined. A run inside an opaque blob still reads as a quantity: 001d in a UUID is one day, and a base64 hash ending /2w== is two weeks. The boundary characters that let those through are the same ones that let -30s and ttl=30s through. Over crate/fixtures/documents/opaque.txt — 280 lines of lockfile hashes, container digests, git object names, UUIDs and signing material, none of which is a quantity — the scan reports 5 false findings, 1.8%, and a test prints that number on every run. Each one carries its line and column, which is what makes it a row you discard rather than a number you trust.

It has no opinions

No "this timeout is too low". No defaults database. No conversion flag, no rewriting, no arithmetic. It reports what a document says and what that means in one base unit; which limits are right is the reviewer's call.

Use it from an AI agent

The same engine runs as an MCP server, so an agent can read quantities directly instead of converting units by hand.

EditorHow
VS Code 1.101+Nothing to install — the extension registers extract_units with agent mode
ZedNo listing yet — add the MCP server by hand
Claude Codeclaude mcp add units-le -- npx -y units-le-mcp
Cursor, Windsurf, anything elsepoint it at npx units-le-mcp
extract_units(content, format?, filename?, dimension?, maxResults?)

It returns the report the editor renders, as data — quantities capped at 500 by default with meta.truncated. It reads no files and makes no network requests. Published as units-le-mcp on npm and as io.github.nolindnaidoo/units-le in the MCP registry. It answers exactly as the Rust CLI's server does: one corpus runs against both, and a differential test feeds both thousands of generated documents in every format — broken ones included, so each parser's error text is compared too.

Configuring it by hand — any host with an MCP config file
json
{  "mcpServers": {    "units-le": {      "command": "npx",      "args": ["-y", "units-le-mcp"]    }  }}

Or install it once with npm install -g units-le-mcp and point at units-le-mcp. It needs no environment variables, no API key and no configuration of its own. To check it:

bash
echo '{"jsonrpc":"2.0","id":1,"method":"tools/list"}' | npx -y units-le-mcp

The CLI

The same extraction runs over a tree from a terminal or a CI step: a Rust CLI in crate/, sharing one corpus with the extension — crate/fixtures/ — so the two can never read a quantity differently.

[units-le in a terminal]

bash
units-le .                                 # every quantity in the tree, one JSON report per fileunits-le --dimension duration config/      # only the timeoutsunits-le --strict config/                  # exit 2 if anything was refusedcat values.yaml | units-le --stdin --format yamlunits-le mcp                               # extract_units and units_le_scan over MCP on stdio

Exit codes follow grep — 0 quantities found, 1 none found, 2 the question was malformed. A refusal does not change the exit code; --strict makes it one.

Commands

CommandDescription
Units-LE: Extract Quantities (Ctrl+Alt+Q / Cmd+Alt+Q)Every quantity in the active document, as the editor holds it
Units-LE: Open SettingsOpen Units-LE settings
Units-LE: Help & TroubleshootingBuilt-in documentation

Settings

SettingDefaultDescription
units-le.dimensions[]Report only these dimensions; empty reports all four. A refusal that names no dimension is always reported
units-le.openResultsSideBySidetrueOpen the report beside the current editor
units-le.copyToClipboardEnabledfalseAlso copy the report to the clipboard
units-le.notificationsLevelsilentall = every notification, important = warnings + errors, silent = errors only
units-le.safety.enabledtrueWarn before reading a large file
units-le.safety.fileSizeWarnBytes1000000Size above which the warning appears
units-le.statusBar.enabledtrueShow the status bar item
units-le.telemetryEnabledfalseLocal-only event log (see Privacy)

Languages

Twelve languages besides English:

German · Spanish · French · Indonesian · Italian · Japanese · Korean · Portuguese (Brazil) · Russian · Ukrainian · Vietnamese · Chinese (Simplified)

Both halves are covered — the manifest (command titles, setting names and descriptions) and everything shown while the extension runs (notifications, the status bar and the report's headings). A refusal's sentence and a parser's error are the engine's English, identical to the CLI's.

Privacy & security

  • No network access. The extension never sends data anywhere. The telemetryEnabled setting only writes events to a local Output Channel you can inspect (Units-LE).
  • It reads the active document and nothing else, and never writes to it.
  • The MCP server holds the same line. It takes content as an argument and returns data: no filesystem access, no network calls, no telemetry.
  • Error notifications redact home directories and credential-shaped fragments.

Documentation

WhatWhere
What the tool is allowed to say — the grammar, the refusals, the output contract, non-goalscrate/SPEC.md
How the extension is built and held together — architecture, invariants, toolchain, releaseAGENTS.md
How the CLI is built and held togethercrate/AGENTS.md
What changedCHANGELOG.md · crate/CHANGELOG.md
The tool's page, and the other fifteenletools.dev/tools/units-le

Performance

InputSizeFoundTimeRateScan speed
YAML config0.88 MB50,000107.63 ms464,557/sec8.2 MB/s
TOML config0.73 MB30,00049.08 ms611,287/sec14.9 MB/s
Log, scanned as text3.25 MB120,000327.9 ms365,967/sec9.9 MB/s

Median of 7 runs after warmup, on Apple M5 Pro, 24 GB RAM, Node 24.3.0. Inputs are generated by scripts/benchmark.ts rather than checked in, so the sizes above are exactly what was measured. Reproduce with bun run benchmark.

These are machine-specific and are not asserted in CI — a benchmark that gates a build only tells you how busy the runner was.

Testing

MetricCoverage
Statements86.16%
Branches78.39%
Functions95.23%
Lines88.06%

531 test cases across 11 files, plus an integration suite that runs in a real VS Code extension host and an end-to-end test that installs the built .vsix into a clean profile.

Generated from a real run — coverage/coverage-summary.json and coverage/test-results.json — by scripts/coverage-readme.js; CI fails if this section drifts. Reproduce with bun run test:coverage, and the case count is the one vitest prints.

More from the LE family

Sixteen single-purpose tools for the work in front of every model. Each ships a Rust CLI and an MCP server. One page: letools.dev

Get it out

  • String-LE — Extract every string in a codebase, with its position, so a person can read them
  • Numbers-LE — Extract every hardcoded number in a codebase, so a person can check them
  • Units-LE — Extract every quantity with its unit, normalized, and refuse the ambiguous ones by name
  • Dates-LE — Extract every date and timestamp, and the exact instant each one resolves to
  • IDs-LE — Extract every UUID, ULID, NanoID, ObjectId and Snowflake, and decode the time inside
  • IPs-LE — Extract every IP address, CIDR block and MAC, normalized and classified by scope
  • URLs-LE — Extract every URL in a codebase, with its protocol and exact position
  • Paths-LE — Extract every file path in a codebase, and say whether it still points at anything
  • Colors-LE — Extract every color in a codebase, and say which ones are not in your palette

Check it

  • Regex-LE — Find every regex in a codebase, and report which can be driven into catastrophic backtracking
  • Versions-LE — Find where one dependency is constrained differently across a repository's manifests
  • i18n-LE — Identify the i18n library a project uses, then audit its catalogs by that library's rules
  • Scrape-LE — Check whether a page is scrapeable before the scraper is written, and say when it cannot tell

Guard it

  • Secrets-LE — Find hardcoded credentials in a codebase, and never print one into the report
  • EnvSync-LE — Compare the dotenv files in a tree, and say which keys are missing from which
  • Unicode-LE — Find the Unicode that hides meaning — bidi controls, invisibles, homoglyphs, mixed scripts

Each stands on its own: no shared crate, no published core. Where two of them agree, it is because the same answer was right twice.

Contact — nolindnaidoo.com · GitHub · LinkedIn

Also by nolindnaidoo

Rust — pixelcoords and pixelactions are one loop: pixelcoords answers where, pixelactions acts there. Their own tools, their own voice — not part of the LE family.

License

MIT © nolindnaidoo

Source: README.md at commit b6c320b

Tools

0
Tool metadata has not been indexed yet.

Version history

1
  1. v1.0.0LatestOct 4, 2026