Skip to content
ChamsEddin98Public

About

Daymark — a local-first study planner and schedule generator. Markdown task files become a timed daily plan with automatic rest breaks, synced to Google Calendar, with a web UI, desktop notifications, and a Claude Code plugin.

Topics

Resources

Stars

1 star

Watchers

0 watching

Forks

Repository files navigation

Daymark — a local-first study planner that schedules your week and syncs it to Google Calendar

License: MIT TypeScript Node.js Claude Code plugin

Daymark is a self-hosted study planner and schedule generator. You write what you want to study as plain Markdown task files; Daymark turns them into a timed daily study schedule with automatic rest breaks, keeps it in sync with a dedicated Google Calendar, serves a local web UI for today, and fires desktop notifications at every task and rest boundary.

It runs as its own service, with no account and no cloud: your plan is Markdown on your disk and SQLite beside it. It also ships a Claude Code plugin, so you can run your week in plain language — but the planner works perfectly well without it.

What it does for you

  • 📅 Time-blocks your week automatically — tasks in, a timed plan out, with a 10-minute break between tasks and an hour off after every 4 hours of work
  • ⏰ Active hours — set when your day starts, when it must stop, and how many hours of work it holds; the plan obeys the fence
  • 🔁 Nothing is lost — unfinished work carries itself to tomorrow, and a day you ignore costs days, not work
  • 🗓️ Two-way Google Calendar sync — idempotent, with optional reminders that reach your phone
  • 🖥️ A daily view built for one question — what am I doing now, and what's next
  • 🔔 Native desktop notifications at every task and rest boundary
  • 🤖 Claude Code plugin — "what's next?", "mark A1 done", "shift today by an hour", "my day starts at 10"
  • 📝 Markdown stays the source of truth — edits through the API rewrite your files surgically, leaving your prose and formatting alone

A daymark is an unlit navigational beacon you fix your position by in daylight — which is what the daily view is for.

Status: every part is built: task files and parser, scheduler, SQLite store, HTTP API, Google Calendar sync, notification daemon, web UI and the Claude Code plugin. npm start runs all of it. Calendar sync is optional. There are two ways to connect it, and you need one: a service account writing into a calendar you own (nothing to expire, no npm run auth), or OAuth with a calendar the planner owns (one consent screen, then a re-auth every 7 days while the Cloud app is in Testing mode). See docs/GOOGLE_SETUP.md; the service account is the recommended one.

Docs: docs/GOOGLE_SETUP.md (connecting Google Calendar, about 10 min) · docs/API.md (HTTP API contract) · docs/PLAN.md (scheduling rules and acceptance criteria) · CLAUDE.md (architecture and conventions).

Quick start (cold clone)

Needs Node.js 22.13 or newer (the store uses node:sqlite) and npm 10+.

git clone <repo-url> planner
cd planner
npm install                      # every workspace; `npm ci` also works (the lockfile is committed)

# Optional, for Google Calendar sync (the planner works without it):
cp .env.example .env             # PowerShell: Copy-Item .env.example .env
#   then set CALENDAR_ID (service account) or GOOGLE_CLIENT_ID/SECRET (OAuth) - docs/GOOGLE_SETUP.md
npm run auth                     # OAuth only; opens the browser once, token to .data/google-token.json

npm start                        # API + daemon + web UI

Then open:

npm start first runs a preflight (scripts/preflight.mjs):

  • It checks the Node version and loads .env. It creates .data/, then creates and migrates the SQLite store once, so the API and the daemon never race to create it. It writes the ports to .data/runtime.json, which the Claude Code skill reads.
  • It says whether Google is authorised, and which credential it found. If neither is usable it prints the hint and carries on.
  • It runs next build for the web UI when the production build is missing or older than its sources. This takes about a minute, on the first run only.
  • It prints the URLs.

It then runs the three processes with concurrently, prefixed [api], [daemon] and [web]. If one of them exits with an error, the others are stopped (kill-others-on-fail). Ctrl+C stops all three.

The web UI runs as a production build (next start). It starts at once and uses much less memory than the dev server. It reads the API address per request, so one build works with any API port. npm run dev uses the Next.js dev server and an auto-restarting API instead.

Ports come from PLANNER_API_PORT (default 4317) and PLANNER_WEB_PORT (default 3417). The start script passes the API address to the web UI (PLANNER_API) and adds the web origin to the API's allowed origins (PLANNER_WEB_ORIGINS), so the two always agree:

PLANNER_API_PORT=4417 PLANNER_WEB_PORT=3517 npm start              # bash
$env:PLANNER_API_PORT=4417; $env:PLANNER_WEB_PORT=3517; npm start  # PowerShell

npm start also reads PLANNER_API_PORT and PLANNER_WEB_PORT from .env. Values already set in the environment win. It writes the ports it used to .data/runtime.json. The Claude Code skill reads that file (then PLANNER_API_PORT / .env, then the default 4317) to find the API, so a non-default port works without extra setup.

Using it day to day

Once npm start is running, the loop is: open http://127.0.0.1:3417, work down the day, tick things off. Everything else happens on its own.

The daily view shows one day — what is running now, what is next, and the whole timeline with rests in place. Each task carries its platform link as a one-click chip. The page needs no reloading; it follows the API over SSE.

Tick a task off with its circle, or from the keyboard — the page works entirely without a mouse (press ? for the list):

Key
j / k next / previous task
x or Space complete / reopen
d skip / unskip
o or Enter open the task's link
s shift the rest of today
p pause / resume
u undo the last change (5 s)
n jump to now

Notifications fire on the desktop at every boundary — task start, task end, rest start, rest end — from the daemon, so they arrive whether or not the browser is open. To get them on your phone instead, see Calendar reminders.

If you don't tick something off

This is the case worth understanding, because the planner acts on its own here.

A task whose slot goes by while it is still pending is missed. What happens next is the onMissed active hour, and neither value loses the work:

onMissed What happens
reflow (default) The work leaves the past, the rest of the day is laid out again from now, and a notification says where it went. A late start slides the day instead of stranding the task.
notify Nothing is touched. The notification says the task is still pending and names your choices: do it, skip it, or let it roll over tonight.

Change it from the gear in the daily view, or:

curl -s -X PATCH http://127.0.0.1:4317/settings \
  -H "content-type: application/json" -d '{"onMissed":"notify"}'

After a reflow the morning ends at the last thing that actually ran, and one "Rest (extended)" covers the gap from there to now. Time that has already gone by cannot be filled, so it reads as time off rather than as an unlabelled hole — and the day's budget is not charged for work nobody did.

Two things to know: a reflow is the daemon's job, so it needs npm start (or the daemon) running — without it you still lose nothing, the midnight rollover just does the carrying instead. And nothing is missed while the plan is paused, because a slot going by is exactly what a pause means.

What happens at midnight

Whatever is still pending carries to the next day, at the front of the queue. Nothing is dropped and nothing is reordered:

  • Neglect costs days, not work. Four consecutive days ignored cost 0 minutes of work owed — verified in the tests. The plan simply reaches further out; the horizon is a rolling window, so there is no programme end to run past.
  • A capped series slides. A daily lesson with occurrences: 28 spends a session only when you act on it. A day that merely went by consumes nothing, so the series finishes later rather than losing sessions.
  • The order holds. The task that led the day you abandoned leads the next one.

Skip is the deliberate exception

Skipping is the one way to spend a task without doing it, and it is permanent: a skipped task does not come back tomorrow, and it still counts as a spent session for a capped series. That is the point — it is how you say "not important", as opposed to simply not getting to it.

Everything else is reversible. u undoes the last change for five seconds, and reopening a task you had ticked off puts its minutes back and re-plans from today — because the work it frees may belong to a higher-priority track than whatever is currently scheduled for the rest of the day.

Full rules and the tests that pin them: docs/PLAN.md, "Carry-forward" and "Missed work".

Pause

Stepping away? Pause in the web UI (or POST /plan/pause) freezes the plan and records the instant. Nothing moves while it is paused, and no notifications fire. Resume shifts everything still to come forward by exactly how long you were gone — to the millisecond, so a 45-second pause moves the day 45 seconds, not a rounded minute — and rewrites the Google Calendar events to match.

Durations, gaps and the order never change. The task you were in the middle of keeps its times, so that one task loses the paused time; everything that had not started yet moves by the full pause. A pause survives a restart, and one left running over 24 hours stops freezing the plan so you are never locked out.

Commands

Run these at the repo root.

Command What it does
npm install Installs every workspace (packages/*, apps/*).
npm start Preflight, then the API (:4317), the daemon and the web UI (:3417, production build).
npm run dev The same with next dev and tsx watch for the API. No web build needed.
npm run auth One-time Google OAuth for calendar sync, for the OAuth route only. Needs .env (see docs/GOOGLE_SETUP.md). npm run auth -- --no-browser prints the URL without opening a browser. A service account needs no such step.
npm test Every workspace's tests. The API, daemon and packages use vitest. The web UI's Playwright suite builds the app and needs npx playwright install chromium once.
npm run tasks:check [-- <path>] Validates task files (default resources/).
npm run preflight Only the preflight checks, including the web build.
npm run build:web next build for the web UI.
npm run api / npm run daemon / npm run web One process on its own. web needs a build first and always uses port 3417 with the API at 4317 (set PLANNER_API to point it elsewhere). api honours PLANNER_API_PORT but only allows the web origin on 3417 unless you set PLANNER_WEB_ORIGINS. For other ports use npm start, which wires all of this up.
npm run dev:mock -w @planner/web The web UI against the fixture API, with no backend.

Useful env vars:

  • PLANNER_DATA_DIR: state dir, default .data/.
  • PLANNER_TZ: time zone, default the system's.
  • PLANNER_NOW / PLANNER_CLOCK: a fixed or compressed clock, for testing.
  • PLANNER_NOTIFY: the daemon's notification sinks.

Active hours

Your working window — when a day may begin, when it must stop, and how much work it holds. One setting for every day, changed from the gear in the daily view or over HTTP:

curl -s http://127.0.0.1:4317/settings
curl -s -X PATCH http://127.0.0.1:4317/settings   -H "content-type: application/json" -d '{"dayStart":"09:00","dayEnd":"20:00","dailyTaskMin":540}'
Setting Default Meaning
dayStart 08:00 the earliest a day may begin
dayEnd 24:00 hard stop; nothing is placed past it (24:00 = no fence)
dailyTaskMin 480 minutes of task time a day holds, rests excluded
onMissed reflow what happens when a task's slot goes by untouched — see Using it day to day

The defaults are the original rules, so an unconfigured planner behaves exactly as before. The rest rules never change: a 10-minute rest between tasks and a 1-hour rest after every 4 hours of work, so a bigger budget makes more blocks rather than longer ones. Work that does not fit before dayEnd moves to the next day.

One thing to know: the clock cost of work is stepped, because every 4 hours of task time buys an hour of long rest. Going from 8 h to 8.5 h of work costs 1 h 40 of clock, so asking for 10 h inside an 08:00–20:00 window grants 8. GET /settings reports what your window actually gives you (effective.dailyTaskMin) and which setting is the limit (effective.boundBy), and the UI shows the same thing rather than echoing the request. Details in docs/PLAN.md, "Active hours".

Calendar reminders

Daymark notifies you twice over, and the two reach different places:

  • The daemon fires native desktop notifications at every task and rest boundary. They are immediate and detailed, but they only reach the machine running the planner.
  • Google Calendar can notify on its own, which is what reaches your phone. This is off by default, so a fresh install is silent on the phone by design — the daemon is already covering the desk, and a Google reminder on top would double every toast.

calendarReminders is what turns the second one on:

curl -s -X PATCH http://127.0.0.1:4317/settings \
  -H "content-type: application/json" -d '{"calendarReminders":10}'
Value What the events carry Use it when
"off" (default) useDefault: false, no overrides — Google never notifies you only work at the machine running the planner
a number a popup that many minutes before the start (0–40320) you want the plan on your phone. 10 is a good start; 0 means "at the start"
"inherit" useDefault: true — the event obeys that calendar's own Event notifications you would rather manage it in Google Calendar

A few things that are easy to get wrong:

  • "inherit" is usually silent. A secondary calendar — which is what the planner syncs into — has no default event notifications until you add one in Google Calendar's settings for that calendar. Pick a number instead unless you have set those up.
  • Don't set reminders by hand in the Google UI. The sync owns the event body, so a manual per-event reminder is overwritten the next time that event is patched. This setting is the only durable way.
  • Changing it rewrites every event, once. The policy is part of the event body, so the stored hash moves and the next sync patches the whole window; the sync after that has nothing to do. The plan itself does not move — unlike the active hours, this changes only how the day is announced, so nothing is re-timed and no work you are in the middle of is disturbed.
  • For the phone you still need the Google Calendar app signed in to the account the calendar belongs to, with notifications allowed by the OS.

Naming the calendar

What the planner's calendar is called:

curl -s -X PATCH http://127.0.0.1:4317/settings   -H "content-type: application/json" -d '{"calendarName":"My Study Plan"}'

It defaults to Daymark, and null puts it back. Changing it renames the same calendar in place on the next sync, keeping every event - it never makes a second one.

It only applies to a calendar the planner owns, which means the OAuth path, where the planner created the calendar itself. If you supplied the calendar through CALENDAR_ID it is yours, you name it in Google Calendar, and the planner cannot rename it - the calendar.events scope cannot read or write a Calendar resource at all. The setting is still stored in that case, because your credential can change later, but GET /settings reports calendarNameApplies: false so nothing pretends it worked.

A name is not a schedule, so this regenerates nothing; it only queues the sync.

Use from Claude Code

The repo ships a Claude Code plugin in plugin/. Claude Code is only a client, so the service must be running (npm start).

The plugin is a parent skill, planner, that routes to eight focused children. The parent identifies which operation is wanted and sends Claude Code to the one child that owns the endpoints for it:

Skill Owns
planner The routing logic and the overall workflow. It carries no endpoint detail but GET /health.
planner-read /today, /plan, /tasks, /tracks, /plans, /notifications, /events
planner-schedule Status, undo, shift, regenerate, reload
planner-create POST /plans, POST /tasks
planner-update PATCH /plans/:track, PATCH /tasks/:uid
planner-delete DELETE /plans/:track, DELETE /tasks/:uid, and the backups that undo them
planner-pause-resume POST /plan/pause, POST /plan/resume
planner-settings GET/PATCH /settings — the active hours and the calendar reminders
planner-calendar-sync POST /sync, /sync/status, /calendar/events, and the Google setup
planner-markdown-sync How the .md write-back works: diffs, dryRun, snapshots, restores

The create, update and delete endpoints rewrite the resources/*.md file themselves, in the same call that re-parses it, regenerates the affected days and queues the calendar sync. That is the rule the skills state and Claude Code follows: never hand-edit a task file to change a task. The dividing line is that anything surviving a POST /reload lives in the Markdown and anything a reload would wipe (done, skipped, progress, days off, the pause) lives in SQLite. See docs/API.md, "Write-back".

The repo is its own plugin marketplace (.claude-plugin/marketplace.json), so installing is two commands from anywhere:

claude plugin marketplace add ChamsEddin98/Daymark
claude plugin install daymark@daymark-local

From a checkout you already have, claude plugin marketplace add ./ at the repo root does the same.

Or load it for a single session without installing it:

claude --plugin-dir ./plugin

Then ask things like "what's next?", "mark bcg A1 done", "shift today by 1 hour", "make A1 two hours", "add a task after A6", "delete that task" or "my day starts at 10". After pulling plugin changes, run claude plugin marketplace update daymark-local. To check the manifests, run claude plugin validate plugin and claude plugin validate ..

Task files

Every task the planner can schedule comes from a Markdown file under resources/. The Markdown is the source of truth. To add work, edit a file or drop in a new one. Nothing else needs to change.

resources/ is gitignored, and deliberately so. A plan file says what someone is studying, which companies they are preparing for and when - personal detail that has no business in a repo, even a private one. So the directory ships empty apart from a .gitkeep, and your plans stay on the machine that runs the planner. Back them up like any other personal document; Git is not doing it for you.

docs/example-task-file.md is the structure, as a complete file you can validate and copy. It lives in docs/ rather than resources/ on purpose: anything under resources/ carrying a schema: line is loaded as a real plan and gets scheduled.

A plan typically looks like this - one file per track, the prep tracks ordered by priority, and a few daily blocks:

File Track Kind Shape
resources/<first>.md e.g. bcg prep, priority 1 a long list of one-off tasks
resources/<second>.md e.g. salesforce prep, priority 2 takes the prep slot when the first finishes
resources/lessons.md lessons lessons 1 daily task, repeat: daily + occurrences
resources/portfolio.md portfolio portfolio 1 daily task, no cap
resources/apply.md apply recurring, starts_after: <track> held back until that track is done

If you converted a plan from HTML, tools/convert_html.py is the one-shot converter, kept so the conversion can be audited. Do not re-run it: the Markdown is the source of truth now and re-running it overwrites hand edits.

Validate every task file:

npm run tasks:check                              # all of resources/
npm run tasks:check -- path/                     # another folder
npm run tasks:check -- docs/example-task-file.md # the worked example

Task-file schema (planner/task-file@1)

A task file is a Markdown file with YAML front matter. Inside the file, a task is any heading that is directly followed by a fenced ```task block. Everything else in the file is ordinary prose. The planner keeps it, but it never becomes a task.

---
schema: planner/task-file@1
track: bcg
title: BCG X AI Engineer — Assessment Prep Plan
kind: prep
priority: 1
---

# BCG X AI Engineer — Assessment Prep Plan

Any prose, tables or notes. Links here are kept as file references.

## Block A — Pandas

### A1 · Boolean filtering

```task
id: A1
duration: 40m
type: coding
link: "[595. Big Countries](https://leetcode.com/problems/big-countries/)"
```

Task body: snippets, notes and extra links. The body runs until the next heading of the
same or a higher level, so `####` sub-headings stay inside it.

Front matter

Key Required Meaning
schema yes Always planner/task-file@1. Markdown files without it are skipped, so a README in the folder is fine.
track yes Slug for the source this file feeds (bcg, lessons, portfolio). Several files can share a track.
title yes Human name of the source.
kind yes prep, lessons, portfolio, or recurring. Decides which slot of the day the tasks go into (see below).
priority prep only Integer, lower runs first. When one prep track is done, the next one takes its slot.
default_duration no Duration used when a task block leaves out duration.
source_html no Where the file was converted from, if it was.
starts_after no A track slug. The file's tasks are only scheduled from the day that track is finished (every task done or skipped).

Unknown keys are an error. That way a typo like priorty gets reported instead of silently ignored.

Task block

Key Required Meaning
id yes Unique within the track. Letters, digits, ., _, -. The global id is track/id (for example bcg/A1). Task status, calendar events and the API all key on it, so do not rename an id once it has been scheduled.
duration yes* Estimated time: 40m, 2h, 2h30m, or a bare number of minutes. *Can be left out if the file sets default_duration.
type yes coding (a problem on a platform), concept (a card to understand), build (make something), mock (a timed rehearsal), drill (recall or repair), reading, or admin.
link no The platform link, as "[label](https://…)" or a bare https://… URL. It can also be a YAML list, in which case the first entry is the primary link. The primary link goes into the calendar event. Quote it, because YAML treats [ specially.
repeat no daily: book this task on every plan day instead of once. Status is then tracked per day.
occurrences no With repeat: stop after this many days. Omit for no end.

The heading text is the task title, and it is used word for word as the calendar event title. So it should name the actual work, like A13 · shift, lag and diff or Read: Chapter 3 — Attention or Build: login form validation. A generic label like Study does not work as a title.

Task order is the order in the file. Task status (done, skipped) is not stored in Markdown. It lives in the service's state, keyed by track/id, so editing a file never resets progress.

Kinds and how the day uses them

Kind Slot in the day
prep The first slot. It takes the lowest-priority prep track that still has tasks left.
lessons After prep.
portfolio A smaller block at the end of the day.
recurring Every task in the file is added to every day, once it is active. For example, apply for positions starts once BCG prep is finished.

Errors

The parser never crashes on a bad file. It reports file:line: message for each problem: a missing or invalid duration, an unknown key or type, a duplicate id, a task block that does not directly follow a heading, a malformed link, or an unclosed fence. The other files still load.

Parsed shape

parseTaskFile(text, path) in packages/core returns { file: { meta, tasks, references }, errors }, where each task is:

{ uid: "bcg/A1", track: "bcg", id: "A1", title: "A1 · Boolean filtering",
  durationMin: 40, type: "coding",
  links: [{ label: "595. Big Countries", url: "https://leetcode.com/problems/big-countries/" }],
  body: "…markdown…", bodyLinks: [...], section: "4. Block A — Pandas, 20 techniques",
  order: 1, file: "resources/bcg.md", line: 97 }

references holds links from prose that belongs to no task, such as sources and tables. Together, links, bodyLinks and references account for every link in the file.

How the HTML plans were mapped

  • Technique cards became tasks. The card number is the id, the problem link is the link, and the snippet, transfer notes and traps are the body.
  • Work rows from each plan's own day-by-day schedule became tasks too, under Practice sessions. These are mocks, level-chaining builds, recall drills, weak-spot repair and next-round prep, and their hours come from the plan. Rest, buffer and technique-day rows did not become tasks, because the technique days already exist as cards. The original schedule tables are still kept as prose.
  • The skip tests became one task per plan, with the checklist in the body. Ticking a check does not delete anything. Mark the retired techniques as skipped through the API instead.
  • Durations come from each plan's own arithmetic. Anthropic techniques are 2h. BCG durations are set per block from its schedule rows:
    • pandas (A): 40m, which is the plan's hard cap
    • NumPy (B): 25m
    • the C1 template: 1h30m
    • concept cards (D): 20m
    • DSA (E): 40m
    • the Titanic run: 40m, so days 12–13 add up to their 4h Salesforce techniques are 2h30m, except H1 and H2, which are 1h each so its day 15 fits in 4h. Salesforce mock-only days are 4h, and days 7 and 14 are a 3h mock plus a 1h recall. Change any of these in the Markdown whenever you like.
  • Where a plan's per-technique budget and its per-day rows disagree, the per-technique budget wins. For example, Salesforce budgets 2.5h per technique, but day 1 lists the skip test plus two techniques in a 4h row. Practice tasks, which have no technique budget, match their day's Hours cell exactly, and a test checks that.
  • A card paragraph that holds nothing but its problem link(s) becomes the task's link field, and the link labels are kept. If the paragraph says more than the links (a scope such as "exercises 21–30", or a variant problem), the whole paragraph is also kept in the body.
  • Anthropic builds #1–#3 and the mocks link to the LibreSignal or PaulLockett repos. This comes from the plan's "Mock material" paragraph, which names those repos as the source for those sessions. The schedule rows themselves have no links.
  • packages/core/test/html-preservation.test.ts checks the conversion. It asserts that every external link in each HTML file is present in the .md and in the parsed objects. It also checks that each card's problem link sits on that card's task, and that every checkbox item and table cell survived.

About

Daymark — a local-first study planner and schedule generator. Markdown task files become a timed daily plan with automatic rest breaks, synced to Google Calendar, with a web UI, desktop notifications, and a Claude Code plugin.

Topics

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages