diff --git a/packages/coding-agent/skills/edit/.gitignore b/packages/coding-agent/skills/edit/.gitignore new file mode 100644 index 000000000..560798033 --- /dev/null +++ b/packages/coding-agent/skills/edit/.gitignore @@ -0,0 +1,4 @@ +__pycache__/ +*.pyc +.venv/ +uv.lock diff --git a/packages/coding-agent/skills/edit/SKILL.md b/packages/coding-agent/skills/edit/SKILL.md new file mode 100644 index 000000000..bb7d1ee89 --- /dev/null +++ b/packages/coding-agent/skills/edit/SKILL.md @@ -0,0 +1,22 @@ +--- +name: edit +description: Replace an exact, unique string in an existing file. Use for targeted single-occurrence edits to files from the IPython kernel instead of rewriting the whole file. +--- + +# Edit + +Make a targeted edit to an existing file by replacing one exact, unique +occurrence of a string. `old_str` must appear exactly once in the file. + +Call directly from the kernel: + + await edit(path="pkg/file.py", old_str=old, new_str=new) + +Use exact old/new strings. If the text contains triple double quotes, use +triple single-quoted variables (`old = '''...'''`) or build `old`/`new` from +inspected file slices. Returns a short confirmation; raises if `old_str` is +missing or matches more than once (widen the snippet to make it unique). + +Or from a shell cell: + + !edit --path pkg/file.py --old-str "..." --new-str "..." diff --git a/packages/coding-agent/skills/edit/pyproject.toml b/packages/coding-agent/skills/edit/pyproject.toml new file mode 100644 index 000000000..151216540 --- /dev/null +++ b/packages/coding-agent/skills/edit/pyproject.toml @@ -0,0 +1,15 @@ +[project] +name = "edit" +version = "0.1.0" +requires-python = ">=3.10" +dependencies = [] + +[project.scripts] +edit = "rlm.skill:cli" + +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[tool.hatch.build.targets.wheel] +packages = ["src/edit"] diff --git a/packages/coding-agent/skills/edit/src/edit/__init__.py b/packages/coding-agent/skills/edit/src/edit/__init__.py new file mode 100644 index 000000000..63f8eae34 --- /dev/null +++ b/packages/coding-agent/skills/edit/src/edit/__init__.py @@ -0,0 +1,41 @@ +"""Exact single-occurrence string replacement for existing files.""" + +from __future__ import annotations + +from pathlib import Path + + +async def run(path: str, old_str: str, new_str: str) -> str: + """Replace a unique string in a file. + + ``old_str`` must appear exactly once in the file at ``path``; that match is + replaced with ``new_str`` and the file is written back in place. Prefer this + over rewriting a whole file for targeted edits. + + Args: + path: File to edit, relative to the working directory, absolute, or + `~`-prefixed (the leading `~`/`~user` is expanded to the home dir). + old_str: Exact text to find. Must occur exactly once in the file. + new_str: Replacement text. + + Returns: + A short confirmation message. + + Raises: + FileNotFoundError: If ``path`` does not exist. + ValueError: If ``old_str`` is absent or matches more than once. + """ + filepath = Path(path).expanduser() + if not filepath.exists(): + raise FileNotFoundError(f"{path} not found") + content = filepath.read_text(encoding="utf-8") + count = content.count(old_str) + if count == 0: + raise ValueError(f"string not found in {path}") + if count > 1: + raise ValueError( + f"found {count} occurrences in {path}, need exactly 1 — " + "widen the snippet to make it unique" + ) + filepath.write_text(content.replace(old_str, new_str, 1), encoding="utf-8") + return f"Edited {path}" diff --git a/packages/coding-agent/test/builtin-skills.test.ts b/packages/coding-agent/test/builtin-skills.test.ts index 35072059e..ec2a023f5 100644 --- a/packages/coding-agent/test/builtin-skills.test.ts +++ b/packages/coding-agent/test/builtin-skills.test.ts @@ -163,6 +163,15 @@ describe("builtin skills", () => { expect(skills.map((s) => s.name)).toContain("skill-creator"); }); + it("ships the edit skill as a python skill importable as `edit`", () => { + const { skills } = loadSkillsFromDir({ dir: getBundledSkillsDir(), source: "builtin" }); + + const edit = skills.find((s) => s.name === "edit"); + expect(edit).toBeDefined(); + expect(edit?.kind).toBe("python"); + expect(edit?.kind === "python" && edit.python.importName).toBe("edit"); + }); + it("loads the skill-creator python template as a valid python skill", () => { const referencePath = join(getBundledSkillsDir(), "skill-creator", "references", "python-skills.md"); const reference = readFileSync(referencePath, "utf-8");