@leftoff/cli 0.0.0-stage → 0.6.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +14 -0
- package/CHANGELOG.md +564 -0
- package/LICENSE.md +9 -0
- package/README.md +28 -2
- package/assets/sample-transcript.jsonl +12 -0
- package/bin/checkpoints.js +7 -0
- package/bin/leftoff.js +585 -0
- package/hooks/README.md +22 -0
- package/hooks/settings-hooks.example.json +82 -0
- package/mod/.claude-plugin/plugin.json +15 -0
- package/mod/hooks/core.js +1397 -0
- package/mod/hooks/hooks.json +4 -0
- package/mod/hooks/register.js +1006 -0
- package/package.json +45 -4
- package/templates/agent-feed-worker/README.md +45 -0
- package/templates/agent-feed-worker/src/index.js +69 -0
- package/templates/agent-feed-worker/wrangler.toml +20 -0
- package/ui/app.css +1282 -0
- package/ui/app.js +2979 -0
- package/ui/apple-touch-icon.png +0 -0
- package/ui/changelog-md.js +64 -0
- package/ui/favicon-32.png +0 -0
- package/ui/favicon-badge-32.png +0 -0
- package/ui/favicon-badge.svg +1 -0
- package/ui/favicon.ico +0 -0
- package/ui/favicon.svg +1 -0
- package/ui/go.js +68 -0
- package/ui/icon-192.png +0 -0
- package/ui/icon-512.png +0 -0
- package/ui/icon-maskable-512.png +0 -0
- package/ui/icons.js +41 -0
- package/ui/index.html +55 -0
- package/ui/manifest.webmanifest +37 -0
- package/ui/needs.js +197 -0
- package/ui/offline.css +11 -0
- package/ui/offline.html +24 -0
- package/ui/pair.html +31 -0
- package/ui/picker.js +294 -0
- package/ui/relay-client.js +187 -0
- package/ui/relay-crypto.js +155 -0
- package/ui/relay-pair.js +48 -0
- package/ui/repos.js +167 -0
- package/ui/settings-nav.js +23 -0
- package/ui/summary-examples.js +66 -0
- package/ui/sw.js +34 -0
- package/ui/theme.js +41 -0
- package/ui/vendor/LUCIDE-LICENSE.txt +45 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,564 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
What changed in Leftoff (called Checkpoints until 0.4.5), newest first. The board's **What's new** page and `leftoff changelog` both read this file.
|
|
4
|
+
|
|
5
|
+
## [0.6.3] - 2026-10-11
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
- **No more zooming in on iPhone** when you tap a text box, like "Add an item" on a list, search, an answer box or a Settings field. Text boxes are a touch bigger on phones instead, and pinch-to-zoom still works.
|
|
9
|
+
|
|
10
|
+
### Changed
|
|
11
|
+
- **`leftoff relay signup` checks the name first**, before it asks for your email:
|
|
12
|
+
- Already yours on this Mac? It says `✓ This Mac already has yourname.leftoff.dev.` and how to pair your phone.
|
|
13
|
+
- Someone else has it? It says `yourname.leftoff.dev is taken. Try another name.` right away. Setup asks for another name.
|
|
14
|
+
- Your name on another Mac, like `studio` or `laptop`? Join it with `leftoff relay enable yourname`.
|
|
15
|
+
|
|
16
|
+
## [0.6.2] - 2026-10-11
|
|
17
|
+
|
|
18
|
+
### Changed
|
|
19
|
+
- **Install with `npm i -g @leftoff/cli`** (or `pnpm add -g @leftoff/cli`), then run `leftoff setup`. The command you type is still `leftoff`.
|
|
20
|
+
- **What's new reads better:** links work, nested notes show as nested, and examples use names like `yourname` instead of real ones.
|
|
21
|
+
|
|
22
|
+
### Fixed
|
|
23
|
+
- `leftoff upgrade` on an npm install no longer prints an "unknown → unknown" line.
|
|
24
|
+
|
|
25
|
+
### Under the hood
|
|
26
|
+
- npm turned down the name `leftoff` as too close to `liftoff`, so it was never published under that name. Upgrades now check the `@leftoff/cli` package and run `npm i -g @leftoff/cli@latest` (or pnpm or bun, whichever installed it).
|
|
27
|
+
|
|
28
|
+
## [0.6.1] - 2026-10-11
|
|
29
|
+
|
|
30
|
+
### Added
|
|
31
|
+
- **Sign up yourself.** Run `leftoff relay signup yourname`, press Confirm in the email we send, and your board is live at `yourname.leftoff.dev`. No invite or admin token needed.
|
|
32
|
+
- Also offered as an optional last step of `leftoff setup`, and in Settings → Phone & remote.
|
|
33
|
+
- Claimed your name on leftoff.dev already? Sign up with the same email and it carries over. Nobody else can take it.
|
|
34
|
+
- Your other Macs join with `leftoff relay enable yourname`, while a Mac that's already on is awake.
|
|
35
|
+
- **Install from npm.** Leftoff installs with one command, with no GitHub access needed, and `leftoff upgrade` keeps it current. New here? See [how Leftoff works](https://leftoff.dev/how-it-works).
|
|
36
|
+
|
|
37
|
+
### Changed
|
|
38
|
+
- `leftoff relay status` points you to `leftoff relay signup` when you don't have a name yet.
|
|
39
|
+
|
|
40
|
+
### Under the hood
|
|
41
|
+
- The email link confirms only when you press the button, so mail scanners can't confirm for you. Links expire after 30 minutes. One name per email for now, and signups are rate-limited.
|
|
42
|
+
- Your name's record on the relay now also keeps your email and plan.
|
|
43
|
+
- Setup installs the Claude Code plugin from the package itself, and falls back to settings hooks if that fails.
|
|
44
|
+
|
|
45
|
+
## [0.6.0] - 2026-10-11
|
|
46
|
+
|
|
47
|
+
### Added
|
|
48
|
+
- **Use your board from anywhere.** Open it on your iPhone or any browser at `yourname.leftoff.dev`, with no Tailscale or home network. It's off by default, and local Leftoff is unchanged.
|
|
49
|
+
- **Private:** the board is end-to-end encrypted between your Macs and your phone. The relay passes it along but can't read it.
|
|
50
|
+
- **Pair your iPhone with a QR code** in Settings → Phone & remote, and revoke a phone there any time.
|
|
51
|
+
- **Answer and Go across Macs** from anywhere. Pick a Mac in the status popover; if it sleeps, an awake one takes over, and if all are asleep you see when each was last seen.
|
|
52
|
+
- A phone can read the board. It can answer prompts only if Settings → Privacy & security allows it.
|
|
53
|
+
|
|
54
|
+
### Changed
|
|
55
|
+
- **`leftoff upgrade` is quiet.** A few ✓ lines, then a short summary: what's new and anything you need to do, like "Upgrade your other Mac too: laptop (0.5.9)". The full output goes to a log, and `--verbose` shows everything.
|
|
56
|
+
|
|
57
|
+
### Under the hood
|
|
58
|
+
- Each Mac keeps one outbound connection to the relay, so no Mac opens a port. Board data is sealed (AES-256-GCM) with a key made at pairing; the relay sees names, timing and sizes, never content.
|
|
59
|
+
- Relay commands: `leftoff relay status`, `pair`, `devices`, `revoke`, `enable` and `disable`.
|
|
60
|
+
- Planned: passkeys as a second factor, and signed app files.
|
|
61
|
+
|
|
62
|
+
## [0.5.9] - 2026-10-11
|
|
63
|
+
|
|
64
|
+
### Changed
|
|
65
|
+
- **Settings has a sidebar.** It had grown into one long page, so now it's split into sections: General, Digest, Spaces, Sync, Agents, Models & summaries, Phone & remote, Privacy & security, and Updates & about.
|
|
66
|
+
- On a desktop (900px and wider), the sections are a sidebar with icons, and the one you pick shows on the right, highlighted in the list.
|
|
67
|
+
- On a phone, Settings is a list like iOS Settings. Tap a section to open it, and use the back link to return.
|
|
68
|
+
- Each section has its own link, e.g. `#/settings/spaces`, and a reload stays on it. The old `#/settings?spaces`, `?sync` and `?remote` links land on the right section, so the session page's and the digest's "Settings → Spaces" links still work, and so does the Syncthing link. The "If it's stuck" box now links its "Settings → Sync".
|
|
69
|
+
- A search box at the top of the sidebar matches section names and every setting's label. Enter opens the first match.
|
|
70
|
+
- A section that needs a look gets a small dot. Sync's dot shows when Syncthing isn't running, shares a different folder or reports an error, a Mac hasn't connected for a day, or a Mac asked to join without the code. Privacy & security's shows when a machine's key changed, Agents' when a feed can't be pulled, and Digest's when the last digest failed. Updates' (a new version is out) and Spaces' (a Mac has no space yet) are quieter blue dots.
|
|
71
|
+
- Between 900 and 1180px wide, where the sidebar takes room, a section's labels sit above their fields so nothing gets squeezed.
|
|
72
|
+
- No setting changed. Moving between sections doesn't redraw the form, so anything you've typed stays, and **Save** still saves every section at once.
|
|
73
|
+
- Answering from other devices moved from Remote access to **Privacy & security**, next to Other machines. Messages that pointed to "Settings → Remote access" or "Settings → Other machines" now name the new section.
|
|
74
|
+
- **A calmer top bar.** The bar is now one row: the logo, then the five main tabs (Board, Needs you, Lists, Timeline, Digest), then on the right a search button, this Mac's status, and What's new and Settings as icons.
|
|
75
|
+
- Nothing wraps or gets cut off from 900px up. Between 900 and 1100px, the search button shrinks to its ⌘K key and the status shrinks to its dot.
|
|
76
|
+
- Search is a **⌘K** panel (Ctrl-K on other systems; **/** opens it too). Results show as you type, grouped into sessions, checkpoints, Settings sections and pages, using the same search as the results page. ↑↓ and Enter open a result, and Enter on the first row opens the full results page.
|
|
77
|
+
- Click this Mac's name to see open sessions, the summarizer, sync health and the version, plus the theme switch.
|
|
78
|
+
- The version chip left the bar. The version is now in the logo's tooltip, the status popover and Settings → Updates & about. The theme switch moved to Settings → General and the status popover.
|
|
79
|
+
- The gear gets a dot when a Settings section has one.
|
|
80
|
+
- On a phone, search, status, What's new and Settings sit as icons on the logo row, with the five tabs below. Search opens full screen.
|
|
81
|
+
- **Leftoff's own dropdowns.** Every `<select>` and the repo field's `<datalist>` used the browser's menu: a plain grey popup on a Mac, and on an iPhone a list that offered "/" as a repo. There were 7 selects and 1 datalist: the board's machine filter, the session page's Space, the timeline's session picker, Settings → Digest's time zone, Models' provider, and Spaces' Mac, repo and agent pickers. All of them are now one component (`ui/picker.js`).
|
|
82
|
+
- It's a rounded menu with a check mark on the chosen item, hover and keyboard highlights, and light and dark themes.
|
|
83
|
+
- It works with the keyboard: arrows, Home/End, Enter, Escape, and type to jump. Long lists like time zones get a filter box.
|
|
84
|
+
- A click outside closes it, and it never leaves the window.
|
|
85
|
+
- On a phone (under 600px), a menu opens as a bottom sheet. The repo field opens a full-width list under itself instead, so the keyboard stays useful.
|
|
86
|
+
- It uses the proper ARIA roles: combobox, listbox and option.
|
|
87
|
+
- Time inputs stay native, since the iPhone's wheel is better there.
|
|
88
|
+
- **Repo suggestions in Settings → Spaces** no longer include "/", home folders, empty names or duplicates. Each shows the repo's name in bold with its parent folder muted (`~/src/work`), the most recently used first, and you can still type a new name. Enter in that field adds the override.
|
|
89
|
+
|
|
90
|
+
### Tests
|
|
91
|
+
- New `scripts/check-settings.py` (`npm run test:settings-ui`) runs on desktop Chromium and iPhone WebKit at 375 and 430px, light and dark.
|
|
92
|
+
- Settings: it visits every section through the sidebar and through its link, with a reload on each. It also covers the old links, the session page's link into Spaces, a typed value surviving a section change and still being saved, the search box, and the Agents dot. On the phone it checks the list, each section and the back link.
|
|
93
|
+
- Dropdowns: it opens every one, including the session page's, the timeline's and the board's. Each must stay inside the window, with the check mark showing, and the keyboard, Escape and outside clicks must work. On the iPhone, each must open as a bottom sheet. It also checks the repo suggestions are clean.
|
|
94
|
+
- It fails on any console error.
|
|
95
|
+
- New `scripts/check-header.py` (`npm run test:header-ui`) checks the top bar.
|
|
96
|
+
- At 900, 1280 and 1600px, light and dark: one row with nothing cut off.
|
|
97
|
+
- Search: ⌘K, Ctrl-K and / open it, results are grouped, arrows and Enter work, and the results page opens.
|
|
98
|
+
- The status popover shows its details and its theme switch works.
|
|
99
|
+
- On the iPhone: the logo row, the tabs below, and full-screen search.
|
|
100
|
+
- New guard `test/unit/ui-no-native-select.test.mjs` fails if anything in `ui/` except the component writes a native `<select>`, `<datalist>` or `list=`. The rule is in the new `docs/UI.md`.
|
|
101
|
+
- New `test/unit/settings-nav.test.mjs` checks the sections and their icons, the deep and old links, that every in-app link names a real section, and the repo suggestions.
|
|
102
|
+
- `scripts/check-mobile-overflow.py` now also opens the Settings list, each Settings section, and the ⌘K search and status popover.
|
|
103
|
+
- Browser checks that went through Settings now use the sidebar or the section links: check-agents, check-agents-reply, check-phone, check-sync, check-spaces, check-pairing, check-syncthing, screenshot-repos, screenshot-needs and screenshot-themes.
|
|
104
|
+
- `screenshot-themes`, `screenshot-repos`, `screenshot-needs` and `check-phone` keep their keys and identity in a throwaway config folder, so running them no longer saves a demo key into this machine's `~/.config`.
|
|
105
|
+
- `check-agents-reply` waits for Codex's session page to draw before checking it has no reply button (it was reading Tab's page, still on screen after the hash change).
|
|
106
|
+
- `test/sync-e2e.mjs` waits for the move to report done instead of reading its status the moment the new daemon answers, which failed on a fast machine.
|
|
107
|
+
|
|
108
|
+
## [0.5.8] - 2026-10-10
|
|
109
|
+
|
|
110
|
+
### Added
|
|
111
|
+
- **Spaces: Work, Personal, or your own names.** Every session belongs to a space. Its own pick comes first, then its repo's override (so a work repo you open on your personal Mac still counts as Work), then its Mac's default. A Mac with no default yet is in a space named after itself until you pick one. Set them in **Settings → Spaces**: one tap on **Work** or **Personal** per Mac, or any name, plus repo overrides you can add and remove. A session's own space is set on its page. The map lives in `spaces.json` in the synced folder, so both Macs agree. Two edits at once (a Syncthing conflict copy) are merged key by key, last writer wins, and folded back into one file. Each board card and session page shows its space as a small chip, and the board has **All / Work / Personal** tabs. Agent feeds (Tab) go to the space of the Mac that pulls them unless you give the agent its own. [docs/spaces.md](docs/spaces.md)
|
|
112
|
+
- **One digest a day for the whole folder.** Both Macs used to write their own near-identical digest, because the synced folder gives each one every session. Now there's one, `digests/YYYY-MM-DD.md`, made by exactly one Mac. Every Mac sorts the same list of Mac names: the first makes it at 07:00, and the next only if there's still none 20 minutes later (`digest.graceMin`). So whichever Mac is awake makes it, and two never do. After a late start or a wake, a Mac waits briefly for the sync before writing. Writes go to a temp name Syncthing skips and then a rename. If a conflict copy still appears, the newest one is shown and the duplicate is hidden. **Generate now** regenerates the shared one. Older per-Mac digests stay listed and readable, labelled with their Mac, and the strip shows one pill per day.
|
|
113
|
+
- **The digest is organized by space.** There's a section per space, Work first and then A–Z, each with Needs you / Still running / Dormant / Finished. An item shows its Mac only when that space spans more than one Mac. The Digest tab has space tabs above the repo chips, and the chips only count the space you picked.
|
|
114
|
+
- **Copy for standup** on each space's section (the main button on Work). It copies plain text for Slack. *Yesterday* is what moved or finished in that space since the previous workday's digest, so on Monday it covers Friday. *Today* is what's running plus the top dormant items with their Next. *Blocked* is Needs you. It's one line per item, with the repo in backticks. On a phone it uses the clipboard when Safari allows it; otherwise the text appears selected, ready to copy. In a terminal: `leftoff standup [--space Work] [--date …] [--copy]`.
|
|
115
|
+
- **`leftoff spaces`**: `list`, `set-machine [mac] <space>`, `set-repo <repo> <space>`, `set-session`, `set-agent`, `unset`. It works with or without the daemon. Also new: `GET/POST /v1/spaces` and `GET /v1/standup`, and sessions now carry `space` and `spaceSource`.
|
|
116
|
+
- Planned, not built yet: spaces will decide which Macs each part of the folder syncs to, so Personal never lands on the work Mac.
|
|
117
|
+
|
|
118
|
+
### Changed
|
|
119
|
+
- The Lists and Top of mind sections still cover the whole folder. The agents section now sits inside its space, as a sub-heading.
|
|
120
|
+
|
|
121
|
+
### Tests
|
|
122
|
+
- New `test/spaces-e2e.mjs` (`npm run test:spaces`, in `npm test`) runs two real daemons, laptop and studio, with a fake Syncthing that makes conflict copies. Exactly one digest is written for the day, by one Mac, and both read it. Spaces edited on both Macs at once merge. Generate now from the other Mac rewrites the shared digest. A stale conflict copy stays hidden. Standup runs from both Macs and the CLI.
|
|
123
|
+
- New `test/unit/spaces.test.mjs` (13). It covers resolution precedence, `spaces.json` merging, the election, the grace period and wake settle, digest conflict copies, sections per space, standup text including Monday, and the CLI.
|
|
124
|
+
- `scripts/check-mobile-overflow.py` now covers the board's space tabs, the digest on Work, the digest with the standup text open, and Settings → Spaces, at 375/430px, light and dark: 21 views. It also checks one pill per day and that a stale conflict copy never shows.
|
|
125
|
+
|
|
126
|
+
## [0.5.7] - 2026-10-10
|
|
127
|
+
|
|
128
|
+
### Fixed
|
|
129
|
+
- **The Digest tab scrolled sideways on an iPhone.** The digest card came out about 860px wide on a 390px screen, so "Stopped at" and "Next" ran off the right edge, the repo chips spilled over, and the whole page scrolled sideways. The cause was a grid column of `1fr`. Its minimum is the card's narrowest possible width, and that came from two things. The repo chips counted at their full length laid end to end, even though that row scrolls on its own. Long unbroken paths, URLs and branch names couldn't wrap either. Safari's text autosizing then enlarged everything in that too-wide column, which is why the past-digests list showed in huge monospace. Now:
|
|
130
|
+
- The column is `minmax(0, 1fr)` and the card's text wraps, breaking long paths and branch names where it has to (`overflow-wrap: anywhere`).
|
|
131
|
+
- The repo chips scroll in their own row, and a very long repo name is cut short with "…".
|
|
132
|
+
- On a phone, the past digests are one compact row you scroll sideways, e.g. "Sat Oct 10 · laptop", instead of a tall column of monospace dates.
|
|
133
|
+
- `-webkit-text-size-adjust: 100%`, so Safari doesn't enlarge text on its own.
|
|
134
|
+
- The digest reads better at 375–430px. "Stopped at" and "Next" each get their own line with a small label. The "(repo · branch · Mac)" part is set smaller, so the title leads. The card has tighter padding and headings.
|
|
135
|
+
- **Snake_case names in the digest no longer turn italic.** `clutch_hockey_companion` showed up as "clutch*hockey*companion" because the text between two underscores was treated as emphasis.
|
|
136
|
+
- **Other tabs with long names also scrolled sideways on a phone.** Board and Needs you cards with a long repo chip or path, the Timeline's session picker and its repo chips, a session page with long file paths, and a session titled with a long path all scrolled sideways too. Each now wraps or fits.
|
|
137
|
+
|
|
138
|
+
### Tests
|
|
139
|
+
- New `scripts/check-mobile-overflow.py` (`npm run test:mobile-ui`). It opens 17 views at 375 and 430px, light and dark, in WebKit with the iPhone 15 profile, over a Tailscale Serve stand-in. The views are the board, Needs you, Lists, a list, Timeline, the digest (today's and another Mac's), a catch-up, Catch me up, What's new, Settings, five session pages (long names, no repo, needs you, another Mac, an agent) and search.
|
|
140
|
+
- On each one it checks `document.scrollingElement.scrollWidth <= innerWidth`. When that fails, it names the elements that stick out.
|
|
141
|
+
- It also checks that the past-digests list is compact and that the CSS turns off text autosizing.
|
|
142
|
+
- It takes a screenshot of each view. Against 0.5.6 it fails on 9 of the 17 views. With 0.5.7 all 17 pass at both widths in both themes.
|
|
143
|
+
- `scripts/seed-demo.mjs --long` seeds the worst case for this: long repo names, branch names, paths and URLs in checkpoints, a session with no repo, past digests from three Macs, and a catch-up.
|
|
144
|
+
|
|
145
|
+
## [0.5.6] - 2026-10-10
|
|
146
|
+
|
|
147
|
+
### Added
|
|
148
|
+
- **Host your own agent feed.** A short guide, [docs/agent-feed-hosting.md](docs/agent-feed-hosting.md), linked from the README and from Settings → Agents (**How to host a feed**). The main path is a ready-made Cloudflare Worker in [`templates/agent-feed-worker/`](templates/agent-feed-worker/): about 5 minutes, free plan, your own account. It keeps the feed private (a read key to `GET`, a write key to `PUT`), sends an ETag so an unchanged pull is a `304` (weak `W/` tags included), and holds no secrets. `wrangler.toml` has only the SHA-256 hashes of the two keys (`READ_KEY_SHA256`, `WRITE_KEY_SHA256`). The guide also covers other hosts (any HTTPS endpoint, a static file on a secret URL, `http://localhost` for a local agent), the custom-domain gotcha ("This Worker does not exist on your account [10007]" on a first deploy: run it again, or add the domain in the dashboard; no DNS changes needed), and **Connect Tab** (text Tab "connect my Leftoff").
|
|
149
|
+
- **`leftoff agents keygen`** prints a new read key and write key (192 random bits each, `lfr_…` / `lfw_…`) and the two `wrangler.toml` lines with their hashes. `--json` too.
|
|
150
|
+
- **`leftoff agents publish <file> --url <url> --write-key -`** lets any agent or script update its feed. It checks the file with the same strict validator the daemon uses and sends nothing if anything's wrong, listing each problem. Then it `PUT`s the file byte for byte with `Authorization: Bearer`, follows no redirects, and never prints the key. `--dry-run` and `leftoff agents validate <file>` check without sending. The key can also come from `LEFTOFF_AGENT_WRITE_KEY`.
|
|
151
|
+
|
|
152
|
+
### Changed
|
|
153
|
+
- **The feed decides the reply button.** 0.5.4 gave every agent's Needs you card a "Reply to Tab ↗" button to `agent.url` and the line "Read-only here. Tab asked this in its own app; reply by texting Tab." That button just opened tab.bot, where you can't reply. Now:
|
|
154
|
+
- `agent.reply` `{ "label": "Text Tab", "url": "sms:+1…" }` (new, optional) puts that button on the agent's Needs you cards, its session page, Catch me up, and on board cards that need you. On a phone, an `sms:` link opens Messages with a text to Tab. **No `agent.reply`, no button.**
|
|
155
|
+
- A reply link may be `https:`, `sms:`, `imessage:`, `mailto:` or `tel:` only. The daemon rejects anything else (the whole feed fails, as with any bad link), and the board checks the scheme again before drawing it. Non-https links open in place, not in a new tab.
|
|
156
|
+
- `agent.url` stays https-only and now only links the agent's chip and name. The "Open Tab ↗" buttons are gone.
|
|
157
|
+
- The footer under each item is short and accurate: "From Tab · reply by texting Tab", using `agent.replyHint` (new, optional, up to 120 characters), or "From Tab · Read-only here" without one.
|
|
158
|
+
- The digest and Catch me up mention a reply only when the feed gives one. Catch-ups saved by 0.5.4/0.5.5 still read back.
|
|
159
|
+
- Leftoff 0.5.5 and older reject a feed with `agent.reply` (strict schema). Upgrade before your agent adds it.
|
|
160
|
+
- **The GitHub repo is now `OneSillyApp/leftoff`** (it was `joelash/checkpoints`, briefly `joelash/leftoff`). Install and upgrade lines (`npx github:OneSillyApp/leftoff setup`), the join line for a new Mac, and links in the board and the docs all use the new name. The npm package name stays `checkpoints`. The old names keep working everywhere they're read: GitHub redirects both, and Leftoff maps `joelash/checkpoints` and `joelash/leftoff` (as `owner/repo`, `github:`, https, `git@` or `ssh://`) to the new name. So an install that recorded an old source upgrades from the new one, and `leftoff upgrade` on a git clone also points its `origin` at the new URL.
|
|
161
|
+
|
|
162
|
+
### Tests
|
|
163
|
+
- New `test/agents-hosting-e2e.mjs` (9): keygen → the real Worker template on loopback → publish (invalid refused, wrong key 403, dry run) → read keys, 401s, ETag/weak ETag/304 → `leftoff agents add` → an update that drops `agent.reply`.
|
|
164
|
+
- Unit: the reply scheme allow-list on both sides (daemon and board, including `javascript:`, `data:`, `http:`, mixed case, quotes, spaces, bidi characters, empty targets), the `agent.reply`/`replyHint` schema, the footer copy, escaping, `replyOf` re-checking a synced status file, the reply on every surface and gone when the feed drops it; the old repo name mapping to the new one.
|
|
165
|
+
- Browser: `scripts/check-agents-reply.py`, Tab's card with **Text Tab** next to Codex's with no button, on desktop and iPhone, light and dark. `scripts/check-agents.py` updated.
|
|
166
|
+
|
|
167
|
+
## [0.5.5] - 2026-10-10
|
|
168
|
+
|
|
169
|
+
### Fixed
|
|
170
|
+
- **Pairing a second Mac with Syncthing could hang at "Waiting for the first files…".** On two real Macs (Syncthing v2.1.6), the first Mac saw the new one knock as plain `laptop.local`, without the pairing proof, so it rightly didn't accept it, and the join waited forever. Three things combined, all on the joining Mac:
|
|
171
|
+
- It added the first Mac **before** putting the proof in its own device name. Syncthing dials a newly added device straight away, and its Hello carries the device's name as configured at that moment, so the first Hello went out plain.
|
|
172
|
+
- It then saw the first Mac listed as **connected** and printed "Connected to studio". That was the moment before the first Mac hung up: Syncthing lists a peer as connected as soon as the Hellos are exchanged, even when the other side is about to refuse an unknown device.
|
|
173
|
+
- Believing it was connected, it **restored its plain name**, so every later Hello (Syncthing redials about once a minute) was plain too. The proof never reached the first Mac.
|
|
174
|
+
- Now the joining Mac sets the proof name first, then adds the first Mac. If it already knew the first Mac (an earlier try), it pauses and resumes that one device so a fresh Hello goes out at once.
|
|
175
|
+
- "Connected" now means accepted: connected **and** the first Mac shares the Leftoff folder back (Syncthing's `remoteState` for the folder is `valid`). It then says "studio accepted this Mac. Syncing…". If the connection drops during the first sync, it says "Lost the connection to studio. Waiting for it to come back…" and carries on when it's back. If the first Mac added this one but didn't tick Leftoff, it says exactly that.
|
|
176
|
+
- The first Mac also copes with a **stale pending record**: a device that knocks without the proof while a code is live has its pending record dismissed once (that one device, no config change), so it can show up again with its current name.
|
|
177
|
+
|
|
178
|
+
- **Restored Ghostty tabs showed `·` in their titles** (`✳ colorGenie  · cp:207651`). The restore hands each tab's folder and `claude --resume … --name '<title>'` command to `osascript` as arguments, and osascript decodes its arguments by the locale. Leftoff's daemon runs as a login service with no `LANG`, so each UTF-8 byte was read as a Latin-1 character: `·` (C2 B7) became `·`, and `é` would have become `é`. Now every argument goes as ASCII hex of its UTF-8 bytes and Leftoff's AppleScript turns it back into text itself (`«data utf8…» as text`; the script source is pure ASCII too), and every osascript Leftoff runs also gets a UTF-8 locale. Names with accents or emoji now come back exactly.
|
|
179
|
+
- **Tab titles are just the session's name** (`✳ colorGenie`), without the `· cp:207651` marker. Go finds the tab by its tty (Ghostty after 1.3.1), then the marker if a title has one, then (new) a name only one tab has, then its folder. The daemon asks the mod to add the marker only when Go couldn't tell the tab apart otherwise: an older Ghostty and another open session on the same Mac with the same name or folder. A restore does the same for the tabs it opens together.
|
|
180
|
+
- Old titles keep working: a `cp:` marker still matches, and a resumed session drops it when it isn't needed. A config that still has the old default `tabTitleFormat` (`"{name} · cp:{id}"`) is read as the new default (`"{name}"`). A custom format may now leave out `{id}`.
|
|
181
|
+
- The mod's own rename coming back from Claude Code (now without a marker to recognise it by) is still never taken as a name you chose.
|
|
182
|
+
|
|
183
|
+
### Added
|
|
184
|
+
- **If it's stuck.** After a minute of waiting, `leftoff join` prints, and the joining Mac's board shows, the by-hand way: on the first Mac, open http://127.0.0.1:8384, click **Add Device** on the "laptop.local wants to connect" banner, then tick Leftoff on the **Sharing** tab.
|
|
185
|
+
- **Accept a Mac that knocked without the proof.** The first Mac's Settings → Sync and `leftoff doctor` show "A Mac named laptop.local asked to connect without the pairing proof", with an **Accept laptop.local** button (and `leftoff sync accept [name]`, which asks first). It's only for a device that knocked while a code was live, only on the Mac itself (never the phone), and only after a confirm click. It adds that device, shares only the Leftoff folder with it, and ends the pairing. Nothing is ever accepted without the proof or that click.
|
|
186
|
+
- The peer status on both Macs no longer shows a Mac as "connected" while it isn't sharing the folder ("not sharing Leftoff with this Mac yet").
|
|
187
|
+
|
|
188
|
+
### Why the device name is still the proof
|
|
189
|
+
Before a device is accepted, Syncthing passes on only what's in its Hello: ID, name and version. A folder label can't carry the proof, because folder offers only travel after both sides have added each other. The details are in [docs/SECURITY.md](docs/SECURITY.md#pairing-a-second-mac).
|
|
190
|
+
|
|
191
|
+
### Tests
|
|
192
|
+
- The fake osascript now decodes its arguments the way osascript does without a UTF-8 locale. Against 0.5.4 the restore e2e reproduces the mojibake (`café`); with 0.5.5 a title like “colorGenie · réglages ✨” and a folder like `larry-db café` arrive intact. New unit and e2e cases cover titles without the marker, Go matching by name (and by name and folder), when the daemon asks for the marker, and the old default format.
|
|
193
|
+
- The fake Syncthing now behaves like v2.1.6 where it matters: it dials a new or unpaused device before the API call returns (so a later rename misses the first Hello), lists a refused connection as connected for a moment, reports `remoteState` only from what the peer really shares, and supports pause/resume and dismissing pending devices. With `FAKE_ST_PENDING_NAME=first` it keeps the name from the first Hello until the record is dismissed.
|
|
194
|
+
- New `test/syncthing-pairing-e2e.mjs` replays the real failure. Against 0.5.4 it hangs exactly as on real Macs ("Connected to studio. Syncing…", then "Waiting for the first files…" forever). With 0.5.5 it passes in both pending-name modes: laptop's first Hello carries the proof, the stale record is cleared once, laptop is accepted by proof and syncs; a Mac with an unverifiable proof is never accepted, gets the stuck steps, is accepted by the explicit click, and a dropped connection mid-sync is reported honestly.
|
|
195
|
+
|
|
196
|
+
## [0.5.4] - 2026-10-10
|
|
197
|
+
|
|
198
|
+
### Added
|
|
199
|
+
- **External agents.** Work you do with an AI assistant outside Claude Code (Tab today, later Codex and others) shows up on your board as if it were another machine. The agent publishes a private feed ([docs/agent-feed.md](docs/agent-feed.md), `leftoff-agent-feed/1`), and Leftoff pulls it every 2 minutes:
|
|
200
|
+
- Each feed is a **machine chip** with the agent's name (✦ Tab), and is in the board's machine filter.
|
|
201
|
+
- Its **sessions** are on the board with their status, and their **checkpoints** are on the session page, with the feed's https links.
|
|
202
|
+
- What it's waiting on goes in **Needs you**, read-only, with **Reply to Tab ↗** (you answer Tab by texting).
|
|
203
|
+
- Its **lists** merge into the Lists page, read-only.
|
|
204
|
+
- It's all in **Catch me up** and the **digest** ("From your agents").
|
|
205
|
+
- **Add a feed:** Settings → **Agents** → **Add agent feed** (name, URL, key), or `leftoff agents add <name> <url> --key <key>` (`--key -` reads it from stdin). Also `leftoff agents`, `leftoff agents refresh [name]` and `leftoff agents remove <name>`.
|
|
206
|
+
- **Errors show:** an unreachable, refused, too-big or invalid feed is shown in Settings → Agents, `leftoff agents` and `leftoff doctor`, with its last successful pull. The board keeps the last good pull.
|
|
207
|
+
|
|
208
|
+
### How it works
|
|
209
|
+
- **One Mac pulls each feed:** the one you added it on, which holds the key. It writes each session as Markdown in `agents/<name>/` in the folder, and your other Macs read those files through the sync. Two Macs pulling would write the same files (sync conflicts) and need the key on both. Adding the feed on another Mac moves the pulling there.
|
|
210
|
+
- Pulls use `ETag`/`If-None-Match`, so an unchanged feed is a 304. The key goes as `Authorization: Bearer`, with `?key=` as a fallback when the server refuses the header.
|
|
211
|
+
- History persists: checkpoints accumulate across pulls, and a session that leaves the feed stays as ended.
|
|
212
|
+
|
|
213
|
+
### Safety
|
|
214
|
+
- The key lives in `~/.config/checkpoints/agents/<name>.key` (0600) on the pulling Mac only, never in the synced folder, logs or error messages. The source list (name, URL, which Mac pulls) syncs without it.
|
|
215
|
+
- Feeds are validated against a strict schema with a 1 MB limit. One bad field fails the whole pull, and the error says where. Everything is shown as escaped plain text, never HTML, and only `https://` links are allowed.
|
|
216
|
+
- Nothing from a feed is written outside `agents/<name>/`. Feed sessions are never offered for Go, restore or answering. Adding and removing a feed only work on the Mac itself, not from the phone.
|
|
217
|
+
|
|
218
|
+
## [0.5.3] - 2026-10-10
|
|
219
|
+
|
|
220
|
+
### Added
|
|
221
|
+
- **Easy Syncthing.** Settings → Sync now gets Syncthing working in a few clicks, without opening Syncthing:
|
|
222
|
+
1. **Install Syncthing (recommended).** It shows the exact command (`brew install syncthing && brew services start syncthing`) and runs it only after you click Install. Homebrew's output streams into the page, and a failure gets a plain sentence ("Homebrew couldn't download Syncthing…") and **Try again**. Without Homebrew you get two short directions and **Check again**. Leftoff uses the formula with `brew services` (a background login service) rather than the menu-bar app.
|
|
223
|
+
2. **Sync this folder.** It does the verified move into `~/Sync/Leftoff` (or `~/Leftoff` when `~/Sync` is another Syncthing folder, or your pick), then adds a Syncthing folder with the ID `leftoff` through Syncthing's local API, plus a `.stignore`.
|
|
224
|
+
3. **Pair another Mac.** You get a one-time code (15 minutes, works once). On the other Mac: `leftoff join <code>`, `npx -y github:joelash/checkpoints join <code>`, or **Join another Mac** on its board. It installs or starts Syncthing (asking first), adds your first Mac, and is accepted automatically, but only because it proves it holds the code. You see *Waiting for studio…*, *Syncing 214/530 files…* and *Done*, and then its sessions are merged in.
|
|
225
|
+
- **Status:** Settings → Sync, `leftoff sync` and `leftoff doctor` show whether Syncthing is running, the peers that are connected (and how far along they are), the last sync and folder errors. **Stop syncing** (`leftoff sync stop`) removes the Leftoff folder from Syncthing and keeps the files.
|
|
226
|
+
- **CLI:** `leftoff sync [install|start|setup|pair|stop]` and `leftoff join <code>`.
|
|
227
|
+
- **Security notes:** [docs/SECURITY.md](docs/SECURITY.md) covers what's in the folder, where it lives with Syncthing, iCloud, Dropbox or another folder, what never syncs, the pairing trust model, cross-machine commands, phone access, and what Leftoff doesn't protect against. A short "How your data is protected" panel appears in the sync walkthrough and in Settings → Other machines.
|
|
228
|
+
|
|
229
|
+
### Changed
|
|
230
|
+
- **API keys stay on each Mac.** The Ollama cloud and Anthropic keys now live in `~/.config/checkpoints/secrets/` (0600) instead of `.secret/` inside the folder, where a sync tool could copy them. The daemon moves an existing one at start, and `leftoff doctor` flags one that's still in the folder. A Mac that joins needs its key entered once.
|
|
231
|
+
- With Syncthing, `.secret/` (the board token and phone sign-ins) stays on each Mac.
|
|
232
|
+
|
|
233
|
+
### Safety
|
|
234
|
+
- Leftoff only ever changes its own Syncthing folder (`leftoff`), devices it added, and this Mac's own device name while a pairing runs. Your other folders and devices are never touched, and a folder nested inside one of them is refused.
|
|
235
|
+
- Syncthing's API key and the pairing secret never appear in logs, on the board or on the phone. Installing, pairing and joining only work on the Mac itself.
|
|
236
|
+
|
|
237
|
+
## [0.5.2] - 2026-10-10
|
|
238
|
+
|
|
239
|
+
### Added
|
|
240
|
+
- **Set up syncing.** Settings → Sync → **Set up syncing** walks you through putting the folder in a synced folder, so every Mac you use is on one board:
|
|
241
|
+
1. **Pick a folder.** It finds your Syncthing folders (from Syncthing's own config), Dropbox and iCloud Drive, each with a one-line trade-off. Syncthing is recommended; for iCloud, choose "Keep Downloaded". You can also type any path.
|
|
242
|
+
2. **Check.** A dry run with the file counts, whether it's joining another Mac's data, and where the backup goes.
|
|
243
|
+
3. **Move.** Leftoff stops the daemon, copies everything, checks every file's sha-256, keeps the old folder as `<name>.leftoff-backup-<date>`, points the mod, the hooks and the login service at the new folder, and restarts there. A failure at any step puts everything back.
|
|
244
|
+
4. **Other Macs.** The one line to run on each other Mac: `leftoff move ~/Sync/Leftoff`.
|
|
245
|
+
- **`leftoff move <folder>`** does the same in a terminal (`--dry-run`, `--yes`, `--backups`, `--delete-backup`). `leftoff setup` uses the same move when you give it a different folder. The backup is never deleted unless you ask.
|
|
246
|
+
- **Joining merges.** A folder that already has another Mac's data is merged into: each machine's files live under its own name, so nothing of theirs is overwritten. The joined folder keeps its own settings, with this Mac's name carried over. A list both sides changed is merged item by item. A different Mac already using this Mac's name is caught before anything is copied.
|
|
247
|
+
- **Answer and Go on another Mac, no Tailscale needed.** Allow, deny or answer another Mac's permission prompts and questions, reply to its sessions, and press **Go**, all from this Mac's board. Its prompts show here with a **⌂ machine** chip.
|
|
248
|
+
- **Opt-in per Mac, off by default:** Settings → Other machines → "Allow other machines to answer and Go here", or `leftoff machines allow`. While it's off, that Mac doesn't listen at all.
|
|
249
|
+
- When an action isn't possible, the button is greyed out and says why: not allowed there, unreachable, key changed, or that Mac needs 0.5.2.
|
|
250
|
+
- **How it's kept safe:**
|
|
251
|
+
- Each Mac has its own Ed25519 key. The private half stays on the Mac (`~/.config/checkpoints/identity/`, 0600, outside the synced folder). The public half is published in the folder at `machines/<machine>.json`, which is how your Macs learn to trust each other.
|
|
252
|
+
- Keys are pinned the first time they're seen, and a changed key is refused until you confirm it.
|
|
253
|
+
- Every command is signed, encrypted for the one Mac it's meant for, and expires in 60 seconds. A replayed command is refused.
|
|
254
|
+
- Commands run through the same code as that Mac's own board, and are recorded in the session's `answers.md` as "from studio's board".
|
|
255
|
+
- **Two ways to get there:**
|
|
256
|
+
- Direct to the other Mac's LAN or Tailscale address.
|
|
257
|
+
- Through a relay both Macs connect out to, which forwards envelopes it can't read. No open ports. This is ready for a hosted `<you>.wtf.leftoff.dev`.
|
|
258
|
+
- The relay client and a local fake relay ship now (the hosted relay doesn't yet). The protocol is in `docs/peer-protocol.md`.
|
|
259
|
+
- `leftoff machines` lists the other Macs and whether you can act on them from here. `leftoff machines trust <name>` confirms a changed key.
|
|
260
|
+
- API: `GET /v1/sync`, `POST /v1/sync/plan`, `POST /v1/sync/move`, `POST /v1/sync/backup/delete`, `GET /v1/peers`, `POST /v1/peers/allow`, `POST /v1/peers/:machine/trust`, `POST /v1/peers/check`, and the SSE event `peers.updated`. `/v1/health` now carries `pid` and `folder`.
|
|
261
|
+
|
|
262
|
+
### Changed
|
|
263
|
+
- From the phone, nothing changes: answering follows Settings → Remote access on the Mac you're signed in to, and Go needs a Mac's own browser.
|
|
264
|
+
- The board reconnects its live updates after the daemon restarts (an upgrade, a move), instead of going quiet until a reload.
|
|
265
|
+
|
|
266
|
+
## [0.5.1] - 2026-10-10
|
|
267
|
+
|
|
268
|
+
### Added
|
|
269
|
+
- **Lists.** Claude often writes you a task list ("next steps to submit to the App Store"). Now he can keep it and tick it off. How it works, in three steps (also behind the **?** on the board's Lists page and in the README):
|
|
270
|
+
1. Ask Claude for steps.
|
|
271
|
+
2. Save them: the **Save** chip on the session card, or `/cp-todo` in the session.
|
|
272
|
+
3. Tick them off on the board or phone, with `/cp-done 3`, or by telling Claude "step 3 is done".
|
|
273
|
+
- **It notices, you decide.** When Claude's reply has a checklist of 3 or more items, the Stop hook flags it. This is a regular expression with no model, and it works with the mod or settings hooks alone. The session card then shows **Claude wrote a 7-step list · Save**. One click saves it, named from its heading. Nothing is ever saved without that click. If Claude repeats a list you've already saved (with a box ticked, say), no new chip appears.
|
|
274
|
+
- **What counts:** `- [ ]` / `- [x]` checklists always do. Numbered or bulleted lists count when the heading or the line above is about steps ("Next steps", "Here's the plan:", "Before we launch:", "To do", …). Lists inside code blocks never count, and nested bullets fold into the item above.
|
|
275
|
+
- **`/cp-todo [name] [--pick [N]]`** saves the most recent checklist in this session (`--pick` lists them all). **`/cp-done <n…> [in <list>]`** ticks items (`/cp-done undo 3`, `/cp-done upload the build`). Both fall back to `/leftoff-todo` and `/leftoff-done` if a later Claude Code takes the names.
|
|
276
|
+
- **Claude ticks items itself.** When a session's lists change (saved, ticked on the board, a resumed session), the next prompt carries a short note for Claude. The note lists the items and their numbers, and says to run `leftoff todo done <list> <n>` when it finishes a step or you say one is done. It goes through the mod's `prompt.submit` context, once per change. Checked against the real Claude Code 2.1.295: the note reached the model, which ran the command, and the file changed.
|
|
277
|
+
- **Existing sessions, open or ended, including ones from before this version:** **Find lists in this session** on the session page reads Claude Code's own transcript and offers every checklist in it, each with **Save**. In a terminal: `leftoff todo capture <session name|id> [--pick [N]] [--name …]`.
|
|
278
|
+
- **Plain Markdown in the synced folder:** `lists/<repo>/<slug>.md`. The frontmatter has the name, repo, source session and machine, created and updated, and the body is a `- [ ]` checklist. Each item's number lives in an invisible `<!-- #3 -->` comment and never changes or gets reused, so "step 3" always means the same step. Hand edits round-trip: an item typed without a number gets the next one, and notes and blank lines stay put.
|
|
279
|
+
- **Edits are never lost:** every write re-reads the file under a per-machine lock and writes it atomically, so the board, the CLI, the mod and your editor can all write one list at once (the tests race 8 processes). If two Macs edit a list before the sync catches up, the sync tool's conflict copy is merged back in item by item (Syncthing's `.sync-conflict-…`, Dropbox's "conflicted copy", iCloud's "name 2.md"). The later change wins for an item both sides touched, and the copy is moved to `lists/.merged/`, never deleted.
|
|
280
|
+
- **On the board:** a **Lists** tab with open lists grouped by repo, plus checkboxes, **Add an item**, rename and **Archive**. Each list has its own page, a **4/9** chip on its session's card, and a **Lists** section on the session page. **Catch me up** has a **Still open** section (what's next, and what got ticked in the window), and the **digest** lists the items completed that day.
|
|
281
|
+
- **`leftoff todo`** shows open lists. Also `todo done|undo <list> <n…>`, `todo add`, `todo show`, `todo rename`, `todo archive|unarchive`, `todo new` and `todo capture`. They go through the daemon, or straight to the folder when it's down.
|
|
282
|
+
- **From the phone:** reading lists works from any signed-in device. Changing them from another device (ticking, adding, saving, renaming, archiving) follows the same opt-in as remote answering, Settings → Remote access, off by default. With it off, the phone shows lists read-only and says why.
|
|
283
|
+
- **Claude Code's own TodoWrite list is left alone.** Leftoff never reads or writes it.
|
|
284
|
+
- API: `GET/POST /v1/lists`, `GET /v1/lists/:repo/:slug`, `POST /v1/lists/:repo/:slug/change`, `GET /v1/sessions/:id/lists`, `GET /v1/sessions/:id/lists/find`, `POST /v1/sessions/:id/list-hint/dismiss`, and the SSE event `lists.updated`.
|
|
285
|
+
|
|
286
|
+
### Fixed
|
|
287
|
+
- The header no longer crowds the search box between 1181 and 1400 px wide now that there's one more tab: "What's new" uses its short label there.
|
|
288
|
+
|
|
289
|
+
## [0.5.0] - 2026-10-10
|
|
290
|
+
|
|
291
|
+
### Added
|
|
292
|
+
- **Catch me up.** This is the "WTF was I working on?" moment after a weekend or a morning away. Pick repos (some or all) and a stretch of time, and you get one recap. On top: a short summary of 3–6 bullets. Then, per repo and per session: **Done**, **Stopped at**, **Next**, open questions, and anything waiting on you.
|
|
293
|
+
- **Windows:** "since I last caught up" (the default), the last 24 hours, "since Friday" (the last workday: Friday on a weekend or a Monday, else yesterday), 7 days, or custom (a from and an optional to). `catchup.workdays` in config.json changes which days count as workdays.
|
|
294
|
+
- **Repos and machines:** every repo with activity in the window is ticked by default. Untick the ones you don't want. With more than one machine in the folder, you can pick machines too.
|
|
295
|
+
- **Built from the folder:** the checkpoint Markdown files and each session's `session.md`, so a synced folder reads the same on every machine.
|
|
296
|
+
- **Summary:** the configured summarizer writes it (local Ollama, Ollama cloud, Anthropic, `claude -p`, or this session's own model from `/cp-catchup`). With no model, or when the model fails or doesn't answer in bullets, Leftoff builds it itself: what moved, what's waiting on you, what got done, what to pick up first. The file records which one wrote it, and why when it fell back.
|
|
297
|
+
- **Limits:** `summary.length` sets how much it says (short, informative, long). The redaction patterns apply to everything in it, including what goes to the model. Empty sessions and ones known only by id (the 0.4.8 rules) are left out, as are archived ones.
|
|
298
|
+
- **On the board:** a **Catch me up** button on the Board and Digest pages opens the picker. The result is a page with the summary, then **Needs you now**: what's waiting right now in those sessions, answerable right there like on Needs you. Then each repo's sessions, each with Done / Stopped at / Next, a link to its page, **Go** and **Copy resume**. Past catch-ups are listed on the left. The page fits a phone with no sideways scroll. From another device it follows the same rules as the rest of the board: Go and answering stay on this machine unless you've allowed remote answering. Making a catch-up from the phone works, because it only reads the folder and writes one Markdown file, like the digest's Generate now.
|
|
299
|
+
- **Saved to the folder:** each one goes to `catchups/YYYY-MM-DDTHHMM.md` (local time). The frontmatter has the window, repos, machines and model. It syncs like everything else. There's one folder for every machine, because catching up is something you do, not a machine. A second catch-up in the same minute gets a `-2` suffix and never overwrites. "Since I last caught up" starts where the newest one ended.
|
|
300
|
+
- **`leftoff catchup`** (also `checkpoints catchup`): `--since 24h|3d|1w|workday|friday|yesterday|<date>|<time>`, `--repo` (repeatable, or `a,b`), `--all`, `--machine`, `--to`, `--print` (stdout only: no file, and it doesn't move "since I last caught up"), `--json`, `--list`, `--short` / `--long`. The default window is since the last catch-up, else 24 hours. It goes through the daemon when one is running, and builds straight from the folder when not.
|
|
301
|
+
- **`/cp-catchup [window] [repo…|here|all]` in Claude Code**, and **Catch me up** (**u**) in the `/leftoff` pane. The daemon assembles the recap, and this session's own model writes the summary when the summarizer is `auto`, `claude` or `anthropic`, the same rule as checkpoints. It's saved like the others. The command falls back to `/leftoff-catchup` if a later Claude Code takes the name.
|
|
302
|
+
- **API:** `GET /v1/catchups`, `GET /v1/catchups/options?since=`, `GET /v1/catchups/<name>`, `POST /v1/catchups` (`since`, `to`, `repos`, `machines`, `save`), and `POST /v1/catchups/prepare` (the mod's first step). A window it can't read is a 400 `bad_window` that says what it accepts.
|
|
303
|
+
|
|
304
|
+
## [0.4.8] - 2026-10-10
|
|
305
|
+
|
|
306
|
+
### Fixed
|
|
307
|
+
- **The "Untitled session" card with no folder, no pid, no terminal and 0 turns.** It showed up on a Mac right after upgrading to 0.4.6. It was a real session resumed with `claude --resume 27bc7529…` (pid 54004) 47 minutes before. Thanks for the report, the screenshots and the `ps` output that pinned it down. What happened:
|
|
308
|
+
1. The daemon only writes a session to disk once it has a turn, a name or a checkpoint. A session that was open but hadn't had a prompt since it was opened or resumed lived only in the daemon's memory.
|
|
309
|
+
2. `leftoff upgrade` restarts the daemon, and the restart forgot it.
|
|
310
|
+
3. The mod in that still-running Claude Code kept sending its once-a-minute heartbeat. The new daemon built a record from the heartbeat alone: no folder, no pid, no terminal, 0 turns, "started" at the first heartbeat, and shown as open because it was still heartbeating.
|
|
311
|
+
|
|
312
|
+
Reproduced with the real Claude Code 2.1.295 binary, for a new session and for `claude --resume <id>`, and against 0.4.7 itself in `test/ghost-e2e.mjs`. The upgrade never started a `claude` of its own: Leftoff's own `claude` runs were already marked internal.
|
|
313
|
+
- **Open sessions survive a daemon restart.** A session with no turns yet is kept in `.open-sessions.<machine>.json` until it gets a folder or ends.
|
|
314
|
+
- **A heartbeat from a session the daemon doesn't know gets `hello: true`.** A 0.4.8 mod answers by sending the session's folder, repo, model, pid, tty and terminal again, at most every 5 minutes.
|
|
315
|
+
- **The daemon fills in sessions it only knows by id.** This covers sessions still running a 0.4.7-or-older mod, which ignore the hello. It reads the folder and earlier prompts from Claude Code's transcript (`~/.claude/projects/*/<id>.jsonl`). It finds the process with `ps`: a `claude` whose arguments carry the id, or else the only unclaimed claude in that folder (so `claude --resume <name>` and claude started through `ollama launch claude` work too). It finds the terminal from the process's parents (Ghostty, iTerm2, Terminal.app, WezTerm, kitty). A title with Leftoff's own `cp:` tab marker isn't taken as a name. `CHECKPOINTS_HEAL=off` turns this off.
|
|
316
|
+
- **The board's header no longer runs off the right edge between phone and wide screens.** At 1280 px the status label and the theme switch were cut off. The search box now shrinks, and the label and tab names hide below 1480 and 1180 px.
|
|
317
|
+
|
|
318
|
+
### Changed
|
|
319
|
+
- **Empty sessions fold into one row on the board.** An empty session has no prompt, no name, no checkpoint, and isn't a resumed conversation. These sit in a closed row under the board, **N empty sessions**, that opens on a click and stays open across live updates. A session shows as itself after its first prompt. Pinned sessions and ones that need you always show. A resumed conversation is never folded, even before you type: it shows "resumed (N before)", counted from Claude Code's transcript.
|
|
320
|
+
- Empty sessions are left out of the digest, `leftoff restore`, the Restore banner and the `/leftoff` pane. An empty session that ends gets no folder.
|
|
321
|
+
- **Cleanup on upgrade.** When the new daemon starts, it archives the empty records with no folder or process that 0.4.6 and 0.4.7 wrote to disk. They're archived, not deleted. `leftoff upgrade` says how many.
|
|
322
|
+
- **`leftoff doctor` reports** sessions known only by id (with their short ids), empty ones, ones the daemon filled in, and ones it archived.
|
|
323
|
+
- Every `claude` Leftoff runs (`claude plugin …`, the summarizer's `claude -p`) now carries `LEFTOFF_INTERNAL=1` as well as `CHECKPOINTS_INTERNAL=1`, and the mod ignores either.
|
|
324
|
+
- `/v1/sessions` items carry `empty`, `ghost`, `historyTurns`, `startSource` and `healedVia`. `/v1/health` has a `sessions` block with `empty`, `ghosts`, `healed`, `archivedGhosts` and `lastArchivedAt`.
|
|
325
|
+
- Stopping the daemon closes open long-polls (a mod's reply wait) right away, instead of waiting for them.
|
|
326
|
+
|
|
327
|
+
### Correction
|
|
328
|
+
- 0.4.7's notes said Claude Code 2.1.295's `--resume <name>` opens its picker. Checked against the real binary: `claude --resume <name>` resumes directly when exactly one session has that name (same session file). Only when two sessions share a name does it open the picker, filtered. `leftoff restore` still always passes the session id, which is never ambiguous.
|
|
329
|
+
|
|
330
|
+
## [0.4.7] - 2026-10-10
|
|
331
|
+
|
|
332
|
+
### Added
|
|
333
|
+
- **`leftoff restore`: after a restart, put your sessions back.** It lists the Claude Code sessions that were open on this Mac when it restarted. All are checked: space toggles one, `a` toggles all, enter goes. Each opens in a new Ghostty tab, in its folder, running `claude --resume <session id> --name "<its name>"`, so the tab gets its title back. The command is typed into your own shell, so your PATH and aliases apply and the tab stays open when Claude Code exits.
|
|
334
|
+
- `leftoff restore <name|id>` restores one session. It can be any closed session on this Mac, not only one from the last restart.
|
|
335
|
+
- `--all` / `--yes` skip the picker, `--dry-run` shows what would open, and `--print` prints the commands instead of opening tabs.
|
|
336
|
+
- `--since=12h` sets how far back before the restart to look (default 24 hours), and `--repo=NAME` limits it to one repo.
|
|
337
|
+
- It always resumes by session id, because Claude Code 2.1.295's `--resume <name>` opens its picker instead of resuming. Leftoff maps the names itself.
|
|
338
|
+
- **A Restore banner on the board after a restart:** "12 sessions were open before your Mac restarted". **Restore…** opens a list with a checkbox for each session, then **Restore N**, **Select all/none**, **Copy commands**, and ✕ to hide it until the next restart. Like Go, opening tabs only works from the Mac's own browser. Other devices see the list with the button off.
|
|
339
|
+
- `leftoff status` says when sessions are waiting to be restored.
|
|
340
|
+
|
|
341
|
+
### How it decides what was open
|
|
342
|
+
- The restart is the Mac's boot time (`sysctl kern.boottime`) together with the daemon's first start after it. The daemon now records each start in `.secret/daemon-starts-<machine>.json`. The daemon's own start also counts, which covers a logout, or a machine whose boot time can't be read.
|
|
343
|
+
- A session counts when all of these are true:
|
|
344
|
+
- it ran on this Mac;
|
|
345
|
+
- it was heard from (heartbeat, event or activity) in the hours before the restart and not after;
|
|
346
|
+
- it ended around the restart: its Claude Code process was gone when the daemon came back, or Claude Code ended it with reason "other" while the Mac shut down, or it never ended at all.
|
|
347
|
+
- These don't count: sessions you ended yourself (`/exit`, Ctrl-D, `/clear`, logout), sessions closed hours before the restart, sessions already running again, and sessions restored in the last 10 minutes.
|
|
348
|
+
|
|
349
|
+
### Fallback
|
|
350
|
+
- Without Ghostty's AppleScript, Leftoff prints one `cd … && claude --resume …` line per session instead. That covers Ghostty older than 1.3.0, Ghostty not installed, Automation permission denied, or not on macOS. A tab that Ghostty refuses on its own is reported with its command.
|
|
351
|
+
|
|
352
|
+
## [0.4.6] - 2026-10-10
|
|
353
|
+
|
|
354
|
+
### Added
|
|
355
|
+
- **Your phone, signed in with one scan.** `leftoff tailscale on` and the new `leftoff link` print a QR code in the terminal. Scan it with the iPhone camera and Safari opens the board, already signed in. `--no-qr` prints just the link, and `--qr` forces the code when the output isn't a terminal. Settings → Remote access → **Show QR code** shows the same code on the board, with a countdown until it expires.
|
|
356
|
+
- **A typed sign-in code for the home-screen app.** An iPhone home-screen web app keeps its own cookies, separate from Safari's, so signing in Safari didn't sign the app in, and 0.4.5's sign-in screen asked for a link the app has nowhere to open. Every QR code now comes with a 6-character code such as `K7Q-M4P`. The app's sign-in screen asks for it. A phone that's already signed in can get a fresh code from Settings → Remote access → **Get a code**.
|
|
357
|
+
- **Leftoff as an iPhone home-screen app:** a web app manifest (name Leftoff, the WTF bubble icon at 192 and 512 px plus a maskable one), standalone display, a status bar and theme color that follow light and dark, and room for the notch and the home indicator. Settings → Remote access walks through **Add to Home Screen**.
|
|
358
|
+
- **An offline page.** When the Mac can't be reached (Tailscale off on the phone, the Mac asleep, Leftoff stopped), the app shows what to check instead of Safari's error page. The service worker that serves it caches nothing else: no sessions, no checkpoints, no API data.
|
|
359
|
+
- **Checkpoint length**, `summary.length`. The setup wizard asks for it, and Settings → Summaries shows an example of each length as you pick it, plus **Try the selected length on your latest session** (generated with your summarizer, not saved).
|
|
360
|
+
- **Short** (~300 tokens) is the three lines from before: Doing, Stopped at, Next.
|
|
361
|
+
- **Informative** (~700 tokens, the new default) adds key decisions, open questions, the files touched and why, and what didn't work.
|
|
362
|
+
- **Long** (~1500 tokens) adds a short narrative recap and the next 3 steps.
|
|
363
|
+
- **`/cp --short` and `/cp --long`** pick the length for one checkpoint, and so does `leftoff checkpoint <id> --long`. A note still works alongside: `/cp --long trying the mock next`.
|
|
364
|
+
- **⌘↵ sends** in every Needs you text box: a question's typed answer, the Reply box and the Deny note. Ctrl+↵ does the same off a Mac. The button shows a small ⌘↵ hint. Plain Enter still adds a new line in the Reply box. It presses the same button as a click, so the same checks and rate limit apply, and a held key sends once.
|
|
365
|
+
- **Start Ollama from Settings**. When Ollama is installed but not running, the model picker has a **Start Ollama** button. It runs the start command on this machine only, waits for Ollama to answer, then refreshes the list.
|
|
366
|
+
|
|
367
|
+
### Changed
|
|
368
|
+
- **The checkpoint file** gains `### Key decisions`, `### Open questions`, `### Files touched`, `### What didn't work`, `### Recap` and `### Next steps` sections below the three lines, and `length:` in the front matter. The three lines are unchanged, so older checkpoints still read fine and older versions of Leftoff still read new ones. The board shows the extra sections, folded, on a session's page and in the Timeline. The board cards, the `/leftoff` pane and the morning digest still use only the three lines.
|
|
369
|
+
- **Without a model**, the fallback checkpoint at Informative or Long lists the files edited and the tool calls that failed, read straight off the transcript.
|
|
370
|
+
- **Remote sign-ins last 30 days after the last visit,** down from 400 days. Each visit moves the 30 days on. A sign-in (QR code or typed code) works once, for 15 minutes, down from 30.
|
|
371
|
+
|
|
372
|
+
### Fixed
|
|
373
|
+
- **The phone board over Tailscale.** We went through why it could fail to load or sign in:
|
|
374
|
+
- **The home-screen app could never sign in.** It has its own cookies; the typed code fixes that.
|
|
375
|
+
- **The sign-in cookie is now `SameSite=Lax`, not `Strict`.** A link opened from the Camera or Messages, and the home-screen app's own launch, are navigations Safari may treat as coming from another site, and then a Strict cookie isn't sent. Every API request is still checked against the board's own origin.
|
|
376
|
+
- **`leftoff tailscale on` now checks the address end to end** from the Mac, the way the phone will reach it: MagicDNS, the tailnet's HTTPS certificate, Tailscale Serve and the daemon. When something fails it says which part, for example "this Mac can't resolve …", "no valid HTTPS certificate yet", "the daemon refused the tailnet name" or "Tailscale couldn't reach the daemon". The first HTTPS request is also what makes Tailscale fetch the certificate, which can take up to a minute, so the phone doesn't have to wait for it. `leftoff tailscale status` runs the same check.
|
|
377
|
+
- **When the running daemon didn't take the new tailnet name,** `tailscale on` used to say it was added anyway. That happens when the shell and the background service use different folders, for example `CHECKPOINTS_DIR` set in one and not the other. Now it says the name didn't take, and why. Before, the phone got `403 bad_host`.
|
|
378
|
+
- **A refused address shows a page that says what to do** (run `leftoff tailscale on`) instead of a line of JSON.
|
|
379
|
+
- **A proxy that rewrites `Host`.** Tailscale Serve passes the phone's `Host` through, but a proxy that rewrites it to `127.0.0.1` (an older Serve build, a hand-made proxy) used to get `403 bad_host`. When `X-Forwarded-Host` names a tailnet name in `remoteHosts`, the request is now treated as remote: it signs in like any phone, and is never treated as this Mac.
|
|
380
|
+
- **The board and Settings scrolled sideways on a phone.** A long file path in a checkpoint could widen every card past the screen, and on Settings the theme switch was pulled into a second column by a rule meant for the header, pushing the whole form off the right edge. Both now stay one column wide, and the browser check fails if any phone page scrolls sideways.
|
|
381
|
+
- It's still Tailscale Serve only, never Funnel, and nothing edits `/etc/hosts`.
|
|
382
|
+
- **Settings model picker**: "Ollama is installed but not running. Start it with: [object Object]" now names the actual command, for example `open -a Ollama` or `ollama serve`.
|
|
383
|
+
|
|
384
|
+
## [0.4.5] - 2026-10-10
|
|
385
|
+
|
|
386
|
+
### Changed
|
|
387
|
+
- **Checkpoints is now Leftoff.** WTF was I working on? The old name clashed with Claude Code's own checkpoints (`/rewind`). The board, the terminal, the mod, notifications and the docs all say Leftoff now. The checkpoints themselves are still called checkpoints.
|
|
388
|
+
- **The command is `leftoff`.** `checkpoints` still works and does exactly the same thing, so scripts, aliases and muscle memory keep working: `leftoff go`, `leftoff status`, `leftoff upgrade` and so on.
|
|
389
|
+
- **A new icon:** the WTF speech bubble. It's on the board's header, in the browser tab (a red dot appears while something needs you) and as the iPhone home-screen icon (`apple-touch-icon`).
|
|
390
|
+
- **In Claude Code:** the pane is `/leftoff` (`/checkpoints` still opens it). `/cp`, `/cp-save` and `/cp-name` are unchanged. The plugin is now `leftoff@checkpoints`.
|
|
391
|
+
- **The board's address** is http://leftoff.localhost:7429/. The old http://checkpoints.localhost:7429/ keeps working.
|
|
392
|
+
- **`LEFTOFF_*` environment variables** work as aliases of the `CHECKPOINTS_*` ones, for example `LEFTOFF_DIR` and `LEFTOFF_PORT`. When both are set, `LEFTOFF_*` wins. A repo can be ignored with `.leftoff-ignore` as well as `.checkpoints-ignore`.
|
|
393
|
+
|
|
394
|
+
### Upgrading from Checkpoints
|
|
395
|
+
Run `checkpoints upgrade` (or `leftoff upgrade` once it's installed), then restart your Claude Code sessions. The upgrade takes care of everything else. On the way, 0.4.4's upgrade prints a warning that `claude plugin update checkpoints@checkpoints` failed. That's expected, because the plugin has a new name, and the next lines move it:
|
|
396
|
+
- **Your folder stays where it is.** Nothing in it is moved or renamed: `~/checkpoints`, or wherever `CHECKPOINTS_DIR` or setup points. `CHECKPOINTS_DIR` is still honored.
|
|
397
|
+
- **Login service:** macOS's `dev.checkpoints.daemon` LaunchAgent is unloaded and removed, and replaced with `dev.leftoff.daemon` (still `ProcessType Interactive`, on the same folder). On Linux, `checkpoints.service` becomes `leftoff.service`. Exactly one daemon runs afterwards.
|
|
398
|
+
- **The mod:** `checkpoints@checkpoints` is uninstalled and `leftoff@checkpoints` is installed from the same marketplace. If you had disabled it, it stays disabled.
|
|
399
|
+
- **Settings hooks (hooks-only mode):** the `--tag=checkpoints-hook` entries are replaced with `--tag=leftoff-hook` ones, one per event with no duplicates. Hooks of your own in the same file are left alone. Until you upgrade, the old entries keep working, because `checkpoints` is still installed.
|
|
400
|
+
- **The GitHub repo and install line are unchanged** for now: `npm i -g github:joelash/checkpoints`. The npm package keeps its internal name `checkpoints`, because npm won't install a package called `leftoff` over one that owns the `checkpoints` command. It installs both commands.
|
|
401
|
+
- `leftoff doctor` points out anything still using an old name (the login service or settings hooks).
|
|
402
|
+
|
|
403
|
+
## [0.4.4] - 2026-10-10
|
|
404
|
+
|
|
405
|
+
### Added
|
|
406
|
+
- **Needs you: answer Claude from the board.** When a session stops on a permission prompt or a question, it shows up on the board and you can answer it there. Claude Code's own dialog in the terminal stays live the whole time, and whichever answers first wins.
|
|
407
|
+
- **Permission prompts:** **Allow once**, **Allow for this session** (kept to that Claude Code session, never written to your settings), or **Deny…** with an optional note that Claude reads.
|
|
408
|
+
- **Questions (AskUserQuestion):** the options are buttons, with a text box for a typed answer.
|
|
409
|
+
- **Reply:** when Claude ends its turn on a question, a reply box sends your text as its next prompt. A reply sent while Claude is still working waits for the turn to end. Replies need the mod; in hooks-only mode the card offers **Go** instead.
|
|
410
|
+
- **Answered in the terminal, or Esc:** the board item clears by itself within about a second. Nothing on the board ever answers on its own, and a request no one answers falls back to the terminal (the hook gives up at 9.5 minutes, before Claude Code's 10-minute hook timeout).
|
|
411
|
+
- **A Needs you strip** at the top of the board, longest wait first, whatever the status and repo filters. Cards that are waiting get a badge and show what's being asked (the tool and command or file, or the question). A session's page shows it too, and the `/checkpoints` pane leads with how many sessions need you.
|
|
412
|
+
- **A Needs you tab** (`#/waiting`) with a live count in the nav: everything still waiting with the answer buttons, the board's repo chips, and **Recently answered**, which shows what happened while you were away (answered on the board or in the terminal, cancelled, timed out) and from where. In a background browser tab the title gets a "(N) " prefix and the icon a red dot.
|
|
413
|
+
- **A macOS notification** when a session has waited 10 seconds without an answer, with the session name and what it's asking. Settings → Notify me turns it off (`notify.needsYou`). With `terminal-notifier` installed, clicking the notification runs `checkpoints go` for that session; a plain `osascript` notification can't run anything when clicked.
|
|
414
|
+
- **Settings hooks for hooks-only mode:** `checkpoints init --hooks` now also adds `PermissionRequest` (timeout 600 s, waits for the board) and `PostToolUse` (in the background, to notice an answer in the terminal). `checkpoints upgrade` adds them when settings hooks are installed.
|
|
415
|
+
|
|
416
|
+
### Security
|
|
417
|
+
- **Answering is from this machine only by default.** Over Tailscale, other devices see what's waiting but can't answer it. Settings → Remote access → **Allow answering permission prompts and questions from other devices** (`remote.allowAnswers`, off) changes that, comes with a warning, and can only be changed on the machine running Checkpoints.
|
|
418
|
+
- Every board answer is appended to the session's `answers.md` (what was asked, the answer, when, and from where), answers use the same token, host and origin checks as the rest of the API, and they're rate limited (30 a minute).
|
|
419
|
+
|
|
420
|
+
### Changed
|
|
421
|
+
- `GET /v1/sessions` carries `needsYou` per session and a `viewer` block; new endpoints are listed under API in the README.
|
|
422
|
+
|
|
423
|
+
## [0.4.3] - 2026-10-10
|
|
424
|
+
|
|
425
|
+
### Added
|
|
426
|
+
- **A model picker that's a short list, not a dropdown.** Settings → Summarizer shows a few suggested models as cards for whichever provider you pick. Each card has the name, what it's good for, and its size or price, with the current one selected. The cards are the same layout everywhere:
|
|
427
|
+
- **Ollama (local):** the four suggestions (Qwen 2.5 3B is the default suggestion), plus any other model already on the machine. A suggestion you don't have yet has **Download & use**. It shows a progress bar with MB/s and a Cancel button, then switches the summarizer to that model when the download finishes. Only these four can be downloaded from the board, and only from the machine running Checkpoints, not over Tailscale. If Ollama isn't running, the picker says so and shows how to start it.
|
|
428
|
+
- **Ollama cloud:** its four suggestions. They need an API key, and the key field appears.
|
|
429
|
+
- **Anthropic API:** Claude Haiku 5.5 and Claude Sonnet 5.5, with list prices. The ids are from Anthropic's models page (checked 2026-10-10), and `haiku`/`sonnet` now mean these.
|
|
430
|
+
- **Claude Code:** Haiku or Sonnet on your Claude plan, with no key.
|
|
431
|
+
- **Other model…** is folded away for any other name.
|
|
432
|
+
- **Digest schedule settings.** Settings → Digest now shows:
|
|
433
|
+
- the time zone the time is in, like "America/Chicago (CDT)", with a note when your browser is in a different zone;
|
|
434
|
+
- day-of-week toggles with **Every day** and **Weekdays** presets;
|
|
435
|
+
- **Skip days with no activity** (on by default), so no empty digest is written;
|
|
436
|
+
- an optional time zone override (`digest.timeZone`).
|
|
437
|
+
The Digest tab says "No activity today (Saturday), so no digest was written" or that today isn't a digest day, and when the next one is. Config keys: `digest.days`, `digest.skipEmpty`, `digest.timeZone`.
|
|
438
|
+
- **Timeline filters.** The Timeline has the board's repo chips, with the same remembered pick. It also has a session picker, a "× session: …" pill, and **All / Manual / Automatic / Closed**. Clicking a session's name in an entry shows only that session's checkpoints. The link form is `#/timeline?repo=…&session=…&trigger=…`. A session's page has **Open in Timeline**.
|
|
439
|
+
- **Icons** on the nav tabs and main buttons (Lucide, ISC license, in `ui/vendor/LUCIDE-LICENSE.txt`). On a phone the five tabs are icon over a short label in one row.
|
|
440
|
+
|
|
441
|
+
### Changed
|
|
442
|
+
- **A whole card opens its session.** Click anywhere on a card that isn't a button or link (⌘-click opens a new tab). Each card also has a "N checkpoints →" link, so the per-session history is easy to find.
|
|
443
|
+
|
|
444
|
+
### Fixed
|
|
445
|
+
- Download progress bars and other widths no longer rely on inline styles, which the board's content security policy blocks.
|
|
446
|
+
|
|
447
|
+
## [0.4.2] - 2026-10-09
|
|
448
|
+
|
|
449
|
+
### Added
|
|
450
|
+
- **Filter the board by repo.** A row of repo chips sits under the status counts: **All**, then each repo with its session count, most recently active first. Click one to see only that repo, click again to clear it, and shift- or ⌘-click to pick several. It works together with the status counts (**Needs you** in `clutch_hunt`), and each count follows the other filter. Past 8 repos, the rest are under **More ▾** with a box to type a name. Clicking the repo name on a card filters to it too.
|
|
451
|
+
- The pick is saved in your browser and in the address (`#/?repo=clutch_hunt`), so it survives a refresh and you can bookmark it.
|
|
452
|
+
- A session's repo is its git root (recorded by the mod, or found by walking up to the nearest `.git`), else its folder. Chips show the folder name with the full path on hover, and two repos with the same name get their parent folder (`colorGenie/expo`, `food_fight/expo`).
|
|
453
|
+
- The Digest tab has the same chips for the repos in that digest.
|
|
454
|
+
- `checkpoints status --repo hunt` and `checkpoints go --repo clutch_hunt` (exact name or part of one), and **f** in the `/checkpoints` pane steps through repos. The `go` picker's typing also matches repo names and paths.
|
|
455
|
+
|
|
456
|
+
### Fixed
|
|
457
|
+
- **The Digest tab could stay empty all day.** The automatic digest was made once a day: at 7:00, or at the daemon's first start after 7:00. If that happened before any session had been tracked (a fresh install, a restart, an early morning), it wrote "no sessions" and that stood for the rest of the day. Now an empty digest is made again once there are sessions. The check still runs every 15 seconds, so a Mac that slept through 7:00 gets its digest right after it wakes.
|
|
458
|
+
- **A failed digest is now visible.** It used to be one line in the daemon log. The Digest tab shows the error, `checkpoints doctor` has a digest line, and the schedule waits 10 minutes before trying again instead of retrying every 15 seconds.
|
|
459
|
+
- The Digest tab says when the next automatic digest will be made, and its empty state has **Generate now**.
|
|
460
|
+
- **Settings → Updates → Check now no longer drops you on the Board.** It was a link to `#`, so after the "You're up to date" toast the page navigated to the board. It's a button now, and board actions never follow a `#` link.
|
|
461
|
+
- **The header tabs no longer wrap at phone width.** On a phone, the header is three rows: logo and status, tabs (scrolling sideways if they don't fit), and search. The status counts are one compact row too.
|
|
462
|
+
|
|
463
|
+
### Changed
|
|
464
|
+
- Digest items use the same short, unique repo names as the board.
|
|
465
|
+
- `GET /v1/sessions` returns `repoKey` and `repoLabel` for each session, and `?repo=` takes a label, folder name or full path, comma-separated. `/v1/health` has a `digest` block (`time`, `nextAt`, `today`, `lastMadeAt`, `lastError`).
|
|
466
|
+
|
|
467
|
+
## [0.4.1] - 2026-10-09
|
|
468
|
+
|
|
469
|
+
### Fixed
|
|
470
|
+
- **The board's Go took 10+ seconds with Ghostty, and the button just quietly came back.** On a real Mac with about 10 tabs, Go from the board took 6–13 seconds. The same AppleScript from a terminal took about 0.2 seconds, so the delay was in how the daemon runs. Three changes:
|
|
471
|
+
- On macOS the login service is now an Interactive LaunchAgent (`ProcessType = Interactive`). Without it, launchd throttles the daemon and the `osascript` it starts. `checkpoints upgrade` rewrites the plist and reloads it, and `doctor` flags an old one.
|
|
472
|
+
- Go no longer uses AppleScript's `activate`, which waits for the app to come to the front and can stall when the browser is the active app. Ghostty's own `focus` brings its window forward. Go checks afterwards and uses `open -a Ghostty` only if Ghostty didn't come to the front.
|
|
473
|
+
- Ghostty is no longer searched on every click (see below).
|
|
474
|
+
- **Go no longer asks Ghostty for `tty` on 1.3.0 and 1.3.1,** which don't have it. Whether a Ghostty has `tty` is read from its own dictionary in the app bundle (`Ghostty.sdef`), or else from its version.
|
|
475
|
+
|
|
476
|
+
### Changed
|
|
477
|
+
- **Go with Ghostty is one Apple event.** The daemon remembers each session's Ghostty terminal id, and Go runs `focus terminal id "<id>"`. It fills the map with one batched listing of every terminal's id, title and folder, matched in Checkpoints by the `cp:<id>` title, then tty where Ghostty has it, then folder. A folder with several tabs still counts if only one of them has a Claude Code title. The listing runs:
|
|
478
|
+
- when a session starts or is renamed,
|
|
479
|
+
- when the board opens,
|
|
480
|
+
- every 60 seconds, but only while the board is open or Go was used recently,
|
|
481
|
+
- and only once macOS has allowed the daemon to control Ghostty.
|
|
482
|
+
|
|
483
|
+
If a remembered tab is gone, Go lists the terminals once, finds the tab and updates the map. `checkpoints go` uses the remembered id too.
|
|
484
|
+
- **Go tells you what's happening.**
|
|
485
|
+
- On the board, the button says **Switching…** right away. After 2 seconds a note says the terminal is taking a moment. Then a toast gives the result and how long it took, with a per-step breakdown when it was slow, and the button shows ✓ or ✗ for a few seconds.
|
|
486
|
+
- `checkpoints go` prints `Switching to … in Ghostty…`, then the result with the elapsed time. `--debug` prints each step's milliseconds.
|
|
487
|
+
- The `/checkpoints` pane shows "Switching…" and then the result with the time.
|
|
488
|
+
- The daemon logs every Go with per-step timings.
|
|
489
|
+
- **Every step of a Go has an 8-second limit.** A step that runs over is stopped, and Go says the terminal didn't answer. The tab may still switch a moment later, because the request already reached the app. Only one Go runs at a time, and a double click waits for the first.
|
|
490
|
+
- `checkpoints doctor` has a **Go speed** line that times `osascript` start-up and the batched Ghostty listing, both from your terminal and from inside the daemon, without switching anything.
|
|
491
|
+
- tmux sessions hosted in iTerm2 or Terminal.app are brought forward with `open -a`, like Ghostty, WezTerm and kitty already were.
|
|
492
|
+
|
|
493
|
+
## [0.4.0] - 2026-10-09
|
|
494
|
+
|
|
495
|
+
### Added
|
|
496
|
+
- **Open or closed, for real.** At session start the mod records Claude Code's process id and tty. The daemon checks every 30 seconds that the process is still running. The board shows a green dot for an open session and a hollow grey one for a closed session, with "closed 2h ago". A session whose terminal was closed now ends right away, so its final checkpoint is written right away too, instead of waiting for the heartbeat to go stale. A recycled process id isn't mistaken for the same session. Sessions from other machines still go by their heartbeat, and the board labels them "open (heartbeat)".
|
|
497
|
+
- **Every tab is named.** The mod names each session's tab `<session name> · cp:<id>`, for example `auth refactor · cp:7a1e0b`, and renames it when the session gets its title, when you run `/cp-name`, or when you rename it on the board. Turn this off with `"tabTitle": false`, or change the format with `"tabTitleFormat"`. It uses Claude Code's own `/rename`, so Claude Code keeps the ✳ spinner in front of the name and keeps the title up to date.
|
|
498
|
+
- **Go: jump back to a session's tab.** There's a **Go ↗** button on every open session on the board, and in a terminal you can run `checkpoints go auth`. It fuzzy-matches the name, project, branch, folder or id, and with no query it opens an arrow-key picker of open sessions. In the `/checkpoints` pane, `g` jumps to the selected row (`j`/`k` move the selection) and `1`–`9` jump straight to a row. Supported terminals:
|
|
499
|
+
- **Ghostty 1.3+**, through its AppleScript. Go finds the tab by its `cp:<id>` title, or by tty on newer builds, or by folder when only one tab is in it.
|
|
500
|
+
- **iTerm2**, by session id.
|
|
501
|
+
- **WezTerm**, with `wezterm cli activate-pane`.
|
|
502
|
+
- **kitty**, with `kitten @ focus-window`. This needs remote control turned on.
|
|
503
|
+
- **tmux**, by selecting the pane and then bringing its terminal forward.
|
|
504
|
+
- **Terminal.app**, by tty.
|
|
505
|
+
- `checkpoints doctor` shows which terminal you're in, whether Go supports it, and whether macOS allows automation from your terminal and from the daemon. The first run may show the macOS permission prompt. `checkpoints status` shows each session as open or closed, with its `cp:` id.
|
|
506
|
+
|
|
507
|
+
### Security
|
|
508
|
+
- The board's Go button only works from a browser on the same machine. It's refused for browsers coming in through Tailscale, which get a disabled button, and it's rate-limited.
|
|
509
|
+
- A request that came through a local proxy (it carries `X-Forwarded-*` or Tailscale headers) is never treated as coming from this machine, whatever Host it claims. Before this change, a tailnet browser could send `Host: 127.0.0.1:7429` through `tailscale serve` and be handed the local cookie.
|
|
510
|
+
|
|
511
|
+
## [0.3.3] - 2026-10-09
|
|
512
|
+
|
|
513
|
+
### Fixed
|
|
514
|
+
- **`checkpoints upgrade` said "Authentication failed" on a private repo when your GitHub login is SSH.** The update check and `upgrade` try `https://github.com/…` first and now fall back to `git@github.com:…` over SSH. You can also pin it with `"updates": { "source": "git@github.com:joelash/checkpoints.git" }` in config.json.
|
|
515
|
+
- GitHub now marks `bin/checkpoints.js` executable, which fixes `permission denied: checkpoints` after `npm link` in a fresh clone.
|
|
516
|
+
|
|
517
|
+
## [0.3.2] - 2026-10-09
|
|
518
|
+
|
|
519
|
+
### Fixed
|
|
520
|
+
- **`npm i -g github:joelash/checkpoints` failed with `ENOTDIR` (git dep preparation failed).** A `prepare` script made npm run a nested install for the git dependency, and under `-g` that nested install tried to install globally too. The script is gone. `setup` and `upgrade` record the installed commit themselves.
|
|
521
|
+
|
|
522
|
+
## [0.3.1] - 2026-10-09
|
|
523
|
+
|
|
524
|
+
### Added
|
|
525
|
+
- **View the board from your phone or other machines with Tailscale.** `checkpoints tailscale on` runs `tailscale serve` to give the board an https address on your tailnet, such as https://studio.tail1234.ts.net/. It also prints a one-time login link. Open the link once on each device and that browser stays signed in. `checkpoints tailscale link` makes another link, `revoke` signs every remote browser out, `off` turns it all off, and `status` shows where things stand. Settings → Remote access shows the address too.
|
|
526
|
+
- The daemon answers the names listed in the new `remoteHosts` config setting, and every other name is refused as before. Changes apply without a restart.
|
|
527
|
+
- `checkpoints status` and `doctor` show whether remote access is on.
|
|
528
|
+
|
|
529
|
+
### Security
|
|
530
|
+
- Remote browsers sign in with a one-time link that expires after 30 minutes. They don't get a cookie just by loading the page. Their cookies are `Secure`, and they can't change remote-access settings. Set `"remoteRequireToken": false` to let anyone on your tailnet in without a link.
|
|
531
|
+
- Tailscale Funnel, which would put the board on the public internet, is deliberately not supported. The daemon refuses Funnel traffic even if Funnel is turned on by hand.
|
|
532
|
+
|
|
533
|
+
## [0.3.0] - 2026-10-09
|
|
534
|
+
|
|
535
|
+
### Changed
|
|
536
|
+
- **`/checkpoint` is now `/cp`, and `/name` is now `/cp-name`.** Claude Code already owns both names: its built-in `/rewind` answers to `/checkpoint`, and `/rename` answers to `/name`. `/checkpoints` (the pane) is unchanged.
|
|
537
|
+
|
|
538
|
+
### Added
|
|
539
|
+
- **Ollama cloud** as a summarizer: Ollama's hosted models (`gpt-oss:20b`, `gemma4:31b` and more) with an ollama.com API key. Pick it in `checkpoints setup`, in Settings (with a key box), or with `checkpoints models use <model> --cloud`. The key goes in `.secret/`, never in config.json.
|
|
540
|
+
- **Light, dark and system themes.** The switch is in the header and under Settings → Appearance. System follows your OS as it changes.
|
|
541
|
+
- **`checkpoints open`** reopens the board and starts the daemon if it isn't running. The board also answers at http://checkpoints.localhost:7429/ with no `/etc/hosts` edit. `checkpoints status` prints the URL, and the `/checkpoints` pane has an **Open board** button (press `b`). `/checkpoints open` works too.
|
|
542
|
+
- **Updates:** `checkpoints upgrade` updates the CLI and the mod, restarts the daemon and shows what changed. Once a day the daemon checks whether a newer version is out; when one is, the board, `checkpoints status`, `doctor` and the pane say so. Turn it off in Settings. Nothing installs unless you run the command.
|
|
543
|
+
- **What's new** on the board (the link next to the version in the header), with new entries marked until you've seen them. `checkpoints changelog` prints the same thing in the terminal.
|
|
544
|
+
- `checkpoints doctor` lists the slash commands the mod registered, and the board warns when one couldn't register.
|
|
545
|
+
|
|
546
|
+
### Fixed
|
|
547
|
+
- The arrow on the Settings dropdowns sat against the edge. Dropdowns now match the other fields.
|
|
548
|
+
|
|
549
|
+
## [0.2.0] - 2026-10-09
|
|
550
|
+
|
|
551
|
+
### Added
|
|
552
|
+
- **One-command setup:** `npx github:joelash/checkpoints setup`. It checks Node and Claude Code, installs the mod with `claude plugin`, helps you pick a summarizer, starts the daemon and opens the board. It's safe to re-run.
|
|
553
|
+
- **Local models in setup:** finds Ollama or LM Studio, offers to install Ollama, suggests four small models with their sizes, pulls your pick with a progress bar and writes a sample checkpoint so you can see the result.
|
|
554
|
+
- `checkpoints models` lists, pulls, switches and tests local models later.
|
|
555
|
+
- `--yes` and flags let you script setup.
|
|
556
|
+
|
|
557
|
+
## [0.1.0] - 2026-10-09
|
|
558
|
+
|
|
559
|
+
### Added
|
|
560
|
+
- The Claude Code mod: tracks every session, writes a 3-line checkpoint (Doing, Stopped at, Next) when a session goes quiet, and adds `/checkpoints`, a pane with every open session.
|
|
561
|
+
- The local daemon and web board: session cards grouped by what needs you, a checkpoint timeline, search, a daily digest and Settings.
|
|
562
|
+
- Markdown in `~/checkpoints` as the source of truth, safe to sync across machines.
|
|
563
|
+
- Summarizers: your Claude login, an Anthropic key, Ollama or LM Studio, or no model at all.
|
|
564
|
+
- A settings-hooks fallback for setups where mods can't load.
|