Versions & Diffs
Every time you push a component, Myop stores a new immutable revision. Nothing is overwritten. That means questions like "what changed since the last release?", "who touched this?" and "how does staging differ from production?" have exact answers.
This page documents the myop log and myop diff commands. The same capabilities are
available to AI assistants over MCP — see
MCP Server Setup for the tool signatures.
This feature is experimental. The version-ref grammar is stable, but response fields may still gain properties.
The two axes of "version"
A Myop component has history along two different axes, and teams use both. Whichever one your team uses, the same refs work.
| Axis | What it is | Where it lives |
|---|---|---|
| Revision | successive saves of one variant | documents sharing a variants.id |
| Variant | sibling variants of a component | components.variants_ids[] |
Some teams iterate inside a single variant, so each version is a new revision.
Others create a new variant per version — there are real components with 120+
variants named "Version 1…122". latest~1 only walks revisions and will tell you
"only 1 content revision" on the second shape. prev walks whichever axis actually
holds the previous version, which is why it is the default for a bare myop diff.
Version refs
A version ref is a short string that resolves to exactly one revision. It is the
core API of this feature — the CLI, the MCP tools and get_component all take the
same grammar.
| Ref | Resolves to |
|---|---|
latest / current | the newest content revision of the default variant |
prev | the component's previous version — steps back a variant if the current variant has no earlier revision |
latest~1, latest~2 | N content revisions back, within the same variant |
variant~1, variant~2 | N sibling variants back, newest revision of each |
@production, @staging | the revision that environment is currently serving |
by:someone@acme.com | that person's newest revision |
rev:<24-hex> | one exact revision |
variant:<id>, variant:"Version 3" | that sibling variant's newest revision |
at:2026-08-01T12:00:00Z | the newest revision at or before that instant |
Every form accepts a trailing ~N: @staging~1, by:tal@acme.com~2, variant:"V3"~1.
~1 in a shellA word starting with ~ is tilde-expanded by the shell, so myop diff ~1 fails in
zsh with "not enough directory stack entries" before the CLI ever runs. Write
latest~1 — safe in every shell. myop log prints the shell-safe ref for every
row, so copy it rather than assembling one.
Two rules worth knowing
~N counts content revisions. Releasing a variant writes a revision whose HTML is
identical to its parent, so those are skipped. myop diff latest~1 therefore always
shows a real change instead of an empty diff. Release revisions are addressed by
@<environment> instead, and carry "back": null.
by:X~N stays on that person. by:dana@acme.com~1 is dana's second-newest
revision, not whoever happened to save before her.
CLI
myop log
myop log # history for the component in myop.config.json
myop log <componentId> --limit 30
myop log --author tal@acme.com # only that person's revisions
myop log --env production # only revisions released to production
myop log --variant <variantId> # only one sibling variant
myop log --no-releases # hide release-only revisions
myop log --json # machine-readable
Version Lab 11111111-2222-4333-8444-555555555555
8 revisions across 2 variants, showing 3
Legacy bbbbbbbb-…-000000000002 2 revs · production
Main aaaaaaaa-…-000000000001 6 revs (default) · qa
REF REVISION WHEN WHO SIZE WHAT
latest 6a9123bec2 2h ago dana@acme.com 741 B edit [Main]
latest~1 6a91235ac2 2h ago tal@acme.com 738 B edit [Main]
@qa 6a9122f6c2 2h ago dana@acme.com 657 B release → qa ● live on qa [Main]
The REF column is the exact ref that resolves back to that row — paste it straight
into myop diff.
myop diff
myop diff uses git's argument rules:
| Command | Compares |
|---|---|
myop diff | latest → your local working file |
myop diff <ref> | that version → your local working file |
myop diff <ref> <ref> | two published versions |
myop diff # latest -> your working file
myop diff @production # production -> your working file
myop diff prev latest # the previous published version -> latest
myop diff @staging @production # staging vs production
myop diff latest~2 latest # two revisions back -> latest
myop diff -u # add the actual changed lines
myop diff --max-lines 500 -u # raise the unified-diff cap (default 200)
myop diff --file ./dist/index.html # choose the working file explicitly
myop diff --json # machine-readable
Anything short of two refs puts your working file on the newer side. To compare two
published versions, name both — myop diff prev latest, not myop diff.
Version Lab
@qa (6a9122f6c2, dana@acme.com, 2h ago, live on qa) → latest (6a9123bec2, dana@acme.com, 2h ago)
+3/-2 lines, +84 B · markup +3 B · scripts +81 B · contract: 2 changes · +1 external endpoint
Myop contract
• myop:size meta changed — {"width":400,"height":300} → {"width":"100%","height":420}
• myop_cta_handler changed (+37 bytes)
External endpoints
+ api.acme.com/v2
Sections
markup 213 B → 216 B +3 B
scripts 297 B → 378 B +81 B
Run myop diff --help or myop log --help for the full ref grammar in your terminal.
Comparing your working file
myop diff # working file vs latest
myop diff @production # working file vs what production serves
myop diff -u # with the changed lines
myop diff --file ./dist/index.html
The working file is the newer side, matching git diff <commit>. It is
dist/index.html if that exists, else index.html, overridable with --file. If no
working file is found the command says so and shows you the two-ref form instead of
guessing.
Version Lab
latest (6a9123bec2, dana@acme.com, 3h ago) → local (working copy, 767 B)
+3/-3 lines, +26 B · markup +0 B · scripts +26 B · contract: 2 changes · +2 external endpoints · -1 external endpoint
Myop contract
• myop:size meta changed — {"width":"100%","height":420} → {"width":"100%","height":600}
• myop_cta_handler changed (+19 bytes)
External endpoints
+ app.acme.com
+ metrics.newvendor.io/collect
- api.acme.com/v2
The working file is uploaded so it is analysed by the same server-side engine
as a published revision — identical fields, identical contract and endpoint detection.
Nothing is stored; it is a read-only comparison. The only difference in the response is
that side's descriptor, which reports kind: "inline" and a null rev.
Inline content is capped at 4 MB. Beyond that, push first and compare two published revisions — resolving both sides from refs has no size limit.
MCP tools
list_component_versions
Revision history. Returns metadata only — never component HTML — so it is cheap to call before deciding what to fetch or diff.
Parameters
componentId(required) — the component UUIDvariantId(optional) — restrict to one sibling variantauthor(optional) — substring match on the author's email, case-insensitiveenvironment(optional) — only revisions carrying a release to that environmentlimit(optional) — max revisions to return (default 20)includeReleases(optional) — include release-only revisions (defaulttrue)
{
"success": true,
"componentId": "11111111-2222-4333-8444-555555555555",
"componentName": "Version Lab",
"variants": [
{
"id": "aaaaaaaa-0000-4000-8000-000000000001",
"name": "Main",
"revisions": 6,
"latestRev": "6a9123bec2ddd9e0e993656d",
"latestAuthor": "dana@acme.com",
"latestOn": "2026-08-28T05:59:26.000Z",
"environments": ["qa"],
"isDefault": true
}
],
"revisions": [
{
"rev": "6a9123bec2ddd9e0e993656d",
"ref": "latest",
"variantId": "aaaaaaaa-0000-4000-8000-000000000001",
"variantName": "Main",
"kind": "edit",
"author": "dana@acme.com",
"authoredOn": "2026-08-28T05:59:26.000Z",
"writtenBy": "dana@acme.com",
"writtenOn": "2026-08-28T05:59:26.000Z",
"bytes": 741,
"releases": [
{
"environment": "qa",
"environmentId": "env-qa",
"releasedOn": "2026-08-28T05:56:06.000Z",
"releasedBy": "dana@acme.com"
}
],
"serving": [],
"back": 0
}
],
"totalRevisions": 8,
"shown": 3,
"variantsTotal": 2,
"truncated": false
}
Field notes
| Field | Meaning |
|---|---|
rev | the revision token — the same rev the Myop API returns from a variant save |
ref | the canonical ref that resolves back to this row. Copy it; don't assemble one |
kind | edit (code was written), release (a release was attached, HTML unchanged), create (the variant's first revision) |
author / authoredOn | who wrote the code. Differs from writtenBy on release revisions |
writtenBy / writtenOn | who wrote this document — the releaser, on a release revision |
releases | every release record this revision carries. Later edits copy them forward, so several revisions can carry the same one |
serving | the environments this exact revision is what /consume returns. Only one revision per environment has it |
back | the N for a ~N ref. null on release revisions |
truncated | the scan covered a window of the newest revisions, not the whole history |
releases is not servingThree revisions can each carry releases: [{environment: "qa"}] while only one has
serving: ["qa"]. If you want to know what QA is actually running, read serving — or
just use the @qa ref.
diff_component_versions
Compares two revisions and returns what changed, computed server-side. The component HTML never enters the response.
Parameters
componentId(required)from(optional) — the older side, as a version ref. Defaults toprev, or tolatestwhentoContentis suppliedto(optional) — the newer side. Defaults tolatestfromContent/toContent(optional) — HTML supplied inline for that side instead of resolving a ref. This is howmyop diffcompares a working file: the content is analysed by the same engine, so every field is identical. That side's descriptor reportskind: "inline"with a nullrev. Capped at 4 MB — resolving both sides from refs has no size limitfromLabel/toLabel(optional) — display name for an inline side, e.g."local"mode(optional) —"summary"(default) or"unified"maxHunkLines(optional) — cap the unified diff (default 200)
{
"success": true,
"componentId": "11111111-2222-4333-8444-555555555555",
"componentName": "Version Lab",
"summary": "+3/-2 lines, +84 B · markup +3 B · scripts +81 B · contract: 2 changes · +1 external endpoint",
"identical": false,
"from": {
"ref": "@qa",
"rev": "6a9122f6c2ddd9e0e993656b",
"variantId": "aaaaaaaa-0000-4000-8000-000000000001",
"variantName": "Main",
"kind": "release",
"author": "dana@acme.com",
"authoredOn": "2026-08-28T05:54:26.000Z",
"bytes": 657,
"serving": ["qa"],
"back": null
},
"to": { "ref": "latest", "rev": "6a9123bec2ddd9e0e993656d", "kind": "edit", "bytes": 741, "serving": [], "back": 0 },
"bytes": { "from": 657, "to": 741, "delta": 84 },
"lines": { "from": 24, "to": 25, "added": 3, "removed": 2 },
"sections": [
{ "name": "markup", "fromBytes": 213, "toBytes": 216, "fromLines": 12, "toLines": 12, "changed": true },
{ "name": "styles", "fromBytes": 132, "toBytes": 132, "fromLines": 5, "toLines": 5, "changed": false },
{ "name": "scripts", "fromBytes": 297, "toBytes": 378, "fromLines": 9, "toLines": 10, "changed": true }
],
"contract": [
"myop:size meta changed — {\"width\":400,\"height\":300} → {\"width\":\"100%\",\"height\":420}",
"myop_cta_handler changed (+37 bytes)"
],
"externals": { "added": ["api.acme.com/v2"], "removed": [] },
"lineEndings": { "from": "LF", "to": "LF", "changed": false }
}
Reading a diff
contractis the highest-signal field. It reports changes tomyop_init_interface,myop_cta_handlerand the<meta name="myop:size">tag — the three things that break a component in a host.externalslists third-party endpoints added or removed. A new outbound call appearing between staging and production is exactly the kind of thing worth catching before a release.sectionssplits the file intomarkup/styles/scriptsso you know where to look before asking for lines.lineEndings— if a revision was saved with CRLF and the next with LF, a raw diff would call every line changed. The comparison normalizes line endings and reports the switch once. When that is the only difference you getonlyLineEndings: true.sameRevisionis set when both refs resolve to the same revision — for exampleby:dana@acme.comwhen dana wrote the current version. That is different from "nothing changed", and the accompanyinghinttells you how to compare against what came before.
get_component with a version
get_component accepts a version parameter taking the same ref grammar, so you can
fetch the HTML of any past revision:
{ "componentId": "…", "version": "@production" }
The response gains a resolvedVersion block describing exactly which revision you got.
version supersedes the older environment parameter, and makes environment
resolution deterministic.
Response encoding
Every tool result has two channels — content[0].text and structuredContent. Sending
the payload in both doubles the bytes for no benefit, so each tool puts the bulk in
exactly one of them:
| Tool | Complete record in | Other channel gets |
|---|---|---|
get_component | content[0].text (the HTML is the payload) | metadata + htmlContentBytes |
diff_component_versions | structuredContent | a human-readable rendering; in unified mode the hunks are in the text channel and structuredContent reports hunksCount |
list_component_versions | structuredContent | the same table myop log prints |
Read the channel your tool designates, and do not assume content[0].text is JSON —
for the two version tools it is a rendering, and JSON.parse on it will throw.
Why the text channel carries the heavy payloads: it is the lowest common denominator.
Verified against Claude.ai, whose model receives content[0].text and does not
receive structuredContent — asked to read htmlContentBytes from a get_component
result, it correctly reported the field as absent and listed the keys it actually had
(success, component, resolvedVersion, resolvedEnvironment, latestVariant, htmlContent),
which is the text channel exactly. This is also why each rendering is written to be
self-sufficient: myop log's text includes every ref and full revision token, and a
unified diff's text includes the hunks.
Limits
- Scan depth. History is read as a window over the newest revisions rather than the
whole timeline. A ref that falls outside it (a deep
~N, an oldat:) automatically escalates to a full scan, so correctness is unaffected — only the cost. - Unified diffs are capped at 200 lines by default; raise with
maxHunkLines/--max-lines.truncated: truetells you the cap was hit. - Very large components. A self-contained component that inlines media as base64
data:URIs can reach several MB per revision.list_component_versionsanddiff_component_versionsnever carry HTML and are unaffected, butget_componentreturns the whole file — if it exceeds the endpoint's response limit you get an explicit error suggestingincludeContent: false, the diff tool, or hosting the assets by URL instead of inlining them.
Common tasks
# Have my local edits diverged from what is published?
myop diff
# What went in since the previous published version?
myop diff prev latest
# How far ahead of production is the published code?
myop diff @production latest
# What exactly would shipping my local changes do to production?
myop diff @production -u
# Did that person's last change add any outbound calls?
myop diff by:someone@acme.com latest
For AI assistants: run npx myop train to install skills that teach these commands, or
see Myop MCP Server Setup to connect the MCP server directly.
The CLI refreshes installed skills automatically after it updates itself, so an agent
never works from documentation older than the CLI it is driving.