@leftoff/cli 0.9.8-beta.9 → 0.9.9-beta.1

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/docs/SECURITY.md CHANGED
@@ -79,7 +79,7 @@ You deploy the relay yourself, from `relay/` in this repo, to your own Cloudflar
79
79
 
80
80
  - **On the registry record of each signed-up name: the email address, `plan: "beta"`, `via: "signup"`, the Mac's name and its public signing key, and when it was created.** The email is there for the confirmation link, for "one name per email", and for billing later; it is not shown on the board or sent anywhere else. Names made with the admin token keep `plan: "free"` and no email.
81
81
  - **Pending signups**, for up to 30 minutes plus a day after they expire or are confirmed (so a polling CLI hears what happened): the name, email, Mac name and public key, and **SHA-256 hashes** of the link token and of the CLI's poll secret. The tokens themselves are never stored.
82
- - **Rate-limit stamps** for an hour: a SHA-256 hash of the requester's IP address, and the email address, with the times of their signups (5 an hour per IP, 3 per email).
82
+ - **Rate-limit stamps** for an hour: SHA-256 hashes of the requester's IP address and of its /24 (IPv4) or /64 (IPv6) network, and the email address (with any `+tag` dropped, and dots too for Gmail), with the times of their signups (5 an hour per IP, 15 per network, 3 per email, 200 across the whole relay). A refused signup says which name an email already has only to a Mac that is one of that name's own.
83
83
  - **The confirmation email goes through Resend** (the relay's `RESEND_API_KEY`), so Resend sees the address, the subject and the link, the same as for the site's "Claim your name" emails.
84
84
  - **leftoff.dev sees the name**, when the relay asks who claimed it, and **the name and email** when the relay tells it a signup was confirmed. Both calls carry the shared `RELAY_LOOKUP_KEY` secret as a bearer token; the site's answer to the lookup is the claim's status and email, which the relay compares and doesn't store.
85
85
  - Confirming takes a **POST from the page's Confirm button**; opening the link (a GET) never confirms, so mail scanners and link previews can't. The name is then bound to the Mac's public key from the signup request: only a Mac holding the matching private key can connect as that Mac, exactly like `relay register`.
@@ -106,6 +106,19 @@ Live link is opt-in, per session: only sessions started with `leftoff live` have
106
106
  - **Outside events** need this Mac's event key (`~/.config/checkpoints/secrets/live-events`, mode 600, made by `leftoff live key`, replaced with `--new`). Locally that's a Bearer key. Through the relay only an HMAC signature is accepted (5-minute window, replays refused), so the relay can pass an event along but can't read the key, forge an event or change one. Both the relay and the Mac rate-limit, and bodies are capped at 16 KB. Claude's instructions call an event information, not a command, but it is still text Claude reads: anyone with the key can put words in front of your session. Keep the key in your CI's secrets.
107
107
  - **Remote Restore** opens Ghostty tabs, so a phone or another Mac can only ask. The Mac shows a notification and a card, and only that Mac's own browser can confirm. Requests expire after 5 minutes, at most 3 wait at once, and they're rate-limited.
108
108
 
109
+ ### What it did: tool output and the diff (0.9.8 beta)
110
+
111
+ The session page's **What it did** shows the last tools a session ran with what they printed, and **This folder's changes** (the session's folder's uncommitted changes). This puts command output and file contents on the board and on your phone, so:
112
+
113
+ - **Same doors as the rest of the board.** `GET /v1/sessions/:id/tools` and `GET /v1/sessions/:id/diff` sit beside `/turns`, behind the same Host, Origin and token checks. Through the relay they only travel sealed end to end, like every board call. Another Mac's session is read live on that Mac over the signed, encrypted peer link, and only when that Mac allows other machines (they're not open actions).
114
+ - **Nothing is cached or synced.** Tool output and diffs are read when you open the section (or tap ↻) and never written anywhere: not the Leftoff folder, not a cache. If the session's Mac is offline you get a short note, not an old copy.
115
+ - **Key and env files are never shown.** The contents of `.env*`, `*.pem`, `*.key` (and `*.p12`/`*.pfx`) and SSH keys (`id_rsa`, `id_ed25519`, …) are hidden: a read of one, a command that names one (`cat .env`), and their diff lines. They're listed with +/− counts only. Grep-style hits inside them are blanked too.
116
+ - **Drawn as text, never as HTML.** The board draws a file's changes (and an edit's) node by node with `textContent`; no line from a file is ever parsed as HTML. An edit's change is built on the session's own Mac from the edit itself, redacted the same way, and not for a key or env file.
117
+ - **Redaction on everything else.** Before output or a diff line leaves the Mac it came from, built-in patterns blank values that look like secrets: `NAME=value` lines whose name says SECRET, TOKEN, PASSWORD, API_KEY, …; quoted passwords; `Bearer`/`Basic` credentials; passwords in URLs; npm tokens; GitHub, AWS, Slack, Stripe, OpenAI/Anthropic and Google keys; JWTs; private key blocks. Your `redact.patterns` apply on top. Redaction is a safety net, not a guarantee: a secret in a shape no pattern knows can still show. Binary output isn't shown.
118
+ - **git runs safely.** An argument list (no shell), fixed arguments, `core.fsmonitor=false`, `--no-ext-diff --no-textconv`, no pager, `GIT_OPTIONAL_LOCKS=0` (it never takes `index.lock` from your own git), and a 3 s budget with the process killed past it. A file's lines are only read for a path git itself lists as changed (a literal pathspec after `--`), so the endpoint can't be used to read other files; an untracked symlink shows its target, never what it points to. One honest limit: like `git status`, `git diff` applies your repo's own clean filters (git-lfs, for example) to changed files.
119
+ - **Sizes.** About 8 KB of output per tool in the list and 64 KB for one; 300 files and 64 KB of lines per file; past that "too big to show here". A board answer through the relay is capped at 1 MB, and one that still comes out over the relay's 2 MB frame limit after sealing fails at once with "too big" (before, the relay dropped it and the phone waited for the timeout).
120
+ - **The starting commit.** A new session records the commit its folder was on (read from git's files) in its session record, which syncs. It's a commit id, not code.
121
+
109
122
  ### Logging
110
123
 
111
124
  The Worker never logs request or response bodies (it can't read them anyway) and stores only what's listed above, in your Durable Object. `wrangler.toml` turns Workers observability (request logs) off. Cloudflare's edge keeps its usual metadata for any site (IP, hostname, path such as `/_relay/api`, status), under its own policies.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "leftoff",
3
- "version": "0.9.8-beta.9",
3
+ "version": "0.9.9-beta.1",
4
4
  "description": "Leftoff (formerly Checkpoints): WTF was I working on? Tracks every Claude Code session on this machine, writes a 3-line checkpoint when one goes quiet, and adds /cp (checkpoint now), /cp-name and a /leftoff pane. Talks to the local Leftoff daemon on 127.0.0.1:7429 and falls back to writing markdown in ~/checkpoints.",
5
5
  "author": {
6
6
  "name": "Joel"
package/mod/hooks/core.js CHANGED
@@ -310,6 +310,16 @@ export function wasLiveSession(session) {
310
310
  if (!session || isCodex(session) || (session.backend && session.backend.kind === 'ollama')) return false
311
311
  return !!(session.wasLive || (session.liveLink && session.liveLink.on))
312
312
  }
313
+ // 0.9.8: the same resume, but as Live (Leftoff's channel), for a Claude Code
314
+ // session that wasn't Live: exit it, then run this. null when it already resumes
315
+ // Live, or can't be Live (Codex, Ollama).
316
+ export function liveResumeCommand(session) {
317
+ if (!session || isCodex(session) || (session.backend && session.backend.kind === 'ollama') || wasLiveSession(session)) return null
318
+ const id = session.id || session.sessionId
319
+ const cwd = resumeDir(session)
320
+ const wt = resumeWorktreeName(session)
321
+ return (cwd ? 'cd ' + shArg(cwd) + ' && ' : '') + liveInvocation([...(wt ? ['--worktree', wt] : []), '--resume', id])
322
+ }
313
323
  export function liveInvocation(args = []) { return 'leftoff live --' + (args.length ? ' ' + args.map(shArg).join(' ') : '') }
314
324
 
315
325
  // 0.8.0: a Codex session (its rollout under ~/.codex/sessions) resumes with `codex resume <id>`.
@@ -28,7 +28,7 @@ const END_TIMEOUT_MS = 1000
28
28
  const MODEL_TIMEOUT_MS = 20000
29
29
 
30
30
  // Module state. $.state is not used: the pane redraws with $.ui.invalidate.
31
- const MOD_VERSION = '0.9.8-beta.9' // keep in step with .claude-plugin/plugin.json
31
+ const MOD_VERSION = '0.9.9-beta.1' // keep in step with .claude-plugin/plugin.json
32
32
 
33
33
  const st = {
34
34
  internal: false,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@leftoff/cli",
3
- "version": "0.9.8-beta.9",
3
+ "version": "0.9.9-beta.1",
4
4
  "description": "Leftoff: WTF was I working on? Tracks every Claude Code session, writes a 3-line checkpoint when one goes quiet, and gives you a daily digest and a board, on your Mac and (optionally) your phone.",
5
5
  "keywords": [
6
6
  "claude",