venice-cli 0.40.0__tar.gz → 0.41.0__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.
Files changed (88) hide show
  1. {venice_cli-0.40.0 → venice_cli-0.41.0}/PKG-INFO +2 -1
  2. {venice_cli-0.40.0 → venice_cli-0.41.0}/README.md +1 -0
  3. venice_cli-0.41.0/src/venice/__init__.py +1 -0
  4. {venice_cli-0.40.0 → venice_cli-0.41.0}/src/venice/commands/_code.py +133 -0
  5. {venice_cli-0.40.0 → venice_cli-0.41.0}/tests/test_code.py +91 -0
  6. venice_cli-0.40.0/src/venice/__init__.py +0 -1
  7. {venice_cli-0.40.0 → venice_cli-0.41.0}/.gitignore +0 -0
  8. {venice_cli-0.40.0 → venice_cli-0.41.0}/CONTRIBUTING.md +0 -0
  9. {venice_cli-0.40.0 → venice_cli-0.41.0}/LICENSE +0 -0
  10. {venice_cli-0.40.0 → venice_cli-0.41.0}/Makefile +0 -0
  11. {venice_cli-0.40.0 → venice_cli-0.41.0}/SECURITY.md +0 -0
  12. {venice_cli-0.40.0 → venice_cli-0.41.0}/bin/venice +0 -0
  13. {venice_cli-0.40.0 → venice_cli-0.41.0}/install.sh +0 -0
  14. {venice_cli-0.40.0 → venice_cli-0.41.0}/pyproject.toml +0 -0
  15. {venice_cli-0.40.0 → venice_cli-0.41.0}/src/venice/__main__.py +0 -0
  16. {venice_cli-0.40.0 → venice_cli-0.41.0}/src/venice/audio_player.py +0 -0
  17. {venice_cli-0.40.0 → venice_cli-0.41.0}/src/venice/audio_post.py +0 -0
  18. {venice_cli-0.40.0 → venice_cli-0.41.0}/src/venice/auth.py +0 -0
  19. {venice_cli-0.40.0 → venice_cli-0.41.0}/src/venice/billing.py +0 -0
  20. {venice_cli-0.40.0 → venice_cli-0.41.0}/src/venice/cli.py +0 -0
  21. {venice_cli-0.40.0 → venice_cli-0.41.0}/src/venice/client.py +0 -0
  22. {venice_cli-0.40.0 → venice_cli-0.41.0}/src/venice/commands/__init__.py +0 -0
  23. {venice_cli-0.40.0 → venice_cli-0.41.0}/src/venice/commands/_agent.py +0 -0
  24. {venice_cli-0.40.0 → venice_cli-0.41.0}/src/venice/commands/_audio.py +0 -0
  25. {venice_cli-0.40.0 → venice_cli-0.41.0}/src/venice/commands/_compact.py +0 -0
  26. {venice_cli-0.40.0 → venice_cli-0.41.0}/src/venice/commands/_index.py +0 -0
  27. {venice_cli-0.40.0 → venice_cli-0.41.0}/src/venice/commands/_mcp.py +0 -0
  28. {venice_cli-0.40.0 → venice_cli-0.41.0}/src/venice/commands/_mcp_client.py +0 -0
  29. {venice_cli-0.40.0 → venice_cli-0.41.0}/src/venice/commands/_models.py +0 -0
  30. {venice_cli-0.40.0 → venice_cli-0.41.0}/src/venice/commands/_openai.py +0 -0
  31. {venice_cli-0.40.0 → venice_cli-0.41.0}/src/venice/commands/_queue.py +0 -0
  32. {venice_cli-0.40.0 → venice_cli-0.41.0}/src/venice/commands/_repl.py +0 -0
  33. {venice_cli-0.40.0 → venice_cli-0.41.0}/src/venice/commands/_shared.py +0 -0
  34. {venice_cli-0.40.0 → venice_cli-0.41.0}/src/venice/commands/balance.py +0 -0
  35. {venice_cli-0.40.0 → venice_cli-0.41.0}/src/venice/commands/bg_remove.py +0 -0
  36. {venice_cli-0.40.0 → venice_cli-0.41.0}/src/venice/commands/chat.py +0 -0
  37. {venice_cli-0.40.0 → venice_cli-0.41.0}/src/venice/commands/code.py +0 -0
  38. {venice_cli-0.40.0 → venice_cli-0.41.0}/src/venice/commands/config.py +0 -0
  39. {venice_cli-0.40.0 → venice_cli-0.41.0}/src/venice/commands/contact_sheet.py +0 -0
  40. {venice_cli-0.40.0 → venice_cli-0.41.0}/src/venice/commands/embed.py +0 -0
  41. {venice_cli-0.40.0 → venice_cli-0.41.0}/src/venice/commands/image.py +0 -0
  42. {venice_cli-0.40.0 → venice_cli-0.41.0}/src/venice/commands/image_edit.py +0 -0
  43. {venice_cli-0.40.0 → venice_cli-0.41.0}/src/venice/commands/index.py +0 -0
  44. {venice_cli-0.40.0 → venice_cli-0.41.0}/src/venice/commands/login.py +0 -0
  45. {venice_cli-0.40.0 → venice_cli-0.41.0}/src/venice/commands/master.py +0 -0
  46. {venice_cli-0.40.0 → venice_cli-0.41.0}/src/venice/commands/mcp_serve.py +0 -0
  47. {venice_cli-0.40.0 → venice_cli-0.41.0}/src/venice/commands/models.py +0 -0
  48. {venice_cli-0.40.0 → venice_cli-0.41.0}/src/venice/commands/music.py +0 -0
  49. {venice_cli-0.40.0 → venice_cli-0.41.0}/src/venice/commands/search.py +0 -0
  50. {venice_cli-0.40.0 → venice_cli-0.41.0}/src/venice/commands/sfx.py +0 -0
  51. {venice_cli-0.40.0 → venice_cli-0.41.0}/src/venice/commands/tts.py +0 -0
  52. {venice_cli-0.40.0 → venice_cli-0.41.0}/src/venice/commands/upscale.py +0 -0
  53. {venice_cli-0.40.0 → venice_cli-0.41.0}/src/venice/commands/video.py +0 -0
  54. {venice_cli-0.40.0 → venice_cli-0.41.0}/src/venice/config.py +0 -0
  55. {venice_cli-0.40.0 → venice_cli-0.41.0}/src/venice/image_montage.py +0 -0
  56. {venice_cli-0.40.0 → venice_cli-0.41.0}/src/venice/mcp_server.py +0 -0
  57. {venice_cli-0.40.0 → venice_cli-0.41.0}/src/venice/userconfig.py +0 -0
  58. {venice_cli-0.40.0 → venice_cli-0.41.0}/tests/__init__.py +0 -0
  59. {venice_cli-0.40.0 → venice_cli-0.41.0}/tests/_mcp_fake_server.py +0 -0
  60. {venice_cli-0.40.0 → venice_cli-0.41.0}/tests/test_agent.py +0 -0
  61. {venice_cli-0.40.0 → venice_cli-0.41.0}/tests/test_auth.py +0 -0
  62. {venice_cli-0.40.0 → venice_cli-0.41.0}/tests/test_balance.py +0 -0
  63. {venice_cli-0.40.0 → venice_cli-0.41.0}/tests/test_bg_remove.py +0 -0
  64. {venice_cli-0.40.0 → venice_cli-0.41.0}/tests/test_chat.py +0 -0
  65. {venice_cli-0.40.0 → venice_cli-0.41.0}/tests/test_client.py +0 -0
  66. {venice_cli-0.40.0 → venice_cli-0.41.0}/tests/test_code_command.py +0 -0
  67. {venice_cli-0.40.0 → venice_cli-0.41.0}/tests/test_compact.py +0 -0
  68. {venice_cli-0.40.0 → venice_cli-0.41.0}/tests/test_config.py +0 -0
  69. {venice_cli-0.40.0 → venice_cli-0.41.0}/tests/test_contact_sheet.py +0 -0
  70. {venice_cli-0.40.0 → venice_cli-0.41.0}/tests/test_embed.py +0 -0
  71. {venice_cli-0.40.0 → venice_cli-0.41.0}/tests/test_image.py +0 -0
  72. {venice_cli-0.40.0 → venice_cli-0.41.0}/tests/test_image_edit.py +0 -0
  73. {venice_cli-0.40.0 → venice_cli-0.41.0}/tests/test_index.py +0 -0
  74. {venice_cli-0.40.0 → venice_cli-0.41.0}/tests/test_master.py +0 -0
  75. {venice_cli-0.40.0 → venice_cli-0.41.0}/tests/test_mcp_client.py +0 -0
  76. {venice_cli-0.40.0 → venice_cli-0.41.0}/tests/test_mcp_serve.py +0 -0
  77. {venice_cli-0.40.0 → venice_cli-0.41.0}/tests/test_mcp_tools.py +0 -0
  78. {venice_cli-0.40.0 → venice_cli-0.41.0}/tests/test_models.py +0 -0
  79. {venice_cli-0.40.0 → venice_cli-0.41.0}/tests/test_music.py +0 -0
  80. {venice_cli-0.40.0 → venice_cli-0.41.0}/tests/test_repl.py +0 -0
  81. {venice_cli-0.40.0 → venice_cli-0.41.0}/tests/test_search.py +0 -0
  82. {venice_cli-0.40.0 → venice_cli-0.41.0}/tests/test_sfx.py +0 -0
  83. {venice_cli-0.40.0 → venice_cli-0.41.0}/tests/test_shared.py +0 -0
  84. {venice_cli-0.40.0 → venice_cli-0.41.0}/tests/test_shared_openai.py +0 -0
  85. {venice_cli-0.40.0 → venice_cli-0.41.0}/tests/test_tts.py +0 -0
  86. {venice_cli-0.40.0 → venice_cli-0.41.0}/tests/test_upscale.py +0 -0
  87. {venice_cli-0.40.0 → venice_cli-0.41.0}/tests/test_video.py +0 -0
  88. {venice_cli-0.40.0 → venice_cli-0.41.0}/uninstall.sh +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: venice-cli
