TreeMap
A free, open-source disk-space visualizer. Squarified treemap, duplicate hunting, disk-full forecasting, and cleanup that only ever moves files to the Trash.
翻译准备中
尚未检测到更新。GitHub 发布版本后会显示在这里。
个人 GitHub 账号下公开仓库的所有者可以登录并验证所有权,自行管理介绍,无需管理员批准。
登录并生成验证文件,在 30 分钟内将其提交到此仓库的默认分支。其他分支或分叉仓库中的文件无效。然后在下方检查文件,即可无需管理员批准直接管理项目。请保留文件以维持管理权限。
如需登录,将前往登录页面。登录后请回到此处,再次点击所选的验证按钮。
在项目文件夹中运行 /nomorevibe verify,即可验证所有权并自行管理介绍。
一分钟完成验证正在加载完整 README。 以下为已保存的摘录。
A private markdown pastebin. Each note lives at a secret key that both names it
and grants access to it — no accounts, no sign-in. Usable from a browser or,
via a small HTTP API, from AI agents.查看完整 README ↗A private markdown pastebin. Each note lives at a secret key that both names it and grants access to it — no accounts, no sign-in. Usable from a browser or, via a small HTTP API, from AI agents.
if_rev) so several agents can write to one note without clobbering each other/?key=... moves the key into an httpOnly cookie and redirects to /n, which renders the note server-side, so the key stays out of the address bar and browser history. / itself always shows the home page/?key=...&persist=1 document link that stays in the address bar and survives a browser restartcreator_id, give them a folder title (creator_name) and per-creator tags, and hand a person one /c?token=… link to browse them with AND tag filters. The home page lists the folders this browser has opened. See docs/creators.mdmarker-embed:<id>. It runs for every reader in a sandboxed, opaque-origin frame with no network and scripts only from pinned jsDelivr packages, themed to match the note. See docs/embeds.mdThere is no authentication beyond the key, so treat a key like a password:
generate it randomly (openssl rand -hex 16) and share it only with people and
agents you want to have full read/write access. Anyone with the key can read and
overwrite the note.
Only the current value of a note is stored — there is no version history, so an overwrite cannot be undone.
Notes are excluded from search indexing: robots.txt disallows / for *,
pages send noindex meta and X-Robots-Tag: noindex, nofollow, noarchive, nosnippet,
and nothing is cached. There is no sitemap or public listing.
Creator ids are a second kind of secret. A note attached to a creator_id
is listed in that creator's folder at /c, and anyone holding the id can open
every note in it from a browser. Marker stores only sha256(creator_id), never
the id itself. /c?token=<id> moves the id into an httpOnly cookie and
redirects to a clean /c, and folder rows link to opaque refs, so neither the
id nor any note key appears in the folder's URL or HTML. The home page's
Folders list keeps the ids it remembers in an httpOnly cookie and links to
opaque refs too. Moving a note out of a
folder, or changing its tags, requires that creator's id as well as the note
key. Details: docs/creators.md.
Images are as private as the note. They sit in a private R2 bucket under a random per-note prefix; pages and the API hand out presigned view URLs valid for about an hour, never a public link. Image bytes are stored as uploaded, EXIF metadata included. External images in markdown load from their own host, which sees viewers' IP addresses. Details: docs/images.md.
Embeds run agent-written code, contained. Each runs in a
sandbox="allow-scripts" iframe on the storage host (an opaque origin), with
no referrer, no network, and a CSP that allows scripts only from pinned,
integrity-checked jsDelivr packages. It can't read the note, the key or
cookies, and talks to the page only through a small checked message set
(resize, ready, error, openLink, plus theme and visibility from the page).
Marker's own pages send an enforced Content-Security-Policy
(app/lib/csp.ts). Details: docs/embeds.md.
Link previews are the exception. A persistent URL (/?key=...&persist=1)
is already a public view of that note (the key is in the URL). Marker puts
og:title / og:description (and Twitter Card equivalents) in the HTML,
derived from the start of the note's plaintext, so pasting that URL into Slack,
iMessage, Discord, X, etc. unfurls a snippet. One-off /?key=... shares still
strip the key and redirect, so they do not unfurl. robots.txt allows known
preview bots (Twitterbot, Slackbot, facebookexternalhit, Discordbot, …) to fetch
/ while still disallowing /api/ and leaving search crawlers on Disallow: /.
git clone <repository-url>
cd marker
npm install
Create a .env.local file in the root directory:
KV_REST_API_URL=your_upstash_redis_url
KV_REST_API_TOKEN=your_upstash_redis_token
Images are optional. To enable them, also set R2_ACCOUNT_ID,
R2_ACCESS_KEY_ID, R2_SECRET_ACCESS_KEY and R2_BUCKET, plus CRON_SECRET
for the daily cleanup and RESEND_API_KEY / ALERT_EMAIL_TO for storage
alerts. Without them the app works as before and the images API returns 503.
Bucket and CORS setup: docs/images.md.
npm run dev
You can also open a note directly at /?key=<key>. The server takes the key out
of the URL, stores it in an httpOnly session cookie, and redirects to /n before
rendering — so the key never appears in the address bar, browser history, a
Referer header, or client-side JavaScript.
That cookie is what keeps /n on the note across reloads. It is cleared by
"Change Key" or by closing the browser. Opening or creating a note from the
home page also moves to /n. Visiting / always shows the home page, even
while a session note is remembered; /n without a session note redirects
to /.
The home page also has a Folders section listing the creator folders this browser has opened (most recent first, up to 10), with each folder's name and note count and a ⋯ menu to forget it. See docs/creators.md.
To keep a stable document link instead, turn on Persistent link in the note
header. That puts /?key=<key>&persist=1 in the address bar (and offers a copy
button). Visiting that URL always loads the same note and leaves the key in the
URL so it can be bookmarked. Turning the toggle off restores the one-off
session behavior above.
Because a persistent URL keeps the key visible, treat it like handing someone
the password: it will appear in browser history, in Referer headers, and —
if pasted into a chat app — as a link preview showing the start of the note.
Messenger caches can keep that snippet after the note changes or expires.
Because only the current value of a note is stored, saving from the browser sends the revision it loaded. If an agent wrote to the note while you had it open, you get a warning and a chance to reconsider instead of a silent overwrite.
When a shared note asks Lawrence to react, use checkboxes and ask blocks in the Markdown so he can answer with taps. Checkboxes, choices and Other text autosave as drafts; Lawrence responds in chat. There is no Send control or answer submission, and choices never authorize actions.
Marker doubles as a shared scratchpad for agents: a note is just a URL and a
key, so an agent can read and write one with plain HTTP and no browser access.
Several agents can safely work on the same note — mode: "append" and if_rev
exist specifically so they don't overwrite each other.
To group many notes for one person, the agent can attach each note to a
secret creator_id, tag it, and hand over a single folder link. That is
covered in docs/creators.md and in the "Copy agent prompt"
text on the home page (app/lib/agentPrompt.ts, the canonical brief).
Give an agent the blurb below verbatim (in a system prompt, CLAUDE.md, or a
tool description) and it will know how to use the API.
Marker is a markdown pastebin at https://marker.luk.xyz.
A note is identified by a "key", which is also its only secret: anyone holding
the key can read and overwrite that note. Use a long random key such as the
output of `openssl rand -hex 16`. Keys are trimmed of surrounding whitespace,
otherwise taken literally, and capped at 512 characters.
## Read a note
GET https://marker.luk.xyz/api/content?key=<key>
→ { "content": string | null, // null only when "exists" is false
"exists": boolean,
"rev": number, // 0 if the note doesn't exist yet
"updated_at": number | null, // epoch milliseconds
"created_at": number | null,
"last_accessed_at": number | null,
"expires_at": number | null,
"size": number } // bytes
The response has the same shape whether or not the note exists, so you never
need to branch on "exists" before reading a field. "rev" increases by one on
every write — use it for cheap freshness checks and for "if_rev" below.
`last_accessed_at` is an epoch millisecond timestamp for the last note view or content/comment read, updated at most once per hour. It does not change `rev`, `updated_at`, or expiry. Older notes get a value on their next access; a missing note returns `null`. Revision polling and writes do not count as access. Marker does not automatically delete idle notes.
## Write a note (creates it if it doesn't exist)
POST https://marker.luk.xyz/api/content
Content-Type: application/json
{ "key": "<key>",
"content": "<markdown string>",
"mode": "overwrite" | "append" | "prepend", // optional, default "overwrite"
"if_rev": <number>, // optional
"ttl": <seconds> | 0 | null } // optional
→ { "success": true, "rev": number, "updated_at": number,
"expires_at": number | null, "size": number }
## Which mode to use
- Adding to a running log, checklist, or scratchpad → "append" (or "prepend").
This is applied server-side as one atomic operation, so simultaneous writers
can't lose each other's work. A newline is inserted between the existing
content and yours if there isn't one already. Prefer this whenever you are
only adding material.
- Rewriting or restructuring existing content → GET first, then POST with
"if_rev" set to the "rev" you just read. If anything wrote in between you get
a 409 instead of silently destroying their work.
- Creating a note only if the key is still free → "if_rev": 0.
- Unconditional overwrite → omit "if_rev". Only do this when you own the note
and don't care what was there.
## Handling a 409
409 means the note changed since the "rev" you passed. The response body is
{ "error", "rev", "content" } carrying the current state, so you don't need a
second GET: merge your change into that "content" and retry with that "rev".
Retry a couple of times, then report the conflict rather than looping.
## Expiry
"ttl" is seconds until the note deletes itself; 0 or null removes an expiry.
Omitting "ttl" leaves any existing expiry untouched, so ordinary writes don't
accidentally extend or cancel one. Handy for temporary shares.
## Errors
400 — missing or invalid key, non-string content, bad mode/if_rev/ttl
404 — PATCH rename: the source key does not exist
409 — "if_rev" didn't match (body has the current "rev" and "content"), or PATCH rename destination is taken
413 — the resulting note would exceed the 1 MiB limit
## Examples
Append a line to a shared log:
curl -X POST https://marker.luk.xyz/api/content \
-H 'Content-Type: application/json' \
-d '{"key":"KEY","content":"- deployed build 41","mode":"append"}'
Safe read-modify-write:
curl -s 'https://marker.luk.xyz/api/content?key=KEY'
# → {"content":"# Plan\n- step one","exists":true,"rev":7,...}
curl -X POST https://marker.luk.xyz/api/content \
-H 'Content-Type: application/json' \
-d '{"key":"KEY","content":"# Plan\n- step one\n- step two","if_rev":7}'
Write a note that self-destructs in an hour, then hand it to a human:
curl -X POST https://marker.luk.xyz/api/content \
-H 'Content-Type: application/json' \
-d '{"key":"KEY","content":"# Handoff\n...","ttl":3600}'
# then share https://marker.luk.xyz/?key=KEY
## Limits and caveats
- Content is plain markdown (GitHub-flavored), stored and returned verbatim.
- Only the current value is kept. There is no version history, so an overwrite
is unrecoverable — prefer "append" or "if_rev" over blind overwrites.
- Maximum 1 MiB per note.
- There is no delete endpoint. Write "" to empty a note, or set a short "ttl"
to make it disappear.
- Rename a note (content, rev, and expiry move; destination must be free):
PATCH https://marker.luk.xyz/api/content
{ "key": "<old>", "new_key": "<new>" }
→ { "success": true, "key": "<new>", "renamed": true }
409 if new_key already exists; 404 if key does not.
- https://marker.luk.xyz/?key=<key> is the human-facing view/edit page. Use the
API above rather than fetching that page.
- The key is the only authentication. Don't store credentials or sensitive data
in a note, and don't paste a key anywhere it will be logged or indexed.
## Creator folders and tags
Optional fields on POST /api/content group notes into a private folder:
"creator_id": "<22–128 chars of A-Z a-z 0-9 _ - ~>" | true | null
"previous_creator_id": "<id>" // proof when moving or removing
"creator_name": "<1–80 chars>" // folder title; needs creator_id
"tags": ["plans","q4"] | [] | null // needs a creator; max 10
true generates an id and returns it once as "creator_id". The id is a secret:
holders can list the folder and open every note in it. Moving/removing a note
or editing its tags needs the current creator's id (else 403).
PATCH https://marker.luk.xyz/api/meta changes the same fields without touching
rev or content. GET https://marker.luk.xyz/api/creator?tag=a&tag=b with
Authorization: Bearer <creator_id> lists the folder (AND filter, 50 per page,
"next_cursor"). Give the person https://marker.luk.xyz/c?token=<creator_id>.
Full reference: docs/creators.md.
## Images
Upload, then reference the returned markdown in the note:
POST https://marker.luk.xyz/api/assets
{ "key": "<key>", "content_type": "image/png", "data_base64": "<≤ 1 MiB>" }
or { "key", "content_type", "size": <bytes ≤ 5 MiB> } → PUT to "upload_url"
→ { "asset_id", "markdown": "", ... }
Removing the last reference deletes the image ~24 h later; re-adding it within
that window keeps it. Keep image references you don't mean to delete.
Full reference: docs/images.md.
## Embeds
Upload one self-contained HTML file the same way, with content_type
"text/html" (≤ 512 KiB), and put the returned 
on its own line. It runs sandboxed: no network (inline the data), scripts only
from https://cdn.jsdelivr.net/npm/<pkg>@<version> with integrity, links via
marker.openLink(url), colours from the --paper/--ink-text/... variables.
Max 10 per note, sharing the image limits. Full guide: docs/embeds.md.
marker/
├── app/
│ ├── api/
│ │ ├── assets/ # Image reserve/upload and fresh view URLs
│ │ ├── content/ # API routes for content CRUD
│ │ ├── creator/ # Folder listing, /c cookie session, remembered folders
│ │ ├── cron/assets-sweep/ # Daily image cleanup and storage alerts
│ │ ├── meta/ # PATCH creator, folder name, tags
│ │ └── session/ # Sets/clears the session key cookie
│ ├── c/ # /c folder page, /c/enter, /c/folder/<ref>, /c/open/<ref>
│ ├── components/
│ │ ├── ContentEditor.tsx # Markdown editor component
│ │ ├── ContentViewer.tsx # Markdown viewer component
│ │ ├── CopyButton.tsx # Copy-to-clipboard control with "Copied" feedback
│ │ ├── HomeIdle.tsx # Home page body
│ │ ├── KeyInput.tsx # Key input form
│ │ ├── NoteApp.tsx # Client-side app state and flow
│ │ ├── NoteCreator.tsx # Creator name, tag pills, tag editor in a note
│ │ ├── NoteEmbed.tsx # Sandboxed embed frame, bridge, watchdog, expand dialog
│ │ ├── NoteImage.tsx # Image rendering, view-URL refresh, lightbox
│ │ ├── NoteToolbar.tsx # Compact icon toolbar for a viewing session
│ │ ├── NoteView.tsx # Server-rendered note page shared by / and /n
│ │ ├── RememberedFolders.tsx # Home "Folders" list
│ │ ├── RenameKey.tsx # In-place key rename form
│ │ ├── ScrollActions.tsx # Scroll navigation buttons
│ │ └── ToolbarButton.tsx # Shared icon-button chrome
│ ├── lib/
│ │ ├── alerts.ts # Storage alert email (Resend)
│ │ ├── assetIndex.ts # Image limits and the per-note image index (Lua)
│ │ ├── assetJobs.ts # Post-response image deletes and alerts
│ │ ├── assets.ts # Image reserve, signed URLs, sweep scripts
│ │ ├── clipboard.ts # Clipboard helper
│ │ ├── creatorId.ts # Creator id/name/tag validation, folder cookie
│ │ ├── creators.ts # Creator hashing, refs, folder list/gate scripts
│ │ ├── csp.ts # Marker's enforced Content-Security-Policy
│ │ ├── embedPolicy.ts # Embed limits, CSP, title params, message checks
│ │ ├── embedShell.ts # The shell page that runs an embed in the sandbox
│ │ ├── embeds.ts # Shell upload and embed HTML validation
│ │ ├── folders.ts # Remembered-folders cookie and summary script
│ │ ├── key.ts # Key validation, masking, session cookie
│ │ ├── notes.ts # Note storage schema and atomic read/write scripts
│ │ ├── origin.ts # Request origin for absolute OG URLs
│ │ ├── preview.ts # Title/description from the start of a note
│ │ ├── r2.ts # Cloudflare R2 (S3 SigV4) client and presigning
│ │ ├── redis.ts # Redis client configuration
│ │ └── robots.ts # X-Robots-Tag / preview-bot allowlist
│ ├── n/page.tsx # Session note (/?key= lands here)
│ ├── robots.ts # Disallow-all robots.txt, with preview-bot allows
│ └── page.tsx # Home, or the note for a persist link; OG metadata
├── tests/
│ └── preview.test.ts # Note-prefix title/description derivation
├── proxy.ts # Turns /?key=... and /c?token=... into a cookie + redirect
└── README.md
npm run dev - Start development servernpm run build - Build for productionnpm run start - Start production servernpm run embeds:shell - Optionally pre-upload the content-addressed embed shell (embed-shell-<hash>.html) to R2 (-- --print to preview)npm run lint - Run ESLintnpm test - Run unit tests (note-preview title/description)The easiest way to deploy is using Vercel:
KV_REST_API_URL and KV_REST_API_TOKEN, plus the optional image variables above)Make sure to set up your Upstash Redis instance and configure the environment variables in your deployment platform. vercel.json schedules the daily image sweep (one cron, within the Hobby plan's limit).
Private project.
See the design and API contract and proposed Marker skill addition. This branch is for an unmerged Preview trial. Preview note keys use a fixed isolated Redis namespace; production keys are not addressed. Open /?key=commenting-demo-v1&persist=1 on the Preview deployment to seed/open the synthetic demo. Comments are always available in view mode: select text, tap the nearby speech-bubble icon, type in the already-focused composer and send. The smaller bubble sits beside the end of the last selected line. The toolbar comments icon opens threads. Source positions map atomically through all content writes, preserving comments on repeated phrases; changed quotes stay Outdated. Comments are labeled You in the browser and Agent through the explicit-key API.
Tests use disposable local Redis, never hosted credentials. Install redis-server and OpenSSL, run npm ci, then npm test (anchor, API/Lua concurrency and existing preview tests). For browser tests, run npx playwright install chromium webkit, then npm run test:browser; this builds the production app and runs Chromium plus iPhone-sized WebKit against a temporary HTTPS server and local Redis. The browser suite covers rendered selection, create/reply/resolve/reopen, atomic agent edits, demo idempotency and absence of comments from initial HTML. It does not simulate actual iOS touch handles. Run npm run lint and npm run typecheck for static checks.