extension/
manifest.json MV3. Permisos: sidePanel, storage, tabs. host_permissions: https://api.exa.ai/*
background.js Service worker. Proxy autenticado: único que conoce la API key y toca la red.
sidepanel.{html,css,js} El panel: controles, query, estimador, render, polling de research.
options.{html,css,js} Guardado de la API key en chrome.storage.local.
icons/ Iconos PNG generados por make_icons.py (stdlib pura).
make_icons.py Helper de build, no se carga en el navegador.
No hay build step ni dependencias de terceros. Se carga tal cual en chrome://extensions.
- La API key vive solo en
chrome.storage.localy solo la lee el service worker (getApiKey). El panel nunca la ve: manda{ type, method, path, body }y el worker inyecta la cabecerax-api-key. - El worker valida la ruta contra una allowlist (
/search,/contents,/findSimilar,/research/v0/tasks). El panel no puede forzar una URL arbitraria. - No hay content scripts. La key nunca entra en una página web.
- Único destino de red:
api.exa.ai. Los favicons salen del campofaviconque ya da Exa o de un monograma local, sin llamar a servicios externos.
El primitivo de partida era findSimilar(url), pero sobre una URL concreta devuelve los vecinos más cercanos, que para un repo de nicho son el propio proyecto y sus forks. Prueba real sobre github.com/joshcirre/instruckt-laravel: de 7 resultados, 3 eran la propia URL (releases y PRs) y el resto forks de instruckt.
La solución es buscar por tema con /search, derivando un query de la página.
titleQuery()intenta sacar una descripción del título de la pestaña. En GitHub quita el prefijoowner/repo:y los adornos "GitHub - " / " · GitHub". Si solo quedaowner/repoo vacío, devuelve null. Nunca usa el nombre del repo como query (eso devolvía el propio proyecto: el bug original era el fallback "instruckt open source alternative").- Si
titleQuerydevuelve null (repo sin "About"), se pide a Exa un resumen de función de la URL con/contents+summary(lee el README). Cuesta $0.001 extra y se suma al coste real mostrado. - Con el query resuelto se llama a
/searchcon lacategorydetectada. filterSource()quita de los resultados la propia URL y, en GitHub, el mismo repo.
Validado en vivo: el resumen derivado de joshcirre/instruckt (sin descripción) produce alternativas reales (agent-ui-annotation, agentation, pi-annotate, ...), cero instruckt, por $0.008.
| Nivel | type | contents | Coste aprox (10 resultados) |
|---|---|---|---|
| 1 Rápida | fast | (ninguno) | $0.007 |
| 2 Contexto | auto | highlights | $0.007 |
| 3 Completa | auto | highlights + text | $0.007 |
| 4 Profunda | deep | highlights + summary | $0.012 + $0.001 por resultado |
| 5 Deep research | endpoint /research |
n/a | variable, pide confirmación |
El nivel 5 no usa /search: crea una tarea en /research/v0/tasks (modelo exa-research) con instrucciones de "encuentra alternativas, no el mismo proyecto", y hace polling (cada 2.5s, máx 40 intentos) hasta completed. Nunca se lanza solo al abrir el panel.
- El estimador (
computeEstimate) calcula a partir del tipo del nivel y el nº de resultados, y suma summaries en el nivel 4. - Tras cada llamada se muestra el coste real (
costDollars.total), sumando el paso/contentscuando se usó. - Si la respuesta trae
perRequestPrices/perPagePrices, se guardan enchrome.storage.localy recalibran el estimado para la próxima vez.
detectCategory(url): arxiv.org da research paper, github.com da github, el resto general. Override manual en el desplegable.setCategory()es el punto único de verdad que mantiene estado, desplegable y badge en sync.- "Ocultar la página de origen" filtra en cliente (la propia URL y, en GitHub, el mismo
owner/repo). No se usaexcludeSourceDomainde la API porque en GitHub o arXiv excluiría todo el dominio, que es justo donde están los similares.
Sin key (pide configurar), analizando (cuando hace falta /contents), skeletons de carga, lista de resultados, vacío y error con reintento. Tokens de diseño con claro y oscuro automático (color-scheme y prefers-color-scheme).
- La forma exacta de la respuesta de
/researchse parsea de forma defensiva pero no se ha exprimido en vivo a fondo. - Para empresas, el tema sale de la meta-descripción de la home; si es pobre, el query lo será. Se podría leer el contenido con
/contentstambién para no-GitHub. - Posible conmutador "Más como esta URL" (findSimilar) frente a "Alternativas por tema" (/search), útil para papers de arXiv donde findSimilar por embedding de la página entera a veces va mejor.
- El query no es editable; se podría exponer un campo para afinarlo a mano.
- Sin build. Edita y pulsa recargar (↻) en
chrome://extensions. - Iconos:
python3 extension/make_icons.py(stdlib pura, regenera los PNG). - Prueba en vivo contra la API real sin abrir Chrome:
scripts/exa_smoke.py, que inyecta la key desde el vault de Phase. Ver cabecera del script. - Vista previa de la UI sin Chrome: se sirve
extension/sidepanel.htmlcon un mock dechrome.*(ver historial de la sesión); útil para iterar el diseño en claro y oscuro.