Skip to content

figmog: fold-backed local mirror of a Figma file - #21

Closed
hhff wants to merge 56 commits into
flowercomputers:mainfrom
sanctuarycomputer:worktree-figmog
Closed

figmog: fold-backed local mirror of a Figma file#21
hhff wants to merge 56 commits into
flowercomputers:mainfrom
sanctuarycomputer:worktree-figmog

Conversation

@hhff

@hhff hhff commented Aug 17, 2026

Copy link
Copy Markdown

Hackathon submission

Note: this PR targets our own fork's main, not upstream — the template is filled in to prep the eventual upstream submission, but the acknowledgment box is deliberately left unchecked (that submission is a separate, explicit step).

category

pick one:

  • agent support
  • performance
  • novel interface / gaming

team

  • project name: figmog
  • team / author name(s): hhff / sanctuary.computer
  • contact (optional): hugh@sanctuary.computer

what you built

figmog turns one Figma file into a local, lightning-fast, agent-queryable database — and then becomes the only Figma MCP server an agent needs: a cached proxy in front of Figma's native tooling, backed by a fold mirror.

The mirror (v1). Figma has no delta API and its file endpoints are Tier-1 rate limited. figmog synthesizes the missing delta API from fold's KeyedStream upsert semantics: poll the cheap Tier-3 metadata endpoint, spend one Tier-1 fetch only when last_touched_at moves, flatten the document into deterministic per-node records, and upsert-diff them through one atomic transaction into 14 materialized sinks (node table, children multimap, BM25 text search, inverted indexes for instances/styles/variables/types, design-system tables, proxy cache). A byte-identical re-pull pushes zero deltas (delta-probe tested); one renamed layer costs exactly one retract/insert pair.

The MCP server (v2). figmog serve is an MCP stdio server with the sync loop built in — one process, always fresh. 17 figmog_* tools answer from the mirror at zero API cost, including whole-file structural queries nothing else offers: figmog_search (BM25), figmog_where (match any JSON pointer across every node), figmog_at (spatial hit-test), figmog_stats, figmog_path, figmog_instances, figmog_vars, and more.

The cached proxy (v3). With the Figma desktop app running (Dev/Full seat), figmog discovers the native Dev Mode MCP server's tools at startup and re-exposes them verbatimget_design_context, get_screenshot, get_variable_defs, all of them, native output formats passed through untouched — with responses cached by (tool, args, file version), so a screenshot of an unchanged frame is fetched once, ever. Agents connect to figmog alone and get both worlds; the initialize instructions steer tool choice. CLI parity via figmog tools / figmog call <tool> --args '<json>'. Pull also opportunistically syncs Enterprise variables/local (silently skipped on other plans; plugin-export import + boundVariables inference remain as fallbacks).

Design docs: docs/superpowers/specs/2026-08-15-figmog-build-design.md (+ plans in docs/superpowers/plans/).

Upstream finding (not patched here): fold's Table<K, V> panics when K's postcard encoding is empty (e.g. K = ()) — lsm-tree forbids empty keys. figmog keys its singleton meta row by u8 instead. Also: fold's store-open panics on lock contention; figmog translates these to clean errors at its own layer.

how to run

# from repo root
export FIGMA_TOKEN=figd_…   # figma.com → settings → security → personal access tokens
cargo run -p figmog -- pull "https://www.figma.com/design/<key>/<name>"
cargo run -p figmog -- search "pricing card"
cargo run -p figmog -- stats

# as an MCP server for Claude Code (with built-in sync + native-tool proxy):
cargo build -p figmog
claude mcp add figmog -- $PWD/target/debug/figmog serve "https://www.figma.com/design/<key>/<name>"

Offline demo without a token: save any GET /v1/files/:key response as JSON, then figmog pull --from-file resp.json --db /tmp/demo and figmog serve --no-upstream --no-watch --db /tmp/demo.

demo / notes

  • 112 tests: flatten determinism (byte-identical postcard), delta-probe churn arithmetic (exact push counts), mid-transaction rollback, watch/backoff state machine, MCP protocol contract (15 unit cases), upstream client incl. SSE parsing + session/protocol-version headers against an in-process HTTP fake, cache hit/miss/eviction/collision-guard, proxy routing + isError handling, serve e2e over real stdio incl. lock-contention behavior, variables import/inference/Enterprise-sync round trips.
  • cargo test -p figmog, cargo test -p fold, cargo clippy -p figmog --no-deps -- -D warnings, cargo fmt -p figmog --check all green; zero diff to fold/ese/anny.
  • Built via spec → plan → subagent-driven TDD with per-task review, two adversarial whole-branch review passes, and empirically-verified fix rounds.

