Skip to content

server

The server command connects this machine to a kasl-server - the team server that collects workdays from everyone’s agent and gives a manager a dashboard.

Connecting is optional. kasl works entirely on its own, and nothing in this command changes that: the local database stays the record of your day either way.

Terminal window
kasl server <SUBCOMMAND>
Subcommand Purpose
connect Connect this machine with an agent token
status Show the connection and whether it still works
push Send a day’s work to the server
flush Send every day still waiting to reach the server
queue Show the days still waiting, and why
manifest Show what this server stores about you
backfill Queue every recorded day in a date range and send them
pulse Tell the server whether you are working right now - off until you turn it on
disconnect Forget the connection and remove the stored token

You do not issue your own token. An administrator creates one on the server - under the person it reports for, labelled with the machine it belongs to - and hands it over. The server shows a token once, at issue, and stores only its hash afterwards, so a lost token is replaced rather than recovered.

Terminal window
kasl server connect [OPTIONS]
  • --url <URL>: Server address, e.g. https://kasl.example.com. Prompted for when omitted.
  • --ca-certificate <PATH>: PEM file holding the certificate authority that signed the server’s certificate.

The token is always asked for at the prompt, never taken from an argument: a token on a command line ends up in shell history.

Terminal window
$ kasl server connect
kasl-server URL: https://kasl.example.com
https://kasl.example.com is kasl-server vX.Y.Z
Agent token (issued by your administrator): ****************
Connected as Kirill Lakhtachev (agent 'laptop')

First, that the address is a kasl-server. A URL is asked about before the token is, so a typo in the address is reported as a typo rather than as a rejected token - two different problems with two different fixes.

Then, whose token it is. The server is asked who it thinks is connecting, and the answer is printed. A token is an opaque string; one pasted from the wrong chat window works perfectly and quietly files this machine’s days under a colleague’s name. The check costs one request and puts the mistake in front of a person while they are still looking.

Nothing is written until both checks pass. A half-written connection - an address saved with a token the server never accepted - would leave the agent looking configured while every upload failed.

In the operating system’s credential store: Credential Manager on Windows, Keychain on macOS, the Secret Service on Linux - the same place as every other kasl credential (see ADR 0001). The config file records the address only, so it stays safe to read, copy between machines, and paste into a bug report.

A company kasl-server is routinely behind a private CA or a self-signed certificate, which the system trust store does not know about. Name the CA that signed it:

Terminal window
kasl server connect --url https://kasl.internal --ca-certificate /etc/ssl/company-ca.pem

The certificate is added to the trust store rather than replacing it, so public certificates keep working. There is deliberately no option to turn verification off: that would also accept anyone else’s certificate, on a connection carrying a bearer token.

The scheme is never guessed. http:// and https:// differ by whether the token crosses the network in the clear, which is not a default worth inventing on your behalf.

Terminal window
kasl server status [--json]
  • --json: Print what the checks found as one JSON document - see JSON output.

Reaches the server rather than reading the config back, because what you run this to find out is whether the connection still works:

Terminal window
$ kasl server status
Configured server: https://kasl.example.com
https://kasl.example.com is kasl-server vX.Y.Z
Connected as Kirill Lakhtachev (agent 'laptop')
server vX.Y.Z - api v1 - ok
Pulse: on - the last one went 12 s ago (working).

Each part can fail on its own, and each failure names its own fix - the server unreachable, the token no longer accepted (revoked, or the account deactivated), or a configured server with no token stored, which means connecting again.

The server ... api ... line is the pair that decides whether this agent and that server understand each other: the server’s own version, and the API version it serves this agent under. Both come from the server rather than being assumed here, so when an upload is refused you can see which two versions are actually talking. Reaching that line at all means they are compatible - the checks above it are what connect enforces.

The last line is the pulse. It is read from what the watcher recorded, not from the server, so it is printed whatever the checks above it found - a server that cannot be reached is exactly when “the last one went 20 min ago” is worth reading.

Terminal window
kasl server push [OPTIONS]
  • --last, -l: Send yesterday instead of today.
  • --date <YYYY-MM-DD>: Send a specific day.

Sends one day - its start and end, its pauses, and its tasks - to the connected server.

Terminal window
$ kasl server push
Sent 2026-08-31 to the server: 4 pauses, 6 tasks

Nothing is sent for a day that was never started, which is the honest answer rather than an error:

Terminal window
$ kasl server push --last
No workday recorded for 2026-08-30 - nothing to send.

The server stores a day as a unit, and the last upload wins. Pushing an unchanged day leaves the server exactly as it was; pushing a day you have since corrected - a task renamed, a break added with pauses add - replaces the stored copy with the corrected one. There is no separate “update” to remember.

That is also why a failed push costs nothing: whatever went wrong, the day is still here, and sending it again is the whole fix.

A day is sent whole, including its deletions

Section titled “A day is sent whole, including its deletions”

Every push says “this is everything I hold for this date”. A task you deleted here is therefore deleted there too, and the count is printed when it happens:

Terminal window
$ kasl server push
Sent 2026-08-31 to the server: 4 pauses, 6 tasks
1 task(s) removed on the server - deleted here since the last upload

The deletion is scoped to the date being sent. A task carried across several days keeps its copy on the others, so pushing an old day cannot erase work recorded on a different one.

Every timestamp is sent with this machine’s UTC offset, and the day carries its own calendar date rather than one derived from the clock. Both matter once a team spans time zones: bare wall-clock time from two countries cannot be compared, and near midnight the date of an instant and the date the work belongs to disagree.

The offset is taken per timestamp, so a day spanning a daylight-saving change keeps both of its halves right. The two wall-clock readings that have no single offset - the hour skipped when clocks go forward, the hour repeated when they go back - are refused by name instead of guessed, because either guess would silently move an hour of work.

The two kinds of failure need different things from you, so they are reported differently.

The server refused the day. The payload will not be accepted as sent, and sending it again will not help. The server’s own explanation is printed - which task, which field - so it can be fixed here and pushed again. A revoked token lands here too: the fix is kasl server connect, not another attempt.

The server could not answer. It is down, unreachable, or asking for a pause. The day is unchanged locally, and it is queued:

Terminal window
$ kasl server push
Cannot reach kasl-server at https://kasl.example.com: connection refused
The day is unchanged here and stays queued - `kasl server flush` sends it when the server is back.
2026-08-31 is queued and will be sent with the next successful push.

Only the second kind is queued. A day the server will never accept as sent is not put in a queue that would retry it forever - it is reported, so you can fix it and push again.

A laptop that spends a week off the network still has that week’s work. Every day that could not be delivered is remembered, and the next successful push pays the whole debt off - so in normal use the queue is something you never have to think about.

Two properties are worth knowing:

A queued day is rebuilt when it is sent, not replayed as it was. The queue holds a date, not a stored copy of the payload. A day you correct while the network is down arrives corrected.

A backlog goes in one request. The server takes a batch of days and answers about each one separately, so a single day it will not accept does not strand the rest behind it. Long backlogs are split into batches automatically.

A successful push sends the queue too, on the connection that was just proven to work:

Terminal window
$ kasl server push
Sent 2026-09-03 to the server: 3 pauses, 5 tasks
Sending 4 days...
Sent 2026-08-28 to the server: 2 pauses, 4 tasks
Sent 2026-08-29 to the server: 5 pauses, 6 tasks
Sent 2026-08-30 to the server: 1 pauses, 2 tasks
Sent 2026-08-31 to the server: 4 pauses, 6 tasks
4 sent, 0 refused, 0 still waiting
Terminal window
kasl server flush

Sends what is owed without pushing today - useful from a script, or when you want the backlog gone but today is not finished.

Terminal window
$ kasl server flush
Sending 2 days...
Sent 2026-08-30 to the server: 1 pauses, 2 tasks
2026-08-31 was refused and has been dropped from the queue: tasks[0]: name is empty
1 sent, 1 refused, 0 still waiting

A day the server refuses is named and dropped rather than kept: it would be refused identically on every future attempt. A day the server could not answer for stays, and the summary says so.

Terminal window
kasl server queue [--json]
  • --json: Print the owed days as one JSON document - see JSON output.

Shows what is still owed, with how many attempts each has taken and what went wrong last time. It does not touch the network, so it answers on a train:

