@dropugc/discover
v0.5.0
Published
DropUGC Discover — TikTok slideshow & video format research engine, driven by your own Claude via MCP. Cross-platform companion (Windows/macOS/Linux).
Maintainers
Readme
slideshow-intel
Motor de research de cuentas TikTok slideshow-first, operado desde Claude Code (u otro
cliente MCP) o desde la CLI. Herramienta personal; réplica del núcleo de Scroll Show
(ver docs/LOGICA_SCROLLSHOW.md, docs/SPEC_MVP.md).
- Node 22 + TypeScript.
patchright(fork drop-in de Playwright que cierra la fuga de CDPRuntime.enable) controlando tu Chrome real con un perfil dedicado (%LOCALAPPDATA%\slideshow-intel\chrome-profile), siempre con ventana. Sin stealth JS: en un navegador real la coherencia gana al spoofing (verdocs/GUIA_INDETECTABILIDAD.md). - Coherencia por país: por defecto hereda el locale/timezone del SO del usuario (coherente con su
IP/región, para cualquier país). Override con
SLI_LOCALE/SLI_TZsolo si hace falta. - Intercepta los XHR de TikTok (
/api/search/,/api/post/item_list/,/api/user/detail/…), calcula métricas (views/follower, slideshow %, cadence, consistency 0-10…), guarda todo en SQLite (node:sqlite, sin deps nativas) y descarga slides +metadata.json. - MCP server Streamable HTTP en
127.0.0.1:43121/mcpcon Bearer token (%LOCALAPPDATA%\slideshow-intel\mcp-token.txt).
Instalación (usuario final) — Windows / macOS / Linux
Requiere Node ≥ 22.13 y Google Chrome instalado. Cross-platform vía npm (sin instalador por-OS, sin firma/notarización):
npm i -g @dropugc/discover # o: npx @dropugc/discover setup
sli setup # registra el MCP en Claude Code + imprime los próximos pasos
sli login # inicia sesión en TikTok una vez (Chrome dedicado)
sli link <token> # (opcional) conecta a DropUGC → Discover para ver los resultados en la web
sli sync [runId] [--all] # (opcional) re-envía un run pasado a DropUGC (los runs se sincronizan solos al terminar o detenerse)Luego, en Claude Code: "búscame cuentas ganadoras de slideshows/videos en mi nicho". El daemon
arranca solo. SLI_CHROME=<ruta> si Chrome no está en la ruta estándar.
Uso rápido (desarrollo)
npm install
npm run sli -- doctor # Chrome, data dir, sesión TikTok, librería
npm run sli -- login # abre el Chrome dedicado en tiktok.com; inicia sesión ahí una vez
npm run sli -- discover "glow up tips" "jawline exercises" -t 5 -f slideshow # o -f video / -f any
npm run sli -- analyze hyginmaxxing --days 15
npm run sli -- download -a hyginmaxxing -c 3 --sort views
npm run sli -- download -l https://www.tiktok.com/@user/photo/123456
npm run sli -- library search glow --sort views_per_follower
npm run sli -- runsArquitectura: daemon único
Un solo proceso (el daemon) es dueño del Chrome. Claude Code y la CLI son clientes: le mandan el trabajo por HTTP en vez de abrir su propio navegador, así que nunca chocan por el perfil.
- El daemon sirve
/mcp(para Claude) y/api/*(para la CLI) en127.0.0.1:43121. - Los comandos que tocan el navegador (
discover,analyze,download,login,doctor) levantan el daemon solos si no está corriendo, y hacen streaming de los logs. - Los que no tocan el navegador (
library,runs) leen la SQLite directo (seguro con WAL). - Instancia única: si ya hay un daemon, un segundo
mcp serveno arranca.
Desde Claude Code
npm run sli -- mcp install # una vez: registra el daemon en Claude Code (scope user)
# el daemon arranca solo cuando Claude o la CLI lo necesitan; para dejarlo fijo en primer plano:
npm run sli -- mcp serveLuego, en cualquier sesión de Claude Code: "tengo una app de X, búscame 5 cuentas de slideshows
con ≥100k views en 30 días y dime qué formato usan". Tools: app_status, start_discovery,
analyze_account, start_download, job_status, wait_job, job_results, resume_job, stop_job,
list_runs, sync_run, save_insight, search_accounts, get_account, judge_candidates.
Discovery con objetivo (0.2.0): relevancia + checkpoints de revisión
El score de discovery era 100 % rendimiento (views/follower, save rate, momentum…) sin noción de lo que buscas: un creator de lifestyle con un post patrocinado que TikTok asoció al keyword "ganaba". Desde 0.2.0 el motor es objective-aware en cada punto donde gasta presupuesto:
start_discovery(objective, include_terms, exclude_terms, visual_cues, min_on_topic_pct=20, review=true). Claude escribe una rúbrica una vez (frases multi-palabra:"ai photo","photo edit","epik"— nunca un genérico suelto como"ai"); el daemon la aplica determinísticamente.- Gate del candidato (antes de gastar ~24 s midiendo): con el caption/hashtags/bio del hit de búsqueda,
los autores claramente off-topic no se miden (
skipped_off_topicen el resultado). - Relevancia de la cuenta medida:
on_topic_pct(% de posts recientes que matchean) yon_topic_views_pct→ veredicto automáticoon_topic | adjacent | off_topic.adjacent= pocos posts on-topic pero cargan las views (creator con posts patrocinados que sí ganan en el nicho);off_topic→ rechazada. - Solo los on_topic siembran sonidos/hashtags/sugeridas y el PageRank de la librería (adiós al compounding de basura). Los keywords se buscan intercalados (1 cada 2 mediciones) hasta agotarlos.
- Checkpoint de revisión (
review=true, cada 4 PASS + al final):wait_jobdevuelvestate: "review"con una tarjeta por candidata (bio, top captions, hashtags, ≤3 covers como imágenes). Claude respondejudge_candidates(job_id, batch_id, verdicts);off_topicdes-pasa la cuenta, la saca de los seeds, retira los arms que sembró y libera el slot (el target vuelve a N-1 y el presupuesto de medición se refunda con el coste medio de un ganador) → el run sigue buscando. Sin respuesta enreview_timeout_s(180 s) sigue con los veredictos automáticos. La CLI (sli discover --objective --include --exclude) no tiene reviewer: solo veredictos automáticos. - Los frames de video NO se analizan en discovery (caro, detectable, rara vez cambia el veredicto que ya dan
cover + caption + bio); eso sigue en
start_downloadpara los finalistas.
0.3 — Claude en cada decisión (protocolo de decisión): el job ya no se limita a un checkpoint final. Cede en
cada punto donde se gasta presupuesto y wait_job devuelve state: "decision" con un brief:
triage (tras cada búsqueda: autores con caption/hashtags/bio + cover para los dudosos → triage_candidates:
probe|skip + new_queries/retire_queries), verdict (tras sondas baratas de ~8 s: bio, captions recientes
marcadas [on-topic], 2 covers reducidas, métricas aproximadas → judge_probes: measure|reject + relevancia) y
review (raro: PASS con relevancia solo automática → judge_candidates). Solo lo aprobado recibe la medición
profunda (~23 s). Las decisiones van en cola (una pendiente a la vez) y no bloquean las manos: el daemon sigue
sondeando lo ya aprobado mientras Claude decide; sin respuesta en review_timeout_s deciden las heurísticas.
peek_covers(usernames) sirve covers bajo demanda cuando el texto no basta. Los skips/verdicts de Claude se
recuerdan por colección. Ver docs/ALGORITMO_IDEAL.md.
0.3 fase 2 — LEARN / EXPAND / BUDGET con Claude (fuentes gateadas al nacer, recompensa diferida):
- Recompensa de cada fuente (query / hashtag / sonido): el pull ya no se premia por "autores nuevos" sino por
lo que esos autores llegan a ganar después: probe aprobado (+0.1), verdict
measure(+0.3), PASS (+0.6; se devuelve si se des-pasa). El bandit UCB reparte las búsquedas entre las fuentes vivas con esa recompensa; cada candidata recuerda qué arm la sacó.job_resultsmuestra por fuentesurfaced → probed → measured → passed. learn(tras cada 2 PASS, o antes si la frontera se queda corta): una tarjeta por ganador (métricas, captions, 2 covers, sus sonidos y hashtags con cuántas cuentas del run los comparten y su lift — evidencia, no veredicto, y las cuentas sugeridas del perfil) →approve_expansion:why_it_winspor ganador (queda en resultados y dashboard),follow_sounds/follow_hashtags(se vuelven arms del bandit) yfollow_suggested(true | lista → pasan a la frontera y se sondean). Nada se sigue sin aprobación; ganador omitido o timeout → heurística de lift (la de 0.2.0, solo on_topic). Ademásadd_include/add_excludeevolucionan la rúbrica en caliente (versión +1; los gates siguientes usan la nueva) ynew_queries/retire_queries.budget(cada 4 búsquedas, y al agotarse el presupuesto con PASS < target — antes el run moría en silencio): tabla de rendimiento por fuente + presupuesto restante + eventos recientes →steer_budget:continue|expand(+extra_searches, default 4; las sondas y mediciones crecen en proporción; connew_queries) |narrow(retire_queries) |stop(termina y puntúa). Máximo 5 decisiones de budget por run; las notas quedan ensteeringdel resultado.- Fuentes: las queries nuevas de Claude y los arms que aprueba en
learnnacen "sin pull" (el bandit los busca pronto, una vez); el gemelophotode cada keyword nace con prior bajo (compite por UCB) salvo en runsformat: slideshow, donde sigue siendo primario.retire_queriesretira ambas pestañas. Trasstopno se abren más decisiones: las sondas pendientes se cierran como "stopped before verdict" y no se expande nada. - Memoria por colección con desenlace: además del veredicto (on/adjacent/off) se recuerda si la cuenta pasó en
algún run de la colección (ganador conocido → se mide sin volver a preguntar) o fue rechazada y por qué (se muestra
en el triage como "rejected last time (razón) — probe only if it may have grown"; sin cliente no se vuelve a sondear).
Triage admite
ignore(on-topic pero no vale la pena ahora: no se recuerda nada, a diferencia deskip= off-topic). El brief de triage muestra como máximo 24 autores (primero los dudosos por prior; una página de sonido lista ~100 usuarios al azar): el resto se resuelve sin preguntar (los seguros ya están en la frontera; los dudosos se descartan).retire_queriestambién aceptasound|<id>,ht|<tag>o#tagpara retirar una fuente de expansión. - CLI / sin cliente: no hay learn ni budget; expansión por lift como siempre.
Afinos de tiempo (0.3.0): una visita tiene un suelo anti-detección (nav + 2.5–4 s + 2–3.5 s por scroll) que no se
toca; lo que se recorta es el TRABAJO: (1) toda medición para de hacer scroll en cuanto la ventana de métricas está
cubierta (≥3 posts más antiguos que su inicio) — la sonda salta su scroll y la profunda baja de ~23 s a 5–10 s o a 0 s
(si la primera página de la sonda ya cubría la ventana no se re-visita: se reutilizan sus posts, sonidos y sugeridas);
(2) ganadores conocidos de la colección usan la medición de la librería hasta 7 días (24 h para el resto) → 0 s;
(3) duraciones en los logs (probe — … (7.1 s, covers the window), revisited — 2 scroll(s) (11.2 s)). Medido con
scripts/mcp-autopilot.mjs (reviewer de latencia 0): target 5 en nicho nuevo = 7.7 min, 14/18 profundas gratis; el
coste dominante pasa a ser la sonda (~7 s × N) — reducir N es el trabajo del triage de Claude y de la memoria.
postsPerWeek se calcula sobre el tramo realmente visto (≤90 d), no sobre 90 días fijos.
0.3 fase 3 — cerebro del nicho + calibración + desatendido: todo lo que Claude decide se vuelve memoria de la
colección (collection de start_discovery / --collection), persistida en SQLite (collection_brain,
collection_sources, collection_calibration, run_accounts.why_it_wins):
Cerebro: objetivo + rúbrica tal como Claude la dejó (v0 al crearla, +1 por cada evolución; la numeración continúa entre runs), fuentes (queries, sonidos, hashtags) con rendimiento ACUMULADO
surfaced→probed→measured→passed, quién las creó (user/claude/heuristic/brain) y cuáles retiró Claude, ganadores conocidos con suwhy_it_wins, cuentas juzgadas, lista de "dudosos". Toolcollection_brain(collection?)(sin argumento lista las colecciones) y CLIsli brain [collection].FRAME: un run de una colección conocida arranca desde su cerebro: la rúbrica que pasa Claude se une a la del cerebro (o se usa la del cerebro si no pasa ninguna), el objetivo por defecto es el del cerebro, las queries/arms productivas vuelven como arms con su rendimiento histórico como prior (compiten, no saltan la cola), las retiradas no vuelven solas (si el user las pasa explícitamente como keyword, se reactivan y se avisa), los ganadores conocidos se miden sin preguntar. El prompt
/discover-nichellama acollection_brainantes destart_discovery.Calibración: el daemon cuenta, por colección, cuándo cada regla automática coincide con Claude —
gate(rúbrica en triage vs probe/skip),auto_verdict(relevancia automática vs la de Claude en verdict/review) yauto_probe(measure/reject automático vs Claude). Se muestra en los briefs ("el gate acierta 78 % aquí, n=40") y en el cerebro.Desatendido (CLI /
review=false): usa el cerebro (rúbrica, queries productivas, memoria de cuentas). Una regla actúa sola solo si está calibrada ≥80 % con n≥10 (sin votos aún, actúa — no hay nada mejor); si está calibrada por debajo, lo que ella descartaría queda como "undecided" (no se sonda / se rechaza marcado, sin memoria) y sale enresult.pendingy en el cerebro para que el próximo run con Claude lo resuelva.wait_job(job_id, timeout_s≤120)hace long-poll: Claude no necesita dormir ni sondear desde una shell.start_discovery(..., collection: "Pushup apps")agrupa el run en una colección (nicho/proyecto) del dashboard de DropUGC. Los runs de una colección comparten cuentas, top posts e insights.save_insight(collection, title, body_md): Claude guarda su análisis ("por qué gana el formato") como nota markdown en la colección — la investigación sobrevive al chat.
Sync a DropUGC: cada run de discovery se envía solo al terminar o al detenerse (stop_job /
sli stop): el run detenido se puntúa igual con lo medido, se guarda en la librería (runs.result)
y se sincroniza. El payload lleva, por cuenta ganadora, sus top 5 posts (views, caption, sonido,
link) y un thumbnail chico del cover subido a DropUGC (best-effort; el mp4/frames nunca salen de tu
máquina). Si el push falla (sin internet, DropUGC caído) queda en el outbox (runs.synced_at IS NULL)
y el daemon lo reintenta al arrancar y cada 10 min (SLI_SYNC_RETRY_MIN); job_results muestra el
estado del sync. sync_run / sli sync [runId] [--all] re-envían runs pasados (idempotente).
Runs que quedaron running por un daemon muerto se marcan error al arrancar.
Nota: discovery mide a propósito más allá del target (buffer ×1.5 para la selección diversa) —
llegar al target no es motivo para detenerlo.
Captcha / verificación
Si TikTok muestra una verificación en la ventana de Chrome, el job pasa a paused; resuélvela a
mano y se reanuda solo (o resume_job).
Variables
SLI_HOME (data dir), SLI_CHROME (ruta a chrome.exe), SLI_LOCALE (default en-US),
SLI_MCP_PORT (default 43121).
Estructura
src/browser launch de Chrome, sesión, captcha, pacing
src/tiktok parsers de JSON, captura XHR, perfil, búsqueda
src/metrics fórmulas
src/library SQLite
src/jobs job manager (running/paused/done, partial en disco)
src/core discovery / analyze / download / engine
src/mcp servidor MCP
src/cli comandos
tests/ unit tests (npm test)