ticktick-cli (tick) is a CLI tool for interacting with the TickTick API. It uses cookie-based authentication (not OAuth). The API token is loaded from a .env file via godotenv.
The binary is invoked as tick with subcommands: tick <service> <action> [flags].
Implemented commands:
- Tasks:
tick task list|add|complete|delete|get,tick project list - Pomodoro:
tick pomodoro export|stats|create|delete,tick pomodoro timer list|stats - Habits:
tick habit list|checkin|status
- Go 1.25.6
- CLI framework:
github.com/spf13/cobra - Logging:
log/slog(stdlib) with a thin wrapper ininternal/loggerfor a custom Trace level - Dependencies:
github.com/spf13/cobra,github.com/joho/godotenv - Task runner:
just(Justfile) - CI: GitHub Actions (lint, test, build)
cmd/tick/
main.go — entrypoint: rootCmd.Execute()
root.go — root cobra command, PersistentPreRunE (.env, client, verbosity), wires subcommands
integration_test.go — CLI integration tests (build tag: integration)
task/
task.go — NewCmd/NewProjectCmd: returns cobra command groups
list.go — "task list": --project, --tag, --priority filters, tabwriter output
add.go — "task add": --title, --project, --tags, --priority, --due
complete.go — "task complete ID": optional --project
delete.go — "task delete ID": optional --project
get.go — "task get ID": optional --project, detailed output
project.go — "project list": cobra subcommand, tabwriter output
pomodoro/
pomodoro.go — NewCmd: returns cobra command group
export.go — "pomodoro export": CSV export with filters
stats.go — "pomodoro stats": today/total counts and durations
create.go — "pomodoro create": --task, --duration
delete.go — "pomodoro delete ID"
timer.go — "pomodoro timer list|stats": nested cobra commands
types.go — exportArgs struct, format constants
utils.go — helpers: splitCSV, includeExclude, matchesFilter, monthRange, formatDuration
utils_test.go — unit tests for utils
habit/
habit.go — NewCmd: returns cobra command group
list.go — "habit list": tabwriter output
checkin.go — "habit checkin NAME": --value for Number types
status.go — "habit status": --date, shows done/pending per habit
internal/ticktick/
client.go — Client constructor, HTTP helpers (Get, Post, Put, Delete, PostJSON, etc.)
client_test.go — unit tests for client HTTP methods
task.go — TaskService: Sync, List, Get, Create, Complete, Delete, ListProjects
task_test.go — unit tests for task service
pomodoro.go — PomodoroService: GetTimeline, GetAll, Stats, Create, DeletePomo, ListTimers, TimerOverview
pomodoro_test.go — unit tests for pomodoro service
habit.go — HabitService: List, GetCheckins, Checkin, GetRecords
habit_test.go — unit tests for habit service
integration_test.go — API integration tests (build tag: integration)
types.go — All types: Client, Task, Project, Habit, Pomodoro, BatchResponse, etc.
internal/logger/
logger.go — slog setup: SetVerbosity maps -v/-vv/-vvv to slog levels, custom Trace level
.github/workflows/
ci.yml — CI pipeline: lint, test, build
release.yml — Auto-release on push to main: test → auto-tag (patch bump) → cross-platform build → GitHub release
ticktick.Client has service fields (client.Task, client.Pomodoro, client.Habit) following the go-github pattern. Each service is a separate struct with a back-reference to the client for making HTTP calls. New services should follow this pattern:
- Create the service struct in
internal/ticktick/<service>.go - Add the field to
Clientintypes.go - Wire it in
NewTicktickClientinclient.go - Create
cmd/tick/<service>/package for CLI commands
NewTicktickClient(apiToken, ...Option) uses functional options (WithHTTPClient).
Client.HTTPClient is typed as HTTPClient interface (just Do(*http.Request) (*http.Response, error)). *http.Client satisfies it. Tests inject a mockHTTPClient with a DoFunc field.
Pomodoros wraps []Pomodoro with a service reference. Callers can use .Next() on a result set for manual pagination, or GetAll() which paginates automatically. The API returns results newest-first.
root.go defines the root cobra command with PersistentPreRunE for .env loading, client creation, and verbosity setup. Each service package exports a NewCmd(client **ticktick.Client) *cobra.Command that returns a command group with subcommands. The **ticktick.Client (pointer-to-pointer) pattern lets root's PersistentPreRunE initialize the client before any subcommand RunE executes. All commands return errors instead of calling os.Exit(1) directly.
- 0 (default): Warn+ only (errors, warnings)
- 1 (
-v): Info (progress, counts, results) - 2 (
-vv): Debug (parsed args, pagination details, API responses) - 3 (
-vvv): Trace (raw HTTP requests, individual row data, internal state)
Use slog.Info, slog.Debug directly. Use logger.Trace for the custom trace level.
- Auth: cookie-based, header
Cookie: t=<token>. Token is thetcookie from ticktick.com. - Required headers:
Origin: https://ticktick.comandx-device(JSON) — POST requests fail withaccess_forbiddenwithout these - Base URL:
https://api.ticktick.com/api - Timestamps: format
2006-01-02T15:04:05.000-0700(NOT RFC3339 — no colon in timezone offset) - Timeline API uses millisecond unix timestamps for the
toparameter - API returns ~31 results per page, newest first
- Batch endpoints use
{"add":[], "update":[], "delete":[]}→{"id2etag":{}, "id2error":{}} - Sync endpoint (
GET /v3/batch/check/0) returns all tasks, projects, tags in one call - IDs are 24-char hex strings generated with
crypto/rand - Habit checkin stamps are
YYYYMMDDintegers (e.g.20260301)
- Use
mockHTTPClientstruct withDoFuncfield to mock HTTP calls - Run:
just testorgo test ./... - Test files live alongside source files (
*_test.go)
- Guarded by
//go:build integrationbuild tag - Require
TICKTICK_API_TOKENenv var (loaded from.env) - Run:
just test-integrationorgo test -tags integration -v ./... - NOT run in CI — manual only
- Triggers on push to
mainand PRs tomain - Jobs:
lint(golangci-lint),test(unit tests),build(compilation check) - Build job depends on lint + test passing
just test— run unit testsjust test-v— run unit tests (verbose)just test-integration— run integration testsjust lint— run linterjust build— verify compilationjust run <args>— run the CLI
- Do not build after changes. If you do build, remove the generated binary immediately. Do not leave binaries in the repo.
- Update AGENTS.md when you add new services, change architecture, modify patterns, or add dependencies.
- Update README.md when you add new commands, flags, or change usage.
- Keep Go conventions: unexported names for internal-only identifiers, doc comments starting with the function name, no
SCREAMING_SNAKE_CASE. - All filtering (tags, projects) is case-sensitive and exact match.
- New services should follow the existing package structure:
internal/ticktick/<service>.gofor API logic,cmd/tick/<service>/for CLI commands. - Use Cobra for CLI commands. Each service package exports
NewCmd(client **ticktick.Client) *cobra.Command. Usecobra.ExactArgs,cobra.NoArgs, etc. for arg validation. Define flags on the command, not viaflag.NewFlagSet. - Never pass
nilas a context. Usecontext.TODO()when no context is available. - Write unit tests for new functionality. Use the
mockHTTPClientpattern for HTTP mocking. - Integration tests must use
//go:build integrationbuild tag.