Terminal window
$ kasl server queue
2 days are waiting to be sent:
2026-08-30 - 3 attempts, last: cannot reach kasl-server at https://kasl.example.com
2026-08-31 - 1 attempt, last: cannot reach kasl-server at https://kasl.example.com
Terminal window
$ kasl server queue
Nothing is waiting to be sent.
Terminal window
kasl server backfill [--from <YYYY-MM-DD>] [--to <YYYY-MM-DD>]
  • --from <YYYY-MM-DD>: First date of the range. Without it, the whole history.
  • --to <YYYY-MM-DD>: Last date; defaults to today.

For history that predates the connection - a machine that tracked locally for months before the team got a server:

Terminal window
$ kasl server backfill --from 2026-08-01 --to 2026-08-31
21 recorded days between 2026-08-01 and 2026-08-31
Sending 21 days...
Sent 2026-08-03 to the server: 2 pauses, 4 tasks
...
21 sent, 0 refused, 0 still waiting

With no --from, the range opens at the first day this machine ever recorded - which is usually what a machine connecting for the first time actually owes:

Terminal window
$ kasl server backfill
214 recorded days, the whole history from 2025-11-04 to 2026-09-21
Sending 214 days...
...
214 sent, 0 refused, 0 still waiting

The first recorded date is named rather than left as “the beginning”, because it is the one fact worth reading before a year of your days reaches the server.

Only dates that actually have a workday are queued, so weekends and leave inside the range are skipped rather than queued as days that can never be sent. The days are queued before they are sent, so a run interrupted halfway leaves the rest owed rather than forgotten - running it again, or flush, continues where it stopped.

Terminal window
kasl server manifest [--json]
  • --json: Print the manifest as one JSON document, field by field as the server sent it - see JSON output.

What this installation stores about you, read from the server itself:

Terminal window
$ kasl server manifest
Privacy level on this server: moderate
This server stores your working hours, when you were interrupted, and the names of tasks you logged - but none of the text you typed about them.
What it stores:
workdays - the date, when the day started, when it ended, and whether you marked it as leave, sick or a day off
pauses - each interruption: when it began, how long it lasted, and whether it was a break you entered yourself
tasks - what you logged: the name and how complete you marked it - not your comment
account - your email, display name, role, department, and which machines report for you
live status - whether your agent currently reports you as working, on a break, or not in a day - the latest one only, replaced each time it arrives, never kept as a history
What it never collects:
keystrokes or what you type
window titles
which applications you run
screenshots or camera images
web pages you visit
file names or paths
your location
Who can see it:
you, in your own account
the manager of your department
administrators of this installation
Retention: Kept for as long as the installation keeps it: there is no automatic deletion. A deactivated account keeps its history rather than losing it.
If the level changes: Changing this setting affects what arrives from now on. Narrowing it does not erase what is already stored, and widening it does not bring back what was dropped.
This level was last set 2026-09-02 11:30.
The level is the installation's, set by an administrator on the server. kasl shows it; it cannot widen or narrow it from here.

Every word of that comes from the server. The manifest is generated there from the level the server actually enforces when a day arrives, not written by hand and not kept here - so it describes the installation you report to rather than the one kasl was built against, and it cannot claim a restraint the server does not apply. The last-set time is the exception to “printed as received”: the server reports it in UTC and kasl shows it in your own time zone.

kasl shows the level; it does not set it. An administrator chooses it for the whole installation, and there is no personal opt-out - a flag here that appeared to narrow what leaves your machine would be a promise kasl cannot keep, because the filtering happens on the server as the day is written.

What the command is for is the other half of that: if you are asked to run an agent that notices when you stop typing, you can read what it results in from the terminal you already have open, instead of signing into the server that watches you in order to find out what it watches.

Level What the server keeps
full Your hours, every interruption with the reason you gave, and your tasks with their comments
moderate Your hours, when you were interrupted, and task names - but none of the text you typed
coarse Your hours and how much of the day you were away - not when, and not what you worked on

Narrowing happens at ingest, not on a screen: a field a level excludes is dropped before the day is written, so it never reaches the server’s database or its backups.

Terminal window
kasl server pulse [enable | disable]
  • enable: Start telling the server whether you are working right now.
  • disable: Stop telling it.
  • With neither, shows how the pulse is going, without touching the network.
  • --json: With neither, print how the pulse is going as one JSON document - see JSON output. Refused with enable or disable, which print what they did.

Everything else this command sends is about the past: a day goes up when it is pushed. The pulse is the one thing about the present. Once it is on, the watcher tells the server once a minute whether you are working (in an open workday, not in a pause), paused (in an open workday, in a pause) or idle (no workday open - before it starts, or after kasl end). The team dashboard shows it as who is working right now.

Terminal window
$ kasl server pulse enable
Pulse on: the watcher tells https://kasl.example.com once a minute whether you are working, on a break, or not in a day - the state only, never the task or the reason for a break.
The first pulse arrived: the server shows you as working.

Connecting agrees to send finished days. A live signal of whether you are at your keyboard is a different thing to agree to, so connecting does not turn it on, and nothing on the server can. The choice belongs to the connection: disconnect forgets it, and connecting to a different server starts with it off again - agreeing to tell one installation where you are is not agreeing to tell the next.

The state and the moment it was observed, with your UTC offset - nothing else. Not the task you are on and not the reason for a break: those belong to the day, under the privacy level the manifest describes. The server keeps the latest pulse only, replacing it each time, never as a history, and the manifest lists it at every level. The token says which machine is reporting, so the pulse does not carry a name of its own.

A change goes at once rather than at the next minute: the dashboard is looked at when someone wonders where a colleague went, and a break reported a minute late is wrong at exactly that moment. The interval is the server’s - it answers every pulse with how often it wants one and how long it believes one - so there is nothing to configure here.

The watcher. It is the one process that knows whether this is a pause, so it is the only sender; enable turns the pulse on, waits for the watcher’s first one and tells you how it went, and sends nothing of its own. With no watcher running, nothing goes:

Terminal window
$ kasl server pulse enable
Pulse on: the watcher tells https://kasl.example.com once a minute whether you are working, on a break, or not in a day - the state only, never the task or the reason for a break.
No background watcher is running, so nothing goes out until one starts - `kasl watch`.

The setting is read by the running watcher within seconds, so turning the pulse on or off needs no restart.

Terminal window
$ kasl server pulse
Pulse: on - the last one went 12 s ago (working).

When pulses stop arriving, the reason and the last one that got through are both shown:

Terminal window
$ kasl server pulse
Pulse: on, but the last one (20 s ago) did not arrive: cannot reach kasl-server at https://kasl.example.com: connection refused
The last one that arrived went 6 min ago.
The server stops believing a pulse after 3 min, so it now shows you as offline.

A server that could not answer is asked again at the next interval; one that refused - a revoked token, a server older than the pulse, a clock too far ahead - is asked again five minutes later, and running enable again asks at once. A watcher that has stopped is spotted too: it is overdue for its next attempt, and pulse says so and names kasl watch.

The server measures your clock on every pulse. Past ten seconds, pulse says how far and in which direction, because every time this machine records is off by as much - and the server refuses a pulse stamped more than a minute ahead.

Terminal window
$ kasl server pulse disable
Pulse off. The server keeps the last state it received, and shows you as offline once that is 3 min old.

There is no “stop” to send - the server goes by silence. So the dashboard does not go blank the moment the pulse is off: it shows the last state until that is too old to believe, and then shows you as offline.

kasl and kasl-server ship on their own schedules, so the version of the server on the other end is not something this machine chooses. Each subcommand below names the endpoint it calls and the server version that first answered it.

From kasl Subcommands Endpoint Needs kasl-server
v1.7.0 connect, status, disconnect GET /health, GET /api/v1/agent/whoami 0.14.1
v1.8.0 push POST /api/v1/days 0.14.1
v1.9.0 queue, flush, backfill POST /api/v1/days/batch 0.14.1
v1.13.0 manifest GET /api/v1/privacy/agent 0.14.1
v1.14.0 pulse POST /api/v1/agent/heartbeat 0.17.0

The floor is the same for all but the pulse, and whoami is what sets it. The server has accepted uploads since 0.3.0, backlogs since 0.4.0 and the agent privacy manifest since 0.10.0, but connect asks whose token it is holding before it stores anything, and 0.14.1 is where that question could first be answered. A server older than that cannot be connected to at all, which is the honest outcome: the alternative would be filing this machine’s days under a name nobody checked.

The pulse is the one exception. A server between 0.14.1 and 0.17.0 takes the connection and the days, and answers a pulse with 404; kasl server pulse then says the server does not take one and names 0.17.0, and the watcher asks again only every five minutes. Days are unaffected.

connect reports the refusal and stores nothing. The server answers an unknown /api path with {"error":"no such endpoint"} and 404, so the failure names the endpoint rather than arriving as a hang or as HTML read back as success.

A 404 counts as a rejection, not a temporary fault, so days are not queued against a server that will never take them - see when a push fails.

Always safe. /api/v1 keeps its meaning for as long as agents call it; a change that would alter it arrives as /api/v2 with a migration written for agents. Everything the server has grown since - departments, the audit log, signals, the month heatmap - is read by people in its web UI and changes nothing this agent sends.

Terminal window
kasl server disconnect

Removes the stored token, then the configured address - in that order, so a failure never leaves a working credential behind with nothing pointing at it. The pulse goes with the address: a later connect starts with it off. Disconnecting does not revoke the token on the server; ask an administrator to revoke it if the machine is being handed on.

Local data is untouched. Days already uploaded stay on the server, and everything in the local database stays where it is.

status, queue, pulse and manifest take --json and print one document each; the conventions every kasl document keeps are in kasl in scripts and CI.

kasl server status --json - each check, and the first that failed:

{
"connected": true,
"url": "https://kasl.example.com",
"token": true,
"reachable": true,
"server_version": "X.Y.Z",
"api_version": "v1",
"user": "Ada Lovelace",
"agent": "laptop",
"problem": null,
"pulse": {
"enabled": true,
"sent_at": "2026-09-03T14:05:12",
"state": "working",
"attempted_at": "2026-09-03T14:05:12",
"error": null,
"warnings": []
}
}

The checks run in order and stop at the first that fails, so a later field is null when an earlier one stopped the walk - not asked, rather than failed: no token means the server was never asked, reachable: false means nobody was identified. problem is the failure in the words status prints for it. Not connected at all, everything but connected: false is null.

kasl server pulse --json is the pulse object on its own. state is what the last pulse that arrived said - working, paused or idle; error is why the last attempt did not arrive, null when it did; warnings are the lines status would warn with (failing, silent, shown offline, clock skew). Not connected, it is the pulse switched off: enabled: false, the rest null or empty.

kasl server queue --json:

{
"days": [
{
"date": "2026-08-30",
"queued_at": "2026-08-30T18:02:44",
"attempts": 3,
"last_attempt_at": "2026-08-31T09:15:03",
"last_error": "cannot reach kasl-server at https://kasl.example.com"
}
]
}

kasl server manifest --json carries the manifest’s fields as the server sent them - level, summary, stored (a list of {what, detail}), never_collected, visible_to, retention, on_change - and updated_at in local time, null when the server does not record when the level was set.

Terminal window
# Connect, answering the prompts
kasl server connect
# Connect to a known address without the URL prompt
kasl server connect --url https://kasl.example.com
# Connect to a server behind a company CA
kasl server connect --url https://kasl.internal --ca-certificate /etc/ssl/company-ca.pem
# Check where the connection stands
kasl server status
# Send today's work
kasl server push
# Send yesterday, the morning after
kasl server push --last
# Send one particular day
kasl server push --date 2026-08-24
# See what is still owed, without touching the network
kasl server queue
# Send everything that is waiting
kasl server flush
# Read what this server stores about you
kasl server manifest
# Let the team dashboard show whether you are working right now
kasl server pulse enable
# See how the pulse is going, without touching the network
kasl server pulse
# Stop telling the server
kasl server pulse disable
# Upload history recorded before this machine was connected
kasl server backfill --from 2026-08-01
# Upload everything this machine has ever recorded
kasl server backfill
# Forget the connection on a machine being handed on
kasl server disconnect
  • watch - The watcher, which sends the pulse once it is on
  • report - The daily report, which the corporate si integration submits separately
  • setup - Configure the rest of kasl, including that separate reporting API