penlike 0.0.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,43 @@
1
+ # wads CI — calls the reusable workflow hosted in i2mint/wads.
2
+ #
3
+ # All configuration comes from this repo's pyproject.toml [tool.wads.ci.*].
4
+ # To customize the workflow itself (rare), replace this file with the
5
+ # full inline template `wads/data/github_ci_uv.yml` from i2mint/wads.
6
+ #
7
+ # Pinning: `@master` floats with wads. If you need version stability for
8
+ # a release-sensitive repo, change `@master` to a wads tag (e.g. `@0.2.15`;
9
+ # tags have no `v` prefix). A stub whose `secrets:` block passes the JSON
10
+ # transport (the default below) needs a tag from a release after 0.2.14 —
11
+ # older tags don't declare that secret and GitHub then rejects the
12
+ # workflow at parse time.
13
+ # CI failure does not block a published release — it blocks the publish
14
+ # step itself — so floating master is generally safe.
15
+ #
16
+ # Permissions: GitHub validates that the caller grants AT LEAST the
17
+ # permissions any job in the called workflow requests — at workflow-parse
18
+ # time, not at run-time, even if the job would be skipped via `if:`.
19
+ # The reusable workflow needs:
20
+ # contents: write for the publish job's version-bump push-back
21
+ # and for the github-pages job's gh-pages branch push
22
+ # pages: write for the github-pages job's REST API Pages config
23
+ # Both default to `write` on org-account GITHUB_TOKEN and need to be
24
+ # granted explicitly on personal-account callers (where the default is
25
+ # read-only). No `id-token: write` needed — the publish-github-pages
26
+ # action uses peaceiris/actions-gh-pages (branch-based) + REST API,
27
+ # not the OIDC `actions/deploy-pages` flow.
28
+ name: Continuous Integration
29
+ on: [push, pull_request]
30
+ jobs:
31
+ ci:
32
+ uses: i2mint/wads/.github/workflows/uv-ci.yml@master
33
+ permissions:
34
+ contents: write
35
+ pages: write
36
+ # Transport (NAMED, legacy): explicitly passes only the secrets
37
+ # listed below (PYPI_PASSWORD + those declared in
38
+ # [tool.wads.ci.env]). Every name must be in the frozen wads
39
+ # superset (wads/ci_secrets.py) or GitHub rejects the workflow
40
+ # at parse time. The default JSON transport has no such limit;
41
+ # regenerate with `wads-migrate ci-to-stub` to switch.
42
+ secrets:
43
+ PYPI_PASSWORD: ${{ secrets.PYPI_PASSWORD }}
@@ -0,0 +1,120 @@
1
+ .claude/handoffs/
2
+ .claude/scratch/
3
+
4
+ # Byte-compiled / optimized / DLL files
5
+ __pycache__/
6
+ *.py[cod]
7
+ *$py.class
8
+
9
+
10
+ .DS_Store
11
+ # C extensions
12
+ *.so
13
+
14
+ # TLS certificates
15
+ ## Ignore all PEM files anywhere
16
+ *.pem
17
+ ## Also ignore any certs directory
18
+ certs/
19
+
20
+ # Distribution / packaging
21
+ .Python
22
+ build/
23
+ develop-eggs/
24
+ dist/
25
+ downloads/
26
+ eggs/
27
+ .eggs/
28
+ lib/
29
+ lib64/
30
+ parts/
31
+ sdist/
32
+ var/
33
+ wheels/
34
+ *.egg-info/
35
+ .installed.cfg
36
+ *.egg
37
+ MANIFEST
38
+ _build
39
+
40
+ # PyInstaller
41
+ # Usually these files are written by a python script from a template
42
+ # before PyInstaller builds the exe, so as to inject date/other infos into it.
43
+ *.manifest
44
+ *.spec
45
+
46
+ # Installer logs
47
+ pip-log.txt
48
+ pip-delete-this-directory.txt
49
+
50
+ # Unit test / coverage reports
51
+ htmlcov/
52
+ .tox/
53
+ .coverage
54
+ .coverage.*
55
+ .cache
56
+ nosetests.xml
57
+ coverage.xml
58
+ *.cover
59
+ .hypothesis/
60
+ .pytest_cache/
61
+
62
+ # Translations
63
+ *.mo
64
+ *.pot
65
+
66
+ # Django stuff:
67
+ *.log
68
+ local_settings.py
69
+ db.sqlite3
70
+
71
+ # Flask stuff:
72
+ instance/
73
+ .webassets-cache
74
+
75
+ # Scrapy stuff:
76
+ .scrapy
77
+
78
+ # Sphinx documentation
79
+ docs/_build/
80
+ docs/*
81
+
82
+ # PyBuilder
83
+ target/
84
+
85
+ # Jupyter Notebook
86
+ .ipynb_checkpoints
87
+
88
+ # pyenv
89
+ .python-version
90
+
91
+ # celery beat schedule file
92
+ celerybeat-schedule
93
+
94
+ # SageMath parsed files
95
+ *.sage.py
96
+
97
+ # Environments
98
+ .env
99
+ .venv
100
+ env/
101
+ venv/
102
+ ENV/
103
+ env.bak/
104
+ venv.bak/
105
+
106
+ # Spyder project settings
107
+ .spyderproject
108
+ .spyproject
109
+
110
+ # Rope project settings
111
+ .ropeproject
112
+
113
+ # mkdocs documentation
114
+ /site
115
+
116
+ # mypy
117
+ .mypy_cache/
118
+
119
+ # PyCharm
120
+ .idea
penlike-0.0.2/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Thor Whalen
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
penlike-0.0.2/PKG-INFO ADDED
@@ -0,0 +1,213 @@
1
+ Metadata-Version: 2.5
2
+ Name: penlike
3
+ Version: 0.0.2
4
+ Summary: Model how an author, a group or a corpus writes, register by register, and give agents the skills to write in that style
5
+ Project-URL: Homepage, https://github.com/thorwhalen/penlike
6
+ Project-URL: Repository, https://github.com/thorwhalen/penlike
7
+ Project-URL: Documentation, https://thorwhalen.github.io/penlike
8
+ Author: Thor Whalen
9
+ License: MIT
10
+ License-File: LICENSE
11
+ Keywords: agent-skills,authorship,idiolect,llm,register,style-transfer,stylometry,writing,writing-style
12
+ Classifier: Development Status :: 3 - Alpha
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: License :: OSI Approved :: MIT License
15
+ Classifier: Operating System :: OS Independent
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Topic :: Text Processing :: Linguistic
18
+ Requires-Python: >=3.10
19
+ Requires-Dist: cw<0.2,>=0.1.1
20
+ Requires-Dist: dol
21
+ Provides-Extra: correspond
22
+ Requires-Dist: correspond; extra == 'correspond'
23
+ Provides-Extra: dev
24
+ Requires-Dist: pytest-cov>=4.0; extra == 'dev'
25
+ Requires-Dist: pytest>=7.0; extra == 'dev'
26
+ Requires-Dist: ruff>=0.1.0; extra == 'dev'
27
+ Provides-Extra: docs
28
+ Requires-Dist: sphinx-rtd-theme>=1.0; extra == 'docs'
29
+ Requires-Dist: sphinx>=6.0; extra == 'docs'
30
+ Provides-Extra: mcp
31
+ Requires-Dist: py2mcp>=0.1.12; extra == 'mcp'
32
+ Provides-Extra: screen
33
+ Requires-Dist: ductus; extra == 'screen'
34
+ Description-Content-Type: text/markdown
35
+
36
+ # penlike
37
+
38
+ 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.
39
+
40
+ ```bash
41
+ pip install penlike
42
+ ```
43
+
44
+ ```bash
45
+ penlike new me --me you@example.org
46
+ penlike gather me mbox ~/mail/sent.mbox --until 2023-01-01
47
+ penlike build me
48
+ penlike brief --like me --channel email --to ada@example.org # what to read before writing
49
+ penlike check draft.md --like me --style email.one # what in the draft is not like you
50
+ ```
51
+
52
+ penlike is made to be used by an agent. Its main surface is three skills:
53
+
54
+ ```bash
55
+ gh skill install thorwhalen/penlike penlike --agent claude-code
56
+ gh skill install thorwhalen/penlike penlike-model --agent claude-code
57
+ gh skill install thorwhalen/penlike penlike-source --agent claude-code
58
+ ```
59
+
60
+ or, from the installed package, with no network: `penlike install-skills --write`.
61
+
62
+ | Skill | Use it to |
63
+ |---|---|
64
+ | `penlike` | write or rewrite a text in an author's style ("write like me", "in my technical register") |
65
+ | `penlike-model` | build a model, name its registers, add notes, keep it current |
66
+ | `penlike-source` | choose and read the texts, write a custom sourcer, screen for machine-written text |
67
+
68
+ `gh skill` needs a recent GitHub CLI.
69
+
70
+ ## What a model is
71
+
72
+ 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.
73
+
74
+ A model holds:
75
+
76
+ - the author's **texts**, each filed under a register;
77
+ - 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;
78
+ - what sets each register apart from the author's others;
79
+ - **notes** on what numbers miss, each with the texts it rests on;
80
+ - a way to pick **examples**.
81
+
82
+ 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.
83
+
84
+ ## Your texts stay private
85
+
86
+ 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.
87
+
88
+ ## Use texts written before 2023
89
+
90
+ 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:
91
+
92
+ ```bash
93
+ penlike gather me github your-login --until 2023-01-01
94
+ ```
95
+
96
+ 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.
97
+
98
+ ## Sources
99
+
100
+ ```bash
101
+ penlike sources
102
+ ```
103
+
104
+ | Sourcer | Reads |
105
+ |---|---|
106
+ | `mbox` | a mailbox archive, such as a webmail export |
107
+ | `github` | what one login wrote: discussions, issues, pull requests and the comments on them, not line-by-line review comments (through the `gh` tool) |
108
+ | `correspond` | any channel of the [correspond](https://github.com/thorwhalen/correspond) package (`pip install 'penlike[correspond]'`) |
109
+ | `files` | text files, folders of them, `.eml` messages |
110
+ | `jsonl` | one JSON document per line |
111
+ | `claude-sessions` | what you typed to a coding agent, which is rarely how you write to people |
112
+
113
+ When none fits, a sourcer is a plain function that yields dicts, referenced by where it lives:
114
+
115
+ ```python
116
+ def read(*refs, since=None, until=None, limit=None, me=(), **options):
117
+ for path in refs:
118
+ ...
119
+ yield {"text": body, "date": sent, "channel": "chat", "to": readers, "is_self": True}
120
+ ```
121
+
122
+ ```bash
123
+ penlike gather me ~/.config/penlike/sourcers/chat.py:read ~/exports/chat.json
124
+ ```
125
+
126
+ 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.
127
+
128
+ ## Registers
129
+
130
+ 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:
131
+
132
+ ```bash
133
+ penlike propose me
134
+ penlike register-add me close-colleagues --proposal email.one#2
135
+ ```
136
+
137
+ 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:
138
+
139
+ | You give | Register chosen |
140
+ |---|---|
141
+ | `--style close-colleagues` | the one you named |
142
+ | `--to ada@example.org` | the one you have used most with that reader |
143
+ | `--channel email --audience many` | the one for that situation |
144
+ | nothing | the largest, and the brief says it is a guess |
145
+
146
+ ## One person, a group, a corpus
147
+
148
+ ```bash
149
+ penlike new me
150
+ penlike new house-style --kind group --basis consent
151
+ penlike new field-guide --kind corpus --basis public
152
+ ```
153
+
154
+ | Kind | Kept | Specific to it |
155
+ |---|---|---|
156
+ | `person` | only the author's own texts | registers by reader, greetings and sign-offs, how they write to each person |
157
+ | `group` | everyone's texts | shared conventions; individual habits average out |
158
+ | `corpus` | everything | text types; readers and dates are often unknown |
159
+
160
+ Measuring and checking are the same in all three. What differs is what is kept, how texts are filed, and what may be claimed.
161
+
162
+ ## Screening for machine-written text (optional)
163
+
164
+ ```bash
165
+ penlike screen me --tier cheap # says what it would cost; changes nothing
166
+ penlike screen me --tier cheap --run
167
+ ```
168
+
169
+ | Tier | What it does | Cost |
170
+ |---|---|---|
171
+ | `date` | excludes texts dated on or after a cutoff | none |
172
+ | `cheap` | wording, mechanics, sentence shapes, rhythm (`pip install 'penlike[screen]'`) | no model, no network; milliseconds a text |
173
+ | `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 |
174
+ | `agent` | an agent reads each text | model tokens, in proportion to the corpus |
175
+
176
+ 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](https://github.com/thorwhalen/ductus).
177
+
178
+ ## Writing to someone, in your voice
179
+
180
+ penlike answers "how does the author write?". [acquaint](https://github.com/thorwhalen/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.
181
+
182
+ ## From Python
183
+
184
+ ```python
185
+ import penlike
186
+
187
+ penlike.new("me", me=["you@example.org"])
188
+ penlike.gather("me", "mbox", ["sent.mbox"], until="2023-01-01")
189
+ penlike.build("me")
190
+ brief = penlike.brief(like="me", channel="email", to=["ada@example.org"])
191
+ print(brief["text"])
192
+ report = penlike.check(draft, like="me", style=brief["register"])
193
+ for item in report["discrepancies"]:
194
+ print(item["message"])
195
+ ```
196
+
197
+ 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.
198
+
199
+ ## Limits
200
+
201
+ - 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.
202
+ - Imitation by a language model works best on structured writing and worst on informal, personal writing.
203
+ - A register with under about 2000 words is marked provisional; its figures are rough.
204
+ - The word lists (function words, hedges, greetings) are English. On other languages the lengths, punctuation and formatting features still hold.
205
+ - The thresholds used to propose registers were set on synthetic text. Treat proposals as questions.
206
+
207
+ ## Responsible use
208
+
209
+ 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.
210
+
211
+ ## Research
212
+
213
+ The design follows a review of the literature on stylometry, style imitation with language models, and register: [misc/docs/research_report.md](misc/docs/research_report.md). The design record is [misc/docs/design.md](misc/docs/design.md).
@@ -0,0 +1,178 @@
1
+ # penlike
2
+
3
+ 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.
4
+
5
+ ```bash
6
+ pip install penlike
7
+ ```
8
+
9
+ ```bash
10
+ penlike new me --me you@example.org
11
+ penlike gather me mbox ~/mail/sent.mbox --until 2023-01-01
12
+ penlike build me
13
+ penlike brief --like me --channel email --to ada@example.org # what to read before writing
14
+ penlike check draft.md --like me --style email.one # what in the draft is not like you
15
+ ```
16
+
17
+ penlike is made to be used by an agent. Its main surface is three skills:
18
+
19
+ ```bash
20
+ gh skill install thorwhalen/penlike penlike --agent claude-code
21
+ gh skill install thorwhalen/penlike penlike-model --agent claude-code
22
+ gh skill install thorwhalen/penlike penlike-source --agent claude-code
23
+ ```
24
+
25
+ or, from the installed package, with no network: `penlike install-skills --write`.
26
+
27
+ | Skill | Use it to |
28
+ |---|---|
29
+ | `penlike` | write or rewrite a text in an author's style ("write like me", "in my technical register") |
30
+ | `penlike-model` | build a model, name its registers, add notes, keep it current |
31
+ | `penlike-source` | choose and read the texts, write a custom sourcer, screen for machine-written text |
32
+
33
+ `gh skill` needs a recent GitHub CLI.
34
+
35
+ ## What a model is
36
+
37
+ 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.
38
+
39
+ A model holds:
40
+
41
+ - the author's **texts**, each filed under a register;
42
+ - 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;
43
+ - what sets each register apart from the author's others;
44
+ - **notes** on what numbers miss, each with the texts it rests on;
45
+ - a way to pick **examples**.
46
+
47
+ 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.
48
+
49
+ ## Your texts stay private
50
+
51
+ 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.
52
+
53
+ ## Use texts written before 2023
54
+
55
+ 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:
56
+
57
+ ```bash
58
+ penlike gather me github your-login --until 2023-01-01
59
+ ```
60
+
61
+ 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.
62
+
63
+ ## Sources
64
+
65
+ ```bash
66
+ penlike sources
67
+ ```
68
+
69
+ | Sourcer | Reads |
70
+ |---|---|
71
+ | `mbox` | a mailbox archive, such as a webmail export |
72
+ | `github` | what one login wrote: discussions, issues, pull requests and the comments on them, not line-by-line review comments (through the `gh` tool) |
73
+ | `correspond` | any channel of the [correspond](https://github.com/thorwhalen/correspond) package (`pip install 'penlike[correspond]'`) |
74
+ | `files` | text files, folders of them, `.eml` messages |
75
+ | `jsonl` | one JSON document per line |
76
+ | `claude-sessions` | what you typed to a coding agent, which is rarely how you write to people |
77
+
78
+ When none fits, a sourcer is a plain function that yields dicts, referenced by where it lives:
79
+
80
+ ```python
81
+ def read(*refs, since=None, until=None, limit=None, me=(), **options):
82
+ for path in refs:
83
+ ...
84
+ yield {"text": body, "date": sent, "channel": "chat", "to": readers, "is_self": True}
85
+ ```
86
+
87
+ ```bash
88
+ penlike gather me ~/.config/penlike/sourcers/chat.py:read ~/exports/chat.json
89
+ ```
90
+
91
+ 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.
92
+
93
+ ## Registers
94
+
95
+ 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:
96
+
97
+ ```bash
98
+ penlike propose me
99
+ penlike register-add me close-colleagues --proposal email.one#2
100
+ ```
101
+
102
+ 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:
103
+
104
+ | You give | Register chosen |
105
+ |---|---|
106
+ | `--style close-colleagues` | the one you named |
107
+ | `--to ada@example.org` | the one you have used most with that reader |
108
+ | `--channel email --audience many` | the one for that situation |
109
+ | nothing | the largest, and the brief says it is a guess |
110
+
111
+ ## One person, a group, a corpus
112
+
113
+ ```bash
114
+ penlike new me
115
+ penlike new house-style --kind group --basis consent
116
+ penlike new field-guide --kind corpus --basis public
117
+ ```
118
+
119
+ | Kind | Kept | Specific to it |
120
+ |---|---|---|
121
+ | `person` | only the author's own texts | registers by reader, greetings and sign-offs, how they write to each person |
122
+ | `group` | everyone's texts | shared conventions; individual habits average out |
123
+ | `corpus` | everything | text types; readers and dates are often unknown |
124
+
125
+ Measuring and checking are the same in all three. What differs is what is kept, how texts are filed, and what may be claimed.
126
+
127
+ ## Screening for machine-written text (optional)
128
+
129
+ ```bash
130
+ penlike screen me --tier cheap # says what it would cost; changes nothing
131
+ penlike screen me --tier cheap --run
132
+ ```
133
+
134
+ | Tier | What it does | Cost |
135
+ |---|---|---|
136
+ | `date` | excludes texts dated on or after a cutoff | none |
137
+ | `cheap` | wording, mechanics, sentence shapes, rhythm (`pip install 'penlike[screen]'`) | no model, no network; milliseconds a text |
138
+ | `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 |
139
+ | `agent` | an agent reads each text | model tokens, in proportion to the corpus |
140
+
141
+ 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](https://github.com/thorwhalen/ductus).
142
+
143
+ ## Writing to someone, in your voice
144
+
145
+ penlike answers "how does the author write?". [acquaint](https://github.com/thorwhalen/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.
146
+
147
+ ## From Python
148
+
149
+ ```python
150
+ import penlike
151
+
152
+ penlike.new("me", me=["you@example.org"])
153
+ penlike.gather("me", "mbox", ["sent.mbox"], until="2023-01-01")
154
+ penlike.build("me")
155
+ brief = penlike.brief(like="me", channel="email", to=["ada@example.org"])
156
+ print(brief["text"])
157
+ report = penlike.check(draft, like="me", style=brief["register"])
158
+ for item in report["discrepancies"]:
159
+ print(item["message"])
160
+ ```
161
+
162
+ 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.
163
+
164
+ ## Limits
165
+
166
+ - 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.
167
+ - Imitation by a language model works best on structured writing and worst on informal, personal writing.
168
+ - A register with under about 2000 words is marked provisional; its figures are rough.
169
+ - The word lists (function words, hedges, greetings) are English. On other languages the lengths, punctuation and formatting features still hold.
170
+ - The thresholds used to propose registers were set on synthetic text. Treat proposals as questions.
171
+
172
+ ## Responsible use
173
+
174
+ 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.
175
+
176
+ ## Research
177
+
178
+ The design follows a review of the literature on stylometry, style imitation with language models, and register: [misc/docs/research_report.md](misc/docs/research_report.md). The design record is [misc/docs/design.md](misc/docs/design.md).
@@ -0,0 +1,88 @@
1
+ """Model how an author, a group or a corpus writes, and write in that style.
2
+
3
+ penlike keeps a *model* of a writer: their texts, filed by register (the situation
4
+ a text was written in), a measured profile of each register, notes on what numbers
5
+ miss, and examples. An agent asks for a brief before writing and checks its draft
6
+ afterwards.
7
+
8
+ >>> import penlike
9
+ >>> files = {}
10
+ >>> _ = penlike.new("ada", files=files)
11
+ >>> penlike.models(files=files)["summary"]
12
+ '1 model(s)'
13
+
14
+ Everything a model holds is private and lives under the user's data folder
15
+ (``~/.local/share/penlike`` by default), never in a project.
16
+ """
17
+
18
+ from penlike.base import AI_ERA_START, PenlikeError, normalize_doc
19
+ from penlike.routing import situate
20
+ from penlike.sourcers import SOURCERS, resolve_sourcer
21
+ from penlike.store import ModelStore, data_dir
22
+ from penlike.tools import (
23
+ TOOLS,
24
+ assign,
25
+ batches,
26
+ brief,
27
+ build,
28
+ check,
29
+ docs,
30
+ exclude,
31
+ exemplars,
32
+ gather,
33
+ install_skills,
34
+ measure,
35
+ models,
36
+ new,
37
+ note,
38
+ notes,
39
+ propose,
40
+ register_add,
41
+ register_edit,
42
+ register_merge,
43
+ registers,
44
+ remove,
45
+ route,
46
+ screen,
47
+ show,
48
+ sources,
49
+ use,
50
+ )
51
+
52
+ __all__ = [
53
+ "AI_ERA_START",
54
+ "SOURCERS",
55
+ "TOOLS",
56
+ "ModelStore",
57
+ "PenlikeError",
58
+ "assign",
59
+ "batches",
60
+ "brief",
61
+ "build",
62
+ "check",
63
+ "data_dir",
64
+ "docs",
65
+ "exclude",
66
+ "exemplars",
67
+ "gather",
68
+ "install_skills",
69
+ "measure",
70
+ "models",
71
+ "new",
72
+ "normalize_doc",
73
+ "note",
74
+ "notes",
75
+ "propose",
76
+ "register_add",
77
+ "register_edit",
78
+ "register_merge",
79
+ "registers",
80
+ "remove",
81
+ "resolve_sourcer",
82
+ "route",
83
+ "screen",
84
+ "show",
85
+ "situate",
86
+ "sources",
87
+ "use",
88
+ ]