Skip to main content

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.

Work in Progress

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.

AxisWhat it isWhere it lives
Revisionsuccessive saves of one variantdocuments sharing a variants.id
Variantsibling variants of a componentcomponents.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.

RefResolves to
latest / currentthe newest content revision of the default variant
prevthe component's previous version — steps back a variant if the current variant has no earlier revision
latest~1, latest~2N content revisions back, within the same variant
variant~1, variant~2N sibling variants back, newest revision of each
@production, @stagingthe revision that environment is currently serving
by:someone@acme.comthat 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:00Zthe newest revision at or before that instant

Every form accepts a trailing ~N: @staging~1, by:tal@acme.com~2, variant:"V3"~1.

Don't type a bare ~1 in a shell

A 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:

CommandCompares
myop difflatest → 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
Two refs, or the working file

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
Same engine, same answer

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 UUID
  • variantId (optional) — restrict to one sibling variant
  • author (optional) — substring match on the author's email, case-insensitive
  • environment (optional) — only revisions carrying a release to that environment
  • limit (optional) — max revisions to return (default 20)
  • includeReleases (optional) — include release-only revisions (default true)
{
"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

FieldMeaning
revthe revision token — the same rev the Myop API returns from a variant save
refthe canonical ref that resolves back to this row. Copy it; don't assemble one
kindedit (code was written), release (a release was attached, HTML unchanged), create (the variant's first revision)
author / authoredOnwho wrote the code. Differs from writtenBy on release revisions
writtenBy / writtenOnwho wrote this document — the releaser, on a release revision
releasesevery release record this revision carries. Later edits copy them forward, so several revisions can carry the same one
servingthe environments this exact revision is what /consume returns. Only one revision per environment has it
backthe N for a ~N ref. null on release revisions
truncatedthe scan covered a window of the newest revisions, not the whole history
releases is not serving

Three 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 to prev, or to latest when toContent is supplied
  • to (optional) — the newer side. Defaults to latest
  • fromContent / toContent (optional) — HTML supplied inline for that side instead of resolving a ref. This is how myop diff compares a working file: the content is analysed by the same engine, so every field is identical. That side's descriptor reports kind: "inline" with a null rev. Capped at 4 MB — resolving both sides from refs has no size limit
  • fromLabel / 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

  • contract is the highest-signal field. It reports changes to myop_init_interface, myop_cta_handler and the <meta name="myop:size"> tag — the three things that break a component in a host.
  • externals lists 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.
  • sections splits the file into markup / styles / scripts so 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 get onlyLineEndings: true.
  • sameRevision is set when both refs resolve to the same revision — for example by:dana@acme.com when dana wrote the current version. That is different from "nothing changed", and the accompanying hint tells 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

For clients built directly against the MCP server

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:

ToolComplete record inOther channel gets
get_componentcontent[0].text (the HTML is the payload)metadata + htmlContentBytes
diff_component_versionsstructuredContenta human-readable rendering; in unified mode the hunks are in the text channel and structuredContent reports hunksCount
list_component_versionsstructuredContentthe 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 old at:) 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: true tells 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_versions and diff_component_versions never carry HTML and are unaffected, but get_component returns the whole file — if it exceeds the endpoint's response limit you get an explicit error suggesting includeContent: 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.