npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@segmently/cli

v1.7.0

Published

Segmently CLI for projects, funnels, publishing, and automation

Readme

Segmently CLI

First-party command-line interface for Segmently automation.

The CLI is JSON-first and talks to the stable /api/cli/v1/* facade. It does not read Firestore directly and does not call scattered internal product endpoints as its primary contract.

Installation

npm install -g @segmently/cli
segmently --version
segmently auth login

The first public package version is @segmently/[email protected].

One-off execution without a global install:

npm exec --yes --package @segmently/cli -- segmently --version

Production is the default target. Public users do not need to pass an environment flag.

Full installation and update instructions are documented in modules/cli/INSTALLATION.md.

Authentication

segmently auth login
segmently auth status
segmently auth logout

Production is the default environment. Public users do not need to pass --env prod.

For automation, create a scoped project service token and use it through SEGMENTLY_API_KEY.

segmently capabilities

segmently auth token create \
  --project <projectId> \
  --name "CI publish" \
  --scopes projects:read,funnels:read,publish:write \
  --expires-at 2026-06-25T10:00:00.000Z

SEGMENTLY_API_KEY=sgly.prod... segmently publish web --project <projectId> --web-placement <webPlacementId>
segmently auth token list --project <projectId>
segmently auth token revoke <tokenId> --project <projectId>

For read-only launch checks, service tokens can use funnels:read,themes:read,stripe:read,publish:read,analytics:read,domains:read,content-plan:read. Tokens with publish:write also satisfy publish:read; tokens with analytics:write also satisfy analytics:read; tokens with content-plan:write also satisfy content-plan:read. Image upload automation requires assets:write.

The raw service token is returned only once by auth token create. Store it in the CI secret manager, not in the CLI credential file.

Service tokens created by regular project users are short-lived. If --expires-at is omitted, Segmently sets the expiration automatically. The create response includes the exact expiration time.

Service-token scopes are necessary but not sufficient for project operations. Project-scoped product commands are also checked against Billing Access on the server: Web Onboarding commands require web_onboarding.access or a wildcard such as web_onboarding.*, block generation requires web_onboarding.block.regenerate, and Content Plan commands require content_plan.access or content_plan.*, and article commands require articles.access or articles.* (the Segmently Articles subscription). CLI image uploads require cli_assets.upload_image or cli_assets.*.

auth token create, auth token list, and auth token revoke require user auth from browser login. They intentionally do not use SEGMENTLY_API_KEY, because service tokens should not manage other service tokens.

Unit economics (offline, no account)

segmently ue runs the arithmetic of the public calculator (https://www.segmently.ai/unit-economics) on your machine. Nothing is sent anywhere and no login is needed; every result ends with a link that opens the same scenario on the page. It prints a scenario, never a forecast.

npx -y @segmently/cli ue init my-app --product "Monthly~19.99~1month~none" --platform web
npx -y @segmently/cli ue evaluate .ue/my-app.json --explain
npx -y @segmently/cli ue link .ue/my-app.json
npx -y @segmently/cli ue parse "https://www.segmently.ai/unit-economics?v=2&…"
npx -y @segmently/cli ue --version      # CLI version + the hashes of the bundled arithmetic and the served method
npx -y @segmently/cli ue check answer.md --project-file .ue/my-app.json   # lint a finished answer before it is sent
npx -y @segmently/cli ue target .ue/my-app.json --goal roas=1.5             # what each lever must be, alone, to reach a goal
npx -y @segmently/cli ue plan .ue/my-app.json --growth 5 --months 24        # the scenario month by month

ue init <slug> starts a project from your products: --product <name>~<price>~<cadence>~<trial> is repeatable, and both long and short cadence spellings are accepted (Monthly~19.99~1month~none and Monthly~19.99~1m~none are the same product). A trial nobody stated is left off (Monthly~19.99~1month): it is read as none, the product is recorded in the project's unstatedTrials, and walk calls that none assumed ("not stated — the CLI's init reads it as none") until the file carries a trial. It writes .ue/<slug>.json, and that file is accepted anywhere the other verbs take a scenario — evaluate, link, parse and rank all read a .ue/<slug>.json project file the same way they read a plain scenario JSON file, a page link, or - for stdin. ue parse of a project (the file, or its JSON) also prints the whole project document as project, beside its scenario; any other input prints no project key.

parse, evaluate, link, init, rank, check and every stat verb take --format table for a human-readable view instead of JSON (ue --version does too); ue mcp is a server and has no table view. ue evaluate --format table prints the same words the public page prints for each figure (valuePerTap, cacPerPayer, profitPerStart, roas, …), plus the readiness line and a unit column saying what each figure is measured per. The JSON carries the same map as units: every deducted.* amount is per_tap (per tap on Buy) and every waterfall row is per_payer, so a figure is quoted in the unit its own field states.

ue evaluate --explain also prints walk, the scenario's inputs placed for a boundary: walk.opener (the boundary's first sentence), walk.lines (one reader-facing line per input the scenario carries, the chain's four rates on one line per class — the chain, each product's price, cadence, trial, take, charges, completion and trial conversion, the volume, every deduction, the book's first renewal behind payback, observed inputs — each marked measured or assumed with its source) and walk.items (the same inputs as { id, class, source, text }). A take equal to the split ue init proposes for the same products reads "assumed: the CLI's init split".

--explain also adds words.breakEvens: the break-evens as the answer prints them, one line per knob of breakEvens.chain in the order cps, p1, p2, p3, each beside the value it starts from in the scenario — "cost per start ≤ $0.90 (from $1.50)", "p3 ≥ 58.31 % (from 35 %)". A threshold is rounded away from its claim, so the printed value itself clears: a rate's "≥" rounds up at two decimals and the cost per start's "≤" rounds down to the cent (unlike the nearest rounding of the other number words). A rate in breakEvens.unreachable reads "p2 — no conversion rate at that step clears this scenario on its own (from 44 %)" instead of a figure above 100 %. A knob with no break-even (nobody pays) gets no line. Charges per payer are not in the list: they are found by sweeping paymentsCounted, one evaluate per point.

On a .ue/<slug>.json project file, ue evaluate also prints stage (read off the file) and offers — the stage table's three next steps for that stage, the same list ue init prints.

Every result that carries a link carries privacy beside it — ue link, ue evaluate, ue init and ue rank (recommendation.privacy.first / .runnerUp for its two lifted links): due (the link carries a figure that is not the book's), carries (which of prices, chain, volume, takes, charges, completion, trialConv, mechanics, deductions), pricesOnly (only prices are not the book's) and sentence, the privacy sentence to print on its own line above the link when due is true. Since 1.7.0 carries also names goal (the link carries goal=) and dynamics (it carries dyn=): a reader's target or plan figures are never the book's, so either makes it due.

Inputs are a JSON scenario file, a .ue/<slug>.json project file, a page link, or - for stdin. The public page reads every key a minted link carries, fee= (the provider scheme in its fourth field included), tax=, refunds=, disputes=, activation=, fees=, basis=, measured= and upgrade= among them, so a link opens there on the figures the CLI prints (checked on the deployed page 2026-09-23). A key the page does not read raises one page_shows_gross warning naming it; the warning says GROSS only when that key is a deduction. goal= and dyn= (1.7.0, minted by ue target and ue plan, and by ue link for a scenario that carries a goal or month-by-month inputs) are not read by the deployed page yet: until its section «What it takes» is live, the warning names them ("The public page does not read goal= yet…") and the page opens the scenario without them.

Which experiment first — ue rank

ue rank answers "which test do I run first?" for one scenario. For each lever — p1, p2, p3 (the funnel steps), close (payment completion), bought (the paid share of buy-taps), mix (the take of the product whose share move gains the most), trialConv and renewals — it moves that one input by a relative lift through the calculator's own controls, evaluates the moved scenario once, and prints what the lift is worth and what it costs to learn at your traffic. Every lifted value lands on the calculator's own grid before it is evaluated (the same round trip ue link/ue evaluate use); a landed lift ≠ the asked one carries snapped (within half a control step) or clamped (further away) in warnings:

npx -y @segmently/cli ue rank .ue/my-app.json --override p3=0.25   # +25 % on paywall → buy-tap, +10 % elsewhere

A script reads rows[] in the CLI's own order (learnable within the shortest horizon first, then by gain per day of testing): per row lever, lift (the landed lift — a control that clamps or snaps it says so in warnings), liftLabel (assumed lift for ue rank's default, your belief for an --override or a typed --lift, null when the lever did not move), gainPerMonth, roasAfter, populationPerDay, populationName (the people this lever's test draws from, in words), askedLift (the relative lift this row was tried at), nPerArm, days, spendRouted, mde (keyed by horizon), realistic and note; then base (profitPerStart, roas, startsPerDay), recommendation.first / recommendation.runnerUp (realistic rows that gain, or null) with their round-trip-checked recommendation.links, recommendation.mostProfit (the realistic row with the largest gainPerMonth > 0, or null), and warnings. --lift, --horizon 30,90 and a repeatable --override <lever>=<lift> change the defaults (+10 %, 30 and 90 days). A figure that cannot be computed is null (n/a in the table). boundary is the run's whole boundary, to print after the table: opener on its own line, then each of lines as a bullet — the base's walk.lines (as ue evaluate --explain prints them for the same input), sentences.beliefs, sentences.assumedLift, the horizon line, the 50/50 split line (a card run: "the 50/50 split — assumed: the test design"), sentences.decidable and sentences.notTestableSoon, a null one left out. --horizon-default (with exactly one --horizon, else refused, field horizon-default) labels that horizon the default nobody named: "the 30-day horizon — assumed: default horizon, none named". sentences holds the run's own sentences, each filled only from that run's fields: traffic, ranking, population, realism[], ceiling[], blocked[], price, decidable, notTestableSoon, assumedLift, beliefs[], horizons, pick, mostProfit and warnings[] (each warnings entry's message as a line of its own, in the same order; pick — "p2 is the pick (recommendation.first), p1 the runner-up (recommendation.runnerUp)." from the run's own recommendation, "…; there is no runner-up." with none, "No lever that gains is learnable in this run — there is no pick." without a first — to print after the last belowTable line and above the table's boundary; horizons — "the 14-, 21- and 30-day horizons of this choice table — assumed: offered for the choice, none picked yet" — only when --horizon was typed with more than one value, a choice run; null for a single horizon and for a run with no --horizon), plus the table itself: table[] (the markdown table of rows — header, separator, one line per row, each cell the row's field rounded as the table view rounds it, the lift cell with its label and any typed → landed, a | inside a cell escaped as \|) and belowTable[] (ranking, population, realism[], ceiling[], blocked[], price, then warnings[] — never mostProfit or pick: belowTable's order is pinned by UC-10/UC-11) — print traffic, table, a blank line, then each belowTable line as a - bullet, in that order.

Repeatable --cost <lever=usd> and --odds <lever=p> price a believed lift for a lever that already has its own --override (never rank's own default lift): the row gains bet — gainPerMonthIfHolds (that row's gainPerMonth), cost and odds (each null when not given — never defaulted), expectedGainPerMonth (odds × gainPerMonthIfHolds, null without odds), costRecoveredInMonths (cost ÷ gainPerMonthIfHolds, null without a cost or when the row does not gain) and sentence (all three, with their operands, in words), and sentences.beliefs carries, right after that lever's belief line, one boundary line in the same words — "p2: your cost to build $1,600.00 and your odds of 40 % that it holds — assumed: your estimate" (either part alone when only one was given; no line without either). A cost or odds for a lever without its own --override, odds outside 0–1, or a negative or non-numeric cost is refused by field (cost / odds):

npx -y @segmently/cli ue rank .ue/my-app.json --horizon 21 --override p2=0.15 --cost p2=1600 --odds p2=0.4

Only on an --override row, guardrail names the steps that must not drop while that lever is tested: steps (the steps after the lever, in funnel order p1 → p2 → p3 → bought → close → trialConv → renewals, minus any this scenario blocks — no trial product, no trialConv), nearest (the first of them, null when no step follows), cancellingDrop (the relative drop of nearest's rate at which the row's gainPerMonth is 0, found on the calculator's own evaluation; null when there is no nearest step, the row does not gain, or no drop inside that step's own control cancels it), horizon (the shortest horizon), testDays (the length the test runs: the row's own days, or the shortest horizon when the row has none), mde (the smallest change of nearest readable within testDays; null for renewals, a calendar read, for a step whose trial clock outlasts the test, or for a rate at 100 %), checkpointNPerArm (the people per arm on nearest at which the cancelling drop is readable — the same n ue stat size gives for that drop), checkpointDay (the day the test reaches that n on nearest, its trial clock included), visible (checkpointDay ≤ testDays; false when the trial clock outlasts the test; null with no cancelling drop or on renewals), watch and sentence (the drop and whether it is readable within the test, in words: the day it becomes readable, or when it is not, how long the test would have to run). Over ue mcp, ue_rank keeps the earlier guardrail — mde and visible at the shortest horizon, no testDays or checkpoint.

With exactly one --override, --card <file> also writes that lever's lifted scenario — the one its row was evaluated on — to <file> as a scenario document (parent folders created, an existing file overwritten — never a ue_project file), labelled with the landed lift and a measuredOn of "believed lift, not observed, ", and adds card: prepared[] (each field the lever moved, "chain.p2 0.61 × 1.15 = 0.7015 — your numbers, prepared"), snapped[] (where the calculator's grid put it, "chain.p2 0.7015 → 0.7"), evaluateCommand, linkCommand and roasTie (the file's ROAS equals the row's roasAfter — checked before it is printed). No --override, a second one, -, the rank input itself, a project file, a lever that does not move in this scenario or an unwritable path is refused by field (card):

npx -y @segmently/cli ue rank .ue/my-app.json --horizon 21 --override p2=0.15 --card .ue/my-app/card-p2.json

Experiment sizing — ue stat

ue stat sizes and reads a two-arm test: size (sample size per arm for a relative lift over a baseline rate), mde (the smallest relative change readable at a population per day over a number of days), read (a finished test's verdict from each arm's n/x) and means (sample size for a difference of means, given σ):

npx -y @segmently/cli ue stat size --p 0.35 --lift 0.10                 # nPerArm 2978
npx -y @segmently/cli ue stat mde --p 0.35 --per-day 148 --days 30
npx -y @segmently/cli ue stat read --a 5000/1500 --b 5000/1700
npx -y @segmently/cli ue stat read --a 1864/708 --b 1864/772 --project-file .ue/my-app.json --lever p1 --horizon 21
npx -y @segmently/cli ue stat read --a 1864/708 --b 1864/772 --project-file .ue/my-app.json --lever p1 --rebase
npx -y @segmently/cli ue stat read --a 1000/957 --b 1000/975 --project-file .ue/my-app.json --lever close --horizon 21
npx -y @segmently/cli ue stat means --sigma 40 --delta 4

A script reads nPerArm and pTarget (size), mde (mde, null when even a rate of 100 % is not readable in that window), read.verdict (significant, not yet or underpowered), read.ci95 (the 95 % interval on the difference), read.needPerArm (read), and nPerArm (means); every result carries warnings. A request that cannot be sized is refused by field, never answered: a target rate p × (1 + lift) at or above 1, a lift or a delta too small to give a finite sample size, or a malformed number (--p 0.3abc) — exit 1, stderr { "error": { "code": "input_refused", "field", "message" } }.

ue stat read --project-file <file> --lever <lever> [--horizon <days>] reads the test against the project's own rate for a rank lever and adds context: lever, subject (how the rate is named: "p1", "close (payment completion)", "mix (Annual's take)"), product (mix only: the product whose take the read counts, else null), today (the project's rate before the test), drift ("p1 scenario 38 % → observed 41.42 % (3.42 points higher)"), realizedLift (read.pB ÷ today − 1, six decimals, on a significant verdict only), realizedGainCommand (the ue rank --override command for that lift; needs --horizon, and a lift above 0 — a significant drop has none, since --override takes a lift > 0 only), rebaseCommand (this same read plus --rebase, the command an MCP host — whose server writes no files — hands the reader to run in their own terminal; on a significant read of a ue_project file only: null for -, a link, JSON text or a scenario file, and on the --rebase run itself), walkLines (the project's walk.lines) and boundaryAdds (the read's own boundary lines, after walkLines; the first opens with subject). --horizon without --project-file and --lever is refused (field horizon), and so is --project <file> (field project-file): -p, --project is the root's Segmently project id, never this file. context.boundary is the read's whole boundary (opener, then lines: walkLines, boundaryAdds, the card's belief line, the horizon, the 50/50 split, and on a card's read — with --belief — the card's table run's notTestableSoon, "in the card's -day table run"). Every read also prints read.lines: the nine fields as one line, then context.drift and context.more.sentence or context.realized.sentence with its warnings. --belief <lift> (the card's believed lift as typed; needs --horizon, field belief) adds the card's belief line (from the card's own rank run) and its table run's line to context.boundary. --horizon-default (needs --horizon, field horizon-default) labels the horizon the default nobody named.

--lever takes every rank lever but renewals, and today is the rate ue rank lifts for it, so the counts must count that same rate. p1–p3 read a chain rate: n people reach the step, x pass it. close reads the paywall's take-weighted completion (the paid share over the pick share, all paywall products as one rate: n picked a paid product, x paid); bought reads the paid share of buy-taps (n buy-taps, x paid purchases). mix reads the take of the product ue rank's own mix row shifts picks toward (n buy-taps, x picks of that product); trialConv reads the trial products' take-weighted trial → paid (n trial starters, x paid). renewals is refused (field lever) with what to read instead: charges counted per payer are a calendar read, not a share of n — read them over a billing period (RevenueCat subscription_retention → paymentsCounted) and write them into the scenario. A lever ue rank blocks on this scenario (no one picks, no one buys, fewer than two products for mix, no trial product for trialConv) is refused with rank's own reason, and any other name by field lever. mix never has a realized-gain run — ue rank --override mix=<lift> picks its target again at that lift, so the run could move another product's take — so its realized and realizedGainCommand are null, and its realized effect is the re-base's lines.

context.more (a read that is not significant; null on a significant one) says how many more people the observed difference needs: needPerArm (read.needPerArm), smallerArm, morePerArm, populationPerDay (the lever's populationPerDay in ue rank on this project), maturityDays (the lever's trial clock — trialConv's longest trial + 3, else 0), days (the more per arm over half that population a day, plus maturityDays) and a sentence to copy. Arms that read the same rate have no sample size: needPerArm, morePerArm and days are null, and the sentence says to record the test as not yet. context.realized (a significant read with --horizon and a lift above 0, else null) is the realized-gain ue rank run the CLI makes itself on the project before any re-base: its landed lift, gainPerMonth, roasAfter (the re-based ROAS), warnings and assumedLift (that run's sentences), and a sentence; when the grid moves the lift as printed (two decimals; a landing that prints the typed figure is none), boundaryAdds carries the landing line ("typed +8.99 % → landed +9.21 % (snapped)", or the row's reason, "(below the control's step)", when the grid holds the lever at its rate — then the sentence says so and quotes the row's note, never "$0.00 a month"). boundaryAdds also carries that run's assumedLift line, last — right after the landing line, else right after the realized-lift line — so a boundary that copies boundaryAdds whole walks the other levers' default lifts; context.realized.assumedLift keeps the same text. A significant drop has no realized-gain run: its boundary line reads "the realized lift on p1 −0.096454 (no realized-gain run: ue rank takes a lift above 0)", and no assumedLift line follows it (nor on mix, a read that is not significant, or one without --horizon).

--rebase (CLI only; the MCP server writes no files) re-bases the project file on a significant read, after context is built. It moves the old scenario into variants[] ("base before the p1 A/B read"), appends the counts to observed[] as an ab_test document and a decision with the read's boundary to decisions[], and sets the lever at the read. A chain lever gets read.pB as printed (the loader lands it on the grid); every other lever moves exactly the way ue rank lifts it, by the factor read.pB ÷ today: each paywall product's completionRate (close, held between 0.3 and 1) or trialConv (trialConv, held between 0.02 and 0.95) times that factor, the paywall takes scaled until the paid share is read.pB (bought, capped where the takes reach 100 % of buy-taps), or the target's take set to read.pB with the other takes giving it up in proportion (mix, capped where the target holds every pick; its observed[] entry also names the product). A re-base the control or the calculator's range stops short of read.pB (a cap, a floor, a chain rate clamped into range) says so in its decision instead of recording read.pB as the re-based rate. It sets measuredOn to "p1 A/B read: 1864/708 vs 1864/772", adding "; before: " when the whole fits in 80 characters. It bumps updatedAt and re-derives stage. context.rebase prints file, measuredOn, updatedAt, variantName, prepared (one line per field written: "chain.p1 0.38 → 0.414163 (read.pB as printed)", or the factor and then "Monthly with trial's completionRate 0.95 × 1.019164 = 0.968206", "(capped at 1)" or "(floored at 0.3)" when the control stopped it, "(capped: the takes reach 100 % of buy-taps)" or "(capped: Annual holds every pick)" on a capped share move), snapped (each written value's landing on the grid, or "(clamped to the calculator's range)"), lines (base → re-based profit per start, ROAS, CAC per payer, payback and stage), evaluateCommand and linkCommand. The re-based project is evaluated before it is written, and written through a temporary file and a rename. It is refused, with the file left byte-identical, on a read that is not significant, on a read already in observed[] (the same lever and counts: never applied twice), on anything but a ue_project file on disk (a scenario file, - or a link: only a project has variants[]) — each field rebase — and without --project-file (field project-file).

ue stat watch --project-file <file> --lever <lever> --a <n>/<x> --b <n>/<x> [--guardrail <step>=<control n>/<x>,<variant n>/<x>]… [--today <step>=<control n>/<x>,<variant n>/<x>]… [--date YYYY-MM-DD] [--card <file>] [--belief <spec>] is the daily check of a RUNNING test (CLI-only — no ue_watch MCP tool, ue mcp unchanged). It reads, never writes, the project's experiment ledger: it needs a ue exp started entry for --lever and refuses by name (ue exp start) without one. --a/--b are the primary step's cumulative counts, control and variant — for progress only; the watch never prints the primary's rate, lift, p-value or verdict, only its n per arm against the plan and the day the read becomes due (readOn); once the smaller arm reaches the planned nPerArm it prints the one ue stat read … --lever command to run. Each --guardrail <step>=… (cumulative counts, repeatable; p1 p2 p3 bought close) is checked by three rules: (1) crash — a full day's --today count of zero conversions on the variant → stop, "funnel broken", no statistic printed for that step. The crash rule's expectation is the base's rate (the card's, on a step the change moves) × the day's own arrivals in the variant — never the plan's population, which is often the book's: 0 converted → stop at 20 or more expected, a warning naming both arms' arrivals from 5, and below 5 an ordinary day, noted on the line (expectedPerArm is that expectation). On cumulative counts the same arithmetic stops a variant at 0 of n (rate × n ≥ 20) and warns when both arms sit at 0 % with that support (the funnel or its tracking broken in both). (2) a significant drop past the drop that cancels the gain — pValue below 0.05 ÷ plannedDays (a Bonferroni correction over the planned daily looks, named once in lines as a stand-in for a sequential boundary) and the observed drop at or past the step's cancellingDrop → stop. The daily look's bar is 0.05 ÷ the planned days: on a plan of many days the kill switch is in practice the crash rule. (3) a significant but smaller drop → warning, and the CLI runs a net check — a calculator evaluation of a variant carrying both the primary's and the guardrail's observed rates — whose profitPerStart against the base's decides whether the warning escalates to stop, "gain cancelled downstream". Anything else is noise, printed with today's readable threshold (mde at the elapsed days) and the n the cancelling drop needs for its read at the planned end — never "fine". A step the change itself moves by design gets the crash check only (its drop is the primary's: no statistic, no drop search, no net check) and is otherwise unwatchable, as is a calendar step (trialConv before its clock, renewals, charges: no daily counters). On status: stop the output carries the ue exp close <project> <id> --reason "<reason>" command; the watch itself writes no file.

Lint an answer — ue check

ue check <file|-> [--project-file <file>] [--runs <dir>] [--expect <case:block>] lints a finished answer before it is sent and prints { pass, findings[], warnings }, each finding { rule, line, message, fix }; a completeness finding also carries file. Its thirteen rules are the nine below and the four 1.4.2 adds after them: banned (the forecast / prediction strings and computed ratios), approximation (~, ≈, ±, "about", "roughly" … before a number), privacy (a link that carries the reader's numbers with no privacy sentence on its own line above it), call_above_link (the ue link call, or the ue rank run, above each link), round_trip (a link that does not decode, or is not the one the CLI mints, or carries no measured= — the measured_missing warning its ue link gives), boundary_opener (a verdict in an answer that carries no verbatim boundary at all — a card's bet.sentence or guardrail.sentence line and ue rank's sentences.pick included; a verdict after the last boundary is restated_verdict's), measured_missing (a line that reports the measured_missing warning while no link after it carries measured=; a line that names the re-mint — "→ re-minted with measured=…" — while any link of the answer carries it passes), walk_rates (with --project-file: every boundary names every chain rate of the project's walk, or of a scenario its variants[] holds — the pre-test base after a re-base) and provenance (with --project-file: a walk line whose text is one of the base walk's items keeps a class the base's walk or a variant's files it under — "the fee … — assumed: the book's preset" where the walk says "measured" is flagged, with the walk's own line to copy; a session line — a belief, the assumed lift, the horizon, the split, the counts, boundaryAdds — and a variant's own edit (a proposed product, a corridor corner, a sweep point) are never checked; the one residual: a labelled sweep whose first point equals a base value the base files as measured is flagged; a line walked "measured" whose tail says "believed lift" — a card file's own ue evaluate --explain walk copied in place of card.walkLines — is flagged too). With --project-file, the reader's own product names, label and measuredOn inside a copied line are theirs, never read as banned or approximation.

1.4.2 adds --runs <dir> (a directory holding the JSON output of each run the answer prints, one file per run, or a single run file) and --expect <case:block>, and four rules: restated_verdict (on every check: a verdict line after the answer's last boundary that is neither a question nor a next step), completeness (with --runs: every line the answer owes of those runs — the walk, the break-evens and the rank table once it shows a line of them, the rank run's warnings and boundary, the card's lines, read.lines, the read's boundary and its sentences, the re-base lines, and the privacy sentence above the run's own link — is in the answer, whitespace collapsed and backticks ignored, at most 20 findings per run; its file names the run — in --format table a column shown only when a finding carries one), expected_block (with --expect, e.g. uc-11:card: every label of that block — the card's fields (1)–(10) — opens a line) and unresolved_slot (on every check: a {{…}} slot marker outside fenced code — a worked example's placeholder, never an answer's). A slip is pass: false with findings, exit 0; only an answer, project or run file that cannot be read, an unknown --expect key — or a --project <file>, the root's project id (field project-file) — is refused, by field, with a fix.

Goal seek and month by month — ue target, ue plan

ue target <input> --goal <roas|payback|cac|profit>=<value> answers "what would it take": for a ROAS of at least 1.50×, a payback by month 6, a CAC of at most $40 or a profit per start of at least $1, what each lever would have to be on its own — the cost per start, the three chain rates, tapped Buy → bought, each product's price and charges counted, trial → paid — holding every other number. It is the public page's Goal fold: the rows are the core's own solve and every line is the page's words for the same state.

npx -y @segmently/cli ue target .ue/my-app.json --goal roas=1.5
npx -y @segmently/cli ue target "https://www.segmently.ai/unit-economics?v=2&…&goal=roas~1.50"   # the link's goal, no flag

The JSON a script reads: goal (kind, value, basis — ROAS and profit read the tiles' 12-month/first-charge and gross/net basis, payback twelve months of charges, CAC no value), current (the metric now; null when nobody pays, or a payback not inside twelve months), met, reachCount, leverCount, rows[] (lever — cps, p1, p2, p3, bought, price:<id>, checks:<id>, trial:<id>, or trial for "no free trial on this paywall" — label, current, required (a number exactly when state is reach or holds, else null, never 0), state (reach, holds, holds_any, unreachable, no_effect, no_trial, no_reading), figure and line), lines (the reading as the page prints it — print them whole), privacy, link (the scenario with goal=<kind>~<value>, round-tripped like ue link's) and warnings.

ue plan <input> [--growth <pct>] [--months 12|18|24] [--mrr <usd>] [--by <month>] [--cost-rises <pct>] is the page's Month-by-month fold: the ad budget growing --growth % a month (0–30; 30.5 lands on 30 with a clamped warning), each month's new payers renewing like the first, for 12, 18 or 24 months (past month 12 the renewal tail is an assumption, assumed: true). --mrr watches for an MRR (the month it is reached); with --by <month> it is goal mode: the growth, the paywall → tapped Buy rate and the month-1 budget that each reach that MRR by that month, alone, with their cash cost. --cost-rises <pct> assumes the cost per start rises that many percent each time the budget doubles (month 1 never moves).

npx -y @segmently/cli ue plan .ue/my-app.json --growth 5 --months 24 --mrr 100000
npx -y @segmently/cli ue plan .ue/my-app.json --growth 5 --months 24 --mrr 150000 --by 12

The JSON a script reads: dynamics (the inputs this run read), months[] (month, budget, cps, newPayers, active, mrr, cashIn, flow, cumulative, assumed — every payer figure null when nobody pays), milestones (flowPositive — the first month that collects more than it spends on ads — low { month, cumulative } or null when cash never goes below zero, neverBelowZero, backAboveZero, mrrReached; null when nobody pays), totals (spend, cumulative), month1Budget, cac, basis, goal (goal mode: mrr, byMonth, atTarget, met, levers[] with required, cash and cashLine; otherwise null), lines, privacy, link (with dyn=… while any input differs from its default) and warnings. It prints a scenario month by month, not a plan: nothing in it is measured.

An input whose document or link already carries a goal (goal=) or month-by-month inputs (dyn=) needs no flag; a flag replaces the input's value and an overridden warning says so. Both verbs read the same inputs as every other verb (a file, a .ue/<slug>.json project file, a page link, or - for stdin), take --format table (one row per lever / per month, then the answer's lines), and refuse by field with a fix: goal (no goal anywhere, an unknown kind, a ROAS at or below 0, a payback outside 1–12, a CAC at or below $0, a value that is not a number), months, growth, mrr, by (outside the horizon, or with no MRR to reach) and cost-rises (outside 0–100). Both are CLI-only: ue mcp has no tool for them.

MCP

The same seven handlers (evaluate, link, parse, init, rank, stat, check) plus an eighth, ue_guide, are reachable as eight MCP tools over stdio — the CLI already ships the server, nothing extra to install. The server carries the method: its instructions, ue_guide (the segmently-unit-economics skill, topic by topic) and four prompts (launch-card, trial-or-not, store-vs-web, hypothesis-card) work without that skill installed anywhere.

Every tool result carries a second text block, next: …, the step guidance for that tool's result; until the connection has called ue_guide once, it opens with "Call ue_guide {} first, then the topic of this case." ue_check takes the answer text (answer), an optional project, and optional runs (the run results as the JSON texts the tools returned) and expect; ue_stat's read takes the same context as request.context (project, lever, horizon, and belief / horizonDefault for --belief / --horizon-default). ue_rank takes "card": true where the CLI takes --card <file>: the same UC-11 card comes back inline — its scenario document as card.scenario, in place of the file and its two commands, and field 5's block whole as card.lines — and no file is written; "horizonDefault": true is --horizon-default. The server's serverInfo.version is the CLI version ue --version prints.

claude mcp add ue -- npx -y @segmently/cli ue mcp
codex mcp add ue -- npx -y @segmently/cli ue mcp

Claude Desktop and Cursor: add the same command under mcpServers in their MCP configuration file:

{
  "mcpServers": {
    "ue": { "command": "npx", "args": ["-y", "@segmently/cli", "ue", "mcp"] }
  }
}

claude.ai in the browser and ChatGPT need a remote server — not offered here; this server speaks stdio only, spawned as a local process by the host above.

1.5.0

Structural levers — ue rank --lever <spec> (price:, trial-add:, trial-remove:, trial-paid:, mix:, downsell:, upsell:, upgrade:), --belief, --variant <file>, --together (2–3 arms), --metrics-only; each structural row prints its break-even when the change has one (searched on the model's grid) — or that it clears at every point / never clears —, the threshold test that reads it, links to the base and the variant and a short difference. ue stat read --lever <spec> reads the test against its threshold (holds / fails / not_enough, --arm, --rebase on holds). ue exp list|start|close keeps the project's experiment ledger (experiments[]). ue evaluate adds pools and a readiness clause per live mechanic. The MCP server (ue mcp) is unchanged — the same tools, schemas and method as 1.4.2.

1.6.0

ue rank's nearest guardrail is judged at the test's own length (the row's days when it has one, else the horizon): guardrail.testDays, mde at that length, checkpointNPerArm (the per-arm n at which the cancelling drop is readable) and checkpointDay decide guardrail.visible — a guardrail whose step is still blind at the test's planned length says so and names the day it would become readable, or how many days to extend to. ue stat watch is new: the daily check of a running test with a kill switch (CLI-only — ue mcp and its tools are unchanged, byte-identical to 1.4.2's recording).

1.7.0

Goal seek and month by month: ue target (what each lever must be, on its own, to reach a ROAS, a payback month, a CAC or a profit per start) and ue plan (the scenario month by month, with goal mode's three levers) — the public page's section «What it takes», words and all; their links carry goal= / dyn=, which ue link and ue parse read and write too. The chain rates p1, p2, p3 now reach 100 % (a skipped funnel step is entered as 100 %): a value above 95 % is no longer clamped — trial → paid still stops at 95 %. ue check owes a ue target / ue plan run's lines whole and reads "around month 4" as an approximation. ue mcp keeps its eight tools and its 1.4.2 method; its outputs are unchanged except for an input that sets a chain rate above 95 %, which now lands as typed.

Environments

segmently env list
segmently env current

Production is the public default. env list shows only the public production target.

Shell Completion

segmently completion bash > ~/.local/share/bash-completion/completions/segmently
segmently completion zsh > ~/.zfunc/_segmently
segmently completion fish > ~/.config/fish/completions/segmently.fish

completion is local-only: it does not call the backend and does not require credentials.

Examples

segmently projects list
segmently funnels list --project <projectId>
segmently funnels versions <funnelId> --project <projectId>
segmently funnels export <funnelId> <versionId> --project <projectId> --output flow.json
segmently funnels audit <funnelId> <versionId> --project <projectId> --required-variable goal,name,email --require-paywall
segmently funnels diff --project <projectId> --source-funnel <funnelId> --source-version <versionId> --target-funnel <funnelId> --target-version <versionId> --explain
segmently funnels graph dry-run --project <projectId> --funnel <funnelId> --version-id <versionId> --file graph.json
segmently funnels graph apply --project <projectId> --funnel <funnelId> --version-id <versionId> --file graph.json
segmently funnels screens apply --project <projectId> --funnel <funnelId> --version-id <versionId> --file screens.json
segmently funnels variables apply --project <projectId> --funnel <funnelId> --version-id <versionId> --file variables.json --dry-run
segmently funnels edges apply --project <projectId> --funnel <funnelId> --version-id <versionId> --file edges.json
segmently funnels conditions apply --project <projectId> --funnel <funnelId> --version-id <versionId> --file conditions.json
segmently stripe products --project <projectId> --mode test
segmently launch preflight --project <projectId> --funnel <funnelId> --version-id <versionId> --web-placement <webPlacementId> --required-variable goal,name,email --require-paywall --require-payment-provider
segmently analytics settings get --project <projectId>
segmently analytics settings apply --project <projectId> --file analytics-settings.json --merge
segmently analytics probe --project <projectId> --required-platform facebook_pixel,facebook_capi --required-url-param fbclid,utm_source,utm_medium,utm_campaign --require-client-side --require-server-side
segmently domains status --project <projectId>
segmently domains verify --project <projectId>
segmently domains home set <alias> --surface article --project <projectId>
segmently domains home clear --surface article --project <projectId>
segmently domains base-url verify https://www.example.com/blog --project <projectId>
segmently domains base-url set https://www.example.com/blog --project <projectId>
segmently domains base-url clear --project <projectId>
segmently assets upload-image ./hero.png --project <projectId> --raw-url
segmently assets upload-image ./hero.png --project <projectId> --asset
segmently assets upload-image ./hero.png --project <projectId> --folder onboarding/reference
segmently content-plan strategies list --project <projectId> --include-counts
segmently content-plan strategies get <strategyId> --project <projectId> --include-posts --output content-plan-strategy.json
segmently content-plan strategies audit <strategyId> --project <projectId> --required-platform linkedin,x --require-generated-posts
segmently content-plan posts schedule <postId> --project <projectId> --author <authorId> --strategy <strategyId> --scheduled-at <iso> --platform facebook --dry-run
segmently content-plan profile export --project <projectId> --author <authorId> --platform linkedin,x --output creator-profile.json
segmently content-plan profile apply --project <projectId> --file creator-profile.json --dry-run
segmently content-plan pillars apply --project <projectId> --author <authorId> --file pillars.json --dry-run
segmently content-plan post-templates apply linkedin --project <projectId> --author <authorId> --file linkedin-templates.json --dry-run
segmently fixtures launch seed --project <projectId> --alias-prefix qa-launch --name "QA Launch" --dry-run
segmently support evidence from-run --file run-artifact.json --output evidence.md
segmently generate block --project <projectId> --file request.json
segmently tasks get <taskId> --project <projectId>
segmently publish web --project <projectId> --web-placement <webPlacementId>
segmently publish web --project <projectId> --funnel <funnelId> --version <versionId> --alias <alias>
segmently publish verify --project <projectId> --url /ab-launch --public-base-url https://api.segmently.ai --query-param fbclid=fb-test,utm_source=facebook --required-variant control,treatment
segmently ab-tests list --project <projectId> --status published --tag copy-test --baseline-variant control --iteration iter-2026-05
segmently ab-tests publications <abTestId> --project <projectId> --tag copy-test --baseline-variant control --iteration iter-2026-05

Articles on your main domain (path mode)

domains base-url serves the article site at a path of your own site (for example https://www.example.com/blog) through your reverse proxy. It acts on the article domain only (--surface article, the default); --surface web is refused before any request.

  • base-url verify <url> is a dry check (writes nothing, CLI-only): it validates the URL and confirms it reaches this project through your proxy. Prints { ok, projectId, publicBaseUrl }. Failures carry a code such as base_url_wrong_project, base_url_not_proxied or base_url_unreachable; rate_limited means wait and retry.
  • base-url set <url> / base-url clear store the setting, rewrite the site files and republish every live article. The CLI drives the switch job: it calls the server step endpoint repeatedly, prints progress to stderr and finally prints { mode, publicBaseUrl, homeAlias, results, relatedRefresh, warnings, summary } where summary is "N republished, M skipped, K failed". A live article with unpublished edits (or one retired meanwhile) is skipped with a canonical stays <url> until republished note. clear in subdomain mode prints note: "already in subdomain mode" and exits 0.
  • Exit code 1 when any article failed to republish (run the same command again) or the related-articles refresh failed (run segmently content-plan articles related refresh).
  • Ctrl-C leaves the job running on the server. Run the same set / clear command again: the server answers mode_switch_in_progress, and the CLI resumes the running job. If the running job is a different switch than the one you asked for, it is finished, the CLI exits 1 and tells you to re-run yours.

assets upload-image calls the CLI facade route /api/cli/v1/projects/:projectId/assets/upload/image, which reuses the existing Builder Assets upload and resize pipeline. By default, uploaded files are stored under projects/<projectId>/cli-assets/images/.... The normal JSON response includes the original URL, the small URL, the full legacy assetUrl variants, and the V2 asset shape { "original": "...", "small": "..." }.

The command requires assets:write on service tokens plus project Billing Access capability cli_assets.upload_image.

Paddle catalog and sandbox paywall products (requires matching backend support)

segmently paddle status --project <projectId>
segmently paddle products --project <projectId> --environment sandbox
segmently paddle prices --project <projectId> --product <paddleProductId> --environment sandbox

Catalog commands default to Paddle sandbox; --environment production selects the merchant's production catalog independently of the Segmently API environment. Omitting --product lists all active prices. Status returns saved connection metadata for both Paddle environments plus cached webhook health when the backend supports it. The connection flag is not a live credential, checkout or wallet verification. Use hasApiKeySecretRef, hasClientToken and hasWebhookSecret for saved-field presence within each environment. These booleans do not expose values or prove that a referenced secret can be read. Older backends omit the first two fields; omission means unknown. Inspect webhookStatus.status, coverage and measuredAt before using the cached counters: a configured secret does not prove receipt, receipt does not prove downstream delivery, and reading status does not refresh the measurement. A project without a saved Paddle connection returns paddle_not_connected.

These project-scoped reads require paddle:read and Web Onboarding access. paddle:write implies read access and also permits sandbox paywall-product ensure. Stripe scopes do not grant Paddle access. Tokens and secret references are never part of status or catalog responses.

Inspect one exact checkout hostname separately (requires a backend with domain-probe support):

segmently paddle status --project <projectId> --checkout-domain <checkoutHostname> --environment sandbox

This optional read returns checkoutDomain with its environment, observation time, website approval and Apple Pay domain verification. Supply a hostname without a scheme, path or port. The domain check defaults to Sandbox; --environment on status requires --checkout-domain. The Paddle API key needs checkout_domain.read, in addition to the Segmently paddle:read access described above. Missing provider permission, transient failure or disconnected selected environment yields actionable unknown evidence and does not change the saved connection flag. An older backend that ignores the query is reported as unsupported rather than a successful domain check.

The API returns related domains too; only an exact hostname match is accepted here. not_found means no exact record was returned, not that an existing Sandbox card checkout has failed. Apple Pay domain verification is separate from device eligibility; walletDeviceAvailability remains unknown. Google Pay does not use Apple's domain verification. This command does not verify a merchant return policy or execute a payment. See Paddle checkout domains and Express checkout.

Create a Segmently paywall product from an existing Paddle Sandbox price:

segmently paddle paywall-product ensure --project <projectId> --name "Monthly" --price <paddlePriceId> --environment sandbox
segmently paddle paywall-product ensure --project <projectId> --file paddle-product.json
{
  "version": "cli.paddle-paywall-product.v1",
  "name": "Monthly",
  "paddlePriceId": "pri_example",
  "paddleEnvironment": "sandbox"
}

Use a file or named options, not both. Optional type is derived from the verified price when absent; an explicit mismatch is rejected. paddleProductId may assert the expected parent product. Named equivalents include --type, --product, --description and --checkout-mode.

Ensure creates a Segmently reference, never a Paddle product/price or a production product. Repeating a compatible normalized name returns the existing id with created:false, without updating display copy or rechecking the provider price. Conflicting price/type/product/mode returns conflict. Current stored checkout aliases are redirect (bakes to overlay) and embedded (bakes to inline); these are not proof that true merchant-page redirect or inline runtime acceptance is complete.

Provider-neutral launch preflight

launch preflight --require-payment-provider checks saved Stripe test or Paddle Sandbox connection metadata. --require-stripe-sandbox remains a deprecated alias for one release. Both flags select the same neutral check; neither establishes live credential or payment readiness.

For bound Paddle products, a selected placement also produces the separate paddle.checkout.prerequisites row on supporting backends. It uses the placement policy and only referenced products, checks an exact HTTPS domain for production/Express when available, and retains unknown device/return-policy evidence as a warning. No placement scope means the mode check is skipped. A configured account can therefore pass the connection check while its checkout still needs review. Stripe-only flows are unchanged. This check does not execute a payment or verify webhook delivery.

For service tokens, preflight requires all of funnels:read, themes:read, publish:read, plus at least one effective stripe:read or paddle:read scope. paddle:write satisfies Paddle read. Web Onboarding Billing Access is also required. The capabilities response expresses common requirements in scopes and alternatives in anyOfScopes; both conditions must be met.

Only granted provider connection metadata appears in the report. An ungranted provider is null in evidence.providers, with restrictedProviders explaining the missing visibility. It is not a disconnected provider and cannot satisfy the connection check. The deprecated stripe.sandbox.connected row mirrors the neutral verdict, while Stripe account metadata stays hidden from Paddle-only tokens. Provider catalog commands still require their own scope.