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.
- penlike-0.0.2/.github/workflows/ci.yml +43 -0
- penlike-0.0.2/.gitignore +120 -0
- penlike-0.0.2/LICENSE +21 -0
- penlike-0.0.2/PKG-INFO +213 -0
- penlike-0.0.2/README.md +178 -0
- penlike-0.0.2/penlike/__init__.py +88 -0
- penlike-0.0.2/penlike/__main__.py +83 -0
- penlike-0.0.2/penlike/base.py +311 -0
- penlike-0.0.2/penlike/data/__init__.py +1 -0
- penlike-0.0.2/penlike/data/agents/penlike-reader.md +47 -0
- penlike-0.0.2/penlike/data/skills/penlike/SKILL.md +121 -0
- penlike-0.0.2/penlike/data/skills/penlike-model/SKILL.md +159 -0
- penlike-0.0.2/penlike/data/skills/penlike-source/SKILL.md +151 -0
- penlike-0.0.2/penlike/features.py +343 -0
- penlike-0.0.2/penlike/mcp.py +56 -0
- penlike-0.0.2/penlike/profile.py +623 -0
- penlike-0.0.2/penlike/routing.py +363 -0
- penlike-0.0.2/penlike/screening.py +158 -0
- penlike-0.0.2/penlike/sourcers.py +506 -0
- penlike-0.0.2/penlike/store.py +231 -0
- penlike-0.0.2/penlike/tools.py +1370 -0
- penlike-0.0.2/pyproject.toml +212 -0
- penlike-0.0.2/tests/__init__.py +0 -0
- penlike-0.0.2/tests/corpus.py +108 -0
- penlike-0.0.2/tests/test_no_private_data.py +106 -0
- penlike-0.0.2/tests/test_smoke.py +269 -0
- penlike-0.0.2/tests/test_sourcers.py +251 -0
- penlike-0.0.2/tests/test_surfaces.py +100 -0
|
@@ -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 }}
|
penlike-0.0.2/.gitignore
ADDED
|
@@ -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).
|
penlike-0.0.2/README.md
ADDED
|
@@ -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
|
+
]
|