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. Drafts are instructed to use your work as evidence. Automated checks flag unsupported numbers and selected formatting problems; they do not verify every claim or translation. You review, copy, and post it yourself. Drafts you edit before copying become voice examples (unedited copies only fill in as shape references), and edits produce guide suggestions for you to review. Confirm actual publication and its time to track available performance signals; add the URL later if needed. Copying alone is not treated as publishing.
Today, publishing is manual. Drafts are grounded in supplied material and checked for selected issues, but unsupported claims and awkward wording can still slip through. Review the source before posting.
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. The prompt asks the model to use numbers from the raw material (release notes, PR titles, commits, README, repo stats) and omit numeric claims without supporting material. The checker flags selected unsupported numbers rather than guaranteeing factual accuracy. The digest is checked too: summary lines flagged by the numeric checker are excluded from the judge and draft inputs, and stay visible on the candidate page. Draft numbers flagged by the checker — including multipliers like "3x" or "twice" — ask for your confirmation when copying. The checker compares digits with their time and size units (2s ≠ 2ms), scale words ("1k", "1 million", 1만), spelled-out multipliers ("halved", "ten times", 절반) and vague magnitudes ("hundreds of", 수백), and flags superlatives such as "the only" or "fastest" that the source doesn't use. It cannot tell when a number from the source is reused with a different meaning, and it does not verify claims without these markers.
|
Inbox Ready drafts first, then candidates ranked by score, with reasoning and a suggested next review. "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 Confirm the actual posting time after you publish. The URL is optional and can be added or corrected later. From then on: stars gained in 7 days minus what the pre-post 7-day trend would have added (rebuilt from GitHub star times when somun had no snapshot yet), visitors (only with a token that has admin access to the repo), and reactions (fetched for X and HN, typed in for the rest). Only completed 7-day observations with sufficient snapshots contribute to channel star averages and recommendations. Overlapping posts on the same repository have unattributed star gains and are excluded from those averages. Observed star changes do not establish a causal effect of publicity. You can also record post-specific visits, installs, and signups verified in external analytics; blank fields remain unmeasured. Per-channel results feed back into which channels the judge suggests; one everyday channel (X, Threads, LinkedIn) with fewer than two posts is added to the judge's picks so it can still collect results. 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, 2–5 short 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 it is or what changed; why / how it differs / decisions / limits only when the evidence supports them; no requests for comments | ko |
| Blog | outline only: 3 title candidates, sections, which numbers go where | ko |
Every draft runs through a slop lint before you see it (warnings remain visible even when automatic repair cannot resolve them): banned phrases, emoji bullets, numbers not found in the source, unsupported superlatives, invented limitations, selected author-role claims that need review, a wrong repo name, missing link, exclamation marks, vote requests, length, and Blog outline structure.
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. Review outcomes also show reviews started, copied drafts, confirmed publications, unfinished or discarded reviews, discard reasons, and regenerations. Median active review time for copied drafts counts time when the review screen is visible and focused; samples with zero measured seconds are excluded. Tracking starts with this feature and excludes external editing and publishing. Rewrite share describes observed editing before copying; it does not establish that learning caused an improvement or include edits made later on another platform. 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.
The shared Gemini pool allows 50 model executions per account per UTC day and 20 queued/running server jobs per account by default. Profiles, analysis, drafts, and automatic repair use execution capacity; failed executions count too. Provider retries within an execution do not use additional account slots, but provider limits still apply. Your own API key and local agent workers are outside these shared-pool limits. The work-status panel shows your usage and reset time, and excess queue requests return 429 with retry guidance. Limits are configurable through SOMUN_SHARED_MODEL_DAILY_LIMIT and SOMUN_SHARED_MODEL_PENDING_LIMIT.
| 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 session stats (+ first 120 chars of each first prompt)Start with one Korean X draft. Choose the first channel and language in Inbox, then add other channels in Settings after finishing a first draft. Existing saved channel settings are preserved.
Add 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.