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 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 registry.create(content, buffer_id=buffer_id)
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 registry.edit(buffer_id, operation)
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 registry.history(buffer_id)
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 registry.rollback(buffer_id, version)
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 registry.select_command(command_id, buffer_id=buffer_id)
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.1
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
- Until a PyPI release exists, install directly from GitHub:
202
+ Or install with `uvx` without a persistent environment:
203
203
 
204
204
  ```bash
205
- pipx install 'editbuffer[mcp] @ git+https://github.com/averagedigital/editbuffer.git'
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=-x3-8sXpW1vcMb5jFZ-bMsJus5YxeG8ydkLTh4OtF6w,6712
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.1.dist-info/licenses/LICENSE,sha256=ctvjyyJh_lx1XsyH6NKaxtB-DqJJ4tWYsXEJ15uNaTc,1071
14
- editbuffer-0.2.1.dist-info/METADATA,sha256=L6Diepd98YZsjl2fly6dgPTFjpDtUFxTU1qF9tSCfuQ,6329
15
- editbuffer-0.2.1.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
16
- editbuffer-0.2.1.dist-info/entry_points.txt,sha256=cL9-TdYS4juRT-WIvNlcuo9hdxojHMVDyo0MlafDVfA,95
17
- editbuffer-0.2.1.dist-info/top_level.txt,sha256=898lOqlpIlnmCPUHF0K7C4E5-PWrmWLWy4YHzYAsMsU,11
18
- editbuffer-0.2.1.dist-info/RECORD,,
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,,