@kikedealba/bita
v0.6.0
Published
Registro de tiempo local que documenta el trabajo y lo vuelca a Jira, con su integracion para Claude Code.
Maintainers
Readme
bita
Registro de tiempo local, en SQLite, pensado para que Claude Code lo lea y lo escriba. Mide el trabajo mientras ocurre y después lo vuelca a Jira: un issue por título y proyecto, un worklog por cada bloque medido, con estimación y cierre.
El nombre viene de bitácora.
Por qué existe
Antes esto hablaba con la API de Toggl Track. El plan gratuito tiene un límite horario de llamadas que bloqueó el trabajo tres veces en una sola sesión, dos de ellas a mitad de una escritura, dejando Jira por delante del registro de tiempo.
Casi toda la complejidad del CLI servía para rodear ese límite, no para resolver el problema: el throttle, los reintentos, la paginación, el caché de 24 horas, el espejo del cronómetro en curso y los tags usados como estado porque no había dónde guardarlo. Con una base local todo eso desaparece.
Quedan dos ventajas que no se buscaban:
- Varios cronómetros a la vez. El límite de uno era de Toggl. Aquí un
cronómetro corriendo es una fila con
stopped_atnulo, y puede haber las que hagan falta. - El estado es una clave foránea. Una entrada está pendiente mientras no
tenga fila en
jira_links. No hay tag que pueda diverger ni retaggeo a medias.
Requisitos
Node 24 o superior. No es negociable: bita usa node:sqlite y el borrado de
tipos nativo, así que desde un clon corre los .ts sin compilar. No hay
dependencias de runtime ni bundler.
El paquete de npm sí lleva JavaScript compilado, y no por gusto: node se
niega a borrar tipos en archivos bajo node_modules, sin bandera que lo
levante. El build es un tsc que sólo borra los tipos —erasableSyntaxOnly
está activo— y reescribe las extensiones de los imports.
node -v # debe decir v24 o másInstalación
Desde npm
npm install -g @kikedealba/bita
bita setup
bita app installbita setup enlaza la skill y los comandos de barra en ~/.claude apuntando al
paquete instalado, y mete los permisos y el hook SessionStart en tu
settings.json. Al actualizar el paquete se actualizan con él, porque son
symlinks. bita app install descarga la última release del escritorio y la deja
en /Applications.
Node 24 o más nuevo, por node:sqlite.
Cómo se publica
Nadie publica a mano. Al publicar una release en GitHub, el workflow
.github/workflows/publish.yml corre el typecheck y las pruebas, comprueba que
el tag y la versión de package.json coinciden —si no, falla antes de subir
nada—, instala el tarball como lo haría una persona y lo ejecuta, y hace
npm publish --provenance.
Ese último paso no es ceremonia: el paquete se instala en node_modules, que es
un entorno en el que no corre lo mismo que en el clon. Comprobar el contenido
del tarball no lo detecta; ejecutarlo sí.
La procedencia ata el paquete de npm al commit y al workflow que lo construyó,
así que cualquiera puede comprobar de dónde salió. Hace falta el secreto
NPM_TOKEN en el repositorio, un token de automatización con permiso de
escritura sobre @kikedealba.
Desde el repositorio
Para trabajar sobre el código. El instalador enlaza contra tu clon, así que un
git pull actualiza el binario, la skill y los comandos a la vez.
1. Clonar e instalar
git clone [email protected]:KikeDeAlba/bita-cli.git
cd bita-cli
pnpm installLas únicas dependencias son TypeScript y @types/node, y solo para el
typecheck.
2. Correr el instalador
./scripts/install.shHace cuatro cosas, y todas son idempotentes: puedes volver a correrlo cuando quieras.
| Paso | Qué hace |
|---|---|
| Binario | Enlaza bita en tu directorio de binarios ($PNPM_HOME/bin, o ~/.local/bin) |
| Skill | ~/.claude/skills/bita → skill/ del repo |
| Comandos | ~/.claude/commands/bita-*.md → commands/ del repo |
| Settings | Añade los permisos y el hook SessionStart a ~/.claude/settings.json |
Todo son symlinks al repo, a propósito: cuando actualizas el repo, la skill y los comandos se actualizan contigo, y un cambio de flag en el CLI viaja en el mismo commit que su documentación.
Antes de tocar settings.json deja una copia en settings.json.backup, y si no
lo puede parsear no lo escribe: imprime el bloque para que lo pegues a mano.
Si tu directorio de binarios está en otro sitio:
BITA_BIN_DIR=~/bin ./scripts/install.sh3. Comprobar
bita --version
bita projectsLa base se crea sola en ~/.local/share/bita/bita.db al primer uso. BITA_DB_PATH
la mueve a otro sitio, que es también la forma de probar cosas sin tocar la real.
BITA_CONFIG_PATH hace lo mismo con la configuración (~/.config/bita/config.json).
4. Dar de alta un repositorio
Esto es lo que enciende la integración con Claude. Un solo comando crea el proyecto y lo mapea:
bita repo init # el repositorio actual
bita repo init ~/dev/otro/repo # o el que le pasesEl nombre del proyecto sale de la carpeta; --name "Otro nombre" lo cambia. Si
ya existe un proyecto con ese nombre lo reutiliza, que es lo que quieres
cuando el front y el back de lo mismo deben compartir proyecto.
Si prefieres separarlo en dos pasos, o mapear varios repositorios a un proyecto que ya existe:
bita project add "Mi proyecto" # devuelve un id
bita scope set . <projectId>Mientras un repositorio no esté mapeado, el hook no dice nada. En cuanto lo está, al abrir una sesión de Claude Code en él se inyecta la regla que le pide ofrecer el cronómetro cuando el trabajo vaya a dejar un artefacto —un commit, un archivo, un despliegue— y callarse cuando solo vayas a leer o preguntar.
El mapeo se guarda por el slug del repositorio, que sale del remoto de git
(github.com/kikedealba/bita-cli), así que sobrevive a que muevas la carpeta.
Reabre la sesión de Claude Code para que cargue el hook, la skill y los comandos.
5. Conectar Jira
Jira no se toca desde el CLI: lo escribe Claude por el conector de Atlassian. Lo único que se guarda aquí es a qué tablero va cada proyecto, y se pregunta solo la primera vez:
bita map set <projectId> <JIRAKEY> --parent <JIRAKEY-123> # siempre a esa épica
bita map set <projectId> <JIRAKEY> --no-epic # al tablero: la épica se elige en cada corrida
bita map listUn proyecto apunta a una épica cuando todo su trabajo cae siempre en la misma, o al tablero cuando se reparte entre varias. Cambiar de uno a otro conserva el resto del mapeo: la transición de cierre, los tipos y las Historias ya creadas, que se guardan por épica.
Uso
bita start "Despliegue de infraestructura" # arranca; puede haber varios
bita ls # qué está corriendo ahora
bita note path 12 --create # el documento de la entrada
bita note save 12 # regístralo tras editarlo
bita stop 12 # para uno y cierra su documento
bita log "Sesión con QA" --from 14:00 --for 1h
bita summary --pending --json # agrupado y listo para Jira
bita link 12 13 --issue DD-1896 # marca como registradas
bita delete 12 --dry-run # qué se llevaría por delante
bita repo init ~/dev/otro/repo # da de alta otro repositoriobita --help lista todo.
Borrar
bita delete <ids...> quita entradas que nunca debieron registrarse: el
contador que arrancó solo, el bloque de tres segundos, la prueba. Se lleva
consigo el enlace a Jira, los archivos tocados, la fila del documento y el
archivo del documento en disco, salvo con --keep-doc.
Hay tres guardas, y todas paran la corrida entera antes de tocar nada:
| Situación | Qué pasa |
|---|---|
| La entrada sigue corriendo | Se niega y remite a bita cancel, que es el comando de descartar un cronómetro vivo |
| La entrada ya llegó a Jira | Se niega: el worklog sigue allá y el conector no puede borrarlo. --force borra la entrada local de todos modos |
| El id no existe | Se niega antes de borrar ninguno de los otros |
Sin terminal —o con --json— exige --yes, porque no hay a quién preguntarle.
--dry-run describe lo que pasaría, incluido que se negaría, y no escribe nada.
bita project delete <id|nombre> hace lo propio con un proyecto: borra su mapeo
de Jira, sus Historias cacheadas y los prefijos de scope que apuntaban a él.
Se niega si el proyecto tiene entradas, porque borrarlo las deja sin proyecto en
vez de borrarlas; --force acepta ese resultado y bita project archive es la
alternativa cuando el histórico importa.
Los documentos
Cada entrada tiene un documento en markdown que se escribe mientras el cronómetro corre, no al pararlo. Viven en espejo del proyecto:
~/.local/share/bita/
├── bita.db
└── docs/
└── apartados/2026/09/20-128-migracion-del-worker.mdLa raíz sale de --docs-dir, de BITA_DOCS_DIR, o del directorio de la base de
datos, en ese orden. Como se deriva de la base, apuntar --db-path a un archivo
de pruebas arrastra los documentos con él.
La base guarda la ruta, no el texto. El archivo es el original: se puede abrir en un editor, indexar o respaldar sin pasar por el CLI, y el árbol entero se puede mover sin reescribir nada. Lo que sí guarda la base es el checksum, con lo que se nota si un documento cambió por fuera.
Las secciones son fijas —Contexto, Qué se hizo, Decisiones, Hallazgos, Verificación, Pendiente, Tocado— porque son la superficie sobre la que se construye la descripción del issue de Jira y, más adelante, una página de Confluence.
Si vienes de las notas en NDJSON, bita notes migrate --dry-run enseña qué
documentos se crearían, y sin el flag los crea. El archivo viejo no se toca.
Navegar lo escrito
bita note siempre habla de una entrada concreta. Para moverse por el corpus
—que es lo que necesita un lector, dentro o fuera de la terminal— está
bita docs, que solo lee:
bita docs tree --months # proyectos, con sus meses y conteos
bita docs ls --project ARSM # entradas y su documento, o «sin nota»
bita docs show 735 # markdown, front matter y secciones
bita docs search "cognito" --project ARSM # con fragmentos alrededor de cada aciertodocs ls devuelve siempre las siete secciones con su estado —written,
empty o absent— para que quien pinte un índice no tenga que llevar su propia
copia de la lista. Las entradas sin documento salen como filas con doc: null,
porque no tener nota escrita también es información.
Un archivo que falta o que cambió por fuera no es un error: sale en
meta.files y en meta.warnings con ok: true. Que el documento vaya por
delante de la base entre note path --create y note save es el flujo normal,
no una avería.
La búsqueda lee de disco, acotando antes por la base: el catálogo dice qué archivos existen y el archivo dice qué contiene. No hay índice que invalidar, y buscar dentro de un proyecto solo toca los documentos de ese proyecto.
Formato de salida
Todos los comandos aceptan --json y emiten un solo documento en stdout:
{ "schemaVersion": 3, "ok": true, "command": "summary", "data": {}, "meta": {} }Los errores salen con ok: false y un error.code estable. Los avisos van a
stderr, nunca a stdout, para que el JSON se pueda parsear tal cual.
Contadores en blanco
El caso normal es arrancar el reloj antes de saber en qué se trabaja:
cd ~/dev && bita start # sin título y sin proyectoEso crea un borrador. Mientras siga sin título queda fuera de summary, así que
no puede llegar a Jira por accidente. Se rellena después, y en buena parte solo:
| Qué | Quién lo pone | |---|---| | Título y descripción | Claude, en cuanto un mensaje dice en qué se va a trabajar | | Proyecto | Claude por el prompt, o el hook por el primer archivo que se cambia | | Archivos tocados | El hook, en cada edición |
El hook UserPromptSubmit recuerda que hay un contador sin nombre y se calla
solo en cuanto lo tiene. A mano:
bita amend --draft --title "Lo que sea" --project ApartadosProyectos y repositorios
Un repo no es un proyecto. Los proyectos suelen ser grupos con varios repos
dentro, y el grupo no tiene .git: lo tienen los repos.
El mapeo va por prefijo de ruta, y gana el más largo que empate:
bita scope set gitlab.com/vivaaerobus/vb_solemti/apartados 42
bita scope which . # que prefijo empata aqui
bita scope listCon eso, apartados/api, apartados/front y apartados/workers resuelven los
tres a Apartados sin configurar nada mas. Se puede mapear un grupo y luego
excepcionar un repo dentro, porque el prefijo mas largo manda. El empate es por
segmentos, asi que .../apartados nunca cubre .../apartados-legacy.
Si no hay prefijo, bita repo init propone uno comparando los segmentos de la
ruta con los nombres de proyecto que ya existen, ignorando mayusculas, guiones y
guiones bajos. Solo empata si tras normalizar son identicos.
Cómo se agrupa
Un grupo es proyecto + título, a lo largo de todo el rango, y se convierte en un issue de Jira. Cada entrada del grupo es un worklog con su hora real.
La estimación original se redondea hacia arriba al siguiente medio punto: 3h
43m medidas se registran como 4h de estimación con worklogs que suman 3h 43m. Un
issue admite como máximo 8 horas; lo que se pasa se parte en (1/n), (2/n).
Solapes
Los cronómetros simultáneos están permitidos, así que un día puede sumar más
tiempo del que marca el reloj. summary y entries lo avisan:
Warning: 2026-09-19: 4h 8m tracked over 3h 7m of clock time (1h overlapping)No lo impide. Solo evita que pase inadvertido.
Los comandos de Claude Code
commands/ tiene siete slash commands, enlazados por symlink desde
~/.claude/commands/:
| Comando | Qué hace |
|---|---|
| /bita-start [título] | Arranca un cronómetro. Sin título, lo infiere de la sesión y lo enseña antes |
| /bita-stop [id] | Cierra el documento de lo que se hizo y para. Con varios abiertos, pregunta cuál |
| /bita-timers | Qué está corriendo y cuánto llevas hoy |
| /bita-log <texto> | Registra un bloque que ya pasó, cuando se trabajó sin cronómetro |
| /bita-init [ruta] | Da de alta un repositorio: crea su proyecto, lo mapea y revisa el tablero |
| /bita-check [id] | Anota un checkpoint en el documento, mientras el cronómetro corre |
| /bita-amend [id] | Rellena a mano el título o el proyecto de un cronómetro |
Viven en el repo por la misma razón que la skill: usan los flags del CLI, así que cambian en el mismo commit.
La skill
skill/SKILL.md es la skill de Claude Code que envuelve el CLI: decide cuándo
proponer el cronómetro, infiere la Historia de Jira a partir de las notas, y
maneja la jerarquía Épica → Historia → Subtarea. Está enlazada por symlink desde
~/.claude/skills/bita, para que el procedimiento y los flags cambien en el
mismo commit.
Desarrollo
pnpm typecheck
pnpm testLos tests corren con node --test sobre los .ts directamente. No hay bundler.
Estructura
src/db/ el almacén: esquema, migraciones y consultas
src/domain/ lógica pura: agrupación, duraciones, zonas horarias, solapes
src/cli/ comandos y formato de salida
src/docs/ los documentos de cada entrada: rutas, markdown y escritura
src/state/ configuración y notas heredadas en disco
skill/ la skill de Claude Code
commands/ los slash commands
scripts/ el instaladorEl punto de corte es EnrichedTimeEntry (src/domain/types.ts): todo lo que
está aguas abajo no sabe de dónde salieron los datos.