3
- Version: 0.40.0
3
+ Version: 0.41.0
4
4
  Summary: A stdlib-only Python CLI for the Venice.ai API: audio, video, images, chat, and embeddings.
5
5
  Project-URL: Homepage, https://github.com/gobha-me/venice-cli
6
6
  Project-URL: Repository, https://github.com/gobha-me/venice-cli
@@ -915,6 +915,7 @@ complete — a loud stderr warning is printed).
915
915
  | `project_search` | semantic search over the `.venice` index (if built) | no |
916
916
  | `write_file` | create/overwrite a file (atomic) | yes |
917
917
  | `edit_file` | replace an exact, unique string in a file | yes |
918
+ | `apply_patch` | apply a batch of edits grouped per file, atomically per file (use `occurrence=N` for non-unique strings) | yes |
918
919
  | `run` | run a shell command (`/bin/sh -c`) at the root | yes |
919
920
  | `venice_image` / `venice_image_edit` / `venice_sfx` / `venice_music` / `venice_tts` / `venice_upscale` / `venice_bg_remove` / `venice_video` | generate/edit images, audio & video into the project — **opt-in with `--assets`** | yes |
920
921
 
@@ -877,6 +877,7 @@ complete — a loud stderr warning is printed).
877
877
  | `project_search` | semantic search over the `.venice` index (if built) | no |
