Compare products by evidence, not feature-count theater.
FitLens researches 2–8 similar products, separates facts from claims and inference, then scores the shortlist against the way you actually work.
Quick start · How it works · Evidence model · Privacy · Architecture
Product pages are written to persuade you. FitLens is built to help you decide.
Give it product URLs, your workflow, and the criteria that matter. It collects official pages, documentation, and linked GitHub repositories; keeps verified facts separate from vendor claims and bounded inference; makes unknowns visible; and produces a recommendation you can inspect, reweight, test, save, refresh, and export.
| Research a shortlist | Compare 2–8 products in one report. Reorder candidates without losing their evidence, scores, pricing, privacy findings, or trial results. |
| Read official registries | Enrich npm, PyPI, Apple App Store, and Chrome Web Store listings with normalized versions, requirements, licensing, release, rating, and repository signals when published. |
| Keep a candidate inbox | Paste product links as you discover them, add notes and tags, search or archive them, then promote any 2–8 candidates into a comparison without fetching a page. |
| See the evidence quality | Every claim is labeled verified, vendor, or inferred. Coverage, freshness, source diversity, contradictions, and calibrated confidence are separate signals. |
| Review before trusting | Accept, reject, edit, and annotate individual claims. Rejected evidence stops affecting confidence, coverage, freshness, and conflict checks without erasing the audit trail. |
| Decide for your workflow | Use editable criteria and reusable templates. Reweight the decision instantly without another model request. |
| Reuse how you decide | Save complete decision profiles with workflow context and criteria, then apply them to the next product category. |
| Keep the messy parts visible | Pricing uncertainty, missing disclosures, conflicting claims, and source failures stay explicit instead of being smoothed into a confident answer. |
| Build a local research memory | Search and filter up to 50 saved reports. Reopen a decision, reuse its inputs, add manual evidence, refresh sources, and review deterministic diffs. |
| Keep a durable decision record | Export JSON, Markdown, self-contained HTML, an ADR, or a print-optimized PDF; share-safe copies remove private context, notes, trials, revisions, and manual evidence. |
| Replay a decision offline | Every live run records non-secret version/hash provenance. A private bounded replay bundle can rerun validation and deterministic finalization without fetching sources or paying for another model call. |
npx fitlens demoThat renders a complete comparison offline from a bundled sample: scored
candidates, labeled evidence, calibrated confidence, conflicts, and unknowns.
Nothing is fetched and no credentials are read, so the output is identical on
every machine. Add --locale zh-CN for Chinese.
Output format follows where it is going, so the same command is readable for a human and parseable by a script:
| Destination | Default | Override |
|---|---|---|
| Terminal | Styled text with score bars and evidence badges | --format json, --format markdown |
| Pipe or redirect | JSON | --format text, --format markdown |
--output <path> |
Follows --format; never writes ANSI |
— |
NO_COLOR is honored.
export OPENAI_API_KEY=...
npx fitlens analyze \
--url https://example-a.com \
--url https://example-b.com \
--context "What you actually need this tool to do, in a sentence or two." \
--template developer-tools \
--format markdownfitlens --help lists every command: demo, analyze, replay, watch,
and doctor. Run fitlens doctor if something looks wrong; it prints redacted
diagnostics.
Candidate URLs can be piped instead of repeated as flags:
cat urls.txt | fitlens analyze --context "..." # one URL per lineTwo flags make analyze usable in a pipeline. --replay-out <path> writes the
run's offline replay bundle so the exact decision can be re-derived later with
fitlens replay --bundle <path>, no network or model call. --min-confidence <n> exits with code 2 when the winning candidate's confidence is below n,
so a scheduled job can flag a shaky recommendation:
fitlens analyze --url ... --url ... --context "..." --min-confidence 70 \
|| echo "recommendation is not confident enough to act on"Exit codes: 0 success, 1 error, 2 a threshold gate was not met.
The CLI covers analysis, replay, and watching. The interactive workbench — reweighting criteria, reviewing evidence claim by claim, saved research library, exports — runs locally from a clone. Requires Node.js 20.18.1+ and pnpm 10.
git clone https://github.com/wp043/fitlens.git
cd fitlens
pnpm install --frozen-lockfile
pnpm devOpen http://localhost:3000.
The bundled cmux/Otty report works without an API key. For a live comparison,
enter a key for the current browser session or create .env.local with an
OpenAI or Anthropic key:
# One of these two providers:
OPENAI_API_KEY=
OPENAI_MODEL=gpt-5.6-luna
# ANTHROPIC_API_KEY=
# ANTHROPIC_MODEL=claude-sonnet-5
GITHUB_TOKEN=
FITLENS_BROWSER_FALLBACK=0GITHUB_TOKEN is optional; it only raises GitHub API rate limits.
FITLENS_BROWSER_FALLBACK=1 opts into rendering thin JavaScript application
shells. Install the matching local browser once with:
pnpm exec playwright install chromiumStatic HTML remains the default. The renderer starts only when the initial page has very little readable text and recognizable application-shell markers. It blocks media, fonts, WebSockets, service workers, non-GET requests, oversized responses, and excessive request counts; scripts and data requests are fetched through the same public-address and redirect policy as the static collector. If rendering is unavailable or produces less content, FitLens keeps the static result.
Run the cross-platform preflight before reporting an environment problem:
pnpm fitlens doctor
pnpm fitlens doctor --check-playwright
pnpm fitlens doctor --json --output .fitlens/doctor.jsonDoctor checks the supported Node and pnpm versions, the checkout, and model
provider configuration. Playwright/Chromium readiness is opt-in because the
browser renderer is optional. A live provider check is also opt-in with
--probe-provider; it makes one five-second GET /models request, records only
the result status, and never stores a credential or response body. Diagnostic
objects are redacted again before printing or writing: known secret environment
values, bearer tokens, key-like fields, URL credentials, and the home directory
are removed. Review any bundle before sharing it, as with every diagnostic
artifact. See Troubleshooting for symptom-based fixes.
flowchart LR
A[2–8 product URLs] --> B[Safe source collection]
W[Your workflow] --> D[Structured comparison]
C[Editable criteria] --> D
B --> E[Evidence bundle]
E --> D
D --> F[Fit scores]
D --> G[Confidence + coverage]
D --> H[Unknowns + conflicts]
F --> I[Inspectable recommendation]
G --> I
H --> I
I --> J[Trial · notes · refresh · export]
- FitLens validates every candidate URL, collects its official page or supported registry/store metadata, follows high-value links for pricing, documentation, privacy, security, and release history, then inspects one linked GitHub repository when available.
- The configured model turns those sources into one strict report schema. It cannot add criteria, omit products, or silently invent source URLs.
- Deterministic local code recalculates weighted fit, evidence coverage, freshness, conflicts, privacy risk, and confidence.
- The browser stores the report locally. Refreshing collects new sources and produces a field-level diff against the previous revision.
If one candidate cannot be collected, the model is not called. FitLens marks the affected row, preserves the draft, and lets you correct the URL or retry.
| Signal | Question it answers | Source |
|---|---|---|
| Fit | How well does this product match this workflow and these priorities? | Model dimension judgments + local weighted calculation |
| Confidence | How much should I trust the available evidence? | Deterministic calibration from verification, diversity, freshness, transparency, and conflicts |
| Coverage | How much useful public evidence did FitLens find? | Deterministic evidence quantity, level, source count, and implementation visibility |
A closed-source product can have high fit and low confidence. An open-source product can have excellent coverage and still be the wrong tool for your workflow. FitLens keeps those cases distinct.
| Level | What it means | Typical source |
|---|---|---|
| Verified | Directly checkable public evidence | Source code, license, repository metadata, README |
| Vendor | A statement made by the product owner | Official website or documentation |
| Inferred | A bounded conclusion from the available material | Missing public links or likely workflow implications |
Not finding a capability is not proof that it does not exist. Inference stays labeled as inference, published prices remain verbatim, and missing privacy disclosures remain unknown rather than being interpreted as safe or unsafe.
FitLens also detects opposing claims about pricing, accounts, telemetry, source availability, offline use, and self-hosting. A conflict is a review prompt, not an automatic verdict: both sources remain linked so you can decide which is newer and better supported.
- Capture hands-on findings, screenshots, pricing checks, and privacy notes as manual evidence. Manual entries survive source refreshes.
- Review collected claims in place. Corrections preserve the model's original wording, and review status, notes, and edits survive source refreshes.
- Turn the generated trial plan into a saved pass/fail checklist with notes.
- Run the same hands-on task against any two shortlisted products and keep a separate win/loss/tie standing that does not pretend to be model evidence.
- Flag aging and stale sources, then refresh the report when the decision matters again.
- Search saved research by product, workflow, evidence claim, source URL, recommendation, unknown, or note.
- Capture links in a separate local candidate inbox, deduplicate tracking URLs, and build a shortlist only when the comparison is worth running.
- Export complete JSON/Markdown backups, offline HTML, architecture decision records, print/PDF artifacts, or share-safe copies for someone else.
Portable reports are versioned and validated on import. Current complete exports use v4; existing v1, v2, and v3 reports remain importable. Complete JSON may contain the private decision context and replay snapshots. Share-safe JSON and Markdown remove that material while retaining non-secret run provenance.
FitLens works with OpenAI or Anthropic, one provider per run. Set whichever
key you have; if only one is present, that provider is used automatically. A
session key entered in the UI stays in sessionStorage; a key in .env.local
stays on the local Next.js server.
# OpenAI (default when its key is set)
OPENAI_API_KEY=
OPENAI_MODEL=gpt-5.6-luna
# Anthropic (used automatically when only this key is set)
ANTHROPIC_API_KEY=
ANTHROPIC_MODEL=claude-sonnet-5When both keys are present, OpenAI is chosen unless you set
FITLENS_MODEL_PROVIDER=anthropic.
The two providers reach structured output differently. OpenAI uses strict JSON-Schema structured output, which the full comparison schema compiles to cleanly. Anthropic's strict grammar rejects a schema this large ("compiled grammar is too large"), so the Anthropic path uses a forced tool call whose input is validated with the same schema. That is guidance, not a grammar, so an occasional response violates a constraint and is retried. Both paths disable extended thinking — this is structured extraction, not open reasoning.
Practical differences, measured on a 2-product comparison:
gpt-5.6-luna (OpenAI) |
claude-sonnet-5 (Anthropic) |
|
|---|---|---|
| Latency | ~20s | ~75s (about 3× slower) |
| Reliability | strict grammar, consistent | ~70–80%; occasional schema-violation retry |
| Cost / comparison | ~$0.05 | ~$0.10 |
Because Anthropic runs longer and may retry, the CLI's default analysis budget
is 180s (the web app stays at 55s); raise or lower it with --timeout <seconds>.
For FitLens's structured, latency-sensitive workload, gpt-5.6-luna is the
stronger default; Anthropic is a good option when you specifically want Claude's
judgment and can absorb the extra latency.
FitLens can also use a compatible Responses API with JSON Schema structured output:
FITLENS_MODEL_PROVIDER=compatible
FITLENS_MODEL_BASE_URL=http://127.0.0.1:11434/v1
FITLENS_MODEL_MODEL=your-structured-output-model
FITLENS_MODEL_API_KEY=Remote compatible endpoints must use HTTPS. Plain HTTP is allowed only for loopback addresses. Base URLs containing credentials, query parameters, or fragments are rejected. Credentials and provider endpoints never enter saved reports; only the non-secret provider kind and model identifier are retained as run provenance.
The CLI runs the same validation, guarded source collector, provider adapter, and structured analysis service as the browser route:
pnpm fitlens analyze \
--url https://product-a.example \
--url https://product-b.example \
--context "I need a local-first tool for daily agent workflows." \
--format markdown \
--output comparison.mdIt reads provider credentials from the same environment variables described
above. Use --criteria criteria.json for custom criteria, --context-file context.txt for longer workflows, or pnpm fitlens --help for the complete
command reference. Built-in general, developer-tools, privacy-first, and
daily-use templates are available through --template; JSON is the default
stdout format for scripts.
Live JSON output includes a versioned run manifest and, when real sources were collected, a bounded replay bundle. Save or export that bundle and replay it later with no network or provider access:
pnpm fitlens replay \
--bundle comparison.fitlens-replay.json \
--format markdown \
--output replayed.mdReplay verifies the request, source, and validated model-output hashes, then reruns the same structured parser and deterministic result finalizer. Provider, model, and output identity are bound into the run ID. It reproduces the captured decision; it does not refresh evidence or ask the model for a new judgment. Replay files include your workflow context and public page snapshots, so treat them as private complete backups rather than share-safe exports.
For recurring research, create a local fitlens.watch.json:
{
"version": 1,
"entries": [
{
"id": "terminal-tools",
"urls": ["https://product-a.example", "https://product-b.example"],
"context": "I need a local-first terminal for daily agent work.",
"template": "developer-tools",
"locale": "en",
"intervalHours": 168,
"notifications": "changes"
}
]
}Run due entries manually or from cron/launchd:
pnpm fitlens watch --config fitlens.watch.jsonEach successful run writes an immutable timestamped snapshot, latest.json, a
bounded trend.json, and a self-contained trend.html score timeline under
.fitlens/snapshots/<watch-id>/, then atomically updates lastRunAt in the
config. Snapshots after the first also contain a deterministic change summary.
Set notifications to changes or always for native macOS, Linux, or Windows
alerts; the default is off, and an unavailable notification service never
invalidates a successful refresh. Use --force to refresh everything regardless
of interval. .fitlens/ is ignored by Git so research snapshots are not
published accidentally.
Desktop notifications need a session, which cron and CI do not have. --alert-on
and --min-confidence turn a watched change into a non-zero exit (code 2) and
an alerts.json summary, so any scheduler can act on the result:
fitlens watch --config fitlens.watch.json \
--alert-on winner,unknowns --min-confidence 65Conditions: winner (the recommendation flipped), confidence (winner below
--min-confidence), unknowns (new unknowns appeared), any (anything changed).
Exit codes stay consistent with the rest of the CLI: 0 nothing to report, 1 a
run failed, 2 an alert fired. alerts.json lands in the output directory:
{
"generatedAt": "2026-07-23T09:00:00.000Z",
"alerts": [
{ "entryId": "terminal-tools", "winner": "product-b", "confidence": 58,
"reasons": ["winner changed from product-a to product-b",
"winner confidence 58% is below 65%"] }
]
}A weekly cron entry that emails on an alert:
0 9 * * 1 cd ~/research && fitlens watch --config fitlens.watch.json \
--alert-on winner --min-confidence 65 | mail -s "FitLens alert" you@example.comBecause the alert is an exit code, a GitHub Actions or systemd wrapper can just let the step fail to trigger its own notification path.
FitLens has no account system and no hosted database.
| Data | Where it lives | How long |
|---|---|---|
| Reports, revisions, notes | Browser IndexedDB with localStorage fallback |
Until you clear it |
| Candidate inbox, notes, tags, archive state | Browser IndexedDB with localStorage fallback |
Until you clear it |
| Criteria templates, decision profiles, and locale | Browser localStorage |
Until you clear it |
| Key entered in the UI | Browser sessionStorage |
Current tab session |
.env.local keys and provider config |
Local server environment | Until the file changes |
| Product source material | Configured model provider | According to that provider |
| Markdown and JSON exports | Files you choose | Under your control |
Remote source collection resolves hostnames before every request, rejects non-public IPv4/IPv6 ranges, validates every redirect, strips credentials on cross-origin redirects, restricts content types, and caps streamed response size. This is application-layer SSRF hardening, not a network sandbox; do not expose FitLens as a public URL-fetching service.
The browser API is intentionally loopback-only. /api/analyze accepts only a
matching loopback Host and Origin/Referer, requires JSON, streams at most
64 KB per request, and runs no more than two source/model analyses at once.
Production responses add CSP, framing, MIME-sniffing, referrer, cross-origin,
and browser-capability restrictions. These controls protect a local server from
ambient browser requests; they are not authentication for a hosted service.
Collected pages, documents, repository metadata, and README text are placed in an explicit untrusted-data envelope for the model. Provider instructions say that embedded commands cannot change scoring, candidate order, rules, or the output schema. Adversarial fixtures test that separation, but model prompt isolation remains defense in depth rather than a mathematical guarantee.
See Architecture for the exact trust boundaries and invariants.
flowchart TB
UI[Browser workbench] --> API[Analysis route]
API --> SRC[Guarded source collector]
API --> MODEL[Model provider adapter]
SRC --> ADAPTERS[Source adapter registry]
ADAPTERS --> WEB[Official pages + GitHub]
MODEL --> RESP[Structured Responses API]
RESP --> DOMAIN[Validated comparison]
DOMAIN --> DET[Deterministic scoring, evidence, confidence, privacy, diffs]
DET --> UI
UI <--> LOCAL[(Local research library)]
UI --> FILES[JSON · Markdown · HTML · ADR · PDF]
The important seam is between model judgment and deterministic local logic. The model produces a constrained comparison; it does not control weighting, confidence calibration, evidence coverage, conflict detection, persistence, redaction, or report migration.
Read docs/ARCHITECTURE.md for module ownership, data flows, invariants, persistence, test layers, and deliberate non-goals.
app/
api/analyze/ request orchestration and public error responses
examples/ bundled no-key comparison
scripts/
fitlens headless CLI entry point
performance-* deterministic budget contract, fixtures, and local soak
components/
compare-workbench stateful research coordinator
comparison-* builder, report, product, and follow-up presentation owners
candidate-inbox quick capture, filtering, archive, and shortlist UI
pairwise-trials head-to-head trial capture and standings UI
lib/
source guarded website and GitHub collection
source-adapters/ official document and marketplace metadata adapters
model-provider provider configuration and structured Responses adapter
analyzer prompt, response schema, and cross-field validation
analysis-service shared browser and headless orchestration
request-guard loopback origin, body-size, media-type, and concurrency policy
security-headers production browser response policy
cli deterministic command parsing and help
markdown-report portable headless Markdown rendering
durable-exports escaped offline HTML and ADR rendering
watchlist scheduling, snapshot trends, and offline trend charts
local-notifications argument-safe native desktop alerts
scoring deterministic preference weighting
confidence deterministic evidence confidence
conflicts opposing-claim detection
privacy privacy-risk calibration
report portable schema, migration, and coverage
candidate-inbox URL capture, normalization, deduplication, and search
decision-profiles reusable workflow-and-criteria validation
pairwise deterministic trial standings
persistence IndexedDB adapter and safe localStorage migration
research-library local search index and facets
workbench-state pure draft readiness, criteria, and candidate-list transitions
test/ deterministic domain and security tests
fixtures/real-sites curated offline compatibility snapshots
e2e/ browser workflows, accessibility, and visual baseline
.github/ CI and dependency maintenance
playwright.config.ts Chromium test and local server contract
docs/ architecture and worked product research
Analysis cancellation, retry policy, concurrency/time budgets, and the
privacy-safe public-source cache are documented in
docs/RESILIENT-JOBS.md.
pnpm check
pnpm test:performance
pnpm test:soak -- --iterations 500
pnpm test:production
pnpm exec playwright install chromium
pnpm test:e2e
pnpm audit --prodpnpm check runs the fast deterministic tests, bounded performance contract,
focused workflow coverage,
the production file-size ratchet, ESLint, TypeScript, and the production build.
See Code-health ratchets for the 500-line default,
explicit legacy baselines, coverage thresholds, and update procedure.
The separate pure-workflow soak repeats migrations, replay validation, diffs,
watch trends, cache churn, and candidate mutations with explicit-GC heap
diagnostics. It requires no network or API key. See
Performance contracts and soak testing for exact budgets,
failure meanings, CI schedule, and limitations.
pnpm test:production independently builds the application,
starts that exact artifact with next start, and exercises local pages,
production headers, and the guarded analysis endpoint without provider or
public-network access. Run those parts individually when iterating:
pnpm test
pnpm test:performance
pnpm test:coverage-ratchet
pnpm check:code-health
pnpm lint
pnpm exec tsc --noEmit
pnpm buildThe test suite covers scoring, report migration, i18n parity, confidence,
conflicts, privacy, redaction, research search, provider configuration, source
diagnostics, prompt isolation, local request guards, production headers, and
URL/DNS/redirect safety without requiring live network calls.
Curated npm, PyPI, App Store, Chrome Web Store, cmux, and Otty snapshots keep
real public response shapes under regression coverage without making CI depend
on third-party availability.
Playwright covers candidate capture, evidence review, local API rejection,
browser security headers, automated WCAG checks, and a Linux-pinned full-page
visual contract (regenerate with pnpm snapshots:update, which runs the
matching Playwright container; it skips on other platforms). GitHub Actions runs the core
quality gate on Linux, macOS, and Windows, with browser contracts, a hermetic
production-artifact harness, and production
dependency auditing on Linux. Dependabot keeps pnpm and workflow dependencies
visible. A privileged follow-up workflow never checks out dependency-PR code;
it verifies the Dependabot author and tested head SHA through the GitHub API,
requests rebases for outdated branches, retries one failed CI attempt, and
merges minor or patch updates only after the complete CI matrix passes. Major
updates and persistent failures remain open with an agent-review label.
pnpm release <version> cuts a release in one command: it checks the tree is
clean and on main, that the tag does not exist, and that CHANGELOG.md
documents the version; syncs package.json and lib/version.ts; runs the full
gate; then publishes to npm, tags, and creates the GitHub release. Preview it
first with pnpm release <version> --dry-run, which runs the read-only checks
and prints, without executing, the commands that would mutate anything.
# 1. Add a "## [x.y.z] - <date>" section to CHANGELOG.md
pnpm release 0.2.0 --dry-run # preview
pnpm release 0.2.0 # publish + tag + GitHub release- Source collection starts with one official page, follows at most one page per supported document kind, and inspects at most one discovered GitHub repository plus its latest release per product.
- The opt-in browser fallback does not bypass logins, CAPTCHAs, consent walls, or content that requires user interaction.
- Dimension scores are explainable model judgments, not objective measurements.
- Reports and the 50-item research library stay in one browser unless exported.
- A report retains at most five prior revisions.
- Compatible models must implement the Responses API and JSON Schema structured output used by FitLens.
- Watchlists require an external local scheduler such as cron or launchd; the browser does not claim to run reliable background jobs while it is closed.
- Provider endpoint probing is diagnostic only; it verifies
/modelsreachability but does not prove that the configured model supports FitLens structured output.
Further extensions should preserve the local-first boundary. A useful direction is richer trend visualization across watch snapshots.
MIT © Wendy Pan
