mirror of
https://github.com/NCBM/plyngent.git
synced 2026-11-07 07:17:54 +08:00
docs: describe deletes that go to the trash
A new page, `doc/trash.md`, covers what a delete does now: where a path lands on each platform, what is still deleted outright (an explicit `permanent=true`, a host that cleared `[trash]`, scratch paths under the OS temp dir, the `copy_path` / `move_path` overwrite, the temporary-workspace cleanup), what happens when a trashing is refused — the file stays and the error says how to delete it on purpose — and the two things the trash cannot help with: a shell `rm` and anything a plugin or MCP server does on its own. `[trash]` joins the example config and the README snippet, the README gains the one-paragraph version, `AGENTS.md` the tool/config notes (including the confirm wording a delete now uses), and the default tool playbook tells the model that `delete_path` is undoable and what `permanent=true` costs.
This commit is contained in:
@@ -60,7 +60,7 @@ lmproto/deepseek/anthropic/ ← DeepSeek Anthropic client (extends Anthropic
|
||||
|
||||
### Config (`config/`)
|
||||
|
||||
TOML load/store (`ConfigStore`): `[providers]` tagged union presets, `[database]` section. Default path via platformdirs. Providers may set `timeout` as a float or `{ connect, read }` (`HttpTimeoutConfig`); omitted → product defaults (10s connect / 600s read). `DeepseekProvider.convention` (Literal: `""`/`"openai"`/`"anthropic"`/`"responses"`) selects the DeepSeek API surface — `"responses"` → OpenAI Responses, `"anthropic"` → Anthropic Messages on `https://api.deepseek.com/anthropic` (default url follows the convention); a per-model `ModelConfig.convention` overrides it (empty = inherit). `[mcp]` (`McpConfig`): `servers.<name>` stdio server definitions (`McpServerConfig`: `command`/`args`/`env`/`cwd`/`timeout`/`read_only`, `url` reserved for streamable HTTP) + `disable` gate; defining a server opts it in. `[skills]` (`SkillsConfig`): `paths` (explicit roots first), `discover` (`plyngent` user dir + `claude` dirs) with `include_project_roots`, `enabled`, and `allow_write` (standing write grant); a malformed section degrades to defaults like `[agent]` / `[mcp]`.
|
||||
TOML load/store (`ConfigStore`): `[providers]` tagged union presets, `[database]` section. Default path via platformdirs. Providers may set `timeout` as a float or `{ connect, read }` (`HttpTimeoutConfig`); omitted → product defaults (10s connect / 600s read). `DeepseekProvider.convention` (Literal: `""`/`"openai"`/`"anthropic"`/`"responses"`) selects the DeepSeek API surface — `"responses"` → OpenAI Responses, `"anthropic"` → Anthropic Messages on `https://api.deepseek.com/anthropic` (default url follows the convention); a per-model `ModelConfig.convention` overrides it (empty = inherit). `[mcp]` (`McpConfig`): `servers.<name>` stdio server definitions (`McpServerConfig`: `command`/`args`/`env`/`cwd`/`timeout`/`read_only`, `url` reserved for streamable HTTP) + `disable` gate; defining a server opts it in. `[skills]` (`SkillsConfig`): `paths` (explicit roots first), `discover` (`plyngent` user dir + `claude` dirs) with `include_project_roots`, `enabled`, and `allow_write` (standing write grant); a malformed section degrades to defaults like `[agent]` / `[mcp]`. `[trash]` (`TrashConfig`): `enabled` — whether `delete_path` moves to the system trash or removes the path outright; a typo degrades to the default (trash on).
|
||||
|
||||
`reasoning.py`: thinking strength as `reasoning_effort` (`ReasoningEffortConfig`: `""` = send nothing | `none` … `high` … `max`) and `thinking_budget_tokens`, settable on `[agent]`, a provider, or a model entry — `resolve_reasoning(agent, provider, model)` merges the layers per field (model > provider > agent) and clamps `max` (DeepSeek chat only) to `xhigh` for every other surface, returning a `ReasoningConfig` the agent loop and `create_client` consume.
|
||||
|
||||
@@ -99,11 +99,11 @@ Module-level `@tool` handlers. Call `set_workspace_root()` before use.
|
||||
|
||||
- **`workspace`**: path resolve under primary root **or** temporary allowlist roots **or** a directory-access grant (mode-aware: `read` < `write` < `exec`, default required `write`; a file grant matches only that file); path substring denylist (wins over grants); command denylist via `match_command_denylist` (checks every program of the wrapper/`run` chain, resolved by `tools/command_scan.py`; an entry is a program, plus optional argument words that make it dangerous — `systemctl reboot poweroff …` matches `systemctl reboot` and `systemctl start reboot.target`, not `systemctl status`; basenames are lower-cased, so `RM` / `rm.exe` count; defaults cover privilege `sudo`/`su`/`doas`/`pkexec`/`run0`/`systemd-run`, destructive `rm`/`mkfs`/`dd`/`chown`/`mount`/…, and power state `shutdown`/`reboot`/`poweroff`/`halt`/`init`/`telinit`/`kexec`/`pm-*`/`s2*`; CLI: timed human allow override, default deny on timeout; **not** skipped by YOLO); escape error names the `request_directory_access` mode the call needs (`call request_directory_access with mode='write'`).
|
||||
- **`access`** (`tools/access.py`): `request_directory_access` (LOCAL|INSTANCE|SESSION) asks the host hook for out-of-workspace access; grants are one `path → mode` pair in one of three stores — `WorkspacePolicy.config_allow` (static `[agent].allow_paths`), `SessionState.access_grants` (human-approved, persisted on the session row), `WorkspacePolicy.yolo_allow` (`--yes`/`/yolo`, process only). `AccessDecision(mode, persist)` from the hook; denylisted paths can never be granted; cap `MAX_ACCESS_GRANTS`. Not a sandbox: `exec` grants (and any command) reach the whole FS.
|
||||
- **`file`**: `read_file` (`with_lineno`, `max_chars` default 32k; results start with a 1-based `L{begin}-{end}` range; an image file is refused with a pointer to `view_image`), `view_image` (a picture reaches the model as an image part; format sniffed from the bytes, `detail` = `auto` default / `low` / `high` / `original`), `write_file` (refuses to replace an existing file unless `overwrite=true`; that replacement is the one soft-confirmed), `listdir`, `tree` (model picks `format=markdown` default / `flat` paths / `decorated`; VCS + default noise dirs + optional `skip_dirs` / denylist walk), `glob_paths`, `regex_files` (Python `re` per line, not a glob; literal file/dir `path`, no glob expansion; skips VCS/binary), `edit_replace` (`max_replaces`, reports remaining matches), `edit_lineno` (1-based range; only lines read via `with_lineno`; read state resets on any file write/edit/copy/move/delete), `copy_path` / `move_path` / `delete_path`, `new_temporary_workspace` (system temp allowlist; cleanup on chat exit; no session rebind), `get_truncated` (resume any truncated result via its `truncate_token=...`).
|
||||
- **`file`**: `read_file` (`with_lineno`, `max_chars` default 32k; results start with a 1-based `L{begin}-{end}` range; an image file is refused with a pointer to `view_image`), `view_image` (a picture reaches the model as an image part; format sniffed from the bytes, `detail` = `auto` default / `low` / `high` / `original`), `write_file` (refuses to replace an existing file unless `overwrite=true`; that replacement is the one soft-confirmed), `listdir`, `tree` (model picks `format=markdown` default / `flat` paths / `decorated`; VCS + default noise dirs + optional `skip_dirs` / denylist walk), `glob_paths`, `regex_files` (Python `re` per line, not a glob; literal file/dir `path`, no glob expansion; skips VCS/binary), `edit_replace` (`max_replaces`, reports remaining matches), `edit_lineno` (1-based range; only lines read via `with_lineno`; read state resets on any file write/edit/copy/move/delete), `copy_path` / `move_path` / `delete_path` (moves the path into the system trash — freedesktop trash / Finder trash / Recycle Bin — so a delete can be undone; `permanent=true`, `[trash] enabled = false`, or a path under the OS temp dir delete it for good, and a trashing that fails reports the reason and leaves the path alone; [doc/trash.md](doc/trash.md)), `new_temporary_workspace` (system temp allowlist; cleanup on chat exit; no session rebind), `get_truncated` (resume any truncated result via its `truncate_token=...`).
|
||||
- **`process`**: `run_argv` (argv, no shell, timeout, optional stdin/env); `run_argv_batch` (serial steps, `pipe_out` on provider, `mix_stderr`, `stop_on_error`); PTY `open_pty` / `read_pty` / `write_pty` (literal) / `write_pty_keys` (escapes) / `ask_into_pty` (human→PTY only; `secret` no-echo; answer never in tool result) / `close_pty` (POSIX: `pty`+`fork`; Windows: ConPTY via `pywinpty` env marker dep).
|
||||
- PTY: backend in `pty_backend.py`; structured status (`alive`/`exit_code`/`data`); `read_pty(..., until=)` (**CSI sanitized** so host TTY is never reset); keys via `write_pty_keys` only (`\xHH`, `ctrl+x`, `key=esc`); secrets via `ask_into_pty` (not `write_pty` data); session limit/idle TTL/output budget; close terminate→kill.
|
||||
- CLI limit hooks: interactive confirm to raise tool-loop rounds, PTY session cap, or PTY output budget. The prompt accepts `y`/empty (once), `n`, or `yyy` = stop asking for the rest of the turn (`cli.limits` module flag, reset by `run_turn_with_retries`, `/btw`, session switches, and chat exit). `--auto-continue` / `[agent] auto_continue_limits` set the process-wide default (covers one-shot/PTY); the flag does not imply YOLO and vice versa.
|
||||
- Destructive confirms: `classify_danger` + `ToolRegistry(on_confirm=…)`; CLI default deny; config `confirm_destructive` / `path_denylist`. Soft confirm also for interpreters — bare shells/REPLs, scripts, and `-c` one-liners alike (`-c` is code only for an interpreter, so `grep -c` / `od -c` stay unflagged; argv + code preview) —, detached runners (`nohup`), and wrapper options that carry a command line (`env -S "…"`, `su -c "…"`, whose text is printed under its own `env -S:` label), each resolved past env wrappers and `<launcher> run`; deny may include a user comment for the model. Session YOLO: `/yolo on|off|once` and `--yes` (skip the soft confirms of `YOLO`-tagged tools — interpreter runs included — while untagged ones still prompt and a command that escalates privileges keeps its confirm through `DangerReason(yolo_skip=False)`; hard denylists unchanged; `once` expires after the next user turn). A round of tool calls asks **once**: `ToolRegistry.prepare_batch_confirm` pre-scans it (unknown tools, undecodable arguments, calls missing a required argument, and calls YOLO already covers are left out — nobody is asked about a call that cannot run or will not ask), two or more soft reasons collapse into one `ToolBatchConfirmHook` prompt whose `BatchConfirmDecision` `execute(..., decision=…)` applies to each call (CLI box lists every call: `y` = allow these, `m` = review them one at a time through the per-call prompt, `N` = deny with one comment shared by all), the answer covers that round only (the next one asks again), and a lone request keeps the plain one-call prompt.
|
||||
- Destructive confirms: `classify_danger` + `ToolRegistry(on_confirm=…)`; CLI default deny; config `confirm_destructive` / `path_denylist`. Soft confirm also for interpreters — bare shells/REPLs, scripts, and `-c` one-liners alike (`-c` is code only for an interpreter, so `grep -c` / `od -c` stay unflagged; argv + code preview) —, detached runners (`nohup`), and wrapper options that carry a command line (`env -S "…"`, `su -c "…"`, whose text is printed under its own `env -S:` label), each resolved past env wrappers and `<launcher> run`; deny may include a user comment for the model. The `delete_path` confirm names what will happen — `move path 'x' to the system trash`, or `delete path 'x' permanently` when the call asks for it, the host cleared `[trash]`, or the path is scratch — and the pretty line prints `(to trash)` for a delete that landed there. Session YOLO: `/yolo on|off|once` and `--yes` (skip the soft confirms of `YOLO`-tagged tools — interpreter runs included — while untagged ones still prompt and a command that escalates privileges keeps its confirm through `DangerReason(yolo_skip=False)`; hard denylists unchanged; `once` expires after the next user turn). A round of tool calls asks **once**: `ToolRegistry.prepare_batch_confirm` pre-scans it (unknown tools, undecodable arguments, calls missing a required argument, and calls YOLO already covers are left out — nobody is asked about a call that cannot run or will not ask), two or more soft reasons collapse into one `ToolBatchConfirmHook` prompt whose `BatchConfirmDecision` `execute(..., decision=…)` applies to each call (CLI box lists every call: `y` = allow these, `m` = review them one at a time through the per-call prompt, `N` = deny with one comment shared by all), the answer covers that round only (the next one asks again), and a lone request keeps the plain one-call prompt.
|
||||
- **`vcs`**: read-only VCS tools (`vcs_kind` / `vcs_status` / `vcs_diff` / `vcs_log` / `vcs_branch`) via `VcsBackend` protocol; **git** implemented; detectors are pluggable for other systems.
|
||||
- **`chat`**: human prompts as tools — `ask_user_line` / `ask_user_choice` / `ask_user_form` (shared `prompting` core); `wait` (line prompt with timeout; Enter disturbs). Container args are shape-checked (`tools/chat/shape.py`): a mis-typed string `options`/`fields` (or a non-string `question`) returns an explained error — `options="a, b"` used to iterate characters into a per-character menu. `wait`'s `duration` accepts numeric strings (`"5"` = 5s) and rejects booleans/text/containers.
|
||||
- **`todo`**: LIFO stack of **task groups** (not a queue of tasks) — `todo_push` creates one group of siblings; `todo_pop` removes the whole top group; `todo_update` by the number a listing shows (render order, TOP group first; the stored `id` stays internal); DFS breakdown push[T1,T2]→push[T1.1…]→pop→push[T2.1…]; stored on session row; open items = unfinished work (`[TODO OPEN WORK]`); all-terminal non-empty = hygiene (`[TODO HYGIENE]`); nag channel via `[agent] todo_nag_strategy` (`developer`/`user` = prose nag; `synthetic_tool` = forged `todo_list` call + **real** `stack.render()` result + ToolCall/Result events; `none`).
|
||||
|
||||
@@ -180,6 +180,10 @@ max_context_tokens = 200000
|
||||
# channels = ["auto"] # auto | desktop | terminal
|
||||
# actions = true # allow / deny / review buttons + reply field
|
||||
# include_reason = false # off: the reason may name a command or URL
|
||||
|
||||
# Deletes: `delete_path` moves the path to the system trash. See doc/trash.md.
|
||||
# [trash]
|
||||
# enabled = true # false = a delete is final again
|
||||
```
|
||||
|
||||
Per-provider **`timeout`** is passed to the HTTP session for chat/completions, Responses, and `GET /models`. A single number sets one timeout; `{ connect, read }` splits TCP/TLS setup vs idle wait between response bytes (SSE can run longer than `read` while chunks keep arriving). Tool/process timeouts (`run_argv`, PTY, policy confirm) are separate.
|
||||
@@ -204,6 +208,8 @@ Third-party **plugins**: install a package that declares `project.entry-points."
|
||||
|
||||
**Notifications while a prompt waits**: a turn stops at a confirm, a directory-access request or an `ask_user_*` question, so if you walked away you only find out on your return. After `[notify].grace_seconds` (5s) the prompt is announced — with **Allow once / Deny / Go to terminal** buttons for a confirmation and a reply field for a question, where the platform renders them — and the answer is taken as if typed at the terminal (never as a session grant). The desktop channel needs the optional extra (`pip install 'plyngent[notify]'`); without it the CLI writes an OSC 777 / OSC 9 escape sequence to `/dev/tty`, which every modern terminal turns into a native notification with no dependency at all. Answering at the terminal inside the grace period stays silent, `[notify].include_reason` keeps the reason line (a command, argv or URL) out by default, and no channel can break a prompt: a missing extra or a refusing desktop falls through to the next one. Details: [doc/notifications.md](doc/notifications.md).
|
||||
|
||||
**Deletes go to the trash**: `delete_path` moves the path into the desktop's own trash — the freedesktop trash on Linux, the Finder trash on macOS, the Recycle Bin on Windows — so a delete the agent made on its own can be undone from the file manager you already use. That matters because the tool is YOLO-tagged: under `--yes` / `/yolo on` it runs without a confirmation. Ask for the old behaviour with `permanent=true`, or turn it off for every call with `[trash] enabled = false`. Paths under the system temp directory are scratch and are deleted outright, a trashing that fails (a volume with no usable trash directory, a broken install) leaves the file alone and says so instead of deleting it, and a shell `rm` never uses the trash. Details: [doc/trash.md](doc/trash.md).
|
||||
|
||||
**MCP servers** (Model Context Protocol over stdio): define servers under `[mcp.servers.<name>]` (`command` + `args`, optional `env`/`cwd`/`timeout`/`read_only`) and their tools become agent tools namespaced `mcp__<server>__<tool>` (LOCAL tags; `read_only = true` also marks them READ_ONLY, eligible for `/btw --tools=read`). Names in `[mcp].disable` stay disconnected. A server may also return optional `instructions` (usage guidance) in its MCP `initialize` response; when tools are on those are folded into the agent system prompt as `MCP server <name> instructions:` blocks (`[agent] mcp_instructions = false` disables), and `/mcp` previews them. In the REPL, `/mcp` lists per-server status and tool counts; `/mcp reconnect` re-reads the config file and restarts every enabled server (adopts newly added ones).
|
||||
|
||||
Supported provider presets today:
|
||||
|
||||
@@ -45,4 +45,6 @@ Images (a turn's pictures, a tool result's pictures, what each wire surface
|
||||
takes): [images.md](./images.md).
|
||||
Prompt notifications (a prompt that waits on the human, and how it can be
|
||||
answered from the notification): [notifications.md](./notifications.md).
|
||||
Deletes (a `delete_path` call moves the path into the system trash, and what
|
||||
that does not cover): [trash.md](./trash.md).
|
||||
|
||||
|
||||
@@ -63,6 +63,14 @@
|
||||
# include_reason = false
|
||||
# protocol = "" # terminal protocol: "" = detect, or osc777 / osc9 / bell
|
||||
|
||||
# Deletes. `delete_path` moves the path into the desktop's trash (freedesktop
|
||||
# trash / Finder trash / Recycle Bin) so it can be restored from the file
|
||||
# manager; `permanent=true` on the call removes it for good, and paths under the
|
||||
# system temp dir are always deleted outright. `enabled = false` restores the
|
||||
# old "a delete is final" behaviour for every call. See doc/trash.md.
|
||||
# [trash]
|
||||
# enabled = true
|
||||
|
||||
# MCP (Model Context Protocol) servers exposed as agent tools.
|
||||
# Defining a server opts it in; tools appear as mcp__<server>__<tool> with
|
||||
# LOCAL tags (confirm flow applies). Names in disable stay disconnected.
|
||||
|
||||
+100
@@ -0,0 +1,100 @@
|
||||
# Deletes and the system trash
|
||||
|
||||
`delete_path` is the agent's only way to remove a path, and it is tagged
|
||||
`YOLO`: under `--yes` / `/yolo on` it runs with no confirmation at all. So the
|
||||
default has to be recoverable — a delete moves the path into the desktop's own
|
||||
trash, where the file manager the human already uses can put it back.
|
||||
|
||||
```text
|
||||
deleted file notes.md (moved to system trash)
|
||||
deleted directory build (moved to system trash)
|
||||
deleted file notes.md # permanent=true
|
||||
```
|
||||
|
||||
`permanent=true` is the old behaviour, chosen on purpose. `[trash] enabled =
|
||||
false` makes every delete that way.
|
||||
|
||||
## Where a path goes
|
||||
|
||||
| Platform | Trash |
|
||||
|---|---|
|
||||
| Linux / BSD | The freedesktop trash specification: `$XDG_DATA_HOME/Trash` (`~/.local/share/Trash` by default), or a per-volume trash when the path is on another filesystem |
|
||||
| macOS | The Finder's trash (`FSMoveObjectToTrashSync`; `NSFileManager` when pyobjc is installed) |
|
||||
| Windows | The Recycle Bin (`IFileOperation` with pywin32, else `SHFileOperation`) |
|
||||
|
||||
The platform work is [send2trash](https://github.com/arsenetar/send2trash), a
|
||||
required dependency because a missing safety net that fails silently is worse
|
||||
than no safety net. It is pure Python over `ctypes` — nothing to compile, no
|
||||
runtime dependencies.
|
||||
|
||||
Nothing is renamed away piecemeal: the whole path moves into the trash in one
|
||||
`rename`, so a large directory costs nothing to delete (across filesystems,
|
||||
where the volume has no trash of its own, the fallback is a move into the home
|
||||
trash, which does copy). A path trashed twice keeps both copies (`dup.txt`,
|
||||
then `dup 1.txt`).
|
||||
|
||||
## What is deleted outright
|
||||
|
||||
| Case | Why |
|
||||
|---|---|
|
||||
| `permanent=true` | The call said so |
|
||||
| `[trash] enabled = false` | The host said so |
|
||||
| A path under the OS temporary directory | Scratch space: nobody restores it, and a trash on the `/tmp` volume would only collect it. The same rule that `new_temporary_workspace` cleans up with |
|
||||
| `new_temporary_workspace` cleanup at chat exit | Same reason |
|
||||
| `copy_path` / `move_path` with `overwrite=true` | The destination is being replaced by an explicit call; the source content is the point of the operation |
|
||||
|
||||
## When the trash is unavailable
|
||||
|
||||
A trashing that fails is reported, never downgraded to a deletion — the human
|
||||
was told the path is recoverable, so guessing would be worse than stopping:
|
||||
|
||||
```text
|
||||
error: cannot move to trash: Permission denied (use permanent=true)
|
||||
error: cannot move to trash: send2trash is not installed (use permanent=true)
|
||||
```
|
||||
|
||||
The usual causes are a path on a filesystem with no `.Trash` directory that the
|
||||
user cannot create one on (network mounts, some FUSE filesystems, other
|
||||
people's volumes), a parent directory without write permission, and a broken
|
||||
install. In every case the path is untouched and the error names the way
|
||||
forward.
|
||||
|
||||
## Restoring
|
||||
|
||||
Restore from the desktop's own trash — the file manager's "Trash" / "Recycle
|
||||
Bin", which is where a human would look anyway. plyngent does not offer a
|
||||
`restore` tool: `send2trash` does not report where a path landed, and the
|
||||
platform trash formats differ. A delete therefore reports that the path is in
|
||||
the trash, not where.
|
||||
|
||||
## What the trash does not cover
|
||||
|
||||
- **A shell command.** `run_argv(["rm", "-rf", …])`, a script, or anything run
|
||||
through a PTY reaches the filesystem directly; the command denylist and the
|
||||
soft confirm of interpreter runs are the only gates there, and `rm` does not
|
||||
use the trash.
|
||||
- **Plugins and MCP servers.** They bring their own handlers.
|
||||
- **Writes.** `write_file` with `overwrite=true` replaces a file's contents in
|
||||
place — it never removes a path. Only `delete_path`, the `copy_path` /
|
||||
`move_path` overwrite, and the temporary-workspace cleanup remove paths.
|
||||
|
||||
## Privacy
|
||||
|
||||
A trashed path is still on disk until the trash is emptied — that is the point
|
||||
of it. `[agent].path_denylist` still runs first, so a denied path is never
|
||||
trashed either, and a trashed file has left the workspace (it is no longer in a
|
||||
`git status`, and no longer readable by a path tool).
|
||||
|
||||
## Platform limits worth knowing
|
||||
|
||||
- **Windows**: the shell deletes some things outright even when the call asked
|
||||
for the Recycle Bin — empty folders (a folder only reaches the bin with
|
||||
content), files above the bin's size limit for that drive, and paths on
|
||||
network shares. That is Windows' behaviour, not a fallback here.
|
||||
- **Linux**: a path on another volume needs a trash directory there
|
||||
(`volume/.Trash/$UID` with the sticky bit, or a creatable `volume/.Trash-$UID`).
|
||||
Without one the file is moved into the home trash instead, which crosses a
|
||||
filesystem boundary and therefore copies.
|
||||
- **macOS**: without pyobjc the library calls the older
|
||||
`FSMoveObjectToTrashSync`; installing `send2trash[nativeLib]` switches it to
|
||||
`NSFileManager`. Both trash; neither needs anything from plyngent.
|
||||
@@ -50,6 +50,8 @@ retrying that path.
|
||||
fix `old_string` or `max_replaces`; shell workarounds rarely help.
|
||||
- `write_file` refuses to replace an existing file (read it, or change it with \
|
||||
`edit_replace` / `edit_lineno`); a full replacement needs `overwrite=true`, which asks the human.
|
||||
- `delete_path` moves the path into the system trash, so a delete stays undoable \
|
||||
(`permanent=true` deletes for good; scratch paths under the OS temp dir always do).
|
||||
- `edit_replace` defaults to the first match; if remaining matches are reported, \
|
||||
raise `max_replaces` or narrow `old_string`.
|
||||
- `edit_lineno` edits only lines you read via `read_file(path, with_lineno=true)`; \
|
||||
|
||||
Reference in New Issue
Block a user