Skip to main content

penlike

Model how an author writes, register by register, and give an agent what it needs to write in that style. The author can be one person (by default: you), a group, or any corpus.

pip install penlike
penlike new me --me you@example.org
penlike gather me mbox ~/mail/sent.mbox --until 2023-01-01
penlike build me
penlike brief --like me --channel email --to ada@example.org   # what to read before writing
penlike check draft.md --like me --style email.one             # what in the draft is not like you

penlike is made to be used by an agent. Its main surface is three skills:

gh skill install thorwhalen/penlike penlike --agent claude-code
gh skill install thorwhalen/penlike penlike-model --agent claude-code
gh skill install thorwhalen/penlike penlike-source --agent claude-code

or, from the installed package, with no network: penlike install-skills --write.

Skill Use it to
penlike write or rewrite a text in an author's style ("write like me", "in my technical register")
penlike-model build a model, name its registers, add notes, keep it current
penlike-source choose and read the texts, write a custom sourcer, screen for machine-written text

gh skill needs a recent GitHub CLI.

What a model is

A person does not have one style. They write one way to a close colleague, another to a whole team, another in a technical discussion. penlike calls each of these a register and models each one separately.

A model holds:

  • the author's texts, each filed under a register;
  • a measured profile of each register: sentence and word lengths, punctuation, contractions, function words, greetings and sign-offs, formatting habits, word sequences the author returns to;
  • what sets each register apart from the author's others;
  • notes on what numbers miss, each with the texts it rests on;
  • a way to pick examples.

When asked to write, penlike brief picks the register that fits the situation, and returns its profile, notes and examples. penlike check measures a draft against the same register and lists what differs, with figures.

Your texts stay private

A model of how you write is private data, and so is everything in it. It is stored under ~/.local/share/penlike (set PENLIKE_DATA_DIR to move it), never in a project folder. Do not commit a model, a profile, or texts it was built from to a repository.

Use texts written before 2023

Since late 2022 a growing share of what people send has been drafted or polished by a language model. A model of you built from such text learns the machine's habits as yours. The simplest protection is a date:

penlike gather me github your-login --until 2023-01-01

The trade-off is age: older text is cleaner and may be less like how you write now. Whatever you choose, gather tells you how many of the texts it kept are dated in the era of language models.

Sources

penlike sources
Sourcer Reads
mbox a mailbox archive, such as a webmail export
github what one login wrote: discussions, issues, pull requests and the comments on them, not line-by-line review comments (through the gh tool)
correspond any channel of the correspond package (pip install 'penlike[correspond]')
files text files, folders of them, .eml messages
jsonl one JSON document per line
claude-sessions what you typed to a coding agent, which is rarely how you write to people

When none fits, a sourcer is a plain function that yields dicts, referenced by where it lives:

def read(*refs, since=None, until=None, limit=None, me=(), **options):
    for path in refs:
        ...
        yield {"text": body, "date": sent, "channel": "chat", "to": readers, "is_self": True}
penlike gather me ~/.config/penlike/sourcers/chat.py:read ~/exports/chat.json

Only text is required. to and channel decide the register, date allows a cutoff, is_self keeps other people's writing out. Quoted replies and forwarded messages are removed from every text by rules over plain text, which can miss an unusual mail program: read a sample of what was gathered (penlike docs me --full). The penlike-source skill tells an agent how to write one for you.

Registers

Texts are filed by situation first: email by number of readers (email.one, email.few, email.many), GitHub writing by opening or reply, and so on. Then penlike looks inside each for groups written differently, and proposes them:

penlike propose me
penlike register-add me close-colleagues --proposal email.one#2

A proposal is a question: you name the register, or decline. A named register keeps its identifier for good, and later texts join it when they are close enough. Routing, when you ask to write:

You give Register chosen
--style close-colleagues the one you named
--to ada@example.org the one you have used most with that reader
--channel email --audience many the one for that situation
nothing the largest, and the brief says it is a guess

One person, a group, a corpus

penlike new me
penlike new house-style --kind group --basis consent
penlike new field-guide --kind corpus --basis public
Kind Kept Specific to it
person only the author's own texts registers by reader, greetings and sign-offs, how they write to each person
group everyone's texts shared conventions; individual habits average out
corpus everything text types; readers and dates are often unknown

