A CLI tool that generates personalized LinkedIn outreach messages. Paste a job description, get a grounded message in under a minute.
$brew install pipx
$pipx install tmessage-cli
$tmessage
The tradeoff is brutal — send a generic message in 30 seconds, or spend 10 minutes researching someone and write something actually personalized. Neither felt right.
I wanted a CLI tool that could do both: research the person, understand the company, and write a grounded message that doesn't sound like it came from a template.
The obvious first idea was: take a LinkedIn URL, scrape the profile, research the company, generate a message. Clean. Simple. Wrong.
LinkedIn aggressively blocks scraping. So I made the first real design decision: the user pastes the job description directly. One copy-paste, and the tool gets far better signal than any scraper could return reliably.
Don't make the agent guess at something the user already has in front of them.
For users without a job description, the tool degrades gracefully across three tiers:
key_facts (grounded responsibilities) and persona_inference (only when directly supported by text — empty list is fine). Also generates a disambiguated search_query.Extraction and evaluation use a smaller, faster model. Message generation uses a larger one — only where writing quality actually matters.
The most embarrassing bug in testing: I ran the tool on someone who worked at Greenlight — the fintech app that teaches kids about money. The tool came back with a message about AI-ready data center infrastructure.
It had found Greenlight Data Centers. A completely different company.
The root cause was the search query. Naive {company} {title} was ambiguous enough to pull the wrong result. The fix: have the extraction model generate a disambiguated query as part of its output — "Greenlight fintech kids finance app" instead of just "Greenlight". One extra field, dramatically better retrieval.
After generating a message, a second model call scores it against a checklist: under 100 words, no buzzwords, low-pressure ask, claims grounded in context, no fabricated Tavily use cases. Fails → regenerate with critique, up to 3 retries.
Calibrating this was the most iterative part. The first version kept flagging messages for mentioning Tavily — reasoning it wasn't in the search results, so any reference was "unsupported." Which is technically true and completely misses the point.
Tavily is the product being pitched. It's always going to be in the message. Only flag specific claims about the prospect's stack that aren't grounded in context.
After that calibration, the loop started catching the right things — like when the generator implied a prospect was using "search and retrieval layers" with no evidence, or wrote about data center infrastructure for someone at a fintech app.
Using a large model for everything is the lazy path. Smaller models for structured tasks, larger only where writing quality matters.
The wrong Greenlight bug wasn't a Tavily problem — it was a query problem. The information to fix it was already in the job description.
A checklist without intent will flag the wrong things. The evaluator needed to know that Tavily mentions are always valid — that took iteration.
Empty persona inference list. Generic fallback message. A weird, confident hallucination is worse than a forgettable generic one.