editbuffer 0.2.1__py3-none-any.whl → 0.2.2__py3-none-any.whl
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/mcp_server.py +169 -7
- {editbuffer-0.2.1.dist-info → editbuffer-0.2.2.dist-info}/METADATA +20 -3
- {editbuffer-0.2.1.dist-info → editbuffer-0.2.2.dist-info}/RECORD +7 -7
- {editbuffer-0.2.1.dist-info → editbuffer-0.2.2.dist-info}/WHEEL +0 -0
- {editbuffer-0.2.1.dist-info → editbuffer-0.2.2.dist-info}/entry_points.txt +0 -0
- {editbuffer-0.2.1.dist-info → editbuffer-0.2.2.dist-info}/licenses/LICENSE +0 -0
- {editbuffer-0.2.1.dist-info → editbuffer-0.2.2.dist-info}/top_level.txt +0 -0
editbuffer/mcp_server.py
CHANGED
|
@@ -5,6 +5,14 @@ from typing import Any
|
|
|
5
5
|
from uuid import uuid4
|
|
6
6
|
|
|
7
7
|
from .buffer import EditBuffer
|
|
8
|
+
from .errors import (
|
|
9
|
+
AmbiguousTargetError,
|
|
10
|
+
EditBufferError,
|
|
11
|
+
FuzzyMatchError,
|
|
12
|
+
InvalidOperationError,
|
|
13
|
+
StaleVersionError,
|
|
14
|
+
TargetNotFoundError,
|
|
15
|
+
)
|
|
8
16
|
from .history import EditRecord
|
|
9
17
|
|
|
10
18
|
|
|
@@ -61,6 +69,12 @@ class BufferRegistry:
|
|
|
61
69
|
def command_history(self) -> list[dict[str, Any]]:
|
|
62
70
|
return list(self._commands)
|
|
63
71
|
|
|
72
|
+
def current_version(self, buffer_id: str | None) -> int | None:
|
|
73
|
+
if buffer_id is None:
|
|
74
|
+
return None
|
|
75
|
+
buffer = self._buffers.get(buffer_id)
|
|
76
|
+
return None if buffer is None else buffer.version
|
|
77
|
+
|
|
64
78
|
def select_command(
|
|
65
79
|
self,
|
|
66
80
|
command_id: str,
|
|
@@ -140,7 +154,11 @@ def create_server() -> Any:
|
|
|
140
154
|
buffer_id: str | None = None,
|
|
141
155
|
) -> dict[str, Any]:
|
|
142
156
|
"""Create an in-memory pending output buffer."""
|
|
143
|
-
return
|
|
157
|
+
return _tool_result(
|
|
158
|
+
lambda: registry.create(content, buffer_id=buffer_id),
|
|
159
|
+
registry,
|
|
160
|
+
buffer_id=buffer_id,
|
|
161
|
+
)
|
|
144
162
|
|
|
145
163
|
@server.tool()
|
|
146
164
|
def buffer_list() -> list[dict[str, Any]]:
|
|
@@ -150,7 +168,7 @@ def create_server() -> Any:
|
|
|
150
168
|
@server.tool()
|
|
151
169
|
def buffer_view(buffer_id: str) -> dict[str, Any]:
|
|
152
170
|
"""View current content, version, snapshots, and commit state."""
|
|
153
|
-
return registry.view(buffer_id)
|
|
171
|
+
return _tool_result(lambda: registry.view(buffer_id), registry, buffer_id=buffer_id)
|
|
154
172
|
|
|
155
173
|
@server.tool()
|
|
156
174
|
def buffer_edit(
|
|
@@ -164,22 +182,79 @@ def create_server() -> Any:
|
|
|
164
182
|
Operations: append, replace, insert_before, insert_after, delete, rollback.
|
|
165
183
|
Targets: exact/context/range/fuzzy/block. Ambiguous edits fail without mutation.
|
|
166
184
|
"""
|
|
167
|
-
return
|
|
185
|
+
return _tool_result(
|
|
186
|
+
lambda: registry.edit(buffer_id, operation),
|
|
187
|
+
registry,
|
|
188
|
+
buffer_id=buffer_id,
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
@server.tool()
|
|
192
|
+
def buffer_append(buffer_id: str, text: str) -> dict[str, Any]:
|
|
193
|
+
"""Append text to a pending buffer."""
|
|
194
|
+
return _tool_result(
|
|
195
|
+
lambda: registry.edit(buffer_id, {"op": "append", "text": text}),
|
|
196
|
+
registry,
|
|
197
|
+
buffer_id=buffer_id,
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
@server.tool()
|
|
201
|
+
def buffer_replace(
|
|
202
|
+
buffer_id: str,
|
|
203
|
+
target: dict[str, Any],
|
|
204
|
+
text: str,
|
|
205
|
+
) -> dict[str, Any]:
|
|
206
|
+
"""Replace a selection with text. Target can be exact/context/range/fuzzy/block."""
|
|
207
|
+
return _selection_tool(registry, buffer_id, "replace", target, text)
|
|
208
|
+
|
|
209
|
+
@server.tool()
|
|
210
|
+
def buffer_insert_before(
|
|
211
|
+
buffer_id: str,
|
|
212
|
+
target: dict[str, Any],
|
|
213
|
+
text: str,
|
|
214
|
+
) -> dict[str, Any]:
|
|
215
|
+
"""Insert text before a selection. Target can be exact/context/range/fuzzy/block."""
|
|
216
|
+
return _selection_tool(registry, buffer_id, "insert_before", target, text)
|
|
217
|
+
|
|
218
|
+
@server.tool()
|
|
219
|
+
def buffer_insert_after(
|
|
220
|
+
buffer_id: str,
|
|
221
|
+
target: dict[str, Any],
|
|
222
|
+
text: str,
|
|
223
|
+
) -> dict[str, Any]:
|
|
224
|
+
"""Insert text after a selection. Target can be exact/context/range/fuzzy/block."""
|
|
225
|
+
return _selection_tool(registry, buffer_id, "insert_after", target, text)
|
|
226
|
+
|
|
227
|
+
@server.tool()
|
|
228
|
+
def buffer_delete(buffer_id: str, target: dict[str, Any]) -> dict[str, Any]:
|
|
229
|
+
"""Delete a selection. Target can be exact/context/range/fuzzy/block."""
|
|
230
|
+
return _tool_result(
|
|
231
|
+
lambda: registry.edit(buffer_id, {"op": "delete", "target": target}),
|
|
232
|
+
registry,
|
|
233
|
+
buffer_id=buffer_id,
|
|
234
|
+
)
|
|
168
235
|
|
|
169
236
|
@server.tool()
|
|
170
237
|
def buffer_history(buffer_id: str) -> list[dict[str, Any]]:
|
|
171
238
|
"""Return the audit trail for successful edits."""
|
|
172
|
-
return
|
|
239
|
+
return _tool_result(
|
|
240
|
+
lambda: registry.history(buffer_id),
|
|
241
|
+
registry,
|
|
242
|
+
buffer_id=buffer_id,
|
|
243
|
+
)
|
|
173
244
|
|
|
174
245
|
@server.tool()
|
|
175
246
|
def buffer_rollback(buffer_id: str, version: int) -> dict[str, Any]:
|
|
176
247
|
"""Restore a prior snapshot as a new audited version."""
|
|
177
|
-
return
|
|
248
|
+
return _tool_result(
|
|
249
|
+
lambda: registry.rollback(buffer_id, version),
|
|
250
|
+
registry,
|
|
251
|
+
buffer_id=buffer_id,
|
|
252
|
+
)
|
|
178
253
|
|
|
179
254
|
@server.tool()
|
|
180
255
|
def buffer_commit(buffer_id: str) -> dict[str, Any]:
|
|
181
256
|
"""Commit final output, close the buffer, and remember it as a reusable command."""
|
|
182
|
-
return registry.commit(buffer_id)
|
|
257
|
+
return _tool_result(lambda: registry.commit(buffer_id), registry, buffer_id=buffer_id)
|
|
183
258
|
|
|
184
259
|
@server.tool()
|
|
185
260
|
def command_history() -> list[dict[str, Any]]:
|
|
@@ -192,11 +267,98 @@ def create_server() -> Any:
|
|
|
192
267
|
buffer_id: str | None = None,
|
|
193
268
|
) -> dict[str, Any]:
|
|
194
269
|
"""Create a new pending buffer from a previous command instead of regenerating it."""
|
|
195
|
-
return
|
|
270
|
+
return _tool_result(
|
|
271
|
+
lambda: registry.select_command(command_id, buffer_id=buffer_id),
|
|
272
|
+
registry,
|
|
273
|
+
buffer_id=buffer_id,
|
|
274
|
+
)
|
|
196
275
|
|
|
197
276
|
return server
|
|
198
277
|
|
|
199
278
|
|
|
279
|
+
def _selection_tool(
|
|
280
|
+
registry: BufferRegistry,
|
|
281
|
+
buffer_id: str,
|
|
282
|
+
op: str,
|
|
283
|
+
target: dict[str, Any],
|
|
284
|
+
text: str,
|
|
285
|
+
) -> dict[str, Any]:
|
|
286
|
+
return _tool_result(
|
|
287
|
+
lambda: registry.edit(buffer_id, {"op": op, "target": target, "text": text}),
|
|
288
|
+
registry,
|
|
289
|
+
buffer_id=buffer_id,
|
|
290
|
+
)
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
def _tool_result(
|
|
294
|
+
call: Any,
|
|
295
|
+
registry: BufferRegistry,
|
|
296
|
+
*,
|
|
297
|
+
buffer_id: str | None = None,
|
|
298
|
+
) -> Any:
|
|
299
|
+
try:
|
|
300
|
+
return call()
|
|
301
|
+
except (EditBufferError, KeyError, ValueError) as error:
|
|
302
|
+
return {
|
|
303
|
+
"ok": False,
|
|
304
|
+
"error": _structured_error(
|
|
305
|
+
error,
|
|
306
|
+
current_version=registry.current_version(buffer_id),
|
|
307
|
+
),
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
def _structured_error(
|
|
312
|
+
error: Exception,
|
|
313
|
+
*,
|
|
314
|
+
current_version: int | None,
|
|
315
|
+
) -> dict[str, Any]:
|
|
316
|
+
payload: dict[str, Any] = {
|
|
317
|
+
"type": _error_type(error),
|
|
318
|
+
"message": _message(error),
|
|
319
|
+
}
|
|
320
|
+
if current_version is not None:
|
|
321
|
+
payload["current_version"] = current_version
|
|
322
|
+
if isinstance(error, AmbiguousTargetError):
|
|
323
|
+
payload["candidates"] = [list(candidate) for candidate in error.candidates]
|
|
324
|
+
if isinstance(error, FuzzyMatchError):
|
|
325
|
+
payload["reason"] = error.reason
|
|
326
|
+
payload["candidates"] = [list(candidate) for candidate in error.candidates]
|
|
327
|
+
return payload
|
|
328
|
+
|
|
329
|
+
|
|
330
|
+
def _error_type(error: Exception) -> str:
|
|
331
|
+
if isinstance(error, TargetNotFoundError):
|
|
332
|
+
return "target_not_found"
|
|
333
|
+
if isinstance(error, AmbiguousTargetError):
|
|
334
|
+
return "ambiguous_target"
|
|
335
|
+
if isinstance(error, FuzzyMatchError):
|
|
336
|
+
return "fuzzy_match"
|
|
337
|
+
if isinstance(error, StaleVersionError):
|
|
338
|
+
return "stale_version"
|
|
339
|
+
if isinstance(error, InvalidOperationError):
|
|
340
|
+
return "invalid_operation"
|
|
341
|
+
if isinstance(error, KeyError):
|
|
342
|
+
message = _message(error)
|
|
343
|
+
if message.startswith("unknown buffer:"):
|
|
344
|
+
return "unknown_buffer"
|
|
345
|
+
if message.startswith("unknown command:"):
|
|
346
|
+
return "unknown_command"
|
|
347
|
+
return "not_found"
|
|
348
|
+
if isinstance(error, ValueError):
|
|
349
|
+
message = _message(error)
|
|
350
|
+
if message.startswith("buffer already exists:"):
|
|
351
|
+
return "duplicate_buffer"
|
|
352
|
+
return "invalid_value"
|
|
353
|
+
return "editbuffer_error"
|
|
354
|
+
|
|
355
|
+
|
|
356
|
+
def _message(error: Exception) -> str:
|
|
357
|
+
if isinstance(error, KeyError) and error.args:
|
|
358
|
+
return str(error.args[0])
|
|
359
|
+
return str(error)
|
|
360
|
+
|
|
361
|
+
|
|
200
362
|
def main() -> None:
|
|
201
363
|
create_server().run(transport="stdio")
|
|
202
364
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: editbuffer
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.2
|
|
4
4
|
Summary: Selection-based mutable output buffer for LLM tools
|
|
5
5
|
Author: averagedigital
|
|
6
6
|
License-Expression: MIT
|
|
@@ -199,10 +199,10 @@ Install the optional server:
|
|
|
199
199
|
pipx install 'editbuffer[mcp]'
|
|
200
200
|
```
|
|
201
201
|
|
|
202
|
-
|
|
202
|
+
Or install with `uvx` without a persistent environment:
|
|
203
203
|
|
|
204
204
|
```bash
|
|
205
|
-
|
|
205
|
+
uvx --from 'editbuffer[mcp]' editbuffer-mcp
|
|
206
206
|
```
|
|
207
207
|
|
|
208
208
|
Connect it to Codex:
|
|
@@ -220,9 +220,14 @@ Claude Desktop and generic MCP client examples are in
|
|
|
220
220
|
The server exposes:
|
|
221
221
|
|
|
222
222
|
- `buffer_create`
|
|
223
|
+
- `buffer_append`
|
|
223
224
|
- `buffer_list`
|
|
224
225
|
- `buffer_view`
|
|
225
226
|
- `buffer_edit`
|
|
227
|
+
- `buffer_replace`
|
|
228
|
+
- `buffer_insert_before`
|
|
229
|
+
- `buffer_insert_after`
|
|
230
|
+
- `buffer_delete`
|
|
226
231
|
- `buffer_history`
|
|
227
232
|
- `buffer_rollback`
|
|
228
233
|
- `buffer_commit`
|
|
@@ -232,6 +237,18 @@ The server exposes:
|
|
|
232
237
|
Buffers are in-memory and live for the MCP server process. The MCP layer calls
|
|
233
238
|
the same core API and does not implement separate edit semantics.
|
|
234
239
|
|
|
240
|
+
Use the first-class selection tools for normal agent use:
|
|
241
|
+
|
|
242
|
+
```json
|
|
243
|
+
{
|
|
244
|
+
"buffer_id": "answer",
|
|
245
|
+
"target": {"type": "exact", "text": "old"},
|
|
246
|
+
"text": "new"
|
|
247
|
+
}
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
`buffer_edit` remains available for raw JSON operations.
|
|
251
|
+
|
|
235
252
|
`buffer_commit` remembers non-empty committed output as a reusable command.
|
|
236
253
|
`command_history` returns the last 10 commands, newest first. `command_select`
|
|
237
254
|
creates a new pending buffer from a previous command so the model can reuse it
|
|
@@ -4,15 +4,15 @@ editbuffer/buffer.py,sha256=Wc1Ode1A37PIILPW4I0aQQk-8ebBImmXgAPwiCaPMrM,5623
|
|
|
4
4
|
editbuffer/cli.py,sha256=pv7Lhrcxmztx6tlQSYdUswCcoTg7MSFbPkka1uoCKYs,4533
|
|
5
5
|
editbuffer/errors.py,sha256=gQ33Uwqo_PKfv0gGzz9fAWfgMez_44TkjbfoqTRM8gQ,831
|
|
6
6
|
editbuffer/history.py,sha256=ADi-ssGTB4OpIX3pDs-YtWGXLDUshg55COoKmxpgWdI,754
|
|
7
|
-
editbuffer/mcp_server.py,sha256
|
|
7
|
+
editbuffer/mcp_server.py,sha256=tFCp3iXeQ3L5DdQmknjVTAKydsmv3CA3zDQuO8_Q78c,11772
|
|
8
8
|
editbuffer/operations.py,sha256=l2bPEnKmwXZKXBSc1jZU4r0kkEY9mdky9fDDotoRUFA,2051
|
|
9
9
|
editbuffer/py.typed,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
|
|
10
10
|
editbuffer/resolver.py,sha256=94wOuBylreBN1ddro6NoHZ_dW2xTUh6Tctp_T1xKMfU,5402
|
|
11
11
|
editbuffer/selection.py,sha256=1favs_qZyTXx51NFT9FG1xxwwyb7YCKr3KzVbVlRq2g,5597
|
|
12
12
|
editbuffer/validators.py,sha256=Nj6nOgP6199yuLtYYl45ASbgQ2jQXvRPm1tsLQKaj9w,534
|
|
13
|
-
editbuffer-0.2.
|
|
14
|
-
editbuffer-0.2.
|
|
15
|
-
editbuffer-0.2.
|
|
16
|
-
editbuffer-0.2.
|
|
17
|
-
editbuffer-0.2.
|
|
18
|
-
editbuffer-0.2.
|
|
13
|
+
editbuffer-0.2.2.dist-info/licenses/LICENSE,sha256=ctvjyyJh_lx1XsyH6NKaxtB-DqJJ4tWYsXEJ15uNaTc,1071
|
|
14
|
+
editbuffer-0.2.2.dist-info/METADATA,sha256=wt3zOgCbgITVO1Jgf_jlK8TSLGZ2azA5wz6Y0ytVfmw,6609
|
|
15
|
+
editbuffer-0.2.2.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
|
|
16
|
+
editbuffer-0.2.2.dist-info/entry_points.txt,sha256=cL9-TdYS4juRT-WIvNlcuo9hdxojHMVDyo0MlafDVfA,95
|
|
17
|
+
editbuffer-0.2.2.dist-info/top_level.txt,sha256=898lOqlpIlnmCPUHF0K7C4E5-PWrmWLWy4YHzYAsMsU,11
|
|
18
|
+
editbuffer-0.2.2.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|