checklist

  • i forked bog-kit and built my project in this fork
  • my project is runnable from this pr (crate name and run command above)
  • i selected exactly one category
  • this pr is my official hackathon submission acknowledgment

hhff and others added 30 commits August 15, 2026 18:20
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ree/get/find)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…/styles/uses/vars/import-variables) and watch loop

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…key integrity)

Closes out the figmog final-wave review: watch's Tier-1 pull failures now
honor Retry-After and back off exponentially instead of hammering the
budget every interval; --json mode emits {"error":...} on stderr instead
of plain text; a failed pull no longer rewrites .figmog/current or leaves
a stale mirror pointer behind; watch's Wait line no longer claims
"rate limited" for ordinary backoff; find --type is case-insensitive like
styles --type; pull --from-file with no established key gets a
pull-specific error instead of "run pull first"; and the variable_edges
dedup comment now matches what the code actually does.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Spec §4/§6 now describe FileMeta.last_modified (the file endpoint's
lastModified) as what's actually stored, and spell out that watch's
change-detection compares it against the meta endpoint's
last_touched_at across two different endpoints, with the manual live
check extended to verify they agree on a real file. Drops the
unimplemented "plus small jitter" note per the no-jitter ruling, and
documents the pull-path backoff now applied to Tier-1 pull failures.
Also fixes stale naming drift (.figmog/config -> .figmog/current,
--interval 10s -> --interval 10) and adds the Tier-3 poll budget and
a pull --fresh variables-wipe caveat to the README.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ions, structural query pack

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…paid-seat pivot)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A failed figmog_sync tool call left pull_backoff and next_deadline
untouched, so a background watch tick could still fire back into a
rate-limit window the server had just been told about. Reuse
pull_failure_wait (same as the tick-triggered pull path) to advance
the backoff and push next_deadline out on failure.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
hhff and others added 26 commits August 15, 2026 23:54
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ough, error caching, do_pull eviction

- C1: cmd_call opened its own store before delegating figmog_sync to
  do_pull (which opens the same path again) — fjall's single-open-per-
  process rule turned that into a panic after the Tier-1 fetch was
  already spent. figmog_sync now returns before cmd_call opens a store.
- C2 (controller-amended constraint, spec §11 point 1): proxied results
  were double-wrapped as escaped text, mangling native output formats
  (e.g. unrenderable image content). mcp::ToolOutput{Json,Raw} lets local
  tools keep the text-block wrap while proxied calls pass through
  verbatim.
- I3: upstream tool-level errors (isError: true) were being cached,
  replaying a stale failure forever; cache-store is now skipped for them.
- I4 (controller ruling): stale proxy_cache eviction after a
  version-changing pull now lives in do_pull itself, covering pull/watch/
  call figmog_sync in one place; figmog serve's own inline blocks are
  unchanged.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…, cache collision guard)

I-1: translate the fold locked-store panic (a CLI command opening a
store `figmog serve`/`figmog watch` already holds) into a clean exit-1
error at every CLI store-opening call site, via a catch_unwind wrapper
that only translates the lock case and re-raises any other panic
unchanged; document the single-writer constraint in the README.

I-2: capture the negotiated protocolVersion from the upstream's
initialize response and send it as MCP-Protocol-Version on every
later request, per the streamable-HTTP transport spec this client
declares.

I-3: cache::lookup now verifies the stored row's tool and
args_canonical against the request before serving a hit, closing the
FNV-64 key-collision gap where one tool's cached response could be
served for another.

Also documents that `figmog tools`/`figmog call` need a resolved
mirror (--db or a prior pull).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
open_store_checked no longer swaps out the global panic hook: doing so
silenced non-lock panics (corrupt store, disk error) entirely, turning
a genuine bug into a silent exit 101 with zero stderr output, and
mutating a process-global hook around one call was racy against other
threads besides. The default hook now stays active throughout — a
lock panic still prints fold's raw trace before the friendly
STORE_LOCKED_MSG follows, and a non-lock panic prints and propagates
exactly as it would with no wrapper at all.

Also route run_serve's own long-lived store open through
open_store_checked, closing the serve-vs-serve / serve-vs-watch
double-owner case the CLI-read fix didn't cover: starting a second
figmog serve or figmog watch against an already-owned store now gets
the same clean locked-store error instead of a raw panic.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…files

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…llowlisted, PATs rejected

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…mog_files

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…elease

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@cowtoolz cowtoolz closed this Aug 20, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants