- TypeScript 57.5%
- Svelte 20.8%
- Shell 11.6%
- CSS 9.7%
- Dockerfile 0.3%
Task detail columns size to minmax(0, 1fr) so a long description stays in .task-main. Relation and subtask lists have no UA indent. Each related task shows its status next to the title. |
||
|---|---|---|
| .forgejo/workflows | ||
| deploy/runtime | ||
| docs | ||
| scripts | ||
| src | ||
| tests | ||
| web | ||
| .dockerignore | ||
| .gitignore | ||
| docker-compose.yml | ||
| Dockerfile | ||
| README.md | ||
sak
sak is a task tracker for agents and the people working with them. One binary
serves an HTTP API and the single-page app built on top of it, and keeps
everything in one SQLite file: projects hold tasks; a task carries text, typed
attributes, labels, comments and relations to other tasks. Every API request
requires a bearer token minted by the same binary; the SPA assets are served
without authentication, and the server listens on 127.0.0.1 unless --bind
says otherwise.
Build from source
You need:
- the sx compiler on
PATH, SX_STDLIB_PATHpointing at an sx standard library whose vendored SQLite is compiled with FTS5 — task search is an FTS5 index,- Node.js 22.13+ and npm, for the SPA.
export SX_STDLIB_PATH=/path/to/sx/library
cd web && npm ci && npm run build && cd ..
sx build src/main.sx -o sak
npm run build type-checks the SPA, runs its unit tests and writes
web/dist/, which is what sak serve hands to the browser.
Run
./sak token create --db sak.db --name laptop
./sak serve --db sak.db
token create prints one token — sak_ followed by 64 hex characters — and
that printed line is the only copy: the database stores its SHA-256. Mint one
per client; deleting a row from the token table revokes it.
serve binds --bind (default 127.0.0.1) on --port (default 4345) and
serves the SPA from --assets (default web/dist). --bind takes a numeric
address — 0.0.0.0 and :: reach the whole network, a LAN address one
interface — and anything else exits 2. --threads (default 16) sets how many
requests are served at once, each on a SQLite connection of its own. The first
line serve logs summarizes what it is running: version and build commit,
bound address, database, assets, log settings and worker count.
Open http://127.0.0.1:4345 and paste a token to get in. Reach the API with the
same token:
curl -H "Authorization: Bearer sak_…" http://127.0.0.1:4345/api/health
Every request then leaves one line on stderr — the UTC instant, method, path, status, response bytes and duration, plus the cause on a 4xx or 5xx:
INFO: 2026-08-02T09:14:22Z GET /api/projects -> 200 412b 1ms
ERROR: 2026-08-02T09:14:21Z GET /api/tasks/8123 -> 404 21b 0ms get_task: NotFound
--log-level chooses how much of that is written: quiet keeps the refusals,
requests (the default) adds a line per served request, debug adds the
request size and the response content type to every line. The startup line is
written at every level, and nothing is written to a file — redirect stderr to
keep the lines. The last --log-ring lines (1000 by default, at most 100000)
also stay in memory, where GET /api/logs serves them back newest first;
docs/api.md documents that route.
Each command creates the database if it is missing.
Data
Everything lives in the one SQLite file named by --db. Migrations run when
that file is opened, so upgrading is: stop the server, swap the binary, start
it again. Back up by copying the file with the server stopped; restore by
putting the copy back.
Run with Docker
Build the release archives on the host, then build and start the image:
bash scripts/release.sh
docker build -t git.swipelab.com/sx/sak:latest .
docker compose up -d
The compose recipe publishes port 4345, keeps the database in the sak-data
volume, and restarts the container unless it is stopped. Create a token inside
the container:
docker exec sak sak token create --db /data/sak.db --name laptop
Back up the database by stopping the service and copying the volume's
sak.db:
docker compose stop
docker cp sak:/data/sak.db ./sak.db
docker compose start
Deploy
docs/deploy.md covers the manual deploy workflow that
builds and publishes the sak docker image to the registry: how to trigger it,
the runtime the job runs on, the secrets it reads, and how the NAS pulls and
rolls back the published image. scripts/deploy.sh carries that logic and
rehearses it locally with --local or --dry-run; deploy/runtime/manifest
is the single authority on every external command the deploy path uses.
API
docs/api.md documents every route: projects, tasks, attributes, labels, comments, relations, search, saved views, paging and the revision protocol that guards concurrent writes.
Release tarballs
bash scripts/release.sh
builds the SPA and the server and writes three archives, each holding sak,
web/dist/, this README and docs/:
dist/sak-<version>-macos-arm64.tar.gzdist/sak-<version>-linux-x86_64.tar.gzdist/sak-<version>-linux-arm64.tar.gz
The macOS archive is a native build, so the script itself runs on a macOS
arm64 host; --linux-only drops it and packages the two Linux archives on any
host. The Linux archives are cross-compiled with
sx --target linux|linux-arm --self-contained, which needs zig on PATH (or
in $SX_ZIG) for its libc. The script takes the compiler from $SX when that
is set, and PATH otherwise.
The version is the VERSION constant in src/app.sx; it names the archives
and answers GET /api/health. To cut a release, edit that constant and rerun
the script. The release script supplies the short git sha of the checkout it
packages. Direct builds carry a valid SAK_BUILD_COMMIT when supplied and
answer unknown otherwise.
Unpack and run:
mkdir -p sak && tar -xzf sak-0.2.0-macos-arm64.tar.gz -C sak && cd sak
./sak token create --db sak.db --name laptop
./sak serve --db sak.db