TreeMap
무료 오픈소스 디스크 사용량 시각화 도구로, 사각형 트리맵·중복 파일 탐색·디스크 가득 예측·정리 시 파일을 휴지통으로만 이동한다
개인 계정의 공개 저장소 소유자는 GitHub로 로그인해 운영자를 확인하고 소개를 직접 관리할 수 있습니다. 관리자 승인은 필요하지 않습니다.
로그인한 뒤 확인 파일을 만들어 이 저장소의 기본 브랜치에 30분 안에 커밋해 주세요. 다른 브랜치나 포크의 파일은 인정하지 않습니다. 파일을 올린 뒤 아래에서 확인하면 관리자 승인 없이 관리할 수 있습니다. 자격을 유지하려면 파일을 남겨 두세요.
로그인이 필요하면 로그인 화면으로 이동합니다. 로그인 후 이곳으로 돌아와 원하는 확인 버튼을 다시 눌러 주세요.
프로젝트 폴더에서 /nomorevibe verify를 실행하면 소유권을 확인하고 소개를 직접 관리할 수 있습니다.
1분 만에 확인하기README 전체를 불러오는 중입니다. 아래는 저장된 발췌입니다.
tinker — a quiet place to be on the web.
A writing surface with a Vercel-hosted web app, an Electron Mac
desktop shell that loads that same production app, and shared
src/renderer/ code. The chrome wears tinker's multi-colored globe
mark on a warm cream background, with Fraunces for display and
Instrument Sans for body.
This is one full product app — there is no separate MCP repo anymore.
The core surfaces live here together:README 전문 보기 ↗tinker — a quiet place to be on the web.
A writing surface with a Vercel-hosted web app, an Electron Mac
desktop shell that loads that same production app, and shared
src/renderer/ code. The chrome wears tinker's multi-colored globe
mark on a warm cream background, with Fraunces for display and
Instrument Sans for body.
This is one full product app — there is no separate MCP repo anymore. The core surfaces live here together:
src/renderer/.cli/ is a globally installable tinker binary: the same
writing coach in a compact terminal session (any cwd; saves to Notion).api/ (Claude, search, pitches, publish, feed, user-data, voice,
email, and the rest of the product surface).api/ layer is what agents and
tooling call. /api/mcp is a Streamable HTTP MCP façade on this
deploy (follow-up questions and LinkedIn drafts). Project MCP config
(e.g. Browserbase) is for debugging against this app. Neither one is
the beginner mail/domains Worker.Payments (Stripe) and identity (Stytch) stay as external services wired in where the product needs them. They are not carved out into their own repos either — just not what this tree is.
tinker)The writing coach also runs as a command-line tool named tinker. It is
meant for a short integrated terminal (for example under browser tabs in
Cursor): one question line, a > prompt, no full-screen TUI. It installs
globally and runs from any directory — including repos that are not this
one — on macOS zsh and other Unix shells.
From this repo (global, any cwd afterward):
npm i -g ./cli
# or, from the repo root:
npm i -g .
Or without a permanent install:
npx --prefix ./cli tinker --help
The cli/ package is self-contained (vendored interview prompt). After
changing src/renderer/interview-prompt.js, run npm run cli:sync-vendor
so the CLI copy stays in sync (CI asserts they match).
tinker login # phone OTP → saves ~/.tinker (never prints the token)
tinker login --notion # also open Notion OAuth in the browser
tinker # coach loop: one question, your answer, next question
tinker --help
tinker logout
Config (env wins over file):
| Source | Keys |
|---|---|
~/.tinker | { "apiBaseUrl", "token" } (mode 0600) |
| Env | TINKER_API_BASE_URL, TINKER_TOKEN |
During a session, type an answer and press Enter. Finish with
this is everything — the CLI stitches founder-only prose (same rules as
the app) and POSTs /api/notion-essays. Success prints Saved to Notion.
Missing parent page prints an explicit error (not a silent skip).
Sample session:
$ tinker
What are you learning about how this work moves?
> It only moves when I ship the smallest cut and leave the rest alone.
What are you noticing that you did not expect?
> Silence after a ship is useful. I kept filling it with more scope.
> this is everything
Saved to Notion.
See also cli/examples/sample-session.txt.
The Mac app is a real desktop window (dock icon, native menus, remembered
size) around production Tinker at https://tinker.beginner.work.
Sign-in, inbox, writing, /feed, and notes-folder sync are the live web
app; the shell only adds native chrome and local folder IPC. Web product
changes appear without reinstalling — only shell changes need a new dmg.
npm install
npm start # loads https://tinker.beginner.work
Use npm run dev to open with DevTools attached. To point the shell at a
local or preview URL instead: TINKER_DESKTOP_URL=http://localhost:3000 npm run dev.
Download
tinker-mac.dmg,
open it, and drag tinker into Applications.
There is no Apple Developer account, so the build is unsigned. macOS Gatekeeper will block the first open. One-time allow:
After that, launches work normally. Auto-update is skipped on purpose — unsigned apps cannot use a trustworthy Mac update channel; the shell loads production so most changes do not need a new dmg.
To package the desktop shell, use
electron-builder — configured in
electron-builder.yml:
npm install
npm run dist # package for the current OS
npm run pack # unpacked dir only, for a quick smoke test
Artifacts land in dist/ (git-ignored). Each OS builds its own formats,
so build on the matching platform (or a CI runner per platform):
| Script | Host OS | Output |
|---|---|---|
npm run dist:linux | Linux | tinker-<version>-x86_64.AppImage, tinker-<version>-x64.tar.gz |
npm run dist:mac | macOS | universal tinker-mac.dmg + .zip |
npm run dist:win | Windows (or Linux + wine) | NSIS installer + portable .exe |
The shell is plain JS with nothing to compile. Packaging collects
src/main/ (and the dock icon) into an asar; the Prisma tree stays out.
Linux/Windows targets still build when CI runs them; Mac is the priority.
tinker-mac.dmg)A downloadable Mac build is published to
https://github.com/beginner-work/tinker/releases/latest/download/tinker-mac.dmg
when either of these happens:
main (preferred for agent PRs). Bump
version in package.json and the contents of .release-version to
the same value in a PR. When that merge lands on main,
release-on-marker.yml
verifies the two match and calls
release.yml with publish: true.npm version patch then git push --follow-tags
pushes a v* tag, which runs the same Release workflow.release.yml with publish: true
(escape hatch).The Release workflow builds on macos-latest, ubuntu-latest, and
windows-latest, pre-creates the GitHub Release for v<version>, and
uploads installers with electron-builder --publish always.
Code signing / notarization: not configured. Builds ship unsigned.
To produce a Gatekeeper-clean Mac build later, add these exact repo
secrets and drop identity: null from electron-builder.yml:
| Secret | Purpose |
|---|---|
MAC_CSC_LINK | Base64 of the Developer ID Application .p12 |
MAC_CSC_KEY_PASSWORD | Password for that .p12 |
APPLE_ID | Apple ID for notarization |
APPLE_APP_SPECIFIC_PASSWORD | App-specific password for that Apple ID |
APPLE_TEAM_ID | Apple Team ID |
Release assets inherit repo visibility; a public download needs a public release (or hosting elsewhere).
The same src/renderer/ codebase ships as a hosted website — no build step.
The web build is phone-OTP-gated via Stytch; search
runs through a Vercel function that holds the Anthropic key.
For local dev with the auth flow, use Vercel's local emulator instead of the plain static host:
npm install
npx vercel link # one-time; pick beginner-work / tinker
npx vercel env pull # pulls STYTCH_PROJECT_ID, STYTCH_SECRET, ANTHROPIC_API_KEY
npx vercel dev # serves http://localhost:3000 with /api/* functions
For static-only browsing (no auth, no search) you can still run the bundled host:
npm run web # serves http://localhost:5173 (static only)
The web build doesn't ask for an Anthropic key. Instead it gates the app behind a phone/PIN sign-in: enter your phone, receive a 6-digit code, verify it. Sign-up and login share the same screen — first-time users get a Stytch user created automatically when they verify.
The flow is:
/api/auth/phone/request with the phone number.otps/sms/login_or_create with
project credentials and returns the phone_id to the browser./api/auth/phone/verify with { phone_id, pin }. The
function calls Stytch's otps/authenticate with a 30-day session
duration and returns the Stytch session_jwt as token. The browser
stores it under tinker_jwt in localStorage./api/search with the JWT as a Bearer token;
the function verifies the JWT against Stytch's JWKS, then calls
Claude Haiku 4.5 with the server-side Anthropic key. The browser
bundle never sees the Anthropic key.Required Vercel env vars:
STYTCH_PROJECT_IDSTYTCH_SECRETANTHROPIC_API_KEYDATABASE_URLSTRIPE_SECRET_KEY — the live secret key for the Stripe account that
holds the pre-seed subscriptions. Required by the membership endpoints
(api/membership/checkout.js, reconcile.js, pause.js, status.js).
Without it, in-app Upgrade returns 503, "Already subscribed? Restore" can
never link a subscription — the reconcile call 503s and the row stays on
"Free plan" — and Pause/Resume returns 503. A test-mode key here is just as
broken: it queries the wrong Stripe
account, finds no customer, and reports the paying member as free. This must
be the same live account beginner's checkout writes to.BROWSERBASE_API_KEY — used by scripts/browserbase-debug.jsBROWSERBASE_PROJECT_ID — used by scripts/browserbase-debug.jsOptional:
STRIPE_PRICE_PRESEED — a recurring $9/month Price id for the in-app
Upgrade checkout. If unset, the checkout builds the price inline, so the
flow still works without dashboard setup.BEGINNER_MCP_TOKEN (or CLOUDFLARE_API_TOKEN) — enables the in-app email
composer (profile menu → "Send an email", api/email/send.js). The function
relays to the beginner mcp Worker's POST /email/send (canonical URL
https://beginner-mcp.tyler-lindow.workers.dev, hardcoded default;
BEGINNER_MCP_URL overrides), which sends from the founder's
beginner.work address via Cloudflare Email Routing. The bearer can be the
Worker's MCP_BEARER_TOKEN or — since the Worker accepts it on the email
route — the account's default CLOUDFLARE_API_TOKEN, so an existing secret
can be reused as-is. No token → the composer gets a friendly 503. Inherited
constraint: Email Routing only delivers to verified destination
addresses on the Cloudflare account.Optional Preview-only vars (enable the Claude-Code → Browserbase loop; see "Closed-loop iteration on a Vercel preview" below):
VERCEL_AUTOMATION_BYPASS_SECRET — auto-set when you enable Protection BypassTEST_AUTH_TOKEN — gates /api/dev-bootstrapTEST_SESSION_TOKEN — long-lived Stytch session_token for the test userTEST_PHONE_NUMBER — E.164 number for the test account; used by scripts/mint-test-session.jsSet the same values for Production and Preview so preview deploys see the
same Stytch users and the same Postgres rows as production. (Preview used
to point at a separate project-test-* Stytch project and got its
localStorage wiped on every load — both are gone now; previews behave
like a second URL pointing at production.)
/autonomy stores each signed-in person's toggles in Upstash Redis, not Postgres. One hash per Stytch user id, named autonomy:<user id>. The id comes from the verified session. Each field is one of the 14 item keys, and the value is JSON with autonomous, note, updated_by, and updated_at. A save writes that one field, so two toggles do not overwrite each other.
Both GET /api/autonomy and PUT /api/autonomy/:key require a signed-in session. A signed-out request is 401. There is no public list and no allowlist. A person with nothing stored yet reads every item off, including LinkedIn profile edits. There is no seed.
Connect the store on the Vercel project environment. The Marketplace injects KV_REST_API_URL, KV_REST_API_TOKEN, and KV_REST_API_READ_ONLY_TOKEN.
GET /api/autonomy uses KV_REST_API_URL with KV_REST_API_READ_ONLY_TOKEN only. If that read-only token is missing, GET returns every item off. It does not use the write token.
PUT /api/autonomy/:key uses KV_REST_API_URL with KV_REST_API_TOKEN, including the read of the one field it merges before writing. If that write pair is missing, PUT returns 503 and saves nothing.
The app does not read KV_URL or REDIS_URL. It does not log the REST URL or either token, and it does not put them in an error or in the page. If the store is missing or cannot be reached, GET still returns 200 and every item is off. PUT returns 503 with Autonomy settings are unavailable right now. and writes nothing.
get_autonomy_settings on POST /api/mcp reads that same hash for the connector user. The id on the mcp_ credential is the Stytch session.user_id saved at approval, and the hash is keyed by that same id. The tool takes no input and no user id. It uses KV_REST_API_READ_ONLY_TOKEN on every call, with no cache. If the store is missing or cannot be reached, the tool returns an error, Autonomy settings are unavailable right now., and no values. A caller treats that error as every setting off. PUT /api/autonomy rejects a bearer that starts with mcp_ before it calls Stytch. Labels and the one-line scope text live in src/renderer/autonomy/catalog.js, which the page and the tool both read.
/career stores one career record per signed-in person in the same Upstash Redis, not Postgres. The key is career:<user id>. The id comes from the verified session. The value is JSON with facts and rules. A fact has an id, a kind, a value, a source document and excerpt, a status (proposed, verified, or rejected), and updated_at. A metric can also carry baseline and mechanism. An employment fact also carries employer, optional title, start_month, end_month (null only when current is true), current, and optional experience_kinds. A rule has an id, a field, the rule text, a machine-usable form, and status verified.
The first visit seeds three proposed facts (Software Engineering Manager, Merchant Advocacy at Affirm; held Merchant Portal at 99.9% availability with zero BFCM incidents; and the combined 1-to-9 plus US/Canada/Poland same-team claim) and the nine verified answer rules (salary blank, San Diego location, blank current company / Affirm most recent, relocate yes, West Coast work city, needs_claire for sensitive fields, no Present without a current employment entry, years-of-experience from employment dates, and exact employer dates). Existing records pick up newly catalogued rules on the next signed-in /career load. Upload accepts a PDF or markdown resume. Proposed facts come back with the source excerpt. Confirm, edit, reject, and restore are browser actions on POST /api/career?action=fact. Employment rows use the same confirm / edit / reject path, including reject from the verified list. Rejected facts stay in a collapsed Rejected section, each with its source excerpt and a Restore button that returns the fact to proposed. Nothing is verified without a confirm click (except owner data loaded out-of-band into Redis). The upload itself is not stored.
GET /api/career uses KV_REST_API_READ_ONLY_TOKEN, then the write token to create the seed when the key is missing and to merge any catalog rules the record still lacks. POST /api/career?action=extract and POST /api/career?action=fact use the write token. A bearer that starts with mcp_ is rejected on every career route before Stytch. If the store is missing or cannot be reached, the route returns 503 with Career record is unavailable right now. and writes nothing.
get_career_record and check_text on POST /api/mcp read that same key for the connector user. Neither tool takes a user id. Both use the read-only token. get_career_record returns verified facts and rules with ids. Proposed facts are returned on unverified_facts with unverified: true and must not be used as facts. Rejected facts are omitted. Restoring a fact returns it to proposed, and the tool lists it again on unverified_facts with unverified: true. If the store cannot be reached, the tool returns Career record is unavailable right now. and no record. check_text lists claims with verdict pass, mismatch, unsupported, or needs_claire. ready is true only when every claim passes, and false when the draft is empty or no claims were found. Numbers, dates, titles, and employer names are compared in code. Present or current employment is a mismatch unless a verified employment entry is marked current. Years-of-experience thresholds are totaled from employment months in code. Employer date claims must match a verified employment range exactly. A title passes only with the employer on that same fact. Baseline and mechanism claims compare the amount, percentage, and baseline. The model only finds claim text. Kinds, rules, and the seeds live in src/renderer/career/catalog.js, which the page and the server both read. Preview logs redact career bodies. The app does not log resume text, facts, or the checked draft, and it does not put the REST URL or either token in an error or a response. Owner employment history is loaded into Redis as data (see scripts/career-upsert-facts.js), not hard-coded in seeds.
Owner backup export: GET /api/career?action=export returns the raw Redis JSON for the owner's career key (every fact and rule, regardless of status), plus the ContentItem row with draftKey career-employment-bootstrap-v1 (or null). Response shape: { exported_at, user_id, redis_key, record, employment_bootstrap } with Cache-Control: no-store. Access is owner-only: a Stytch session whose user id is listed in LEADS_OWNER_ALLOWLIST, or Authorization: Bearer matching CAREER_EXPORT_TOKEN (constant-time compare; if that env var is unset, bearer access is disabled). Bearer calls export the owner's record by taking the first entry in LEADS_OWNER_ALLOWLIST as the Stytch user id (same allowlist the session check uses). Everyone else gets 404 (not 401/403). Reads use KV_REST_API_READ_ONLY_TOKEN. If Redis is unreachable the route returns 503 with Career record is unavailable right now., never an empty 200. Local backup: CAREER_EXPORT_TOKEN=... node scripts/career-export.js --out <dir> writes career-export-YYYY-MM-DD.json and exits non-zero on any non-200 or empty record. Do not commit snapshots to this repo.
DATABASE_URL on both tinker (production + preview) and beginner
production points at the same Neon Postgres database.
TinkerUserData is the shared
row store — see beginner/CLAUDE.md → "Database topology". One
asymmetry: beginner's Vercel project has Neon preview-branching
enabled, so a beginner preview reads from a fresh per-deploy
branch, not the production DB. Cross-project flows (e.g. publishing a
pitch from tinker preview to /daily/ on beginner preview) therefore
won't round-trip in preview; verify on production after merge.
Sign out by clearing tinker_jwt (window.tinkerAuth.signOut() from the
inspector, or localStorage.removeItem("tinker_jwt")).
When a Vercel preview misbehaves in a way you can't reproduce locally — auth flow only breaks behind the edge network, a third-party widget only loads from a non-localhost origin, etc. — there are two ways to point a real Chromium at the preview through Browserbase.
.mcp.json points at Browserbase's hosted MCP server. The next time
you open this repo in Claude Code you'll be asked to approve the
project-scoped MCP once — accept it and Claude gets navigate,
act, observe, and extract tools wired up to a cloud Chromium.
Ask Claude something like "open the latest preview and tell me what's
printed in the console" and it'll spin up a session and look for you.
The hosted server handles its own LLM costs (Browserbase pays for
Gemini under the hood); all you need is BROWSERBASE_API_KEY set in
your Claude Code environment. The key is passed as a URL query
param — no extra model key, no Anthropic key, no Gemini key.
For the full edit → push → preview → poke → fix loop, Claude needs to get past two auth walls in a single browser navigation:
The repo handles both with one URL:
https://<preview>.vercel.app/api/dev-bootstrap
?test_auth=$TEST_AUTH_TOKEN
&x-vercel-protection-bypass=$VERCEL_AUTOMATION_BYPASS_SECRET
&x-vercel-set-bypass-cookie=true
&next=/
Vercel consumes the bypass query params at the edge (and sets a
_vercel_jwt cookie so subsequent navigations on the same session don't
need them). The function then validates test_auth and returns a tiny
HTML page that writes a pre-minted Stytch session token into
localStorage.tinker_jwt and redirects to /. From there the app is
fully authenticated and Claude can use the Browserbase MCP tools
(navigate, act, observe, extract) against the real preview.
One-time setup:
Vercel dashboard → Settings → Deployment Protection → enable
Protection Bypass for Automation. This auto-adds
VERCEL_AUTOMATION_BYPASS_SECRET to project env.
Add three Preview-scope env vars:
vercel env add TEST_PHONE_NUMBER preview # e.g. +15551234567
vercel env add TEST_AUTH_TOKEN preview # any 32+ char random string
vercel env add TEST_SESSION_TOKEN preview # see step 3
Mint the long-lived session token (rerun every ~30 days):
npx vercel env pull
node scripts/mint-test-session.js
# Triggers an SMS to TEST_PHONE_NUMBER, prompts for the 6-digit code,
# prints the session_token plus the exact `vercel env add` command to
# paste it into the Preview scope. Then redeploy.
These vars only exist in the Preview scope on purpose: production stays
unaware of them, so the /api/dev-bootstrap endpoint silently 404s on
the production hostname.
Because preview shares the Live Stytch project and the production
Postgres (see above), signing into a preview lands you on your real
production account with real production data — that's the point, and
it's why preview must not be pointed back at a project-test-*
project (that would isolate you onto an empty test user). The catch:
every preview deploy is a new *.vercel.app origin with empty
localStorage, so the SMS-OTP gate makes you sign in again on each one.
Each sign-in sends a Stytch code, and after ~24 sends in 24h Stytch
rate-limits your number.
The same /api/dev-bootstrap seam that automation uses fixes this for
humans too: it reuses the one long-lived TEST_SESSION_TOKEN (your real
Live session, minted once in step 3 above) instead of triggering a new
code. Mint it once per ~30 days, then on each new preview origin:
npx vercel env pull # populates TEST_AUTH_TOKEN + bypass secret
npm run preview:url -- --url https://tinker-abc.vercel.app
# prints the /api/dev-bootstrap URL — open it in your browser and you're
# signed in as your production account, no SMS. Pass --next /daily/ to
# land on a specific path.
So the rule of thumb for real-account preview testing: mint once with
your real phone, then re-seed with preview:url — never re-run the OTP
just to get back in. That keeps you on your real Live user and real
production data while staying well clear of the 24-in-24h SMS wall.
Vercel stays the source of truth for everything else.
.claude/hooks/session-start.sh runs at the start of each cloud Claude
Code session, pulls the Preview env from Vercel, and exposes it to the
session so .mcp.json's ${BROWSERBASE_API_KEY} placeholder
interpolates correctly when the Browserbase MCP server starts. The hook
needs three bootstrap vars in the Claude Code web environment for this
repo (the only secrets that can't live in Vercel):
VERCEL_TOKEN — a read-only-by-default Vercel API tokenVERCEL_PROJECT_ID — copy from .vercel/project.json after a local vercel linkVERCEL_ORG_ID — same fileWithout them, the hook still installs npm deps and exits cleanly, so
local claude runs that already have .env.local on disk are
unaffected.
For human-driven poking around — when you want the Network tab in your own browser pointed at a live preview — use the CLI script. It prints a live DevTools URL you open locally:
npx vercel env pull # pulls BROWSERBASE_API_KEY + BROWSERBASE_PROJECT_ID
npm run debug:preview -- --url https://tinker-abc.vercel.app
The session auto-expires after 10 minutes of inactivity. Pass
--timeout 3600 for a longer session, or --help to see all flags.
In-app Claude calls (writing, Keep crafting, LinkedIn draft, and related
surfaces) go through same-origin /api/claude/converse on Vercel, gated
by the phone/PIN JWT. The desktop shell uses that same production API —
there is no local Anthropic key and no address-bar search engine in the
app. Server routes such as api/search.js still exist for product
features that need them; they use ANTHROPIC_API_KEY on Vercel.
Open Connections from Settings or the profile menu (/connections).
Tinker acts as an MCP client: Notion is first
(https://mcp.notion.com/mcp, OAuth 2.1 + dynamic client registration +
PKCE). Access and refresh tokens are stored per user in Postgres
(TinkerUserData), encrypted with TINKER_SECRETS_KEY (falls back to
STYTCH_SECRET). Connected read tools (notion-search, notion-fetch)
attach automatically on /api/claude/converse. Write tools stay off
until you confirm Allow writes in the UI (one hour).
This is separate from MCP access below (bots calling Tinker’s own
MCP server). Add more servers later in
api/_lib/mcp-connectors-registry.js.
Separate from the Notion MCP connector above. This is Notion’s public integration OAuth so Tinker can write finished essays into an Essays database.
https://tinker.beginner.work/api/notion-oauth?action=callback
(use your preview host instead of production when testing a preview).
The callback also accepts code / state without action=callback.NOTION_OAUTH_CLIENT_IDNOTION_OAUTH_CLIENT_SECRETNOTION_OAUTH_REDIRECT_URI (same URL as the redirect URI above)OAuth state is a short-lived signed token that embeds the Tinker user id,
so the callback can finish without the PWA session (in-app sheets and
Safari use a separate cookie jar). Tokens are stored per user in Postgres
(TinkerUserData), encrypted with TINKER_SECRETS_KEY (falls back to
STYTCH_SECRET). Not Upstash. Optional MCP tool: notion_write_essay.
Add this URL in the connector:
https://tinker.beginner.work/api/mcp
A client that speaks MCP OAuth gets a 401 whose WWW-Authenticate header points at the protected-resource metadata. It registers any https redirect URI, or an http loopback, with no client secret and no host allowlist. Tinker sends you to sign in, then one Approve button. The browser goes straight back to the client's redirect URI with code and state. The token response is access_token and token_type of Bearer, plus the resource. There is no expires_in and no refresh token. The client stores the credential. You do not paste a sign-in token.
Revoke from MCP access in the profile menu, or open /mcp/access. A revoked credential fails on the next request.
The happy path is authorization code with PKCE S256 and dynamic client registration. The access token is an opaque mcp_ bearer. The server stores a SHA-256 hash, a label, and the time it was created or revoked. It does not keep the plaintext, and it does not issue a refresh token. The credential lasts until you revoke it.
Clients that cannot finish that redirect, and only accept a static Authorization header, use the same MCP access page. Create a credential there. It is shown once, for that header only.
Authorization: Bearer mcp_...
The credential belongs to the tinker account that approved it. MCP access lists and revokes only that account's credentials. The writing app's own sign-in can still call /api/mcp. That path is for the app, not for a connector.
The endpoint is stateless JSON. A missing or revoked bearer is 401. Authenticated GET and DELETE return 405. There is no server-push session.
Tools:
ask_followups. Pass a founder transcript ([{ "q", "a" }], or
[] to open the interview) and the server runs the same interview
contract the writing UI uses. The result is JSON:
{ mode, next_question, questions, stitched_title, stitched_body, done }.
Pass a freeform draft string instead (not both) for three to five
learning questions. Optional priorTurns avoids repeats. Optional
seed, facing, lastPurchased, voice, and transactions
are the same scene cues the browser interview already sends.
forceStitch: true asks for the essay instead of another question.draft_linkedin_post. Pass notes (a topic or bullets) and the
server drafts a LinkedIn post in Tyler's voice for Elevating Developer
Fintech: short plain sentences, contractions OK, no em dashes. Pass
kind: "dm" for a direct message (or start the notes with DM:).
Pass currentDraft to revise, and an optional instruction for what
to change. The result is { post, revised, kind }. The tool returns
copy only. It does not post to LinkedIn; Stanley still posts.list_content. Lists this user's content items. Optional site,
type (page_section, product, event, post, link), and
status (draft or published). A user id in the arguments is ignored.read_content. Pass id. Someone else's id is an error and the item
is not returned.create_content_draft. Pass site, type, slug, and title.
Optional body, fields, noteId (the note it came from), and
draftKey. The same draftKey returns the original draft and does
not change it. status: "published" is rejected and nothing is saved.
The owner publishes from a Tinker session.There is no raw converse tool. Clients cannot supply a system prompt.
The interview prompt lives in src/renderer/interview-prompt.js and is
what both the browser and ask_followups use. The LinkedIn prompt lives
in api/_lib/linkedin-draft.js and is what both the in-app composer and
draft_linkedin_post use. The writing UI still checks that a stitched
essay uses only the founder's words before it publishes. MCP returns the
model's JSON; it does not publish.
/api/mcp uses the existing STYTCH_PROJECT_ID, STYTCH_SECRET,
ANTHROPIC_API_KEY, and DATABASE_URL. The credential tables are
created on first use if prisma migrate deploy has not been run.
BEGINNER_MCP_TOKEN / BEGINNER_MCP_URL are only the in-app email
relay to the beginner Worker. They are not this endpoint.
A content item is the note a site publishes: owner, site, type
(page_section, product, event, post, link), slug, title, body,
structured fields, status (draft or published), noteId, and
updatedAt. Rows live in the existing Postgres (DATABASE_URL), the
same database as MCP keys. There is no new service and no model call.
The signed-in owner uses /api/content (the same Stytch session bearer
as /api/career, which the Expo app can call). GET lists that user's
items, or one item with ?id=. Someone else's id is 404. POST creates
an item. PATCH edits it, including status. A bearer that starts with
mcp_ is rejected. Bots use the content tools above and cannot publish.
Published items for one site are public and cacheable:
GET /api/sites/dreamingwithmarisol.com/content
GET /api/sites/dreamingwithmarisol.com/content/about
Drafts are omitted. The response does not include the owner, noteId,
or draftKey.
POST /api/content with { "action": "import" } copies the
dreamingwithmarisol.com defaults from tlindow/dreamingwithmarisol PR #38
(content/site.ts, folded like lib/content-store.ts) onto the signed-in
user as drafts. Importing again returns those same rows. How that site
would read the public API is in
docs/marisol-content-loader.md.
The sidebar row LinkedIn draft opens on the writing stage — the
same header, card, inputs, and pill buttons as an essay. Topic or
bullet notes in, a post out. Start the notes with DM: (or pass
kind: "dm" on the API) for a direct message. Revise by editing the
draft (or adding "what to change") and submitting again. Copy the
result; posting still goes through Stanley.
The shared prompt in api/_lib/linkedin-draft.js is the only voice.
It asks for short plain sentences, Tyler's Elevating Developer Fintech
niche, and no em dashes (periods, commas, parentheses, or separate
sentences). A caller cannot replace that system prompt. If the model
still returns an em dash, the server rewrites it before the copy is
returned.
The panel calls the existing converse proxy with mode: "linkedin".
That mode ignores any client system prompt and runs
draftLinkedInPost in api/_lib/linkedin-draft.js. The MCP tool calls
the same function. No new serverless route.
Try it locally with the auth emulator (static npm run web has no
/api):
npx vercel dev # http://localhost:3000
Sign in, open LinkedIn draft in the sidebar, and submit a few bullets. A connector that has been approved calls the same tool:
curl -s https://tinker.beginner.work/api/mcp \
-H "Authorization: Bearer mcp_..." \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"draft_linkedin_post","arguments":{"notes":"A portal is where a buyer decides to trust you."}}}'
Native iOS / Android builds ship via Expo, wrapping
the same src/renderer/ web surface that Vercel hosts. Capacitor is no
longer part of this repo — there is no capacitor.config.json, no
@capacitor/* packages, and no mobile:* npm scripts.
| Electron desktop | Web (Vercel) / Expo | |
|---|---|---|
| Product UI | Production tinker.beginner.work in a BrowserWindow | Same src/renderer/ hosted on Vercel |
| Sign-in | Persistent session partition (tinker_jwt) | localStorage / PWA session |
| Claude / APIs | Same-origin /api/* on production | Same-origin /api/* on Vercel, JWT-gated |
| Notes folder | Native folder dialog + IPC | File System Access API (desktop browsers) |
| External links | OS browser via shell.openExternal | System browser / window.open |
src/renderer/platform-mobile.js detects the runtime. On the plain web
(and inside the Expo shell) it installs window.tinker.*. In the
desktop app, Electron preload installs notes/dock bridges first; the
shim merges callClaude onto that object so notes-folder sync keeps
working.
The desktop-app icon (the multi-colored globe mark) is rendered from
src/renderer/tokens/rainbow-web.json — a JSON design dictionary.
The renderer fetches it via src/renderer/lib/rainbow-web.js, which
exposes window.tinkerLogo.buildRainbowWebSvg() for any consumer
that wants the mark as an SVG string.
.
├── api/ # Product + developer-facing APIs (Vercel)
│ ├── auth/ # Phone/PIN via Stytch (identity wire-up)
│ ├── claude/ # Proxied Claude converse (linkedin mode included)
│ ├── mcp.js # Streamable HTTP MCP (ask_followups, draft_linkedin_post)
│ ├── mcp-oauth.js # MCP authorize, token, and revoke
│ ├── _lib/linkedin-draft.js # Shared LinkedIn prompt + draft call
│ ├── search.js # Search essays
│ ├── pitches/ … # Pitch / publish / feed / user-data / …
│ └── membership/ … # Stripe membership wire-up
├── src/
│ ├── main/ # Electron desktop shell (loads production)
│ ├── renderer/ # Shared web / Expo UI (also what desktop loads remotely)
│ └── web/ # Static local host (`npm run web`)
├── prisma/ # Shared Postgres schema
└── package.json
The renderer is plain HTML/CSS/JS — no build step, no bundler. The
desktop shell loads production in a hardened BrowserWindow (persistent
session, native menus, remembered window size). Web and Expo load the
same src/renderer/ files against the Vercel api/ back end.
| Action | Shortcut |
|---|---|
| Reload | ⌘/Ctrl + R |
| Quit | ⌘/Ctrl + Q (macOS app menu) |
| Close window | ⌘/Ctrl + W |
| Toggle fullscreen | Ctrl+Cmd+F (macOS) / F11 |