878
878
  | `write_file` | create/overwrite a file (atomic) | yes |
879
879
  | `edit_file` | replace an exact, unique string in a file | yes |
880
+ | `apply_patch` | apply a batch of edits grouped per file, atomically per file (use `occurrence=N` for non-unique strings) | yes |
880
881
  | `run` | run a shell command (`/bin/sh -c`) at the root | yes |
881
882
  | `venice_image` / `venice_image_edit` / `venice_sfx` / `venice_music` / `venice_tts` / `venice_upscale` / `venice_bg_remove` / `venice_video` | generate/edit images, audio & video into the project — **opt-in with `--assets`** | yes |
882
883
 
@@ -0,0 +1 @@
1
+ __version__ = "0.41.0"
@@ -286,6 +286,101 @@ def edit_file(root: str, path, old, new, *, confirm: bool = False) -> dict:
286
286
  return _ok(path=rel, action="edited")
287
287
 
288
288
 
289
+ def _apply_one_hunk(text: str, hunk: dict, index: int):
290
+ """Apply one {old,new[,occurrence]} hunk to `text`.
291
+
292
+ Returns (new_text, error_message). `occurrence` (1-based) picks which match
293
+ to replace when `old` is not unique; without it, a non-unique `old` is an
294
+ error (same rule as edit_file). Errors name the hunk index so the model can
295
+ retry surgically.
296
+ """
297
+ old = hunk.get("old")
298
+ new = hunk.get("new")
299
+ if old is None or new is None:
300
+ return None, f"hunk {index}: both old and new are required"
301
+ old, new = str(old), str(new)
302
+ if old == "":
303
+ return None, f"hunk {index}: old must be a non-empty string"
304
+ occ = text.count(old)
305
+ if occ == 0:
306
+ return None, f"hunk {index}: old string not found"
307
+ occurrence = hunk.get("occurrence")
308
+ if occurrence is None:
309
+ if occ > 1:
310
+ return None, (
311
+ f"hunk {index}: old string is not unique ({occ} occurrences); "
312
+ "pass occurrence=N (1-based) or add surrounding context"
313
+ )
314
+ return text.replace(old, new, 1), None
315
+ try:
316
+ n = int(occurrence)
317
+ except (TypeError, ValueError):
318
+ return None, f"hunk {index}: occurrence must be an integer"
319
+ if not 1 <= n <= occ:
320
+ return None, f"hunk {index}: occurrence {n} out of range (1..{occ})"
321
+ return _replace_nth(text, old, new, n), None
322
+
323
+
324
+ def _replace_nth(text: str, old: str, new: str, n: int) -> str:
325
+ """Replace the n-th (1-based) occurrence of `old` with `new`."""
326
+ start = -1
327
+ for _ in range(n):
328
+ start = text.find(old, start + 1)
329
+ return text[:start] + new + text[start + len(old):]
330
+
331
+
332
+ def apply_patch(root: str, patches, *, confirm: bool = False) -> dict:
333
+ """Apply a batch of edits, grouped per file, atomically per file.
334
+
335
+ `patches` is a list of ``{path, edits: [{old, new, occurrence?}, ...]}``.
336
+ Each file's hunks are validated and applied in order against the in-memory
337
+ text; only if every hunk for that file applies is it written (so a partial
338
+ failure never leaves a half-edited file). Hunks are checked sequentially --
339
+ a later hunk sees the result of the earlier ones.
340
+ """
341
+ if not isinstance(patches, list) or not patches:
342
+ return _err("patches must be a non-empty list of {path, edits}")
343
+ plan = [] # (real, rel, new_text, n_edits)
344
+ for fi, entry in enumerate(patches):
345
+ if not isinstance(entry, dict):
346
+ return _err(f"patches[{fi}]: must be an object with path + edits")
347
+ path = entry.get("path")
348
+ edits = entry.get("edits")
349
+ if not isinstance(edits, list) or not edits:
350
+ return _err(f"patches[{fi}]: edits must be a non-empty list")
351
+ try:
352
+ real, rel = _safe_path(root, path, must_exist=True)
353
+ except _PathError as e:
354
+ return _err(f"patches[{fi}]: {e}")
355
+ if os.path.isdir(real):
356
+ return _err(f"patches[{fi}]: {rel} is a directory")
357
+ _data, text = _index.read_text(Path(real))
358
+ if text is None:
359
+ return _err(f"patches[{fi}]: {rel} is binary or unreadable")
360
+ for hi, hunk in enumerate(edits):
361
+ if not isinstance(hunk, dict):
362
+ return _err(f"patches[{fi}] hunk {hi}: must be an object")
363
+ text, err = _apply_one_hunk(text, hunk, hi)
364
+ if err is not None:
365
+ return _err(f"{rel}: {err}")
366
+ plan.append((real, rel, text, len(edits)))
367
+ if not confirm:
368
+ total = sum(n for _r, _l, _t, n in plan)
369
+ files = ", ".join(rel for _r, rel, _t, _n in plan)
370
+ return _confirm(
371
+ f"apply_patch will apply {total} edit(s) across {len(plan)} file(s): {files}"
372
+ )
373
+ results = []
374
+ for real, rel, text, n in plan:
375
+ try:
376
+ _atomic_write(real, text)
377
+ except OSError as e:
378
+ return _err(f"write failed for {rel}: {e}")
379
+ results.append({"path": rel, "edits": n})
380
+ return _ok(action="patched", files=results,
381
+ total_edits=sum(r["edits"] for r in results))
382
+
383
+
289
384
  def _scrubbed_env() -> dict:
290
385
  return {k: v for k, v in os.environ.items() if k not in _SECRET_ENV}
291
386
 
@@ -414,6 +509,38 @@ _EDIT_SCHEMA = _obj(
414
509
  },
415
510
  ["path", "old", "new"],
416
511
  )
512
+ _PATCH_HUNK = _obj(
513
+ {
514
+ "old": _p("string", "Exact text to replace."),
515
+ "new": _p("string", "Replacement text."),
516
+ "occurrence": _p(
517
+ "integer",
518
+ "Which match to replace (1-based) when `old` is not unique; omit to "
519
+ "require a unique match.",
520
+ ),
521
+ },
522
+ ["old", "new"],
523
+ )
524
+ _PATCH_SCHEMA = _obj(
525
+ {
526
+ "patches": {
527
+ "type": "array",
528
+ "description": "Edits grouped per file; applied atomically per file.",
529
+ "items": _obj(
530
+ {
531
+ "path": _p("string", "File to edit, relative to the root."),
532
+ "edits": {
533
+ "type": "array",
534
+ "items": _PATCH_HUNK,
535
+ "description": "Hunks applied in order against the file.",
536
+ },
537
+ },
538
+ ["path", "edits"],
539
+ ),
540
+ },
541
+ },
542
+ ["patches"],
543
+ )
417
544
  _RUN_SCHEMA = _obj(
418
545
  {
419
546
  "command": _p("string", "Shell command to run (via /bin/sh -c) in the root."),
@@ -506,6 +633,12 @@ def code_tools(
506
633
  "(preferred over write_file for small changes). Mutating -- "
507
634
  "requires confirmation.",
508
635
  _EDIT_SCHEMA, paid(edit_file), paid=True),
636
+ _agent.Tool("apply_patch",
637
+ "Apply a batch of edits grouped per file, atomically per file "
638
+ "(#63). Prefer this over edit_file for multi-hunk changes or "
639
+ "when a string is not unique (use occurrence=N); use edit_file "
640
+ "for a single unique change. Mutating -- requires confirmation.",
641
+ _PATCH_SCHEMA, paid(apply_patch), paid=True),
509
642
  _agent.Tool("run",
510
643
  "Run a shell command (/bin/sh -c) with the working directory "
511
644
  "set to the project root; returns exit code + captured output. "
@@ -174,6 +174,97 @@ class TestCodeTools(unittest.TestCase):
174
174
  self.assertIn("not unique", self.tools["edit_file"].invoke(
175
175
  {"path": "e.py", "old": "x", "new": "y"})["message"])
176
176
 
177
+ # --- apply_patch (#63) ---
178
+ def test_apply_patch_gate_then_confirm(self):
179
+ self._write("a.py", "x = 1\n")
180
+ gate = self.tools["apply_patch"].invoke(
181
+ {"patches": [{"path": "a.py", "edits": [{"old": "1", "new": "2"}]}]})
182
+ self.assertEqual(gate["status"], "confirmation_required")
183
+ self.assertEqual(Path(self.root, "a.py").read_text(), "x = 1\n") # untouched
184
+ ok = self.tools["apply_patch"].invoke(
185
+ {"patches": [{"path": "a.py", "edits": [{"old": "1", "new": "2"}]}]},
186
+ confirm=True)
187
+ self.assertEqual(ok["status"], "ok")
188
+ self.assertEqual(ok["total_edits"], 1)
189
+ self.assertEqual(Path(self.root, "a.py").read_text(), "x = 2\n")
190
+
191
+ def test_apply_patch_multi_hunk_in_order(self):
192
+ self._write("a.py", "foo\nbar\nbaz\n")
193
+ ok = self.tools["apply_patch"].invoke({"patches": [
194
+ {"path": "a.py", "edits": [
195
+ {"old": "foo", "new": "FOO"},
196
+ {"old": "baz", "new": "BAZ"},
197
+ ]}]}, confirm=True)
198
+ self.assertEqual(ok["status"], "ok")
199
+ self.assertEqual(Path(self.root, "a.py").read_text(), "FOO\nbar\nBAZ\n")
200
+
201
+ def test_apply_patch_occurrence_resolves_non_unique(self):
202
+ self._write("a.py", "x\nx\nx\n")
203
+ ok = self.tools["apply_patch"].invoke({"patches": [
204
+ {"path": "a.py", "edits": [{"old": "x", "new": "MID", "occurrence": 2}]}]},
205
+ confirm=True)
206
+ self.assertEqual(ok["status"], "ok")
207
+ self.assertEqual(Path(self.root, "a.py").read_text(), "x\nMID\nx\n")
208
+
209
+ def test_apply_patch_non_unique_without_occurrence_errors(self):
210
+ self._write("a.py", "x\nx\n")
211
+ r = self.tools["apply_patch"].invoke({"patches": [
212
+ {"path": "a.py", "edits": [{"old": "x", "new": "y"}]}]}, confirm=True)
213
+ self.assertEqual(r["status"], "error")
214
+ self.assertIn("not unique", r["message"])
215
+ self.assertIn("occurrence", r["message"])
216
+ self.assertEqual(Path(self.root, "a.py").read_text(), "x\nx\n") # unchanged
217
+
218
+ def test_apply_patch_atomic_per_file_on_failure(self):
219
+ # Second hunk fails -> the whole file is left untouched (no partial edit).
220
+ self._write("a.py", "keep\n")
221
+ r = self.tools["apply_patch"].invoke({"patches": [
222
+ {"path": "a.py", "edits": [
223
+ {"old": "keep", "new": "CHANGED"},
224
+ {"old": "MISSING", "new": "y"},
225
+ ]}]}, confirm=True)
226
+ self.assertEqual(r["status"], "error")
227
+ self.assertIn("not found", r["message"])
228
+ self.assertEqual(Path(self.root, "a.py").read_text(), "keep\n")
229
+
230
+ def test_apply_patch_multiple_files(self):
231
+ self._write("a.py", "a1\n")
232
+ self._write("b.py", "b1\n")
233
+ ok = self.tools["apply_patch"].invoke({"patches": [
234
+ {"path": "a.py", "edits": [{"old": "a1", "new": "a2"}]},
235
+ {"path": "b.py", "edits": [{"old": "b1", "new": "b2"}]},
236
+ ]}, confirm=True)
237
+ self.assertEqual(ok["status"], "ok")
238
+ self.assertEqual(ok["total_edits"], 2)
239
+ self.assertEqual(len(ok["files"]), 2)
240
+ self.assertEqual(Path(self.root, "a.py").read_text(), "a2\n")
241
+ self.assertEqual(Path(self.root, "b.py").read_text(), "b2\n")
242
+
243
+ def test_apply_patch_sandbox_and_shape_errors(self):
244
+ r = self.tools["apply_patch"].invoke({"patches": [
245
+ {"path": "../escape.py", "edits": [{"old": "a", "new": "b"}]}]},
246
+ confirm=True)
247
+ self.assertEqual(r["status"], "error")
248
+ self.assertIn("escapes", r["message"])
249
+ self.assertEqual(self.tools["apply_patch"].invoke(
250
+ {"patches": []})["status"], "error")
251
+ self.assertEqual(self.tools["apply_patch"].invoke(
252
+ {"patches": [{"path": "a.py", "edits": []}]})["status"], "error")
253
+
254
+ def test_apply_patch_occurrence_out_of_range(self):
255
+ self._write("a.py", "x\nx\n")
256
+ r = self.tools["apply_patch"].invoke({"patches": [
257
+ {"path": "a.py", "edits": [{"old": "x", "new": "y", "occurrence": 5}]}]},
258
+ confirm=True)
259
+ self.assertEqual(r["status"], "error")
260
+ self.assertIn("out of range", r["message"])
261
+
262
+ def test_apply_patch_missing_file(self):
263
+ r = self.tools["apply_patch"].invoke({"patches": [
264
+ {"path": "nope.py", "edits": [{"old": "a", "new": "b"}]}]}, confirm=True)
265
+ self.assertEqual(r["status"], "error")
266
+ self.assertIn("no such file", r["message"])
267
+
177
268
  # --- run ---
178
269
  def test_run_gate_then_exec_cwd_and_scrub(self):
179
270
  gate = self.tools["run"].invoke({"command": "echo hi"})
@@ -1 +0,0 @@
1
- __version__ = "0.40.0"
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes