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

ToolArgumentsWhat it does
timer_starttask, 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_stopid?, note?Closes the running entry (or the named one), appends the finished interval, and returns its exact duration.
timer_statusRead-only: which timer is running, elapsed, entry counts, all-time total, and the current UTC-day total.
timer_logtask, 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_reportfrom?, 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_log never 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 stable error code (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: false with error: "persist_failed".

Configuration

FieldTypeDefaultMeaning
storagePathstring''JSON ledger path. Empty keeps tracking in memory and never calls the storage seam.
defaultProjectstring'general'Project used when a call leaves project empty.
now() => numberDate.nowInjectable clock (epoch milliseconds) for deterministic timing.
storeIo(request) => resultnode:fsInjectable 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