dsh-time-tracker
September 12, 2026 · View on GitHub
Time tracking for DeepSeek Harness: a running timer, manual work-log backfill,
and range reports, exposed to the model as five tools. State lives in memory and
is only written to disk when a deployment configures storagePath; both the
clock and the filesystem are injectable seams, so behaviour is deterministic in
tests and no tool ever shells out or reaches the network.
- Package:
@qingshanjiluo/dsh-time-tracker - Kind: host-only Cordis tool plugin (
inject: ['tools']), no browser bundle - Contract: named exports
name,inject,Config(schemastery),apply
Install
npx -y @deepseek-ai/dsh plugin --profile web add @qingshanjiluo/dsh-time-tracker
The bundled cordis.patch.yml inserts the plugin with memory-only tracking; set
storagePath to a JSON path to keep the ledger across restarts.
Tools
| Tool | Arguments | What it does |
|---|---|---|
timer_start | task, project?, tags?, autoStop? | Opens a running entry stamped with the current time. Fails with timer_running while a timer is open, unless autoStop closes it first. |
timer_stop | id?, note? | Closes the running entry (or the named one), appends the finished interval, and returns its exact duration. |
timer_status | — | Read-only: which timer is running, elapsed, entry counts, all-time total, and the current UTC-day total. |
timer_log | task, start, end?, durationMin?, project?, tags?, note? | Backfills a finished interval. start/end are ISO-8601 or epoch milliseconds; an empty end runs to now, durationMin overrides it. |
timer_report | from?, to?, groupBy? | Aggregates a range into per-group totals, clipped to the window. groupBy is project, task, tag, or day (UTC calendar day, intervals split at midnight). |
Conventions worth knowing:
- Ids are short and stable (
t1,t2, …) and continue across a reload. - Only one entry may run at a time;
timer_lognever disturbs it. - Empty string means "unset" for optional text parameters,
[]for tag lists. - Every tool returns the same storage triple —
saved,storagePath,loadError— plus a stableerrorcode (empty_task,timer_running,no_running_timer,unknown_entry,invalid_start,invalid_end,end_not_after_start,bad_range,unknown_group,persist_failed). - A snapshot write failure never loses the in-memory change: the call still
succeeds and reports
saved: falsewitherror: "persist_failed".
Configuration
| Field | Type | Default | Meaning |
|---|---|---|---|
storagePath | string | '' | JSON ledger path. Empty keeps tracking in memory and never calls the storage seam. |
defaultProject | string | 'general' | Project used when a call leaves project empty. |
now | () => number | Date.now | Injectable clock (epoch milliseconds) for deterministic timing. |
storeIo | (request) => result | node:fs | Injectable persistence seam for reads and writes of storagePath. |
The two seam fields are functions, so a deployment configures them in code (or a
test) rather than in YAML; leaving them out selects the real clock and node:fs.
Snapshot format
{
"version": 1,
"seq": 2,
"entries": [
{ "id": "t1", "task": "design review", "project": "dsh", "tags": ["api"],
"note": "signed off", "startMs": 1714640400000, "endMs": 1714641300000 }
]
}
A reload restores the ledger, including one still-open entry, so a running timer
survives a restart. Unparsable files start an empty ledger and surface the
problem through loadError instead of failing the plugin.
Development
npm install --no-audit --no-fund
npx tsc --noEmit
npm run build
npx vitest run
node scripts/load-smoke.mjs
tests/plugin.spec.ts mounts the plugin against a frozen clock and a Map-backed
storage seam, asserts the export face and the registered tool set, covers every
tool with happy and edge cases, and validates each returned value against the
tool's own output schema. scripts/load-smoke.mjs loads lib/index.js the way
the harness does and proves the built artifact registers and runs all five tools.
License
MIT