Measuring and checking are the same in all three. What differs is what is kept, how texts are filed, and what may be claimed.

Screening for machine-written text (optional)

penlike screen me --tier cheap          # says what it would cost; changes nothing
penlike screen me --tier cheap --run
Tier What it does Cost
date excludes texts dated on or after a cutoff none
cheap wording, mechanics, sentence shapes, rhythm (pip install 'penlike[screen]') no model, no network; milliseconds a text
heavy adds detectors that run small language models on your machine (pip install 'ductus[local]') processor time, timed on a sample first; a model download on first use
agent an agent reads each text model tokens, in proportion to the corpus

Without --run, nothing is changed and the cost is stated. Flagged texts are excluded, never deleted: penlike exclude me <id> --undo puts one back. No detector is reliable on one short text, and each accuses some human writing. The date is the tier to trust. Screening uses ductus.

Writing to someone, in your voice

penlike answers "how does the author write?". acquaint answers a different question: "what does this reader need?". To write to a known person in your own voice, use both: penlike for the voice, acquaint for the reader. The penlike skill does this when acquaint is installed.

From Python

import penlike

penlike.new("me", me=["you@example.org"])
penlike.gather("me", "mbox", ["sent.mbox"], until="2023-01-01")
penlike.build("me")
brief = penlike.brief(like="me", channel="email", to=["ada@example.org"])
print(brief["text"])
report = penlike.check(draft, like="me", style=brief["register"])
for item in report["discrepancies"]:
    print(item["message"])

Every verb returns a JSON-ready dict with ok, a summary and usually a text. The same verbs are served over MCP by penlike-mcp (pip install 'penlike[mcp]'), except those that reach into the machine: gathering, deleting a model, writing batch files and linking skills stay at the terminal.

Limits

  • Passing check means the measurable surface of a draft matches the author. It does not mean a reader who knows them would take the text for theirs.
  • Imitation by a language model works best on structured writing and worst on informal, personal writing.
  • A register with under about 2000 words is marked provisional; its figures are rough.
  • The word lists (function words, hedges, greetings) are English. On other languages the lengths, punctuation and formatting features still hold.
  • The thresholds used to propose registers were set on synthetic text. Treat proposals as questions.

Responsible use

Build a model of your own writing, or of an author who agreed to it. Do not present a text as written by someone who neither wrote nor approved it. Follow the usage policy of the language model you use. Every model records the basis on which it was made.

Research

The design follows a review of the literature on stylometry, style imitation with language models, and register: misc/docs/research_report.md. The design record is misc/docs/design.md.

Metadata

Release files for penlike 0.0.2

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for penlike 0.0.2
File Size Uploaded
penlike-0.0.2.tar.gz 75.6 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for penlike 0.0.2
File Interpreter ABI Platform
penlike-0.0.2-py3-none-any.whl Python 3 none any Details

Total release size: 147.7 kB

Release files / penlike-0.0.2.tar.gz

Download URL penlike-0.0.2.tar.gz
Size 75.6 kB
Tags Source
SHA-256 checksum
How to use checksums
2aa5bdd68d23ff061fdca98998694e1f7f5a2c54125f7528e7e587f17f99e22e
BLAKE2b-256 checksum
How to use checksums
064adc334dd5606413ccb4cf7101cecd465ed32cad54c77c4e086a767c8f06fc
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via uv/0.12.23 {"installer":{"name":"uv","version":"0.12.23","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

Release files / penlike-0.0.2-py3-none-any.whl

Download URL penlike-0.0.2-py3-none-any.whl
Size 72.1 kB
Tags Python 3
SHA-256 checksum
How to use checksums
213eef21ac3ab283bc91d58f3ba0a095bdd46ffa3f264ff98439c3b4d724d7d4
BLAKE2b-256 checksum
How to use checksums
acac01a206769f2fe386b4e6a440c437b07b19a0c53da6ba4e0a703bd9c1804b
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via uv/0.12.23 {"installer":{"name":"uv","version":"0.12.23","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

Release history Release notifications | RSS feed

This release

0.0.2 This release

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page