@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.
Files changed (47) hide show
  1. package/.claude-plugin/marketplace.json +14 -0
  2. package/CHANGELOG.md +564 -0
  3. package/LICENSE.md +9 -0
  4. package/README.md +28 -2
  5. package/assets/sample-transcript.jsonl +12 -0
  6. package/bin/checkpoints.js +7 -0
  7. package/bin/leftoff.js +585 -0
  8. package/hooks/README.md +22 -0
  9. package/hooks/settings-hooks.example.json +82 -0
  10. package/mod/.claude-plugin/plugin.json +15 -0
  11. package/mod/hooks/core.js +1397 -0
  12. package/mod/hooks/hooks.json +4 -0
  13. package/mod/hooks/register.js +1006 -0
  14. package/package.json +45 -4
  15. package/templates/agent-feed-worker/README.md +45 -0
  16. package/templates/agent-feed-worker/src/index.js +69 -0
  17. package/templates/agent-feed-worker/wrangler.toml +20 -0
  18. package/ui/app.css +1282 -0
  19. package/ui/app.js +2979 -0
  20. package/ui/apple-touch-icon.png +0 -0
  21. package/ui/changelog-md.js +64 -0
  22. package/ui/favicon-32.png +0 -0
  23. package/ui/favicon-badge-32.png +0 -0
  24. package/ui/favicon-badge.svg +1 -0
  25. package/ui/favicon.ico +0 -0
  26. package/ui/favicon.svg +1 -0
  27. package/ui/go.js +68 -0
  28. package/ui/icon-192.png +0 -0
  29. package/ui/icon-512.png +0 -0
  30. package/ui/icon-maskable-512.png +0 -0
  31. package/ui/icons.js +41 -0
  32. package/ui/index.html +55 -0
  33. package/ui/manifest.webmanifest +37 -0
  34. package/ui/needs.js +197 -0
  35. package/ui/offline.css +11 -0
  36. package/ui/offline.html +24 -0
  37. package/ui/pair.html +31 -0
  38. package/ui/picker.js +294 -0
  39. package/ui/relay-client.js +187 -0
  40. package/ui/relay-crypto.js +155 -0
  41. package/ui/relay-pair.js +48 -0
  42. package/ui/repos.js +167 -0
  43. package/ui/settings-nav.js +23 -0
  44. package/ui/summary-examples.js +66 -0
  45. package/ui/sw.js +34 -0
  46. package/ui/theme.js +41 -0
  47. package/ui/vendor/LUCIDE-LICENSE.txt +45 -0
package/package.json CHANGED
@@ -1,6 +1,47 @@
1
1
  {
2
2
  "name": "@leftoff/cli",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.6.3",
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
+ "keywords": [
6
+ "claude",
7
+ "claude-code",
8
+ "sessions",
9
+ "checkpoints",
10
+ "digest",
11
+ "cli"
12
+ ],
13
+ "homepage": "https://leftoff.dev",
14
+ "bugs": {
15
+ "email": "hello@leftoff.dev"
16
+ },
17
+ "license": "UNLICENSED",
18
+ "type": "module",
19
+ "bin": {
20
+ "leftoff": "bin/leftoff.js",
21
+ "checkpoints": "bin/checkpoints.js"
22
+ },
23
+ "files": [
24
+ "bin",
25
+ "ui",
26
+ "mod",
27
+ ".claude-plugin",
28
+ "assets",
29
+ "templates",
30
+ "hooks",
31
+ "CHANGELOG.md",
32
+ "README.md",
33
+ "LICENSE.md"
34
+ ],
35
+ "engines": {
36
+ "node": ">=20"
37
+ },
38
+ "os": [
39
+ "darwin",
40
+ "linux"
41
+ ],
42
+ "leftoffBuild": "npm",
43
+ "dependencies": {},
44
+ "publishConfig": {
45
+ "access": "public"
46
+ }
47
+ }
@@ -0,0 +1,45 @@
1
+ # Leftoff agent feed: Cloudflare Worker template
2
+
3
+ A tiny private Worker that holds one `leftoff-agent-feed/1` feed, so an agent (or any script) can put its work on your Leftoff board. The full guide, with alternatives, is [docs/agent-feed-hosting.md](../../docs/agent-feed-hosting.md).
4
+
5
+ - `GET /feed.json` needs the **read key** (`Authorization: Bearer <key>`, or `?key=`). It sends an `ETag` and answers `304` when nothing changed.
6
+ - `PUT /feed.json` needs the **write key**. Only whoever holds it can update the feed.
7
+ - No secrets in this folder: `wrangler.toml` holds only the SHA-256 hashes of the two keys.
8
+
9
+ It runs comfortably on Cloudflare's free plan: one KV read pair per pull, every 2 minutes.
10
+
11
+ ## Deploy
12
+
13
+ ```sh
14
+ cp -R templates/agent-feed-worker ~/my-agent-feed && cd ~/my-agent-feed
15
+
16
+ leftoff agents keygen # prints a read key, a write key and their hashes
17
+ npx wrangler kv namespace create DATA # prints an id
18
+ # paste the id and the two hashes into wrangler.toml
19
+ npx wrangler deploy
20
+ ```
21
+
22
+ `wrangler deploy` prints the address, like `https://leftoff-agent-feed.<you>.workers.dev`. Your feed is that plus `/feed.json`.
23
+
24
+ Then publish a feed and add it to Leftoff:
25
+
26
+ ```sh
27
+ pbpaste | leftoff agents publish feed.json --url https://leftoff-agent-feed.<you>.workers.dev/feed.json --write-key -
28
+ pbpaste | leftoff agents add myagent https://leftoff-agent-feed.<you>.workers.dev/feed.json --key -
29
+ ```
30
+
31
+ (Copy the write key before the first line and the read key before the second.)
32
+
33
+ ## Your own domain
34
+
35
+ Uncomment `routes` in `wrangler.toml`, set your hostname (on a zone in the same Cloudflare account), and deploy. Cloudflare creates the DNS record for a custom domain itself; no DNS changes by hand.
36
+
37
+ If the first deploy with a route says **"This Worker does not exist on your account [code: 10007]"** right after "Uploaded …", the upload worked and the route step ran a moment too early. Run `npx wrangler deploy` again. If it still fails, comment `routes` out, deploy, then add the domain in the dashboard: Workers & Pages → your Worker → Settings → Domains & Routes → Add → Custom domain.
38
+
39
+ ## Rotating a key
40
+
41
+ Run `leftoff agents keygen` again, paste the new hash(es), deploy. Then give the new read key to Leftoff (`leftoff agents add <name> <url> --key -` again) or the new write key to the publisher.
42
+
43
+ ## Committing this folder
44
+
45
+ Fine to commit, including `wrangler.toml`: the KV id is not a secret (only your Cloudflare account can use it), and the key hashes can't be turned back into keys. Never commit the keys themselves.
@@ -0,0 +1,69 @@
1
+ // A private Leftoff agent feed (leftoff-agent-feed/1) on a Cloudflare Worker.
2
+ // Template from the Leftoff repo: templates/agent-feed-worker. The guide is
3
+ // docs/agent-feed-hosting.md.
4
+ //
5
+ // GET /feed.json read key (Authorization: Bearer <read key>, or ?key=)
6
+ // ETag / If-None-Match, so an unchanged pull is a 304
7
+ // PUT /feed.json write key (Authorization: Bearer <write key>)
8
+ //
9
+ // No keys live in the code or the repo: wrangler.toml holds only their
10
+ // SHA-256 hashes (READ_KEY_SHA256, WRITE_KEY_SHA256). `leftoff agents keygen`
11
+ // makes a pair and prints both hashes. The write key can also read.
12
+ // The feed itself is stored in the KV namespace bound as DATA.
13
+
14
+ const MAX_BYTES = 1000000 // Leftoff's own limit is 1 MB
15
+
16
+ async function sha256(s) {
17
+ const d = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(s))
18
+ return [...new Uint8Array(d)].map((b) => b.toString(16).padStart(2, '0')).join('')
19
+ }
20
+
21
+ function keyOf(req, url) {
22
+ const m = (req.headers.get('authorization') || '').match(/^Bearer\s+(.+)$/i)
23
+ return m ? m[1].trim() : url.searchParams.get('key') || ''
24
+ }
25
+
26
+ const HEX64 = /^[0-9a-f]{64}$/
27
+ const text = (body, status, headers = {}) => new Response(body, { status, headers: { 'content-type': 'text/plain; charset=utf-8', 'cache-control': 'no-store', ...headers } })
28
+
29
+ // If-None-Match may be a list, and a CDN may have weakened the tag (W/"…").
30
+ function matches(inm, etag) {
31
+ if (!inm || !etag) return false
32
+ return inm.split(',').map((t) => t.trim().replace(/^W\//, '')).some((t) => t === etag || t === '*')
33
+ }
34
+
35
+ export default {
36
+ async fetch(req, env) {
37
+ const url = new URL(req.url)
38
+ if (url.pathname === '/') return text('Private Leftoff agent feed.', 200)
39
+ if (url.pathname !== '/feed.json') return text('not found', 404)
40
+
41
+ const readHash = String(env.READ_KEY_SHA256 || '').trim().toLowerCase()
42
+ const writeHash = String(env.WRITE_KEY_SHA256 || '').trim().toLowerCase()
43
+ if (!HEX64.test(readHash) || !HEX64.test(writeHash)) return text('not configured: set READ_KEY_SHA256 and WRITE_KEY_SHA256 in wrangler.toml (leftoff agents keygen)', 500)
44
+
45
+ const key = keyOf(req, url)
46
+ const hk = key ? await sha256(key) : ''
47
+
48
+ if (req.method === 'PUT') {
49
+ if (hk !== writeHash) return text('forbidden', 403)
50
+ const body = await req.text()
51
+ if (new TextEncoder().encode(body).length > MAX_BYTES) return text('too big', 413)
52
+ try { JSON.parse(body) } catch { return text('bad json', 400) }
53
+ const etag = '"' + (await sha256(body)).slice(0, 32) + '"'
54
+ await env.DATA.put('feed', body)
55
+ await env.DATA.put('etag', etag)
56
+ return new Response(JSON.stringify({ ok: true, etag }), { headers: { 'content-type': 'application/json' } })
57
+ }
58
+
59
+ if (req.method !== 'GET' && req.method !== 'HEAD') return text('method not allowed', 405, { allow: 'GET, HEAD, PUT' })
60
+ if (hk !== readHash && hk !== writeHash) return text('unauthorized', 401)
61
+
62
+ const [etag, body] = await Promise.all([env.DATA.get('etag'), env.DATA.get('feed')])
63
+ if (!body) return text('no feed yet', 404)
64
+ if (matches(req.headers.get('if-none-match'), etag)) return new Response(null, { status: 304, headers: { etag } })
65
+ const headers = { 'content-type': 'application/json', 'cache-control': 'no-store' }
66
+ if (etag) headers.etag = etag
67
+ return new Response(req.method === 'HEAD' ? null : body, { headers })
68
+ },
69
+ }
@@ -0,0 +1,20 @@
1
+ # A private Leftoff agent feed on Cloudflare Workers.
2
+ # Guide: docs/agent-feed-hosting.md in the Leftoff repo.
3
+ name = "leftoff-agent-feed"
4
+ main = "src/index.js"
5
+ compatibility_date = "2026-08-01"
6
+
7
+ # Your own domain (optional). Leave this commented out for the first deploy:
8
+ # the feed then answers at https://leftoff-agent-feed.<your-subdomain>.workers.dev/feed.json.
9
+ # A custom domain sets up its own DNS record; no manual DNS changes.
10
+ # routes = [ { pattern = "feed.example.com", custom_domain = true } ]
11
+
12
+ [[kv_namespaces]]
13
+ binding = "DATA"
14
+ id = "PASTE_KV_NAMESPACE_ID_HERE" # from: npx wrangler kv namespace create DATA
15
+
16
+ # The SHA-256 hashes of your two keys, from: leftoff agents keygen
17
+ # (hashes only; the keys themselves never go in this file).
18
+ [vars]
19
+ READ_KEY_SHA256 = "PASTE_READ_KEY_SHA256_HERE"
20
+ WRITE_KEY_SHA256 = "PASTE_WRITE_KEY_SHA256_HERE"