editbuffer 0.2.1__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,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 averagedigital
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.
@@ -0,0 +1,267 @@
1
+ Metadata-Version: 2.4
2
+ Name: editbuffer
3
+ Version: 0.2.1
4
+ Summary: Selection-based mutable output buffer for LLM tools
5
+ Author: averagedigital
6
+ License-Expression: MIT
7
+ Classifier: Development Status :: 3 - Alpha
8
+ Classifier: Programming Language :: Python :: 3
9
+ Classifier: Programming Language :: Python :: 3.11
10
+ Classifier: Programming Language :: Python :: 3.12
11
+ Classifier: Typing :: Typed
12
+ Requires-Python: >=3.11
13
+ Description-Content-Type: text/markdown
14
+ License-File: LICENSE
15
+ Provides-Extra: mcp
16
+ Requires-Dist: mcp<2,>=1.27; extra == "mcp"
17
+ Dynamic: license-file
18
+
19
+ # editbuffer
20
+
21
+ `editbuffer` is a small Python runtime for editing an LLM's pending text output
22
+ before it is committed.
23
+
24
+ The public abstraction is selection-based editing, not file patches:
25
+
26
+ - select exact, contextual, fuzzy, block, or character-range targets;
27
+ - replace, insert before/after, or delete the selection;
28
+ - reject missing, ambiguous, invalid, and stale selections;
29
+ - inspect the current buffer and its audit history;
30
+ - commit the final artifact.
31
+
32
+ ## Install
33
+
34
+ ```bash
35
+ pip install editbuffer
36
+ ```
37
+
38
+ For local development:
39
+
40
+ ```bash
41
+ python -m pip install -e .
42
+ ```
43
+
44
+ Python 3.11+ is required. The core library has no runtime dependencies.
45
+
46
+ ## Quickstart
47
+
48
+ ```python
49
+ from editbuffer import EditBuffer, Selection
50
+
51
+ buf = EditBuffer()
52
+ buf.append('find . -name "*.py" -exec grep -n "TODO" {} ;')
53
+ buf.replace(Selection.exact("{} ;"), "{} \\;")
54
+
55
+ final = buf.commit()
56
+ ```
57
+
58
+ Selections can also be dictionaries:
59
+
60
+ ```python
61
+ buf.replace(
62
+ {
63
+ "type": "context",
64
+ "before": 'grep -n "TODO" ',
65
+ "text": "{} ;",
66
+ "after": "",
67
+ },
68
+ "{} \\;",
69
+ )
70
+ ```
71
+
72
+ ## LLM tool-call format
73
+
74
+ ```json
75
+ {
76
+ "op": "replace",
77
+ "target": {
78
+ "type": "context",
79
+ "before": "grep -n \"TODO\" ",
80
+ "text": "{} ;",
81
+ "after": ""
82
+ },
83
+ "text": "{} \\;"
84
+ }
85
+ ```
86
+
87
+ Pass the decoded object to `buffer.apply(operation)`. Supported operations are
88
+ `append`, `replace`, `insert_before`, `insert_after`, and `delete`.
89
+
90
+ Range selections use half-open character offsets and can reject stale edits:
91
+
92
+ ```json
93
+ {
94
+ "op": "delete",
95
+ "target": {
96
+ "type": "range",
97
+ "start": 10,
98
+ "end": 20,
99
+ "expected_version": 3
100
+ }
101
+ }
102
+ ```
103
+
104
+ ## Failure behavior
105
+
106
+ - no match raises `TargetNotFoundError`;
107
+ - multiple matches raise `AmbiguousTargetError` with candidate ranges;
108
+ - `occurrence` can explicitly choose a zero-based match;
109
+ - invalid ranges or operations raise `InvalidOperationError`;
110
+ - a mismatched `expected_version` raises `StaleVersionError`;
111
+ - validator failures do not mutate text or history;
112
+ - fuzzy matching is opt-in and rejects low-confidence or competing candidates.
113
+
114
+ ## Fuzzy and block selections
115
+
116
+ Fuzzy selection uses the standard library and records the accepted confidence:
117
+
118
+ ```python
119
+ buf.replace(
120
+ Selection.fuzzy(
121
+ "run integrtion tests",
122
+ threshold=0.85,
123
+ ambiguity_margin=0.05,
124
+ ),
125
+ "run unit tests",
126
+ )
127
+ ```
128
+
129
+ It never runs as a fallback for exact/context selection. If no candidate meets
130
+ the threshold, or two candidates are too close, `FuzzyMatchError` includes
131
+ candidate ranges and scores.
132
+
133
+ Block selection requires an explicit ID. Fenced code blocks use:
134
+
135
+ ````markdown
136
+ ```python editbuffer:id=setup
137
+ print("old")
138
+ ```
139
+ ````
140
+
141
+ Markdown regions use:
142
+
143
+ ```markdown
144
+ <!-- editbuffer:block summary -->
145
+ old content
146
+ <!-- /editbuffer:block -->
147
+ ```
148
+
149
+ `Selection.block("setup")` selects the content inside the markers, preserving
150
+ the fence or comments. Duplicate IDs are rejected as ambiguous.
151
+
152
+ ## Snapshots and rollback
153
+
154
+ Every successful edit stores an in-memory snapshot:
155
+
156
+ ```python
157
+ buf.append("draft")
158
+ version = buf.version
159
+ buf.replace(Selection.exact("draft"), "final")
160
+ buf.rollback(version)
161
+ ```
162
+
163
+ Rollback restores the snapshot as a new audited version. Snapshots live only
164
+ for the lifetime of the `EditBuffer` process.
165
+
166
+ ## Optional validators
167
+
168
+ ```python
169
+ from editbuffer import EditBuffer
170
+ from editbuffer.validators import valid_json, valid_shell
171
+
172
+ json_buffer = EditBuffer('{"ok": true}', validators=(valid_json,))
173
+ shell_buffer = EditBuffer("echo ok", validators=(valid_shell,))
174
+ ```
175
+
176
+ `valid_shell` invokes the local POSIX `sh -n` parser without executing the
177
+ command.
178
+
179
+ ## CLI
180
+
181
+ The CLI stores an operation log in a JSON state file:
182
+
183
+ ```bash
184
+ editbuffer new /tmp/output.json --text "hello world"
185
+ editbuffer replace /tmp/output.json '{"type":"exact","text":"world"}' "there"
186
+ editbuffer view /tmp/output.json
187
+ editbuffer history /tmp/output.json
188
+ editbuffer rollback /tmp/output.json 1
189
+ editbuffer commit /tmp/output.json
190
+ ```
191
+
192
+ Use `editbuffer apply STATE OPERATION_JSON` for the complete operation schema.
193
+
194
+ ## MCP / agent integration
195
+
196
+ Install the optional server:
197
+
198
+ ```bash
199
+ pipx install 'editbuffer[mcp]'
200
+ ```
201
+
202
+ Until a PyPI release exists, install directly from GitHub:
203
+
204
+ ```bash
205
+ pipx install 'editbuffer[mcp] @ git+https://github.com/averagedigital/editbuffer.git'
206
+ ```
207
+
208
+ Connect it to Codex:
209
+
210
+ ```bash
211
+ codex mcp add editbuffer -- editbuffer-mcp
212
+ ```
213
+
214
+ Codex supports local STDIO MCP servers configured with `codex mcp add`; use
215
+ `/mcp` in the Codex terminal UI to confirm the server is active.
216
+
217
+ Claude Desktop and generic MCP client examples are in
218
+ [`docs/mcp.md`](docs/mcp.md).
219
+
220
+ The server exposes:
221
+
222
+ - `buffer_create`
223
+ - `buffer_list`
224
+ - `buffer_view`
225
+ - `buffer_edit`
226
+ - `buffer_history`
227
+ - `buffer_rollback`
228
+ - `buffer_commit`
229
+ - `command_history`
230
+ - `command_select`
231
+
232
+ Buffers are in-memory and live for the MCP server process. The MCP layer calls
233
+ the same core API and does not implement separate edit semantics.
234
+
235
+ `buffer_commit` remembers non-empty committed output as a reusable command.
236
+ `command_history` returns the last 10 commands, newest first. `command_select`
237
+ creates a new pending buffer from a previous command so the model can reuse it
238
+ instead of regenerating it.
239
+
240
+ ## Examples
241
+
242
+ - [`examples/shell_repair.py`](examples/shell_repair.py)
243
+ - [`examples/json_repair.py`](examples/json_repair.py)
244
+ - [`examples/chat_restructure.py`](examples/chat_restructure.py)
245
+
246
+ ## Scope
247
+
248
+ This project is not:
249
+
250
+ - an agent framework;
251
+ - a retrieval system or repo indexer;
252
+ - a full IDE;
253
+ - a replacement for coding agents;
254
+ - a generic output quality evaluator.
255
+
256
+ Persistent storage, atomic operation batches, TypeScript bindings, and
257
+ syntax-aware block parsing are future work.
258
+
259
+ ## Development
260
+
261
+ ```bash
262
+ PYTHONPATH=src python -m unittest discover -s tests -v
263
+ ```
264
+
265
+ ## License
266
+
267
+ MIT
@@ -0,0 +1,249 @@
1
+ # editbuffer
2
+
3
+ `editbuffer` is a small Python runtime for editing an LLM's pending text output
4
+ before it is committed.
5
+
6
+ The public abstraction is selection-based editing, not file patches:
7
+
8
+ - select exact, contextual, fuzzy, block, or character-range targets;
9
+ - replace, insert before/after, or delete the selection;
10
+ - reject missing, ambiguous, invalid, and stale selections;
11
+ - inspect the current buffer and its audit history;
12
+ - commit the final artifact.
13
+
14
+ ## Install
15
+
16
+ ```bash
17
+ pip install editbuffer
18
+ ```
19
+
20
+ For local development:
21
+
22
+ ```bash
23
+ python -m pip install -e .
24
+ ```
25
+
26
+ Python 3.11+ is required. The core library has no runtime dependencies.
27
+
28
+ ## Quickstart
29
+
30
+ ```python
31
+ from editbuffer import EditBuffer, Selection
32
+
33
+ buf = EditBuffer()
34
+ buf.append('find . -name "*.py" -exec grep -n "TODO" {} ;')
35
+ buf.replace(Selection.exact("{} ;"), "{} \\;")
36
+
37
+ final = buf.commit()
38
+ ```
39
+
40
+ Selections can also be dictionaries:
41
+
42
+ ```python
43
+ buf.replace(
44
+ {
45
+ "type": "context",
46
+ "before": 'grep -n "TODO" ',
47
+ "text": "{} ;",
48
+ "after": "",
49
+ },
50
+ "{} \\;",
51
+ )
52
+ ```
53
+
54
+ ## LLM tool-call format
55
+
56
+ ```json
57
+ {
58
+ "op": "replace",
59
+ "target": {
60
+ "type": "context",
61
+ "before": "grep -n \"TODO\" ",
62
+ "text": "{} ;",
63
+ "after": ""
64
+ },
65
+ "text": "{} \\;"
66
+ }
67
+ ```
68
+
69
+ Pass the decoded object to `buffer.apply(operation)`. Supported operations are
70
+ `append`, `replace`, `insert_before`, `insert_after`, and `delete`.
71
+
72
+ Range selections use half-open character offsets and can reject stale edits:
73
+
74
+ ```json
75
+ {
76
+ "op": "delete",
77
+ "target": {
78
+ "type": "range",
79
+ "start": 10,
80
+ "end": 20,
81
+ "expected_version": 3
82
+ }
83
+ }
84
+ ```
85
+
86
+ ## Failure behavior
87
+
88
+ - no match raises `TargetNotFoundError`;
89
+ - multiple matches raise `AmbiguousTargetError` with candidate ranges;
90
+ - `occurrence` can explicitly choose a zero-based match;
91
+ - invalid ranges or operations raise `InvalidOperationError`;
92
+ - a mismatched `expected_version` raises `StaleVersionError`;
93
+ - validator failures do not mutate text or history;
94
+ - fuzzy matching is opt-in and rejects low-confidence or competing candidates.
95
+
96
+ ## Fuzzy and block selections
97
+
98
+ Fuzzy selection uses the standard library and records the accepted confidence:
99
+
100
+ ```python
101
+ buf.replace(
102
+ Selection.fuzzy(
103
+ "run integrtion tests",
104
+ threshold=0.85,
105
+ ambiguity_margin=0.05,
106
+ ),
107
+ "run unit tests",
108
+ )
109
+ ```
110
+
111
+ It never runs as a fallback for exact/context selection. If no candidate meets
112
+ the threshold, or two candidates are too close, `FuzzyMatchError` includes
113
+ candidate ranges and scores.
114
+
115
+ Block selection requires an explicit ID. Fenced code blocks use:
116
+
117
+ ````markdown
118
+ ```python editbuffer:id=setup
119
+ print("old")
120
+ ```
121
+ ````
122
+
123
+ Markdown regions use:
124
+
125
+ ```markdown
126
+ <!-- editbuffer:block summary -->
127
+ old content
128
+ <!-- /editbuffer:block -->
129
+ ```
130
+
131
+ `Selection.block("setup")` selects the content inside the markers, preserving
132
+ the fence or comments. Duplicate IDs are rejected as ambiguous.
133
+
134
+ ## Snapshots and rollback
135
+
136
+ Every successful edit stores an in-memory snapshot:
137
+
138
+ ```python
139
+ buf.append("draft")
140
+ version = buf.version
141
+ buf.replace(Selection.exact("draft"), "final")
142
+ buf.rollback(version)
143
+ ```
144
+
145
+ Rollback restores the snapshot as a new audited version. Snapshots live only
146
+ for the lifetime of the `EditBuffer` process.
147
+
148
+ ## Optional validators
149
+
150
+ ```python
151
+ from editbuffer import EditBuffer
152
+ from editbuffer.validators import valid_json, valid_shell
153
+
154
+ json_buffer = EditBuffer('{"ok": true}', validators=(valid_json,))
155
+ shell_buffer = EditBuffer("echo ok", validators=(valid_shell,))
156
+ ```
157
+
158
+ `valid_shell` invokes the local POSIX `sh -n` parser without executing the
159
+ command.
160
+
161
+ ## CLI
162
+
163
+ The CLI stores an operation log in a JSON state file:
164
+
165
+ ```bash
166
+ editbuffer new /tmp/output.json --text "hello world"
167
+ editbuffer replace /tmp/output.json '{"type":"exact","text":"world"}' "there"
168
+ editbuffer view /tmp/output.json
169
+ editbuffer history /tmp/output.json
170
+ editbuffer rollback /tmp/output.json 1
171
+ editbuffer commit /tmp/output.json
172
+ ```
173
+
174
+ Use `editbuffer apply STATE OPERATION_JSON` for the complete operation schema.
175
+
176
+ ## MCP / agent integration
177
+
178
+ Install the optional server:
179
+
180
+ ```bash
181
+ pipx install 'editbuffer[mcp]'
182
+ ```
183
+
184
+ Until a PyPI release exists, install directly from GitHub:
185
+
186
+ ```bash
187
+ pipx install 'editbuffer[mcp] @ git+https://github.com/averagedigital/editbuffer.git'
188
+ ```
189
+
190
+ Connect it to Codex:
191
+
192
+ ```bash
193
+ codex mcp add editbuffer -- editbuffer-mcp
194
+ ```
195
+
196
+ Codex supports local STDIO MCP servers configured with `codex mcp add`; use
197
+ `/mcp` in the Codex terminal UI to confirm the server is active.
198
+
199
+ Claude Desktop and generic MCP client examples are in
200
+ [`docs/mcp.md`](docs/mcp.md).
201
+
202
+ The server exposes:
203
+
204
+ - `buffer_create`
205
+ - `buffer_list`
206
+ - `buffer_view`
207
+ - `buffer_edit`
208
+ - `buffer_history`
209
+ - `buffer_rollback`
210
+ - `buffer_commit`
211
+ - `command_history`
212
+ - `command_select`
213
+
214
+ Buffers are in-memory and live for the MCP server process. The MCP layer calls
215
+ the same core API and does not implement separate edit semantics.
216
+
217
+ `buffer_commit` remembers non-empty committed output as a reusable command.
218
+ `command_history` returns the last 10 commands, newest first. `command_select`
219
+ creates a new pending buffer from a previous command so the model can reuse it
220
+ instead of regenerating it.
221
+
222
+ ## Examples
223
+
224
+ - [`examples/shell_repair.py`](examples/shell_repair.py)
225
+ - [`examples/json_repair.py`](examples/json_repair.py)
226
+ - [`examples/chat_restructure.py`](examples/chat_restructure.py)
227
+
228
+ ## Scope
229
+
230
+ This project is not:
231
+
232
+ - an agent framework;
233
+ - a retrieval system or repo indexer;
234
+ - a full IDE;
235
+ - a replacement for coding agents;
236
+ - a generic output quality evaluator.
237
+
238
+ Persistent storage, atomic operation batches, TypeScript bindings, and
239
+ syntax-aware block parsing are future work.
240
+
241
+ ## Development
242
+
243
+ ```bash
244
+ PYTHONPATH=src python -m unittest discover -s tests -v
245
+ ```
246
+
247
+ ## License
248
+
249
+ MIT
@@ -0,0 +1,32 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "editbuffer"
7
+ version = "0.2.1"
8
+ description = "Selection-based mutable output buffer for LLM tools"
9
+ readme = "README.md"
10
+ requires-python = ">=3.11"
11
+ license = "MIT"
12
+ authors = [{ name = "averagedigital" }]
13
+ classifiers = [
14
+ "Development Status :: 3 - Alpha",
15
+ "Programming Language :: Python :: 3",
16
+ "Programming Language :: Python :: 3.11",
17
+ "Programming Language :: Python :: 3.12",
18
+ "Typing :: Typed",
19
+ ]
20
+
21
+ [project.scripts]
22
+ editbuffer = "editbuffer.cli:main"
23
+ editbuffer-mcp = "editbuffer.mcp_server:main"
24
+
25
+ [project.optional-dependencies]
26
+ mcp = ["mcp>=1.27,<2"]
27
+
28
+ [tool.setuptools.packages.find]
29
+ where = ["src"]
30
+
31
+ [tool.setuptools.package-data]
32
+ editbuffer = ["py.typed"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,30 @@
1
+ from .buffer import EditBuffer
2
+ from .errors import (
3
+ AmbiguousTargetError,
4
+ EditBufferError,
5
+ FuzzyMatchError,
6
+ InvalidOperationError,
7
+ StaleVersionError,
8
+ TargetNotFoundError,
9
+ ValidationError,
10
+ )
11
+ from .history import EditHistory, EditRecord
12
+ from .operations import EditOperation
13
+ from .resolver import SelectionResolver
14
+ from .selection import Selection
15
+
16
+ __all__ = [
17
+ "AmbiguousTargetError",
18
+ "EditBuffer",
19
+ "EditBufferError",
20
+ "EditHistory",
21
+ "EditOperation",
22
+ "EditRecord",
23
+ "FuzzyMatchError",
24
+ "InvalidOperationError",
25
+ "Selection",
26
+ "SelectionResolver",
27
+ "StaleVersionError",
28
+ "TargetNotFoundError",
29
+ "ValidationError",
30
+ ]
@@ -0,0 +1,41 @@
1
+ import re
2
+
3
+ from .resolver import ResolvedSelection
4
+
5
+ _FENCE = re.compile(
6
+ r"^(?P<fence>`{3,}|~{3,})[^\n]*\beditbuffer:id=(?P<id>[A-Za-z0-9_.-]+)[^\n]*\n",
7
+ re.MULTILINE,
8
+ )
9
+ _REGION = re.compile(
10
+ r"^<!--\s*editbuffer:block\s+(?P<id>[A-Za-z0-9_.-]+)\s*-->\s*\n",
11
+ re.MULTILINE,
12
+ )
13
+ _REGION_END = re.compile(r"^<!--\s*/editbuffer:block\s*-->", re.MULTILINE)
14
+
15
+
16
+ def find_blocks(content: str, block_id: str) -> tuple[ResolvedSelection, ...]:
17
+ matches = list(_fenced_blocks(content, block_id))
18
+ matches.extend(_regions(content, block_id))
19
+ return tuple(sorted(matches, key=lambda match: match.start))
20
+
21
+
22
+ def _fenced_blocks(content: str, block_id: str):
23
+ for opening in _FENCE.finditer(content):
24
+ if opening.group("id") != block_id:
25
+ continue
26
+ fence = re.escape(opening.group("fence"))
27
+ closing = re.search(rf"^{fence}[^\n]*(?:\n|$)", content[opening.end() :], re.MULTILINE)
28
+ if closing:
29
+ yield ResolvedSelection(
30
+ opening.end(),
31
+ opening.end() + closing.start(),
32
+ )
33
+
34
+
35
+ def _regions(content: str, block_id: str):
36
+ for opening in _REGION.finditer(content):
37
+ if opening.group("id") != block_id:
38
+ continue
39
+ closing = _REGION_END.search(content, opening.end())
40
+ if closing:
41
+ yield ResolvedSelection(opening.end(), closing.start())