Skip to content

feat: replace native edit with hashline-powered custom tool + merge oh-my-opentools patterns#5

Open
aorizondo wants to merge 2 commits into
AngDrew:mainfrom
aorizondo:main
Open

feat: replace native edit with hashline-powered custom tool + merge oh-my-opentools patterns#5
aorizondo wants to merge 2 commits into
AngDrew:mainfrom
aorizondo:main

Conversation

@aorizondo
Copy link
Copy Markdown

Problem

The hashline plugin v1 annotated reads with stable #HL refs (e.g. 12#A3F#9BC) but the edit workflow was broken:

  1. translateHashlineEditArgs converted hashline operations to full-file oldString/newString, making edits fragile — any file change between read and edit caused failure.
  2. Missing .js extension in src/index.ts import prevented ESM loading.
  3. strict: false TypeScript config allowed implicit anys.
  4. rootDir: "." produced nested dist/src/index.js path.
  5. Source lived in .opencode/ — a hidden directory not suitable for npm distribution.

Solution: Hybrid Architecture

This PR merges the best patterns from opencode-hashline v1 (SHA1 hex hashing, anchor hash, read annotation, system prompt injection) with oh-my-opentools (direct file manipulation, edit tool registration, file canonicalization, strict TypeScript).

Core changes

Before (v1) After (v2)
Hooks translate hashline ops to full-file oldString/newString Custom tool.edit applies edits directly on line arrays
.opencode/ directory for plugin source All source in src/, compiled to dist/ with clean rootDir
strict: false, rootDir: "." strict: true, rootDir: ./src
Missing .js extension in entry import All imports use .js extension
No file canonicalization BOM stripping/restore + line ending normalization
No permission checks ctx.ask() integration
Edit fails silently on hash mismatch HashlineMismatchError with context diff + corrected refs

What stays the same

  • Read annotation via tool.execute.after hook
  • System prompt injection via experimental.chat.system.transform
  • Chat file annotation via chat.message hook
  • SHA1 hex hash format (3-4 chars + anchor)
  • opencode-hashline.json config (global + project)

Files changed

Deleted (12 files): .opencode/ directory moved to src/, tsconfig.build.json, src/codemap.md

Created (9 files): src/hash.ts, src/ref.ts, src/file-text.ts, src/edit-ops.ts, src/edit-executor.ts, src/edit-tool.ts, src/hooks.ts, src/shared.ts, tsconfig.json

Modified (4 files): src/index.ts, package.json, opencode.json, test suite (10 -> 33 tests)

Test results

33 tests passed

Comparison with PR #4

Aspect PR #4 (IshanArya) This PR
Dependencies Adds diff library Zero external deps
File canonicalization None BOM + CRLF
Edit API operations[] only operations[] + oldString/newString fallback
Permissions None (ctx.ask() omitted) Full ctx.ask() integration
Deduplication None Collapses identical replace edits
Source structure .opencode/tools/ src/ -> compiled to dist/
Mismatch errors Raw error text Context diff with markers + remaps
Tests ~10 33
TypeScript strict: false strict: true

Closes #3

aorizondo and others added 2 commits May 14, 2026 01:34
…h-my-opentools patterns

## Problem

The hashline plugin v1 annotated reads with stable #HL refs (e.g.
`12#A3F#9BC`) but the edit workflow was broken: `translateHashlineEditArgs`
converted hashline operations to full-file `oldString`/`newString`, making edits
fragile — any file change between read and edit caused failure. The edit hook
also had a missing `.js` extension in `src/index.ts` that prevented ESM loading.

## Solution

Replace the entire edit mechanism with a properly registered `tool.edit`
custom tool that:

1. **Registers via `Hooks.tool.edit`** — overrides the native edit tool
   through the standard SDK `tool()` API (zod schema + execute handler)

2. **Applies edits directly on file content** — reads the file, applies
   splice-based operations (replace, range replace, append, prepend) on the
   line array, and writes back. No fragile full-file `oldString`/`newString`
   translation needed.

3. **Supports both hashline AND legacy modes** — accepts `operations[]` with
   hashline refs for batch editing, plus `oldString`/`newString` fallback
   for simple text replacements.

4. **File text canonicalization** — handles BOM stripping/restore and CRLF/LF
   normalization before editing (ported from oh-my-opentools).

5. **HashlineMismatchError with context** — when refs become stale, shows a
   `>>>`-marked diff of the ±2 lines around each mismatch with corrected refs.

6. **ctx.ask() permissions** — calls `context.ask({ permission: 'edit',
   patterns: [filePath], always: ['*'] })` for proper OpenCode permission
   integration.

7. **SHA1 hex + anchor hash (3-4 chars)** — uses the more collision-resistant
   hash format from opencode-hashline v1 (4096-65536 values), NOT the 2-char
   CID format (256 values) from oh-my-opentools.

## Structural changes

- Deleted `.opencode/` directory — all source moves to `src/`
- Deleted `tsconfig.build.json` — replaced by single `tsconfig.json`
- Deleted `src/codemap.md` — obsolete, described tool registration that
  didn't exist
- New `tsconfig.json` — `strict: true`, `rootDir: ./src`, `sourceMap: true`
- `src/index.ts` — entry point registers `tool.edit` + 4 hooks (read
  annotation, system transform, chat annotation, tool definitions)
- `src/hash.ts` — SHA1 hex hashing + anchor hash + computeFileRev
- `src/ref.ts` — ref parsing, formatting, validation
- `src/file-text.ts` — BOM/CRLF canonicalization
- `src/edit-ops.ts` — operations engine: primitives (set/replace/insert/
  append/prepend) + orchestrator + dedup + overlap detection +
  HashlineMismatchError
- `src/edit-executor.ts` — file I/O, ref resolution, ctx.ask(), metadata
- `src/edit-tool.ts` — `tool()` definition with zod schema
- `src/hooks.ts` — `tool.execute.after` for read annotation,
  `experimental.chat.system.transform` for instruction injection,
  `chat.message` for file annotation, `tool.definition` for descriptions
- `src/shared.ts` — config loading, LRU cache, format/strip helpers,
  exclude patterns

## Tests

33 tests covering: hash functions, ref parsing/formatting, file-text
canonicalization, all edit operations (replace, range, append, prepend,
insert after/before, noop detection), mismatch errors with remaps,
format/strip round-trip, shouldExclude, cache, cache eviction, hooks
(read annotation, system transform, tool definitions), and full
integration tests of the edit executor with real file I/O.

## Comparison with PR AngDrew#4

This implementation differs from IshanArya's PR AngDrew#4 in several key ways:

- **Zero external dependencies**: no `diff` library needed
- **Canonicalization**: BOM + CRLF handling (PR AngDrew#4 lacks this)
- **Dual-mode API**: supports both `operations[]` and
  `oldString`/`newString` (PR AngDrew#4 only supports operations[])
- **ctx.ask()**: proper OpenCode permission integration (PR AngDrew#4 omits this)
- **Deduplication**: identical replace edits at the same position are
  collapsed (PR AngDrew#4 relies on core engine only)
- **Cleaner structure**: all source in `src/`, outputs to `dist/` with
  `rootDir: ./src` (PR AngDrew#4 keeps `.opencode/`)
- **33 tests vs ~10**: broader coverage of all edge cases

Closes AngDrew#3
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

How do I enable the edit tool to use this in OpenCode?

1 participant