Files
omp-edit/README.md
T

213 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# omp-edit
[oh-my-pi](https://github.com/can1357/oh-my-pi)-style editing for the DeepSeek Harness (DSH). It **replaces DSH's built-in `edit` tool** with oh-my-pi's *hashline* patch language. It also wraps `read` and `grep` so their output carries the content-hash tags the patches anchor on.
The model points at line numbers of a tagged snapshot instead of retyping the old text. That avoids whitespace-mismatch "string not found" loops and saves output tokens. A stale snapshot is detected before anything is written, and is rebased automatically when the anchored lines survived.
## What the agent sees
`read` output carries a `[PATH#TAG]` header and `LINE:TEXT` rows (the 4 hex characters are a hash of the whole file):
```
<path>[src/app.py#1A2B]</path>
1:def greet(name):
2: print("Hello, " + name)
```
`grep` groups matches under a tagged header per file. A grep hit counts as having read the file, so it can be edited straight away:
```
[src/app.py#1A2B]
2: print("Hello, " + name)
```
`edit` takes one `input` string with one or more file sections:
```
[src/app.py#1A2B]
PUT 2.=2:
+ print(f"Hi, {name}")
```
It returns the fresh tag plus a compact numbered preview of the changed region, so the agent can chain edits without re-reading. The Web UI shows the usual diff card.
## Hashline operations
| Form | Effect |
| --- | --- |
| `PUT N.=M:` + `+rows` | Replace original lines N..M (`N.=N` for one line) |
| `PUT N*:` + `+rows` | Replace the syntactic block opening at line N (function/class/object, Markdown heading section, Python/YAML indented block; leading decorators included) |
| `PUT <N:` / `PUT >N:` | Insert before / after line N (`<1` = file head, `>$` = file tail) |
| `PUT >N*:` | Insert after the block opening at N |
| `CUT N.=M [@name]` / `CUT N* [@name]` | Delete and capture (anonymous register, or a named one) |
| `PUT <N [@name]` / `PUT >N [@name]` | Paste a register at a gap |
| `PUT N.=M @name` / `PUT N* @name` | Paste a named register over a range/block |
| `REM` | Delete the file |
| `MV DEST` | Move the file after the section's edits |
The rules match oh-my-pi:
- Body rows are `+TEXT`; a lone `+` is a blank line; literal `-x` is written `+-x`.
- All numbers refer to the tagged snapshot, never shifted by earlier hunks.
- Overlapping ranges are rejected.
- Named registers persist for the session, and cross-file moves work (`CUT 1* @fn` in one section, `PUT <1 @fn` in another).
- The whole call is validated before anything is written.
- Tolerant parsing: an optional `*** Begin/End Patch` envelope and markdown fences are stripped, and `PUT 3-5:` / `PUT 3:` are accepted with a warning.
### Stale tags
Every tagged `read`/`grep`/`edit` records a snapshot. When the file changed since the tag (another tool, a formatter, the user):
- if every anchored line still exists unchanged, with a neighbour that moved by the same offset, the patch is **remapped onto the current file** and applied. A warning says so.
- otherwise the edit is **rejected without writing**, with a message to re-read.
A tag that was never issued for that file (e.g. copied from another file) is rejected with a hint.
## Paging: read, grep, glob and bash
Outputs are cut into pages so the model never pulls a whole file, huge search, or giant shell stream into context at once:
- **`read`**: at most **200 lines** per call (`limit` above 200 is capped); the footer gives the `offset` to continue. The UI row indicates the line range being read (`Read lines A–B`).
- **`grep`**: at most **200 matches** per call, grouped under `[PATH#TAG]` headers. Pass `offset` for the next page (`… call grep again with offset=200`).
- **`glob`**: at most **200 paths** per call, with the same `offset` cursor.
- **`bash`**: output is capped at **200 lines** per stream (`stdout` / `stderr`). The complete, uncut output is saved to a temporary file (`/tmp/omp-edit-output/...txt`), and the footer instructs the model how to continue reading with `read` (`read file_path="..." offset=201`).
Change the page size with `pageLines`.
## Guard rule
A `tools/pre-execute` listener ensures file modifications go through appropriate tools while lifting restrictions on reading files via shell. Writing through shell is blocked.
| Denied | Use instead |
| --- | --- |
| `sed -i`, `perl -i`, `ed`/`ex`, `tee`, `>`/`>>` into files, heredocs, `truncate`/`dd` | `edit` (hashline) |
| Inline scripts that write files (`node -e "require('fs').writeFileSync…"`, `python3 -c "open(…,'w')"`, `ruby -e "File.write…"`, `awk` writes) | `edit` / `write` |
| `find -delete`, `find -fprint` | `edit` / file tools |
| `str_replace_editor`, `apply_patch` tools; `write` on an **existing** file | `edit` (`write` stays allowed for brand-new files) |
What stays allowed:
- **Reading files and searching via shell:** `cat`, `head`, `tail`, `less`, `more`, `bat`, `nl`, `sed -n`, `grep`, `rg`, `ag`, `find`, `ls -R`, read-only `awk` are all permitted. Their output is automatically paged to 200 lines by the `bash` wrapper.
- Text inside quotes is ignored (`git commit -m "fix > bug"`).
- Redirection to `/dev/null`, `/dev/stderr`, `/dev/stdout` is fine.
- Filters on piped output keep working (`npm test 2>&1 | tail -5`, `ps aux | grep node`).
- Computation-only one-liners: `node -e "console.log(1+1)"`, `node -p "2**10"`, `python3 -c "print(sum(range(9)))"`.
- Ruby/Perl: `File`, `IO`, `system`, `open`, backticks.
Banning the identifiers that start an I/O path covers obfuscation too: `fs['write'+'File']` still needs `require` or `import` to get `fs`. A program the guard can't read (stdin, `-`) is denied.
If the guard can't check whether a path exists, `write` is denied rather than allowed (it fails closed).
Turn the guard off with `guard: false`, or change the list of blocked tools with `guardTools`.
## Diff cards in the Web UI
The plugin ships a web client (`client.js`) that registers an `edit` tool view. Hashline, sloppy and apply_patch calls would otherwise show raw patch JSON; instead they render with the **same chrome as DSH's own tool rows**, built from DSH's primitives (`DisclosureRow`, `TextShimmer`, `Tooltip`, its icons) with CSS copied from the stock `ToolRow`, `CodeCard` and `DiffBlock` modules:
- the standard 24px row: edit icon, `Edit` title, a dot separator, the file path as a dotted link that opens the file, and a small monospace `+N -M` that turns green/red on hover or when expanded. Rows start collapsed and follow the turn's expand/collapse state, like every other tool;
- a failed edit shows its first error line in red, and expanding it shows the standard **Output** card;
- the expanded diff is the stock code card: a language header with the wrap and copy icon buttons, 22px-padded rows with the inset +/- bar, and the head/tail fold after 9 rows;
- one addition: a **line-number gutter** with the old and new file line of every row.
Colors come only from DSH theme tokens, so the view follows the active theme.
The host attaches `oldStart`, `newStart` and `rows` to every hunk in `meta.diffs`, next to the standard `path`/`oldText`/`newText` fields. Older results without them still render, just without numbers.
## Edit modes
`mode` picks the wire format of the replacement `edit` tool:
| Mode | Input | Notes |
| --- | --- | --- |
| `hashline` (default) | `input` | Described above |
| `replace` | `file_path`, `old_string`, `new_string`, `replace_all`, or `edits: [{old_string,new_string,replace_all}]` | Fuzzy whitespace matching: exact → indentation/trailing-space-insensitive → whitespace-collapsed → smart quotes/dashes → ≥95% similarity. The file's indentation is carried over. Batched edits are atomic |
| `sloppy` | `input` | `*** Edit File:` / `*** Find` / `*** Replace` · `*** Insert Before` · `*** Insert After`, `…` captures, ` all` suffix |
| `apply_patch` | `input` | Codex format: `*** Add/Delete/Update File`, `*** Move to`, `@@` scoped hunks, `*** End of File` |
**Per-model selection**, like oh-my-pi:
1. `modelVariants` (substring of `provider/model` → mode), first match wins.
2. `mode`.
3. With `hashline` and `strict: false`, model families oh-my-pi considers unreliable at hashline fall back to `fallbackMode` (default `replace`). The list is `fallbackModels`, default `kimi, mimo, minimax, deepseek, stepfun, codex-spark, glm-5.3-flash`.
The tool schema follows the model the agent actually uses and switches mid-session when you change models.
## How it replaces the built-ins
DSH registers `edit`/`read`/`grep` in each agent preset. Tools registered in an agent's own scope shadow those, so this plugin listens for `agent/created` and registers into every agent's scope:
- **`edit`** is replaced outright. Nothing of DSH's `edit` is used.
- **`read`** and **`grep`** delegate to DSH's implementations (sandbox, limits, ripgrep, spill files all unchanged) and only add tags to the output.
DSH's safety rules are kept:
- **Read first:** you still need to `read` (or `grep`) a file before editing it. This is `fs-observation-policy`.
- **Guarded writes:** writes go through `ctx.fs` with the version guard.
- **Sandbox:** the session's sandbox policy applies to writes and `REM`.
- **Line endings:** CRLF files stay CRLF.
To use DSH's original tools again, disable this plugin (Settings → Plugins → OMP Edit) or set `replaceEdit/replaceRead/replaceGrep: false`.
## Installation
```bash
dsh plugin --profile web add git+https://git.rizzy.dev/DSH-Plugins/omp-edit.git
```
Or in the Web UI, go to **Settings → Plugins → Install** and paste `git+https://git.rizzy.dev/DSH-Plugins/omp-edit.git`. You can also ask the agent to install it with `plugin_manager` → `install_bundle`.
For development:
```bash
git clone https://git.rizzy.dev/DSH-Plugins/omp-edit.git ~/dsh-plugins/omp-edit
dsh plugin --profile web add link:$HOME/dsh-plugins/omp-edit
```
Then restart DSH (`systemctl --user restart dsh` or re-run `dsh web`). New sessions get the new `edit` tool. Sessions that already exist pick it up once they're re-created, e.g. after the restart.
No dependencies: everything is plain Node.js (≥ 20).
## Configuration
Set options in the `config` of the row in `~/.dsh/profiles/web/cordis.patch.yml`:
```yaml
- id: omp-edit
config:
mode: hashline # hashline | replace | sloppy | apply_patch
strict: false # true: never fall back from hashline
fallbackMode: replace
fallbackModels: [kimi, mimo, minimax, deepseek, stepfun, codex-spark, glm-5.3-flash]
modelVariants: # substring of "provider/model" -> mode
gpt-5-codex: apply_patch
replaceEdit: true
replaceRead: true
replaceGrep: true
fuzzy: true # replace mode fuzzy matching
replaceGlob: true
pageLines: 200 # read/grep/glob page size
guard: true # deny shell edits/reads/searches and non-hashline write tools
guardTools: [write, str_replace_editor, apply_patch] # write: only existing files are denied
```
Outside `hashline` mode, `read` and `grep` return the plain built-in output with no
`[PATH#TAG]` headers. A grep hit still counts as an observation, so the file can be edited
straight away. For Claude Code–style tools, use `mode: replace`, `strict: true`, and
`guard: false`.
## Tests
```bash
npm test # node --test test/*.test.mjs — 34 tests (engine + integration on the real DSH fs-local backend)
```
## Files
| File | Role |
| --- | --- |
| `index.js` | DSH plugin: per-agent tool registration, mode selection, fs policy, paging, guard rule |
| `client.js` | Web client: `edit` tool view rendering hashline/patch results with DSH's native tool-row and diff-card chrome |
| `lib/hashline.js` | Hashline parser and applier |
| `lib/block.js` | `N*` block resolution (brackets, indentation, Markdown, decorators) |
| `lib/store.js` | Session snapshot store, Myers line diff, stale-tag rebase |
| `lib/replace.js` | Fuzzy `replace` mode |
| `lib/sloppy.js` | `sloppy` mode |
| `lib/apply_patch.js` | `apply_patch` mode |
| `lib/diff.js` | Hunk diffs for the UI diff card |
| `lib/prompts.js` | Model-facing tool descriptions (adapted from oh-my-pi's `crates/pi-edit/prompts`) |
Pairs well with [omp-lsp](https://git.rizzy.dev/DSH-Plugins/omp-lsp), which appends language-server diagnostics to every edit.