Skip to content

Latest commit

 

History

11 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

ClaudeToggl

English

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é).

K čemu to je

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.

Proč offline first

Úč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.

Instalace

git clone https://github.com/Lossik/ClaudeToggl.git
cd ClaudeToggl
npm test                       # projede testy — bez sítě a bez gitu
node install.js                # založí ~/.claudetoggl

install.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 settingsAPI Token) a:

node ClaudeToggl.js --sync     # ověří token a stáhne tři měsíce záznamů
node ClaudeToggl.js --today

Nasazení mimo repozitář (Windows)

Pohodlně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).

Config

~/.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.

importTags — jediná hodnota, kterou si nástroj domýšlí

--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.

Víc účtů

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.
  • name je 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. workspaceId je 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 bez defaultAccount se 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.

Povrch

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).

Stránka v prohlížeči

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.

Data na disku

~/.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.

Verzování je dobrovolné

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.

Co nástroj zásadně nedělá

  • Nevymýšlí ani nedopočítává časy a délky. --add bez --for je 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/:45 jen 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 --stop nemůž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í.

Co ještě neumí

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.

Vývoj

npm test        # node --test, bez sítě a bez gitu

ESM, 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.

Licence

MIT — viz LICENSE.

About

Offline-first Toggl Track client for the command line and for Claude Code

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages