Proyecto independiente No afiliado, patrocinado ni avalado por la Watch Tower Bible and Tract Society o Jehovah's Witnesses.
jw-agent-toolkit
EN

Referencia

Referencia: capa de integraciones con JW Library

Contratos completos de los módulos de la Fase 19. Para el “porqué” ver conceptos/integracion-jw-library.md. Para casos de uso ver guias/integracion-jw-library.md.

Mapa del paquete

jw_core/
├── integrations/
│   ├── __init__.py             # Re-exporta API pública de las 4 capas
│   ├── jw_library.py           # Deep linking jwlibrary://
│   ├── jw_library_local.py     # Inspector local + Full Disk Access (macOS)
│   ├── jw_library_sync.py      # Sync incremental con sidecar state
│   └── meps_catalog.py         # Catálogo SQLite docid ↔ pub_code
└── parsers/
    └── jw_library_backup.py    # Parser de archivos .jwlibrary

Los tests viven en packages/jw-core/tests/test_jw_library_*.py y test_meps_catalog.py (5 archivos, 77 tests).


jw_core.integrations.jw_library — Capa 1

Deep linking al esquema jwlibrary://.

class JWLibraryError(RuntimeError)

Excepción raíz del módulo. Se eleva cuando un URL no puede construirse o despacharse.

class VerseRange

@dataclass(frozen=True)
class VerseRange:
    start: int
    end: int

Una sola rango contiguo. end == start para un versículo. Validación en __post_init__:

  • 1 ≤ start ≤ 999, 1 ≤ end ≤ 999
  • end ≥ start

build_bible_url(...) -> str

def build_bible_url(
    book_num: int,
    chapter: int,
    verse_start: int | None = None,
    *,
    verse_end: int | None = None,
    end_chapter: int | None = None,
    end_book: int | None = None,
    wtlocale: str | None = None,
) -> str
ParamDescripción
book_num1..66 (Génesis=1, Apocalipsis=66).
chapterNúmero de capítulo.
verse_startPrimer versículo. None ⇒ verso 1 implícito.
verse_endÚltimo verso del rango. None + end_chapter=None ⇒ verse único.
end_chapterPara rangos multi-capítulo (Mat 3:1–4:11). > chapter.
end_bookPara rangos cross-libro (raro). Default = book_num.
wtlocaleISO (“en”/“es”/“pt”) o JW code (“E”/“S”/“T”). Pasa por get_language si conocido; otherwise pass-through uppercase.

Returns: jwlibrary:///finder?bible=BBCCCVVV[-BBCCCVVV][&wtlocale=LL].

Raises: JWLibraryError si inputs son inconsistentes (book fuera de rango, end_chapter < chapter, verse_end < verse_start en mismo capítulo).

build_bible_urls(...) -> list[str]

def build_bible_urls(
    book_num: int,
    chapter: int,
    ranges: list[VerseRange],
    *,
    wtlocale: str | None = None,
) -> list[str]

Para versos disjuntos (“Juan 1:1, 4, 7-8”) devuelve una URL por rango — ?bible= no soporta múltiples rangos. Vacía ⇒ raise.

build_publication_url(...) -> str

def build_publication_url(
    docid: int | str,
    *,
    paragraph: int | None = None,
    wtlocale: str | None = None,
) -> str

Genera jwlibrary:///finder?wtlocale=LL&docid=N[&par=P]. docid debe ser numérico > 0. paragraph opcional > 0.

build_url_for_ref(...) -> str

def build_url_for_ref(
    ref: BibleRef,
    *,
    wtlocale: str | None = None,
) -> str

Atajo a partir de un BibleRef parseado por parse_reference. Si wtlocale es None, usa ref.detected_language.

detect_platform() -> str

Devuelve "darwin", "win32", "linux" o "unknown". Se basa en sys.platform.

open_jw_library(url, *, dry_run, platform, runner) -> dict

