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.
- editbuffer-0.2.1/LICENSE +21 -0
- editbuffer-0.2.1/PKG-INFO +267 -0
- editbuffer-0.2.1/README.md +249 -0
- editbuffer-0.2.1/pyproject.toml +32 -0
- editbuffer-0.2.1/setup.cfg +4 -0
- editbuffer-0.2.1/src/editbuffer/__init__.py +30 -0
- editbuffer-0.2.1/src/editbuffer/blocks.py +41 -0
- editbuffer-0.2.1/src/editbuffer/buffer.py +178 -0
- editbuffer-0.2.1/src/editbuffer/cli.py +138 -0
- editbuffer-0.2.1/src/editbuffer/errors.py +35 -0
- editbuffer-0.2.1/src/editbuffer/history.py +33 -0
- editbuffer-0.2.1/src/editbuffer/mcp_server.py +205 -0
- editbuffer-0.2.1/src/editbuffer/operations.py +67 -0
- editbuffer-0.2.1/src/editbuffer/py.typed +1 -0
- editbuffer-0.2.1/src/editbuffer/resolver.py +157 -0
- editbuffer-0.2.1/src/editbuffer/selection.py +182 -0
- editbuffer-0.2.1/src/editbuffer/validators.py +23 -0
- editbuffer-0.2.1/src/editbuffer.egg-info/PKG-INFO +267 -0
- editbuffer-0.2.1/src/editbuffer.egg-info/SOURCES.txt +25 -0
- editbuffer-0.2.1/src/editbuffer.egg-info/dependency_links.txt +1 -0
- editbuffer-0.2.1/src/editbuffer.egg-info/entry_points.txt +3 -0
- editbuffer-0.2.1/src/editbuffer.egg-info/requires.txt +3 -0
- editbuffer-0.2.1/src/editbuffer.egg-info/top_level.txt +1 -0
- editbuffer-0.2.1/tests/test_cli.py +49 -0
- editbuffer-0.2.1/tests/test_editbuffer.py +250 -0
- editbuffer-0.2.1/tests/test_mcp_server.py +81 -0
- editbuffer-0.2.1/tests/test_mcp_stdio_eval.py +292 -0
editbuffer-0.2.1/LICENSE
ADDED
|
@@ -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,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())
|