For developers who aren't good at PR: somun turns what you actually shipped into posts your users can understand.
somun (소문) is Korean for "word of mouth". Try it at somun.jiun.dev.
English · 한국어
Candidates → a draft per channel with lint results → copy → link preview check. Recorded on public Open330 repositories.
You built a lot. Explaining it is the hard part.
somun reads your releases, PRs, and commits, decides when something is actually worth telling, and drafts a post for each channel — X, Threads, LinkedIn, Show HN, GeekNews — in words your users understand. Every fact comes from your actual work. You review, copy, and post it yourself. Every edit you make teaches it your voice. Every post you register gets measured.
It never posts for you. It never writes from thin air. It never says "excited to announce".
GitHub · npm · blog · agent sessions
│
▼
┌─────────────────┐ commits, PR titles, release notes, what you struggled with
│ 1. observe │ ──────────────────────────────────────────────────────────
└────────┬────────┘
▼
┌─────────────────┐ keeps only what an outside reader would care about:
│ 2. digest │ user-visible changes · real numbers · reversals · lessons
└────────┬────────┘ drops refactors, chores, CI, bumps
▼
┌─────────────────┐ five criteria, 0–2 each, with reasoning you can argue with:
│ 3. judge │ runnable · numbers · lesson · novelty · audience
└────────┬────────┘ ≥ 6 → draft 4–5 → defer < 4 → just ask
▼
┌─────────────────┐ one draft per channel, in that channel's shape and language,
│ 4. draft │ facts only from the digest, voice from your preset, guide, and copied posts,
└────────┬────────┘ numbers checked against the raw material, slop-linted before it reaches you
▼
┌─────────────────┐ copy · edit-then-copy · drop (with a reason) · "posted, here's the URL"
│ 5. you │
└────────┬────────┘
▼
┌─────────────────┐ copied posts → voice examples · edits, drops → guide suggestions · drops → judge calibration
│ 6. learn │ URLs → star gain beyond the 7-day pre-post trend, fed back into channel picks
└─────────────────┘
Facts from the system, voice from you. A draft may only use numbers that exist in the raw material (release notes, PR titles, commits, README, repo stats). If a number is missing, the claim is left out, not invented. The digest is checked too: a summary line whose number is not in the source never reaches the judge or the draft, and it stays visible on the candidate page. A draft number that still cannot be found in the source — including multipliers like "3x" or "twice" — is flagged, and copying asks you to confirm.
|
Inbox Candidates ranked by score, one line of reasoning each. "Not worth it" is a valid answer and you can overrule it. Candidate Evidence on the left (version, stars, downloads, demo asset, limitations, digest). Drafts on the right, one tab per channel, with lint results and a copy button. |
Published Paste the URL after you post (you can fix or remove it later). From then on: stars gained in 7 days minus what the pre-post 7-day trend would have added, visitors, and reactions (fetched for X and HN, typed in for the rest). Per-channel results feed back into which channels the judge suggests. Settings Sources, channels, rubric weights, banned phrases, voice examples, and which model runs the whole thing. The interface is in English and Korean. It follows your browser language and can be switched from the sidebar; the choice also sets the language of judgment reasoning, error messages, and the weekly summary. |
| Channel | Shape | Language |
|---|---|---|
| X | three lines: problem · what it does · one number or limit + link | en, ko |
| Threads | one or two sentences, ends with a take or a question | ko |
| hook above the fold, 3–5 paragraphs, ≤ 3 hashtags | ko | |
| Show HN | title + the author's first comment: problem, mechanism, design choices, limitations, one open question | en |
| Show GN (GeekNews) | what / why / how it differs / decisions / limits / feedback wanted | ko |
| Blog | outline only: 3 title candidates, sections, which numbers go where | ko |
Every draft passes a slop lint before you see it: banned phrases, emoji bullets, numbers not found in the source, invented limitations, a wrong repo name, missing link, exclamation marks, vote requests, length.
Is it learning? The Voice page shows, for drafts you copied, how often you used them unchanged and how much you rewrote — by week and by voice-setting version. If the guide and examples work, the rewrite share goes down. npm run experiment -- export-holdout turns your copied drafts into a private held-out set (experiments/holdout/, git-ignored) with your final text as the baseline.
| Provider | Default model | Key |
|---|---|---|
| Gemini (default) | gemini-3.5-flash-lite |
server key pool, or your own |
| Anthropic | claude-opus-5 |
your own |
| OpenAI-compatible | gpt-5 |
your own, optional base URL (OpenRouter, Ollama, …) |
| Local agent | your Claude Code or Codex subscription | none — a worker on your machine picks up jobs |
The local agent mode queues every model call — digest, judgment, drafts, repository profiles, and voice lessons — as a job, so no source text goes to a server model. Run the worker where your CLI is logged in:
npm run agent-worker -- --cli claude # or --cli codexOne process, one SQLite file. No external services.
nvm install && nvm use # .nvmrc: Node 22 (same major as CI/Docker)
npm ci
cp .env.example .env # set GITHUB_TOKEN and GEMINI_API_KEYS; SOMUN_ALLOW_ANONYMOUS=true for local
npm run dev # API on :8790, web on :5180
npm run seed # best-practice voice examples (once)
npm run push -- --sources omp --days 14 # optional: attach oh-my-prompt session summariesAdd a GitHub source in Settings (Open330, you/repo), press Check now in the Inbox, and read what it found.
npm run lint && npm run typecheck && npm test && npm run buildTo register a GitHub App through the UI, set SOMUN_ADMIN_OWNER_ID to the administrator’s ownerId from /api/me (local by default for shared-token auth). Anonymous sessions cannot register apps. Behind a reverse proxy, set SOMUN_PUBLIC_URL to the public HTTPS URL. Existing app installations do not require these settings.
Update and restart local workers together with the server: completion now requires a claim token. Claims expire after 10 minutes; abandoned jobs are reclaimed on the next poll, up to 3 attempts. Invalid results and explicit worker failures are not automatically retried. SQL migrations run on server startup, or manually with npm run db:migrate.
A candidate can become a 10 to 20 second video (16:9 or 1:1). Three processes split the work:
- somun builds a brief from the candidate's evidence, the draft you pick, and your voice, and follows the render.
- The video server (
src/video) queues renders, runs headless Chromium under a virtual clock, encodes with ffmpeg, and exposes two MCP tools:check_sceneandrender_video. - The bridge (
scripts/video-bridge.ts) runs on your computer. It polls the video server and runsclaude -pwith only those two tools, billed to your Claude Code account. The server never connects to your machine.
check_scene lints the text visible on screen like a draft (numbers not in the evidence, banned phrases, exclamation marks, script errors). render_video only accepts a scene that passed. Pages may load Google Fonts and nothing else.
npx playwright-core install chromium-headless-shell # once; ffmpeg must be on PATH
echo "VIDEO_SERVICE_TOKEN=$(openssl rand -hex 24)" > .env.video
echo "VIDEO_HOST=0.0.0.0" >> .env.video # to reach it from the LAN; default 127.0.0.1
npm run video-server # :8791Point somun at it with SOMUN_VIDEO_URL (how somun reaches the video server), SOMUN_VIDEO_TOKEN (the VIDEO_SERVICE_TOKEN value) and, when bridges connect through another address, SOMUN_VIDEO_PUBLIC_URL (for example http://192.168.32.55:8791). The Short video block then appears on each candidate.
Each person connects their own Claude Code: Settings › Model › Video bridge issues a token (shown once; somun keeps only its hash and registers the hash with the video server) and shows the command to run in this repository:
VIDEO_SERVER_URL=http://192.168.32.55:8791 VIDEO_BRIDGE_TOKEN=smb_… npm run video-bridge
npm run video-bridge -- --claude "aas exec <profile> -- claude" # to pin the Claude accountA bridge only receives its owner's renders. Revoking a token removes it from the video server immediately, or within 5 minutes if the server was unreachable. Operators can still set fixed tokens with VIDEO_BRIDGE_TOKENS=<ownerId>=<token>,... in .env.video.
If the repository has a homepage, somun reads its colors and Google Fonts (public addresses only, a few hundred KB at most) and adds them to the brief. Only #rrggbb values and font names reach the model.
A single container. The API and the built web are served by the same Node process; the database is a file on a volume.
docker build -f docker/Dockerfile -t somun .
docker run -p 8790:8790 -v somun-data:/data \
-e SOMUN_TOKEN=change-me -e GITHUB_TOKEN=… -e GEMINI_API_KEYS='{"free-1":"…"}' somunAuth is one of three modes, checked in order: a shared SOMUN_TOKEN bearer (single user), an RS256 JWT from an issuer you trust (AUTH_ISSUER, AUTH_JWKS_URL, AUTH_AUDIENCE; this is how somun.jiun.dev uses api.jiun.dev), or SOMUN_ALLOW_ANONYMOUS=true for local development only.
With JWT, several people can sign in; each gets a separate workspace (sources, candidates, drafts, voice, publications). The server GITHUB_TOKEN reads private repositories only for SOMUN_ADMIN_OWNER_ID and the shared-token owner; everyone else reads public repositories with it, or connects their own GitHub App installation.
Security defaults: set SOMUN_SECRET_KEY (any random string of 16+ characters, e.g. openssl rand -base64 32) to encrypt stored API keys, webhook URLs, and the GitHub App private key with AES-256-GCM; plaintext values are encrypted on the next start, and a wrong key stops the server at startup instead of failing requests. In token mode the browser exchanges the token once for an HttpOnly, SameSite=Strict session cookie; the live update stream uses single-use tickets, so no token or JWT ever appears in a URL. Accounts other than the operator cannot point feeds or a model baseUrl at private or link-local addresses. Linking a GitHub App installation for the first time requires proof that the signed-in GitHub user can access it: enable "Request user authorization (OAuth) during installation" on the app, set its callback URL to <public URL>/github/setup, and provide GITHUB_APP_CLIENT_ID/GITHUB_APP_CLIENT_SECRET (apps created from the built-in manifest get all of this automatically). Without it only the operator can link installations.
Layers point inward. core knows nothing about IO; app knows nothing about HTTP; server is thin.
src/
core/ pure domain — no IO, fully unit-tested
channels per-channel shape, rules, media hints, compose links
prompts the three prompts (digest · judge · draft) and their JSON schemas
lint slop lint
cluster signal → candidate rules, milestone thresholds
keypool Gemini 429 classification, PT-midnight day keys
app/ use cases — take an AppContext (db, log, env, event bus)
collect GitHub / npm → signals, evidence, metric snapshots
signals clustering, back-to-back release merging, PR attachment
pipeline digest → judge → draft; provider call or local-agent queue; result application
review copy / edit / drop → voice examples and feedback
publications · candidates · sources · settings · keys · jobs · sessions · scheduler
infra/ adapters — SQLite (Drizzle), GitHub REST, LLM providers, logger
server/ Hono — auth middleware (token · JWT · anonymous), /api routes, SSE, static web
shared/ types shared by server and web
web/ Vite + React — Inbox · Candidate · Published · Settings
scripts/ seed · push-sessions · agent-worker (talk to the HTTP API only)
drizzle/ SQL migrations, applied on start
seeds/ 38 real Show HN first comments that landed, plus per-channel rules
The web never touches the database: it reads /api/* and subscribes to /api/events (SSE) to refetch what changed.
Run npm run experiment to replay fixed cases without model calls. See the experiment guide for explicit live runs, repeated prompt comparisons, blinded human review, and paired regression checks. Automatic checks and publishability are reported separately.