def open_jw_library(
    url: str,
    *,
    dry_run: bool = False,
    platform: str | None = None,
    runner: object = subprocess,
) -> dict[str, object]

Despacha (o no, si dry_run) un URL jwlibrary://.

Returns: {"url", "platform", "dispatched", ...}. En dry_run incluye "dry_run": True. En despacho real incluye "returncode" y "stderr" (truncado a 500 chars).

Raises: JWLibraryError si el URL no empieza por jwlibrary://, contiene caracteres de control, o el opener (open / xdg-open) no está disponible.

Argv por plataforma:

Plataformaargv
darwin["open", url]
win32["cmd", "/c", "start", "", url]
linux["xdg-open", url]

jw_core.parsers.jw_library_backup — Capa 2 (parser)

class JWLibraryBackupError(RuntimeError)

Excepción raíz.

class BackupManifest(BaseModel)

CampoTipoOrigen JSON
namestrname
creation_datestrcreationDate
device_namestruserDataBackup.deviceName
schema_versionint | NoneuserDataBackup.schemaVersion
last_modified_datestruserDataBackup.lastModifiedDate
database_namestruserDataBackup.databaseName (default "userData.db")
hashstrhash o userDataBackup.hash
typeint | Nonetype
versionint | Noneversion
extradictcampos no reconocidos

class Location(BaseModel)

Direccionable bíblico o publicación. is_biblebook_number y chapter_number no son None.

class UserNote(BaseModel)

CampoTipoNotas
note_idintPK SQLite.
guidstrEstable cross-schema.
title, contentstrCuerpo de la nota.
last_modified, createdstrISO timestamp del backup.
block_type, block_identifierint | NoneAnclaje a párrafo/verso.
locationLocation | NoneResuelto por LocationId.
user_mark_idint | NoneUserMark al que está atada.
tagslist[str]Nombres de tags vía TagMap.

class UserHighlight(BaseModel)

CampoTipoNotas
user_mark_idintPK SQLite.
color_index, style_indexintColor / estilo del resaltado.
user_mark_guidstrEstable.
locationLocationSiempre presente — orphans se skippean.
block_rangeslist[dict]Lista de {block_type, identifier, start_token, end_token}.

class Bookmark(BaseModel)

CampoTipo
bookmark_idint
slotint (0..9 por publicación)
title, snippetstr
block_type, block_identifierint | None
locationLocation

class Tag(BaseModel) / class InputField(BaseModel)

Tag: tag_id, name, type (1=user, 2=Favorite built-in, etc.). InputField: location_id, text_tag, value, location (opcional).

class BackupContents(BaseModel)

Contenedor top-level. Atributos: source_path, manifest, locations, notes, highlights, bookmarks, tags, input_fields. Property counts devuelve dict de tamaños.

parse_jw_library_backup(path) -> BackupContents

Abre el ZIP, parsea manifest, extrae userData.db a tempfile, lo abre en URI mode=ro, proyecta cada tabla. Schema-resistant: PRAGMA table_info + select sólo de columnas presentes.

Raises: JWLibraryBackupError si el archivo no existe, no es ZIP, le falta manifest.json o userData.db.

parse_user_data_db(path, *, manifest=None, source="") -> BackupContents

Para cuando ya tienes el SQLite (caso: macOS Full Disk Access). Reutiliza el mismo backend.

notes_for_chapter(backup, *, book_num, chapter) -> list[UserNote]

Filtra notas cuya Location apunta al capítulo dado.


jw_core.integrations.jw_library_sync — Capa 2 (sync incremental)

class SyncEntry

@dataclass
class SyncEntry:
    item_id: str
    source_id: str
    last_modified: str = ""
    content_hash: str = ""

class SyncState

Sidecar para un backup_id. Contiene notes, bookmarks, input_fields (dicts key → SyncEntry) y metadata. Serializable vía to_dict / from_dict.

class SyncStateStore(path)

Backend JSON. Métodos:

MétodoDescripción
load(backup_id) -> SyncStateDevuelve state vacío si el archivo no existe o está corrupto.
save(state)Persiste preservando otros backup_ids.

class SyncPlan / class SyncReport

Campo (Plan)Tipo
new_notes, updated_noteslist[UserNote]
deleted_note_source_idslist[str]
new_bookmarks, updated_bookmarkslist[Bookmark]
deleted_bookmark_source_idslist[str]
new_input_fields, updated_input_fieldslist[InputField]
deleted_input_field_source_idslist[str]

Property is_noop. Method summary() -> dict[str,int].

compute_sync_plan(backup, state) -> SyncPlan

Sin efectos secundarios. Una entrada se considera updated cuando su content_hash cambia. Notas se identifican por guid (fallback id:<note_id>). Bookmarks por bookmark_id. InputFields por (location_id, text_tag).

sync_backup_to_rag(backup_path, store, *, ...) -> SyncReport

ParamDefaultDescripción
state_path<store.path>/jw_library_sync.jsonSidecar JSON.
include_bookmarksTrueTrackear marcadores.
include_input_fieldsTrueTrackear respuestas de campos.
dry_runFalseSi True, computa plan y nada más.
min_chars8Skip de chunks demasiado cortos.

Pasos:

  1. Parse backup → diff vs state.
  2. Si no es dry_run: store.delete_by_source_ids(...) para eliminar viejos.
  3. Para cada new/updated: chunk_paragraphs + store.add. El state se actualiza incluso si se skippeó por min_chars (invariante para no re-reportar como new).
  4. Evict de state los deleted.
  5. state_store.save(state).

Source ids canónicos:

  • Notas: jwlib:note:{note_id}
  • Marcadores: jwlib:bookmark:{bookmark_id}
  • Campos: jwlib:input:{location_id}:{text_tag}

Metadata adjunta a cada chunk

kindCampos extras
user_notenote_id, guid, created, last_modified, tags[], book_num, chapter, key_symbol, document_id, meps_language
user_bookmarkbookmark_id, slot, book_num, chapter, key_symbol, document_id
user_inputlocation_id, text_tag, key_symbol, document_id

Todas llevan source_backup (nombre del manifest o path original).


jw_core.integrations.meps_catalog — Catálogo MEPS

default_catalog_path() -> Path

Lee env JW_MEPS_CATALOG_PATH; default ~/.jw-agent-toolkit/meps_catalog.db.

class CatalogPublication / class CatalogDocument

Dataclasses simples. CatalogPublication por (pub_code, language_index). CatalogDocument por (pub_code, language_index, document_id) con meps_document_id, title, chapter_number, etc.

class MepsCatalog(db_path=None)

Context manager. Métodos:

MétodoDescripción
index_jwpub(jwpub_path) -> dictParse metadata (sin descifrar). Upsert publication + documentos. Idempotente.
list_publications(*, pub_code=None, language_index=None)Filtra y ordena.
find_documents(*, pub_code, document_id, meps_document_id, language_index, chapter_number, limit)Filtros componibles.
resolve_docid(pub_code, *, chapter_number=None, language_index=None) -> CatalogDocument | NoneSelector inteligente: prefiere inglés (idx 0) si no se especifica idioma.
stats() -> dict{db_path, publications, documents}.

Schema interno

CREATE TABLE publication (
    pub_code TEXT, language_index INTEGER, title TEXT, short_title TEXT,
    year INTEGER, publication_type TEXT, source_path TEXT, last_indexed_at TEXT,
    PRIMARY KEY (pub_code, language_index)
);
CREATE TABLE document (
    document_id INTEGER, meps_document_id INTEGER, pub_code TEXT,
    language_index INTEGER, title TEXT, toc_title TEXT, chapter_number INTEGER,
    section_number INTEGER, first_page_number INTEGER, last_page_number INTEGER,
    PRIMARY KEY (pub_code, language_index, document_id)
);
CREATE INDEX idx_document_meps ON document(meps_document_id);
CREATE INDEX idx_document_chapter ON document(pub_code, chapter_number);

Helper index_jwpub(path, *, db_path=None)

Shortcut sin context manager para indexing puntual.


jw_core.integrations.jw_library_local — Capa 3

ENV_OPT_IN = "JW_LIBRARY_LOCAL_READ"

Variable de entorno obligatoria salvo force=True.

class MacOSFullDiskAccessError(RuntimeError)

Específica para casos donde TCC bloquea la lectura del container.

class InstalledPublication

Refleja una fila de Windows publications.db. Campos: publication_id, key_symbol, title, short_title, publication_type, year, issue_tag_number, meps_language, last_modified.

class LocalInspectionResult

CampoDescripción
platform”darwin” / “win32” / “linux” / “unknown”
supportedTrue sólo si pudimos leer datos del usuario.
opt_inEstado del opt-in.
app_detectedSi encontramos la app.
library_pathRuta a publications.db o JW Library.app según plataforma.
user_data_pathRuta a userData.db si accesible.
publicationsLista InstalledPublication.
reasons[] / suggestions[]Mensajes legibles para el usuario.

inspect_local_jw_library(*, force=False) -> LocalInspectionResult

Dispatcher principal:

PlataformaAcción
win32Glob %LOCALAPPDATA%\Packages\WatchtowerBibleandTractSocietyofNewYorkInc.JWLibrary_*\LocalState\ → lee publications.db con PRAGMA-projected select.
darwinLlama check_macos_full_disk_access(). Si OK → busca userData.db. Si bloqueado → instrucciones FDA.
linuxDevuelve supported=False con sugerencia de exportar backup.
unknownDevuelve supported=False.

check_macos_full_disk_access() -> dict

Probe barata: intenta os.scandir(container). Returns {path, readable, error}. No falla — devuelve estado.

read_macos_userdata() -> BackupContents

Workflow:

  1. check_macos_full_disk_access(); si bloqueado, raise MacOSFullDiskAccessError.
  2. _find_userdata_in_container(): probe paths conocidos + rglob de fallback.
  3. shutil.copy a tempfile (el live DB puede estar en WAL mode).
  4. parse_user_data_db(tmp, manifest=…)BackupContents.
  5. Cleanup del tempfile.

Tools MCP expuestos

Inventario completo de la Fase 19 (11 tools nuevos):

ToolCapaSide effects
open_in_jw_library1dry_run=True por default; opcional open real
import_jw_library_backup2Read-only.
list_user_notes2Read-only.
ingest_user_notes2Escribe al RAG store.
sync_jw_library_backup2Diff incremental. Escribe al RAG store y al state file.
register_jwpub_in_catalogEscribe al catálogo MEPS SQLite.
find_publication_in_catalogRead-only.
open_publication_by_symbol1 + catdry_run=True por default.
check_jw_library_full_disk_access3Read-only probe.
read_jw_library_live_userdata3Read-only (copia a tempfile).
inspect_local_jw_library_tool3Read-only. Requiere JW_LIBRARY_LOCAL_READ=1 o force=True.

Variables de entorno

VarDefaultUsado por
JW_LIBRARY_LOCAL_READinspect_local_jw_library (opt-in obligatorio).
JW_MEPS_CATALOG_PATH~/.jw-agent-toolkit/meps_catalog.dbdefault_catalog_pathMepsCatalog.
(sidecar sync)<store.path>/jw_library_sync.jsonsync_backup_to_rag (override por parámetro state_path).

Cobertura de tests

ArchivoTestsCubre
test_jw_library_integration.py30URL builders + dispatcher + safety
test_jw_library_backup.py16Parser ZIP + schema-resilience
test_jw_library_local.py19Inspector + FDA detection + live read
test_jw_library_sync.py9State store + diff engine + apply
test_meps_catalog.py13SQLite catalog + resolve_docid
Total87

Editar esta página en docs/referencia/integraciones.md