jujuleaf bridge is the stable process boundary for programs that integrate
with JujuLeaf. It hides private Overleaf HTTP, Socket.IO, ranges, history-OT,
and cache representations behind a versioned JSON protocol.
Protocol version 1 is read-only and covers comment discovery, authoritative thread context, and comment wake-up events.
- One-shot commands write exactly one compact JSON value followed by
\n. comments watchwrites one compact JSON value per line (NDJSON).- stdout never contains ANSI escapes, progress bars, or human notices.
- Normal operational failures are JSON on stdout followed by a non-zero exit.
- stderr is not part of the protocol and is reserved for optional diagnostics.
--rawand--prettyare rejected for bridge commands.- Callers must ignore unknown optional fields.
Run bridge commands inside a JujuLeaf clone to use its project and profile. An
external caller can instead pass global --project-id and --profile options:
jujuleaf --project-id PROJECT_ID --profile NAME bridge comments list --protocol 1jujuleaf bridge describedescribe does not require authentication or a project. Its data reports the
binary version, supported bridge versions, operations, normalized event types,
and stable error codes. Consumers should select a mutually supported protocol
version rather than comparing JujuLeaf SemVer strings.
A successful one-shot response has this shape:
{
"protocol": {"name": "jujuleaf.bridge", "version": 1},
"kind": "result",
"operation": "comments.get",
"ok": true,
"data": {}
}Stream records additionally contain one process-local session ID and a strictly increasing sequence number:
{
"protocol": {"name": "jujuleaf.bridge", "version": 1},
"kind": "comment.event",
"operation": "comments.watch",
"ok": true,
"sessionId": "uuid",
"sequence": 4,
"data": {}
}The sequence is not a durable Overleaf cursor. A new process or reconnect can
only establish authority through a full comments.snapshot.
jujuleaf bridge comments list --protocol 1comments.list reads live Overleaf state. Its data contains:
{
"project": {"id": "project-id", "name": "My Paper"},
"documents": [
{
"id": "doc-id",
"path": "sections/introduction.tex",
"remoteVersion": 42,
"contentHash": "sha256:..."
}
],
"threads": [
{
"id": "thread-id",
"state": "open",
"messages": [],
"anchors": [],
"anchorState": "detached",
"detachedReason": "range_missing"
}
]
}comments.get returns the same project model, one singular thread, and only
the documents referenced by that thread:
jujuleaf bridge comments get THREAD_ID --protocol 1Thread messages contain stable IDs, content, optional author identity, and
RFC 3339 createdAt/editedAt timestamps. Email addresses and private raw
Overleaf fields are not exposed.
An attached or collapsed anchor has this form:
{
"documentId": "doc-id",
"path": "sections/introduction.tex",
"state": "attached",
"sourceRangeUtf16": {"from": 120, "to": 186},
"visibleRangeUtf16": {"from": 116, "to": 182},
"text": "In this paper, we...",
"before": "preceding context",
"after": "following context"
}- Both ranges use half-open CodeMirror UTF-16 coordinates.
sourceRangeUtf16addresses Overleaf's source/history representation.visibleRangeUtf16addresses the text visible after tracked deletions are hidden. Most consumers should use this range.attachedhas non-empty visible text.collapsedretains a known position but currently covers no visible text.- A detached thread has an empty
anchorsarray and an explanatorydetachedReason. - Multiple array entries preserve history-OT multi-range comments.
before and after are bounded context snippets, not complete documents. The
content hash covers the complete visible UTF-8 document.
jujuleaf bridge comments watch --protocol 1The default reconciliation interval is 60 seconds and can be changed with
--reconcile-interval SECONDS (minimum 5).
JujuLeaf connects the live event stream before reading the initial snapshot, so events that race with the scan are buffered rather than missed. Initial output:
stream.opened
comments.snapshot (reason = initial)
stream.ready
comment.event ...
comments.snapshot.data.snapshot is identical to comments.list.data. Treat
it as a complete replacement for previously known state. Periodic snapshots use
reason = reconcile.
Normalized comment.event.data.type values are:
message.created
message.edited
message.deleted
thread.resolved
thread.reopened
thread.deleted
thread.changed
An event contains project/thread IDs, an optional message ID and actor ID, and
observedAt. It intentionally omits comment content and anchor internals. Read
comments.get after an event to obtain authoritative state.
When the transport closes, JujuLeaf emits stream.reset, retries with bounded
exponential backoff, then emits a full snapshot with reason = reconnect and a
new stream.ready. Ctrl+C produces stream.closed and exit status 0.
Overleaf does not provide a durable comment event queue. Consumers therefore
must not treat comment.event as an audit log; the snapshot and reconciliation
records are the source of truth.
{
"protocol": {"name": "jujuleaf.bridge", "version": 1},
"kind": "error",
"operation": "comments.get",
"ok": false,
"error": {
"code": "AUTH_REQUIRED",
"message": "The selected profile is not authenticated",
"retryable": false
}
}Stable version 1 codes:
JUJULEAF_NOT_INITIALIZED
AUTH_REQUIRED
PROJECT_NOT_FOUND
THREAD_NOT_FOUND
CONNECTION_LOST
RATE_LIMITED
CONFLICT
UNSUPPORTED_PROTOCOL
INTERNAL_ERROR
Exit status is 0 for success, 1 for operational failure, and 2 for an
unsupported protocol selection. Consumers should branch on error.code, not
parse message or invent a more detailed numeric exit-code mapping.
- Adding an optional field is compatible within protocol version 1.
- Renaming/removing a field, changing its type, or changing stream semantics is breaking and requires a new protocol version.
- JujuLeaf should retain the prior protocol version for a migration window.
- Consumers must reject an unsupported higher protocol instead of guessing.
- Existing non-bridge
--rawoutput is outside this compatibility contract.