Klient Toggl Tracku pro příkazovou řádku a pro Claude Code. Drží lokální kopii výkazu na disku, umí do ní zapisovat zpětně i naživo přes timer a obousměrně ji synchronizuje se serverem.
Žádné závislosti — stačí Node 22+ (kvůli vestavěnému fetch). Program jsou
dva soubory: ClaudeToggl.js (CLI) a ClaudeToggl.gui.js (lokální stránka nad
toutéž kopií). Třetí do rodiny k ClaudeMailu a ClaudeCalendaru.
$ ClaudeToggl.cmd --today
# 2026-08-01 Sat — 3 entries, 4:15
4501234567 13:00-14:30 1:30 Katalog Katalog - M2/2026 - filtrování v seznamu
4501234568 15:00-17:15 2:15 Web Web - export do CSV - sjednocení hlaviček
L7 20:00-20:30 0:30 Podpora Podpora - týdenní porada [pending]
# synced 4 min ago · 1 pending · quota 27/30 read, 30/30 write
Nástroj i instalátor mluví anglicky; česky je tenhle README a skill pro Claude Code (popisy záznamů jsou samozřejmě uživatelovy, tedy taky české).
Výkaz se během měsíce plní ručně — zpětným zápisem toho, co se udělalo — a na konci měsíce se z něj exportuje podklad pro fakturaci, do kterého vidí i zadavatel. Přidaná hodnota nástroje není v exportu (ten web umí), ale v tom, že se dá zapsat a zkontrolovat bez klikání ve webovém UI, a že to zvládne agent, který u té práce stejně byl.
Hlavní příkaz je proto --add, ne stopky.
Účet na free plánu má dvě oddělené kvóty po 30 requestech na klouzavou hodinu —
jednu pro čtení (/me/*), druhou pro zápis (/workspaces/*). Server je hlásí
v hlavičkách odpovědi, takže se neodhadují.
Čtení je při ruční práci s výkazem mnohem častější než zápis: každý výpis, každá
kontrola, každé hledání. Kdyby každé sáhlo na server, čtecí kvótu by vyčerpalo.
Proto čtení chodí na disk a na síť sáhne nejvýš jednou za pullTtlMinutes
(výchozích 30 minut). Každý výpis přitom v patičce přiznává, jak stará data ukazuje.
Zápis jde do fronty a odešle se na konci každého běhu — jeden --add tedy
pushne hned. --offline zápis jen uloží a nechá ho ve frontě; --sync ho pošle
později.
Kopie na disku je pracovní kopie, Toggl je pravda. Do Togglu totiž píše i web
a mobilní appka, takže sync musí být obousměrný a záznam změněný na obou stranách
je normální provozní stav — nástroj ho nemerguje sám, ohlásí
! STOP: entry <ref> diverged a rozhodne uživatel.
git clone https://github.com/Lossik/ClaudeToggl.git
cd ClaudeToggl
npm test # projede testy — bez sítě a bez gitu
node install.js # založí ~/.claudetogglinstall.js vytvoří datový adresář, v něm accounts/default/entries/, lokální git
repozitář bez remote, .gitignore na config.json a config — ten vznikne kopií
config.json.dist vedle skriptu, do které doplní zónu stroje
a odpověď na --git. Nic existujícího nepřepisuje, takže se dá pustit znovu — druhý
běh jen vypíše kept: a config i s tokenem nechá být. Jiný adresář se zadá pozičně
(node install.js /jinam) a nástroji se pak musí říct proměnnou
CLAUDETOGGL_DATA_DIR (instalátor to sám připomene). Zónu přebije --timezone <IANA zóna>; node install.js --help vypíše zbytek.
$ node install.js
ClaudeToggl data directory: C:\Users\<user>\.claudetoggl
created: accounts/default/entries/, .gitignore, config.json, .git/ (local, no remote)
first commit done
Next:
1. put the token into "apiToken" in C:\Users\<user>\.claudetoggl\config.json
Toggl → Profile settings → API Token (right at the bottom of the page)
A missing token is the one error that stops even a read — without it
an empty mirror cannot be told from an empty account.
2. node ClaudeToggl.js --sync checks the token and pulls three months of entries
3. node ClaudeToggl.js --today
Zone for both writing and listing: Europe/Prague (from this machine) — change it in config.json
The token in config.json never leaves it and is not printed even in an error.
Pak stačí do configu vložit token (Toggl → Profile settings → API Token) a:
node ClaudeToggl.js --sync # ověří token a stáhne tři měsíce záznamů
node ClaudeToggl.js --todayPohodlnější je dát program mimo repozitář a do PATH jen tenké spouštěče —
ClaudeToggl.cmd a ClaudeTogglGui.cmd počítají s programem
v %USERPROFILE%\.local\ClaudeToggl\ a samy sebou v %USERPROFILE%\.local\bin\:
mkdir %USERPROFILE%\.local\ClaudeToggl
mkdir %USERPROFILE%\.local\bin
copy ClaudeToggl.js %USERPROFILE%\.local\ClaudeToggl\
copy ClaudeToggl.gui.js %USERPROFILE%\.local\ClaudeToggl\
copy install.js %USERPROFILE%\.local\ClaudeToggl\
copy config.json.dist %USERPROFILE%\.local\ClaudeToggl\
copy ClaudeToggl.cmd %USERPROFILE%\.local\bin\
copy ClaudeTogglGui.cmd %USERPROFILE%\.local\bin\
Nasazení i aktualizace je kopie souborů — žádný build, žádné závislosti.
config.json.dist musí jít s sebou: install.js z něj config vyrábí a bez něj
skončí chybou. Data zůstávají v ~\.claudetoggl a kopie se jich netýká.
Pro Claude Code je v skills/vykazy/ skill — zkopíruj ho do
~/.claude/skills/vykazy/. Nese pravidla, která z nápovědy nástroje nevyplývají
(kdy se zeptat na čas, jak má vypadat popis, proč se nepushnutý záznam nesmí
nechat ležet).
~/.claudetoggl/config.json (hledá se $CLAUDETOGGL_CONFIG → vedle skriptu →
datový adresář). Vzor je config.json.dist:
{
"apiToken": "…",
"workspaceId": null,
"timezone": "Europe/Prague",
"createdWith": "ClaudeToggl",
"maxTimerHours": 4,
"pullTtlMinutes": 30,
"git": true,
"importTags": { "default": null, "byProject": {} }
}| Klíč | Význam |
|---|---|
apiToken |
token z Toggl profilu. Místo něj jde použít apiTokenEnv s názvem proměnné prostředí. |
workspaceId |
null = vezme se default_workspace_id z /me |
timezone |
IANA zóna, ve které se zobrazuje i zapisuje. API mluví v UTC a ukázat ho tak, jak přijde, by posunulo časy o dvě hodiny — a u noční práce i na jiný den. |
maxTimerHours |
nad tímhle se běžící timer hlásí jako varování |
pullTtlMinutes |
starší kopie si při čtení sama vyžádá pull |
git |
verzovat datový adresář — viz Verzování je dobrovolné |
importTags |
jediná hodnota, kterou --import doplňuje sám — viz níž |
Token nikdy neopustí config.json — netiskne se ani v chybové hlášce a .gitignore
v datovém adresáři ho drží mimo git.
--import bere dávku návrhů a žádný údaj nedopočítává: řádek bez data, startu
nebo délky je chyba. Jediná výjimka je tag, a jen tam, kde ho config umí odvodit
z projektu:
"importTags": {
"default": "programování",
"byProject": { "Support": "support" }
}Výjimka je možná proto, že v takovém workspace tag nenese vlastní informaci —
plyne z projektu a nemůže změnit fakturovaný čas. Že to tak je, je fakt o jednom
workspace, ne o výkazech obecně, takže je v configu a ne v kódu. byProject má
přednost před default a projekt se páruje stejně jako u --project, tedy bez
ohledu na velikost písmen a diakritiku.
Když config mlčí (jak ho zakládá install.js), je řádek bez tag chyba —
vymyslet cizí konvenci by znamenalo dát vymyšlený tag na fakturovaný čas.
Nastavit se dá i per účet, protože konvence patří workspace.
Kdo vykazuje víc klientům, má v Togglu víc účtů. Nahoře uvedený tvar je jeden účet; víc se napíše jako seznam:
{
"accounts": [
{ "name": "klient", "apiTokenEnv": "TOGGL_TOKEN_KLIENT", "workspaceId": 1234567 },
{ "name": "osobni", "apiToken": "…" }
],
"defaultAccount": "klient",
"timezone": "Europe/Prague"
}- Config s jedním tokenem nahoře funguje dál a čte se jako jeden účet jménem
default. Kvůli tomuhle se nemusí přepisovat nic. nameje zároveň jméno adresáře, takže se ověřuje:[a-z0-9][a-z0-9_-]*, a dvě jména lišící se jen velikostí písmen se odmítnou — na Windows by z nich byl jeden adresář a záznamy jednoho klienta by skončily ve výkazu druhého.- Co účet neuvede, bere ze sdílených hodnot nahoře.
workspaceIdje vždycky jeho vlastní; sdílené by ho nikdo nečetl. defaultAccountříká, kam míří zápis bez--account. S jedním účtem je to ten jediný; s víc účty a bezdefaultAccountse zápis bez určení účtu odmítne — špatný účet znamená hodiny ve výkazu jiného klienta a na obrazovce to nepozná nikdo.
Čtení sčítá všechny účty, protože „co jsem dneska dělal" je jedna otázka;
--account <name> ho zúží. Ve sloučeném výpisu nese ref i účet (klient/L3) — čítač
L<n> běží per účet, takže L3 samo o sobě jsou klidně dva různé záznamy. Ref s účtem
už --account vedle sebe nepotřebuje.
Zápis jde vždycky do jednoho účtu: do toho, který řekne ref, --account, nebo
defaultAccount. Když ho nejde určit, běh skončí chybou a nabídne obě možnosti —
hádat se tu nedá, protože chyba se pozná až na faktuře.
Timery jsou taky per účet (--start … --account osobni); účet se určuje při startu,
protože projekt se ověřuje proti workspace a ten bez účtu není. --stop si timer najde
sám, dokud běží jenom jeden; při víc bězích chce --timer klient/T1 nebo --account.
ClaudeToggl # co běží + dnešek + pending
--now # jen to, co právě běží
--today | --date <YYYY-MM-DD> | --week | --last-week
--from <YYYY-MM-DD> --until <YYYY-MM-DD>
--search "text" # všechny měsíce, co mirror drží
--summary <YYYY-MM> [--by project|tag|day]
--review <YYYY-MM> # kontrola před fakturací
--projects | --tags | --budget
--help | --version
--add "popis" --at "YYYY-MM-DD HH:MM" --for 90m [--project X] [--tag t]
--edit <ref> [--desc … | --at … | --for … | --project … | --tag …]
--rm <ref> --yes
--start "popis" [--project X] [--at HH:MM]
--stop [--timer T1] [--at HH:MM] # timerů může běžet víc naráz
--import <soubor.jsonl> [--yes] # dávka návrhů do fronty
--resolve <ref> --keep-mine|--keep-theirs --yes
--sync [--pull-only | --push-only]
--account <name> --project <název> --json --offline
--search a --summary si nesou vlastní rozsah, takže se s okenními
přepínači nekombinují: hledá se přes celý mirror, sčítá se přes jeden měsíc.
Záznam se počítá celý do dne, ve kterém začal — a tedy i do jeho měsíce. Noční
blok 31. 7. 21:15–01:00 je celý v červencovém součtu, včetně té hodiny odpracované
prvního srpna. Je to jediný rozumný výklad (rozdělit ho by znamenalo vyrobit dva
záznamy, které nikdo nezadal), ale u fakturace po měsících je dobré o tom vědět.
Výpis přes víc dní seskupuje záznamy pod denní hlavičky — samotné HH:MM by
u týdne ani u nálezu z jiného měsíce neřeklo, o který den jde.
--import bez --yes nic nezapíše: vypíše, co by do fronty šlo, a zkontroluje
to proti tomu, co na těch dnech už leží. Dávka jde do fronty celá, nebo vůbec —
půlka, která prošla, se pak dohledává ručně.
--resolve je jediná cesta ven z divergence. Nemerguje: jednu verzi zahodí
a kterou, říká uživatel. Proto --yes — Toggl si historii přepsané verze
nepamatuje.
Ref je Toggl id; dokud záznam v Togglu není, je to L<n> — a L<n> platí
i potom, takže ref, který uživatel jednou viděl, nepřestane ukazovat. S víc
nakonfigurovanými účty nese ref i účet (klient/L3), viz Víc účtů.
Zápis říká na posledním řádku, jak dopadl — na návratový kód se spolehnout nedá,
(pushed) i (pending) končí nulou:
| Konec výpisu | Co se stalo |
|---|---|
# L8 → 4502575531 (pushed) |
na disku i v Togglu |
# L8 is waiting for --sync — it is not in Toggl yet (pending) |
jen na disku, čeká na --sync |
# L6 deleted — it was never in Toggl, so there is nothing to send (done) |
smazaný záznam, který nahoře nikdy nebyl |
! not pushed: <důvod>, N pending |
není v Togglu, a zadavatel to tedy nevidí |
! STOP: entry <ref> diverged |
nedotčeno, rozhoduje uživatel |
Kódy jsou 0 hotovo, 1 nástroj to neuměl (síť, divergence), 2 musí se změnit
zadání nebo config (špatný argument, neznámý projekt, chybějící token).
ClaudeToggl.gui.js servíruje tutéž kopii jako lokální stránku — výpis po dnech,
formulář na přidání a opravu, timery, --sync a vyřešení divergence:
ClaudeTogglGui.cmd # nebo: node ClaudeToggl.gui.js
ClaudeTogglGui.cmd --port 7788# ClaudeToggl GUI on http://127.0.0.1:4747/?t=7cfdf8456c1344cdb2426377551ac15e
# the token is part of the address and changes every run; Ctrl+C stops the server
Poslouchá jen na 127.0.0.1 a adresa nese token, který se s každým během mění —
bez něj vrací 401. Token do Togglu stránka nikdy nedostane: /api/config posílá
jen tokenSet: true.
Je to druhý soubor schválně. Pravidlo „jeden soubor, žádné závislosti" je o nástroji
a HTML stránka do něj nepatří; závislosti nepřidává ani tak, stránku nese jako řetězec.
Každý zápis jde přes tytéž cmd* funkce, co volá CLI — fronta, tombstony, pořadí
zámků i commit po pushi žijí tam a druhá implementace by se do měsíce rozešla, což by
se ukázalo až na faktuře.
~/.claudetoggl/
config.json token a nastavení (v .gitignore)
.git/ volitelná lokální historie bez remote (viz níž)
accounts/default/
entries/2026-08.jsonl jeden řádek jeden záznam, měsíc na soubor
state.json lastPullAt, čítač L<n>, poslední viděná kvóta
catalog.json projekty a tagy workspace ze serveru (obnova jednou denně)
running.json běžící timery; chybí, když nic neběží
.lock zámek kolem zápisu, u souborů, které hlídá
Adresář na účet, ne pole na řádku: každý z těch souborů patří právě jednomu Toggl účtu — kvóta je per token, kurzor pullu taky, katalog je per workspace. Kdyby účet byl pole na řádku, musel by ho filtrovat každý read a každý průchod frontou — a ta trojice by stejně byla per účet, takže by ten rozměr existoval dvakrát.
Zámek je taky per účet, protože nic se přes účty neděje atomicky: fronta, kurzor, kvóta i čítače jsou každý svůj, a čtení zámek nebere vůbec. Dva účty se tak dají synchronizovat naráz. Git je naopak jeden, nad celým adresářem.
Starší instalace má tyhle soubory v kořeni. Při prvním běhu se jednorázově
přesunou do accounts/<defaultAccount>/, celé nebo vůbec, uvnitř zámku a s hláškou;
když už accounts/ existuje, přesun se nespustí — sloučit dva stavy je horší než
přesun, který se nekonal. Se zapnutým gitem je to jeden commit samých přejmenování,
takže git revert ho vrátí.
Vedle Toggl polí nese řádek pět vlastních: id (chybí, dokud záznam nebyl
pushnut), localId (L<n>, stálý po celý život záznamu), dirty
(create/update/delete, co má push poslat), queuedAt (kdy řádek do fronty
přibyl — od toho se počítá stáří pending, ne od kdy se pracovalo) a syncedAt
(serverové at při posledním syncu, podle kterého se pozná divergence).
Zapisuje se přes temp soubor a rename, kolem celé sekvence přečti–změň–zapiš
je zámek — o tentýž adresář se totiž může dělit víc souběžných session. Commit se
dělá až po pushi, aby držel finální stav včetně přiděleného id.
Kopii na disku jde kdykoli znovu stáhnout z Togglu, takže git v datovém adresáři
není architektonicky nutný — je to pojistka, ne součást konstrukce. Zapíná se
polem git v config.json a má tři stavy:
"git" |
co dělá |
|---|---|
true |
commit po každém zápisu; když adresář repozitář není, řekne to |
false |
git se vůbec nespustí a nástroj o něm mlčí |
| chybí | commituje, jen když adresář repozitář už je — a jinak mlčí taky |
Ten třetí stav je kvůli existujícím instalacím: kdo repozitář má, verzuje dál, a
kdo ho nikdy neměl, přestane u každého zápisu dostávat hlášku, že chybí.
node install.js --no-git založí adresář bez repozitáře a zapíše "git": false.
Za co se platí: Toggl nemá koš a nevede historii úprav, takže --rm je
nevratné. Se zapnutým verzováním jde smazaný záznam vytáhnout z historie; bez něj
je pryč nadobro. Proto je výchozí stav zapnuto.
- Nevymýšlí ani nedopočítává časy a délky.
--addbez--forje chyba, ne pozvánka k odhadu; z gitu jde zjistit, co se dělalo, ale nikdy jak dlouho (přestávky se dělají i uprostřed jedné issue a nikde nejsou zaznamenané). Chybějící údaj je důvod se zeptat. - Nesrovnává časy na čtvrthodinu. Mřížku
:00/:15/:30/:45jen hlásí — u startu i konce zvlášť. Srovnat ji je rozhodnutí o celém dni, ne zaokrouhlení dvou čísel. - Nevytváří běžící záznam na serveru. Timer je lokální stav, nahoru jde až
uzavřený záznam — proto
--stopnemůže selhat kvůli síti. Cena: v mobilní appce není vidět, že něco běží. - Nečte git, kalendář ani tracker. Podklad pro výkaz skládá agent (skill), nástroj by tím získal závislosti i domněnky.
- Nespravuje projekty, klienty ani workspace — jen je čte. Neznámé jméno projektu je chyba s návrhem, ne důvod zakládat nový.
- Nemerguje divergenci sám a při 429 neretryuje ve smyčce.
- Nesahá na cizí záznamy ve workspace, pracuje jen s
/me. - Nehádá, do kterého účtu zápis patří, a nepřesouvá záznamy mezi účty. Přesun by byl smazání tady a nový záznam ve výkazu někoho jiného, ne úprava.
- Zapisovat jde jen do tohohle měsíce a dvou předchozích — dál Toggl zápis nepustí a nástroj to řekne dopředu, ne až při odesílání.
Kopie sahá tři měsíce zpět — stejné okno, jaké dovoluje API. Starší historie je jen ve webu a nástroj ji neumí obnovit.
--review schválně nekontroluje dny bez záznamu. Kalendářní pracovní den tu
nic neznamená: ve skutečných datech nemá záznam víc než polovina pracovních dnů
a zato víkendy ho mají. Kontrola, která by hlásila dvanáct nálezů z třiadvaceti,
se přestane číst. Signál „byly commity, ale není výkaz" umí složit jedině skill,
protože nástroj git nečte.
npm test # node --test, bez sítě a bez gituESM, tabulátory, bez závislostí. Kód, komentáře i výstup pro uživatele anglicky;
česky je jen skill a tenhle README, protože je čte člověk a agent, ne program.
Komentář vysvětluje proč, ne co — hlavně tam, kde chování vyplývá z API
(párování podle startu, tříměsíční okno, since vracející i smazané záznamy).
Testy nesmí na síť, na git ani mimo os.tmpdir(): fetch se vstřikuje
(setFetch(impl) globálně, opts.fetch u jednoho volání) a čas se předává jako
ctx.now, aby šel zafixovat. Sada má pět souborů po vrstvách (core, sync,
read, write, gui) a každá vrstva vlastní svoje testy — když se mění chování,
opravuje se kód, ne cizí assertion. Nová funkce = nový test.
MIT — viz LICENSE.