diff --git a/.DS_Store b/.DS_Store index 5d853a8..0ff8ba6 100644 Binary files a/.DS_Store and b/.DS_Store differ diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..e69de29 diff --git a/.opencode/package-lock.json b/.opencode/package-lock.json index abded7b..43b0963 100644 --- a/.opencode/package-lock.json +++ b/.opencode/package-lock.json @@ -5,40 +5,130 @@ "packages": { "": { "dependencies": { - "@opencode-ai/plugin": "1.4.3" + "@opencode-ai/plugin": "1.14.48", + "diff": "^7.0.0" } }, + "node_modules/@msgpackr-extract/msgpackr-extract-darwin-arm64": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-darwin-arm64/-/msgpackr-extract-darwin-arm64-3.0.3.tgz", + "integrity": "sha512-QZHtlVgbAdy2zAqNA9Gu1UpIuI8Xvsd1v8ic6B2pZmeFnFcMWiPLfWXh7TVw4eGEZ/C9TH281KwhVoeQUKbyjw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@msgpackr-extract/msgpackr-extract-darwin-x64": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-darwin-x64/-/msgpackr-extract-darwin-x64-3.0.3.tgz", + "integrity": "sha512-mdzd3AVzYKuUmiWOQ8GNhl64/IoFGol569zNRdkLReh6LRLHOXxU4U8eq0JwaD8iFHdVGqSy4IjFL4reoWCDFw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@msgpackr-extract/msgpackr-extract-linux-arm": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-arm/-/msgpackr-extract-linux-arm-3.0.3.tgz", + "integrity": "sha512-fg0uy/dG/nZEXfYilKoRe7yALaNmHoYeIoJuJ7KJ+YyU2bvY8vPv27f7UKhGRpY6euFYqEVhxCFZgAUNQBM3nw==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@msgpackr-extract/msgpackr-extract-linux-arm64": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-arm64/-/msgpackr-extract-linux-arm64-3.0.3.tgz", + "integrity": "sha512-YxQL+ax0XqBJDZiKimS2XQaf+2wDGVa1enVRGzEvLLVFeqa5kx2bWbtcSXgsxjQB7nRqqIGFIcLteF/sHeVtQg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@msgpackr-extract/msgpackr-extract-linux-x64": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-x64/-/msgpackr-extract-linux-x64-3.0.3.tgz", + "integrity": "sha512-cvwNfbP07pKUfq1uH+S6KJ7dT9K8WOE4ZiAcsrSes+UY55E/0jLYc+vq+DO7jlmqRb5zAggExKm0H7O/CBaesg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@msgpackr-extract/msgpackr-extract-win32-x64": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-win32-x64/-/msgpackr-extract-win32-x64-3.0.3.tgz", + "integrity": "sha512-x0fWaQtYp4E6sktbsdAqnehxDgEc/VwM7uLsRCYWaiGu0ykYdZPiS8zCWdnjHwyiumousxfBm4SO31eXqwEZhQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, "node_modules/@opencode-ai/plugin": { - "version": "1.4.3", - "resolved": "https://registry.npmjs.org/@opencode-ai/plugin/-/plugin-1.4.3.tgz", - "integrity": "sha512-Ob/3tVSIeuMRJBr2O23RtrnC5djRe01Lglx+TwGEmjrH9yDBJ2tftegYLnNEjRoMuzITgq9LD8168p4pzv+U/A==", + "version": "1.14.48", + "resolved": "https://registry.npmjs.org/@opencode-ai/plugin/-/plugin-1.14.48.tgz", + "integrity": "sha512-pb2ywByzn4i35WWJquEYyb8lDC/ph1PLXT+heucJN6Y9U/oeSw98JQV93IG7M6BUBks6MKD3DGDJdQfyD6x0rA==", "license": "MIT", "dependencies": { - "@opencode-ai/sdk": "1.4.3", + "@opencode-ai/sdk": "1.14.48", + "effect": "4.0.0-beta.59", "zod": "4.1.8" }, "peerDependencies": { - "@opentui/core": ">=0.1.97", - "@opentui/solid": ">=0.1.97" + "@opentui/core": ">=0.2.6", + "@opentui/keymap": ">=0.2.6", + "@opentui/solid": ">=0.2.6" }, "peerDependenciesMeta": { "@opentui/core": { "optional": true }, + "@opentui/keymap": { + "optional": true + }, "@opentui/solid": { "optional": true } } }, "node_modules/@opencode-ai/sdk": { - "version": "1.4.3", - "resolved": "https://registry.npmjs.org/@opencode-ai/sdk/-/sdk-1.4.3.tgz", - "integrity": "sha512-X0CAVbwoGAjTY2iecpWkx2B+GAa2jSaQKYpJ+xILopeF/OGKZUN15mjqci+L7cEuwLHV5wk3x2TStUOVCa5p0A==", + "version": "1.14.48", + "resolved": "https://registry.npmjs.org/@opencode-ai/sdk/-/sdk-1.14.48.tgz", + "integrity": "sha512-wKM86jCzV/ZApyWrdm3uP8XdWcS0LMbu3FV+OWz1ChiGGg1wiIWNGMJs5CY8/QX2/rUuZrd1Q1DqvdamZ0zLeg==", "license": "MIT", "dependencies": { "cross-spawn": "7.0.6" } }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "license": "MIT" + }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -53,12 +143,144 @@ "node": ">= 8" } }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "license": "Apache-2.0", + "optional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/diff": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-7.0.0.tgz", + "integrity": "sha512-PJWHUb1RFevKCwaFA9RlG5tCd+FO5iRh9A8HEtkmBH2Li03iJriB6m6JIN4rGz3K3JLawI7/veA1xzRKP6ISBw==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/effect": { + "version": "4.0.0-beta.59", + "resolved": "https://registry.npmjs.org/effect/-/effect-4.0.0-beta.59.tgz", + "integrity": "sha512-xyUDLeHSe8d6lWGOvR6Fgn2HL6gYeTZ/S4Jzk9uc4ZUxMPPsNZlNXrvk0C7/utQFzeX7uAWcVnG2BjbA0SRoAA==", + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.1.0", + "fast-check": "^4.6.0", + "find-my-way-ts": "^0.1.6", + "ini": "^6.0.0", + "kubernetes-types": "^1.30.0", + "msgpackr": "^1.11.9", + "multipasta": "^0.2.7", + "toml": "^4.1.1", + "uuid": "^13.0.0", + "yaml": "^2.8.3" + } + }, + "node_modules/fast-check": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/fast-check/-/fast-check-4.7.0.tgz", + "integrity": "sha512-NsZRtqvSSoCP0HbNjUD+r1JH8zqZalyp6gLY9e7OYs7NK9b6AHOs2baBFeBG7bVNsuoukh89x2Yg3rPsul8ziQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ], + "license": "MIT", + "dependencies": { + "pure-rand": "^8.0.0" + }, + "engines": { + "node": ">=12.17.0" + } + }, + "node_modules/find-my-way-ts": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/find-my-way-ts/-/find-my-way-ts-0.1.6.tgz", + "integrity": "sha512-a85L9ZoXtNAey3Y6Z+eBWW658kO/MwR7zIafkIUPUMf3isZG0NCs2pjW2wtjxAKuJPxMAsHUIP4ZPGv0o5gyTA==", + "license": "MIT" + }, + "node_modules/ini": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/ini/-/ini-6.0.0.tgz", + "integrity": "sha512-IBTdIkzZNOpqm7q3dRqJvMaldXjDHWkEDfrwGEQTs5eaQMWV+djAhR+wahyNNMAa+qpbDUhBMVt4ZKNwpPm7xQ==", + "license": "ISC", + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", "license": "ISC" }, + "node_modules/kubernetes-types": { + "version": "1.30.0", + "resolved": "https://registry.npmjs.org/kubernetes-types/-/kubernetes-types-1.30.0.tgz", + "integrity": "sha512-Dew1okvhM/SQcIa2rcgujNndZwU8VnSapDgdxlYoB84ZlpAD43U6KLAFqYo17ykSFGHNPrg0qry0bP+GJd9v7Q==", + "license": "Apache-2.0" + }, + "node_modules/msgpackr": { + "version": "1.11.12", + "resolved": "https://registry.npmjs.org/msgpackr/-/msgpackr-1.11.12.tgz", + "integrity": "sha512-RBdJ1Un7yGlXWajrkxcSa93nvQ0w4zBf60c0yYv7YtBelP8H2FA7XsfBbMHtXKXUMUxH7zV3Zuozh+kUQWhHvg==", + "license": "MIT", + "optionalDependencies": { + "msgpackr-extract": "^3.0.2" + } + }, + "node_modules/msgpackr-extract": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/msgpackr-extract/-/msgpackr-extract-3.0.3.tgz", + "integrity": "sha512-P0efT1C9jIdVRefqjzOQ9Xml57zpOXnIuS+csaB4MdZbTdmGDLo8XhzBG1N7aO11gKDDkJvBLULeFTo46wwreA==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "dependencies": { + "node-gyp-build-optional-packages": "5.2.2" + }, + "bin": { + "download-msgpackr-prebuilds": "bin/download-prebuilds.js" + }, + "optionalDependencies": { + "@msgpackr-extract/msgpackr-extract-darwin-arm64": "3.0.3", + "@msgpackr-extract/msgpackr-extract-darwin-x64": "3.0.3", + "@msgpackr-extract/msgpackr-extract-linux-arm": "3.0.3", + "@msgpackr-extract/msgpackr-extract-linux-arm64": "3.0.3", + "@msgpackr-extract/msgpackr-extract-linux-x64": "3.0.3", + "@msgpackr-extract/msgpackr-extract-win32-x64": "3.0.3" + } + }, + "node_modules/multipasta": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/multipasta/-/multipasta-0.2.7.tgz", + "integrity": "sha512-KPA58d68KgGil15oDqXjkUBEBYc00XvbPj5/X+dyzeo/lWm9Nc25pQRlf1D+gv4OpK7NM0J1odrbu9JNNGvynA==", + "license": "MIT" + }, + "node_modules/node-gyp-build-optional-packages": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/node-gyp-build-optional-packages/-/node-gyp-build-optional-packages-5.2.2.tgz", + "integrity": "sha512-s+w+rBWnpTMwSFbaE0UXsRlg7hU4FjekKU4eyAih5T8nJuNZT1nNsskXpxmeqSK9UzkBl6UgRlnKc8hz8IEqOw==", + "license": "MIT", + "optional": true, + "dependencies": { + "detect-libc": "^2.0.1" + }, + "bin": { + "node-gyp-build-optional-packages": "bin.js", + "node-gyp-build-optional-packages-optional": "optional.js", + "node-gyp-build-optional-packages-test": "build-test.js" + } + }, "node_modules/path-key": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", @@ -68,6 +290,22 @@ "node": ">=8" } }, + "node_modules/pure-rand": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-8.4.0.tgz", + "integrity": "sha512-IoM8YF/jY0hiugFo/wOWqfmarlE6J0wc6fDK1PhftMk7MGhVZl88sZimmqBBFomLOCSmcCCpsfj7wXASCpvK9A==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ], + "license": "MIT" + }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -89,6 +327,28 @@ "node": ">=8" } }, + "node_modules/toml": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/toml/-/toml-4.1.1.tgz", + "integrity": "sha512-EBJnVBr3dTXdA89WVFoAIPUqkBjxPMwRqsfuo1r240tKFHXv3zgca4+NJib/h6TyvGF7vOawz0jGuryJCdNHrw==", + "license": "MIT", + "engines": { + "node": ">=20" + } + }, + "node_modules/uuid": { + "version": "13.0.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-13.0.2.tgz", + "integrity": "sha512-vzi9uRZ926x4XV73S/4qQaTwPXM2JBj6/6lI/byHH1jOpCzb0zDbfytgA9LcN/hzb2l7WQSQnxITOVx5un/wGw==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist-node/bin/uuid" + } + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -104,6 +364,21 @@ "node": ">= 8" } }, + "node_modules/yaml": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.9.0.tgz", + "integrity": "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==", + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + }, + "funding": { + "url": "https://github.com/sponsors/eemeli" + } + }, "node_modules/zod": { "version": "4.1.8", "license": "MIT", diff --git a/.opencode/plugins/hashline-hooks.ts b/.opencode/plugins/hashline-hooks.ts index 8ec5243..cdc37f6 100644 --- a/.opencode/plugins/hashline-hooks.ts +++ b/.opencode/plugins/hashline-hooks.ts @@ -5,11 +5,8 @@ import { tmpdir } from "node:os" import { fileURLToPath, pathToFileURL } from "node:url" import type { Hooks } from "@opencode-ai/plugin" import { - mapOperationInput, resolveFilePath, runHashlineRead, - runHashlineOperationsDetailed, - type HashlineOperationInput, } from "../lib/hashline-core.js" import { buildCacheEntryKey, @@ -40,10 +37,6 @@ function isFileEditTool(tool: string): boolean { return toolEndsWith(tool, FILE_EDIT_TOOLS) } -function isNativeEditTool(tool: string): boolean { - return toolEndsWith(tool, ["edit"]) -} - const HASHLINE_SYSTEM_INSTRUCTION_MARKER_RE = //i const HASHLINE_SYSTEM_INSTRUCTION_BLOCK_RE = /[\s\S]*?(?:|$)/gi const MAX_SYSTEM_ENTRIES = 128 @@ -119,113 +112,6 @@ function invalidateFileCache( cache.invalidateVariants(canonicalPath) } -function firstString(...values: unknown[]): string | undefined { - for (const value of values) { - if (typeof value === "string" && value.length > 0) { - return value - } - } - - return undefined -} - -function firstBoolean(...values: unknown[]): boolean | undefined { - for (const value of values) { - if (typeof value === "boolean") { - return value - } - } - - return undefined -} - -function hasHashlineEditShape(args: Record): boolean { - return ( - Array.isArray(args.operations) || - typeof args.operation === "string" || - typeof args.ref === "string" || - typeof args.startRef === "string" || - typeof args.start_ref === "string" - ) -} - -function toHashlineOperations(args: Record): HashlineOperationInput[] | null { - if (Array.isArray(args.operations) && args.operations.length > 0) { - return args.operations.map((entry) => { - const item = (entry ?? {}) as Record - return { - op: String(item.op ?? "") as HashlineOperationInput["op"], - ref: firstString(item.ref), - startRef: firstString(item.startRef, item.start_ref), - endRef: firstString(item.endRef, item.end_ref), - content: firstString(item.content, item.replacement), - } - }) - } - - const operation = firstString(args.operation) - if (!operation) { - return null - } - - const ref = firstString(args.ref) - const startRef = firstString(args.startRef, args.start_ref, ref) - const endRef = firstString(args.endRef, args.end_ref) - const content = firstString(args.replacement, args.content) - - if (!startRef && !ref) { - return null - } - - return [ - { - op: operation === "replace" && endRef ? "replace_range" : (operation as HashlineOperationInput["op"]), - ref, - startRef, - endRef, - content, - }, - ] -} - -async function translateHashlineEditArgs( - args: Record, - input: Record, - config: HashlineRuntimeConfig, -): Promise | null> { - if (!hasHashlineEditShape(args)) { - return null - } - - const filePath = firstString(args.filePath, args.file_path, args.path, args.file) - if (!filePath) { - return null - } - - const operations = toHashlineOperations(args) - if (!operations || operations.length === 0) { - return null - } - - const result = await runHashlineOperationsDetailed({ - filePath, - operations: operations.map(mapOperationInput), - expectedFileHash: firstString(args.expectedFileHash, args.expected_file_hash), - fileRev: firstString(args.fileRev, args.file_rev), - safeReapply: firstBoolean(args.safeReapply, args.safe_reapply) ?? config.safeReapply, - dryRun: true, - context: { - directory: typeof input.directory === "string" ? input.directory : undefined, - }, - }) - - return { - filePath, - oldString: result.metadata.filediff.before, - newString: result.metadata.filediff.after, - } -} - const CONTENT_FIELD_KEYS = new Set([ "content", "new_content", @@ -388,10 +274,6 @@ export function createHashlineHooks(config: HashlineRuntimeConfig, cache?: Hashl output.description = `${output.description}\n\nHashline: Returns canonical ${DEFAULT_PREFIX} refs plus a REV token. Copy refs exactly from the output, then plan all same-file changes before calling edit.` } - if (input.toolID === "edit") { - output.description = `${output.description}\n\nHashline: Accepts refs copied from read. Prefer one batched call per file with { filePath, fileRev?, operations:[{ op, ref|startRef/endRef, content? }] } instead of many single edits.` - } - if (input.toolID === "write") { output.description = `${output.description}\n\nHashline: Use write for new files or full rewrites. Prefer edit for targeted existing-file changes; hashline prefixes inside content are stripped automatically.` } @@ -409,21 +291,7 @@ export function createHashlineHooks(config: HashlineRuntimeConfig, cache?: Hashl } const args = (output.args ?? {}) as Record - const sanitizedArgs = stripNestedHashes(args, config.prefix) as Record - - if (isNativeEditTool(name)) { - const translatedArgs = await translateHashlineEditArgs( - sanitizedArgs, - input as Record, - config, - ) - if (translatedArgs) { - output.args = translatedArgs - return - } - } - - output.args = sanitizedArgs + output.args = stripNestedHashes(args, config.prefix) as Record }, "tool.execute.after": async (input, output) => { diff --git a/.opencode/plugins/hashline-routing.ts b/.opencode/plugins/hashline-routing.ts index 3166207..7eaa1bc 100644 --- a/.opencode/plugins/hashline-routing.ts +++ b/.opencode/plugins/hashline-routing.ts @@ -9,7 +9,7 @@ import { HashlineAnnotationCache, resolveHashlineConfig } from "./hashline-share * hashline-hooks.ts, which handles the heavy lifting against the core runtime. */ -const known = new Set(["read", "view", "edit", "patch", "write"]) +const known = new Set(["read", "view", "patch", "write"]) function normalizeName(name: string): string { return name === "view" ? "read" : name @@ -28,16 +28,6 @@ function normalizeArgs(toolName: string, args: Record): Record< } } - if (toolName === "edit") { - if (typeof out.file_path === "string" && typeof out.filePath !== "string") out.filePath = out.file_path - if (typeof out.start_ref === "string" && typeof out.startRef !== "string") out.startRef = out.start_ref - if (typeof out.end_ref === "string" && typeof out.endRef !== "string") out.endRef = out.end_ref - if (typeof out.safe_reapply === "boolean" && typeof out.safeReapply !== "boolean") out.safeReapply = out.safe_reapply - if (typeof out.expected_file_hash === "string" && typeof out.expectedFileHash !== "string") out.expectedFileHash = out.expected_file_hash - if (typeof out.file_rev === "string" && typeof out.fileRev !== "string") out.fileRev = out.file_rev - if (typeof out.dry_run === "boolean" && typeof out.dryRun !== "boolean") out.dryRun = out.dry_run - } - if (toolName === "patch") { if (typeof out.patch_text === "string" && typeof out.patchText !== "string") out.patchText = out.patch_text if (typeof out.file_path === "string" && typeof out.filePath !== "string") out.filePath = out.file_path diff --git a/.opencode/tests/hashline-hooks.test.ts b/.opencode/tests/hashline-hooks.test.ts index 77c2d21..b861801 100644 --- a/.opencode/tests/hashline-hooks.test.ts +++ b/.opencode/tests/hashline-hooks.test.ts @@ -131,3 +131,93 @@ test("tool descriptions nudge agents toward the efficient hashline workflow", as assert.match(writeOutput.description, /Use write for new files or full rewrites/i) assert.match(writeOutput.description, /Prefer edit for targeted existing-file changes/i) }) + +test("edit tool schema is extended with hashline fields", async () => { + const hooks = makeHooks() + const definition = hooks["tool.definition"] + + if (!definition) { + throw new Error("Missing tool definition hook") + } + + const editOutput = { + description: "native edit", + parameters: { + type: "object", + properties: { + filePath: { type: "string" }, + oldString: { type: "string" }, + newString: { type: "string" }, + replaceAll: { type: "boolean" }, + }, + required: ["filePath", "oldString", "newString"], + }, + } + + await definition({ toolID: "edit" } as any, editOutput as any) + + const props = editOutput.parameters.properties as Record + + // Hashline fields must be declared + assert.ok(props.operations, "operations property must exist") + assert.equal(props.operations.type, "array") + assert.ok(props.operations.items, "operations.items must exist") + assert.ok(props.operations.items.properties.op, "op property must exist in operation items") + assert.ok(props.operations.items.properties.ref, "ref property must exist in operation items") + assert.ok(props.operations.items.properties.startRef, "startRef property must exist in operation items") + assert.ok(props.operations.items.properties.endRef, "endRef property must exist in operation items") + assert.ok(props.operations.items.properties.content, "content property must exist in operation items") + + assert.ok(props.fileRev, "fileRev property must exist") + assert.equal(props.fileRev.type, "string") + + assert.ok(props.safeReapply, "safeReapply property must exist") + assert.equal(props.safeReapply.type, "boolean") + + // Native fields must still be present + assert.ok(props.filePath, "filePath must still exist") + assert.ok(props.oldString, "oldString must still exist") + assert.ok(props.newString, "newString must still exist") + assert.ok(props.replaceAll, "replaceAll must still exist") + + // required must no longer include oldString/newString (they conflict with hashline-style args) + const required = editOutput.parameters.required as string[] + assert.ok(required.includes("filePath"), "filePath must remain required") + assert.ok(!required.includes("oldString"), "oldString must not be required") + assert.ok(!required.includes("newString"), "newString must not be required") +}) + +test("edit tool schema extension is safe when parameters is empty", async () => { + const hooks = makeHooks() + const definition = hooks["tool.definition"] + + if (!definition) { + throw new Error("Missing tool definition hook") + } + + // Simulate an empty parameters object (no properties or required) + const editOutput = { description: "native edit", parameters: {} } + await definition({ toolID: "edit" } as any, editOutput as any) + + const params = editOutput.parameters as Record + assert.ok(params.properties, "properties must be created") + assert.ok(params.properties.operations, "operations must be added even to empty params") + assert.ok(params.properties.fileRev, "fileRev must be added even to empty params") + assert.ok(params.properties.safeReapply, "safeReapply must be added even to empty params") +}) + +test("non-edit tools do not get schema modifications", async () => { + const hooks = makeHooks() + const definition = hooks["tool.definition"] + + if (!definition) { + throw new Error("Missing tool definition hook") + } + + const readOutput = { description: "native read", parameters: { properties: {}, required: [] } } + await definition({ toolID: "read" } as any, readOutput as any) + + const readProps = readOutput.parameters.properties as Record + assert.equal(readProps.operations, undefined, "read tool should not get operations property") + assert.equal(readProps.fileRev, undefined, "read tool should not get fileRev property") +}) diff --git a/.opencode/tools/edit.ts b/.opencode/tools/edit.ts new file mode 100644 index 0000000..49ed50c --- /dev/null +++ b/.opencode/tools/edit.ts @@ -0,0 +1,127 @@ +import { tool } from "@opencode-ai/plugin" +import { createTwoFilesPatch } from "diff" +import { + runHashlineOperationsDetailed, + mapOperationInput, + type HashlineOperationInput, + type HashlineOpName, +} from "../lib/hashline-core.js" +import { resolveHashlineConfig, stripHashlinePrefixes } from "../plugins/hashline-shared.js" + +const EDIT_DESCRIPTION = `Edit files using hash-anchored refs from read output. + +WORKFLOW: +1. Read target file to get LINE#HASH#ANCHOR refs and REV token. +2. Copy refs exactly as shown. NEVER guess or fabricate refs. +3. Submit one edit call per file with all related operations. +4. Re-read after each successful edit before editing the same file again. + +RULES: +- All operations in one call reference the ORIGINAL file state (pre-edit). The system applies them bottom-up automatically -- do NOT adjust refs for prior operations. +- replace removes the line at ref and inserts content in its place. Lines before and after ref are UNTOUCHED. +- replace_range removes lines startRef..endRef (inclusive) and inserts content. Content must contain ONLY what belongs inside the consumed range. +- Batch related changes as multiple operations in one call, not one large replace. +- content must be plain text only (no LINE#HASH refs, no diff markers). + +OPERATIONS: + LINE#HASH#ANCHOR format: "{line}#{hash}#{anchor}" copied from read output (e.g. "12#A3F#9BC"). + + replace + ref -> replace single line at ref + replace_range + startRef/endRef -> replace range inclusive + delete + ref -> delete single line + insert_before + ref -> insert content before ref + insert_after + ref -> insert content after ref + +EXAMPLES (given read output with refs 10#B33#19A, 11#F73#8C6, 12#A18#EA7): + Single replace: { op: "replace", ref: "11#F73#8C6", content: " new line content" } + Range replace: { op: "replace_range", startRef: "11#F73#8C6", endRef: "12#A18#EA7", content: " single replacement" } + Delete line: { op: "delete", ref: "12#A18#EA7" } + Insert after: { op: "insert_after", ref: "10#B33#19A", content: " inserted line" } + +RECOVERY: If a hash mismatch error occurs, re-read the file to get fresh refs.` + +export default tool({ + description: EDIT_DESCRIPTION, + args: { + filePath: tool.schema.string().describe("Absolute path to the file to edit"), + fileRev: tool.schema + .string() + .optional() + .describe("REV token from read output (8-char hex, e.g. A2DF5291)"), + operations: tool.schema + .array( + tool.schema.object({ + op: tool.schema + .enum(["replace", "delete", "insert_before", "insert_after", "replace_range"]) + .describe("Operation type"), + ref: tool.schema + .string() + .optional() + .describe("Single-line ref from read output (e.g. 12#A3F#9BC)"), + startRef: tool.schema + .string() + .optional() + .describe("Start ref for replace_range"), + endRef: tool.schema + .string() + .optional() + .describe("End ref for replace_range"), + content: tool.schema + .string() + .optional() + .describe("Replacement or inserted text. Omit for delete."), + }), + ) + .describe("Array of edit operations to apply"), + }, + execute: async (args, context) => { + const projectDirectory = context.directory + const config = resolveHashlineConfig(projectDirectory) + + if (!args.operations || args.operations.length === 0) { + return "Error: operations must be a non-empty array" + } + + // Strip any hashline prefixes the LLM may have accidentally included in content + const sanitizedOps: HashlineOperationInput[] = args.operations.map((op) => ({ + op: op.op as HashlineOpName, + ref: op.ref, + startRef: op.startRef, + endRef: op.endRef, + content: op.content ? stripHashlinePrefixes(op.content, config.prefix) : op.content, + })) + + try { + const result = await runHashlineOperationsDetailed({ + filePath: args.filePath, + operations: sanitizedOps.map(mapOperationInput), + fileRev: args.fileRev, + safeReapply: config.safeReapply, + dryRun: false, + context: { + directory: projectDirectory, + }, + }) + + const { file, before, after, additions, deletions } = result.metadata.filediff + + if (before === after) { + return "Error: No changes made. The edits produced identical content. Re-read the file and provide content that differs from the current lines." + } + + const patch = createTwoFilesPatch(file, file, before, after) + + return { + output: `Updated ${args.filePath} (+${additions} -${deletions})`, + metadata: { + diff: patch, + filediff: { file, patch, additions, deletions }, + diagnostics: {}, + }, + } + } catch (error) { + const message = error instanceof Error ? error.message : String(error) + return `Error: ${message}\nTip: re-read the file to get fresh refs and fileRev.` + } + }, +}) diff --git a/.opencode/tools/resolve-hash-edit.ts b/.opencode/tools/resolve-hash-edit.ts deleted file mode 100644 index 44f7720..0000000 --- a/.opencode/tools/resolve-hash-edit.ts +++ /dev/null @@ -1,73 +0,0 @@ -import { tool } from "@opencode-ai/plugin" -import { resolveFilePath, resolveHashlineEdit, type HashlineOperationInput, type HashlineOpName, mapOperationInput } from "../lib/hashline-core.js" -import { resolveHashlineConfig } from "../plugins/hashline-shared.js" - -export interface HashlineResolveEditArgs { - filePath: string - operations: HashlineOperationInput[] - fileRev?: string - safeReapply?: boolean -} - -export const hashlineResolveEditTool = tool({ - description: - "Resolves hashline line references to native edit format. Use after read() to convert hashline operations to oldString/newString for native edit(). Returns JSON with filePath, oldString, newString, and fileRev.", - args: { - filePath: tool.schema.string().describe("Path to the file to edit"), - operations: tool.schema - .array( - tool.schema.object({ - op: tool.schema.enum(["replace", "delete", "insert_before", "insert_after", "replace_range"]), - ref: tool.schema.string().optional(), - startRef: tool.schema.string().optional(), - endRef: tool.schema.string().optional(), - content: tool.schema.string().optional(), - }), - ) - .describe("Array of hashline operations with refs"), - fileRev: tool.schema.string().optional().describe("Optional file revision from read output (e.g., 1A2B3C4D)"), - safeReapply: tool.schema.boolean().optional().describe("Allow relocating refs if hash matches but line number changed"), - }, - execute: async (args, context) => { - const projectDirectory = context.directory - const config = resolveHashlineConfig(projectDirectory) - - const absolutePath = resolveFilePath(args.filePath) - - try { - const result = await resolveHashlineEdit({ - filePath: args.filePath, - operations: args.operations.map((op) => mapOperationInput({ ...op, op: op.op as HashlineOpName })), - fileRev: args.fileRev, - safeReapply: args.safeReapply ?? config.safeReapply, - dryRun: true, - context: { - directory: projectDirectory, - }, - }) - - return JSON.stringify( - { - filePath: result.filePath, - oldString: result.oldString, - newString: result.newString, - fileRev: result.fileRev, - summary: result.summary, - }, - null, - 2, - ) - } catch (error) { - const message = error instanceof Error ? error.message : String(error) - return JSON.stringify( - { - error: message, - filePath: absolutePath, - hint: "Read the file again to get fresh refs and fileRev", - }, - null, - 2, - ) - } - }, -}) diff --git a/specs/hashline-plugin-schema-gap.md b/specs/hashline-plugin-schema-gap.md new file mode 100644 index 0000000..f909399 --- /dev/null +++ b/specs/hashline-plugin-schema-gap.md @@ -0,0 +1,49 @@ +# Hashline Plugin Schema Gap + +The `@angdrew/opencode-hashline-plugin` adds stable line-referenced editing to OpenCode. It works by hooking into the native `read`/`edit` tool pipeline: reads are annotated with `#HL` refs, and edit calls containing hashline args are translated into native `oldString`/`newString` before execution. + +## Problem + +The plugin's `tool.definition` hook only appends a text hint to the Edit tool's description. It does not modify the tool's JSON Schema (the `parameters` object). The declared schema still only accepts `filePath`, `oldString`, `newString`, and `replaceAll`. + +The hashline edit args (`operations`, `startRef`, `endRef`, `fileRev`, `safeReapply`, etc.) are undeclared. If OpenCode validates tool call arguments against the JSON Schema before the `tool.execute.before` hook runs, calls using hashline-style args are rejected and the translation hook never fires. + +### Relevant code + +In `.opencode/plugins/hashline-hooks.ts`, the `tool.definition` hook: + +```typescript +"tool.definition": async (input, output) => { + if (input.toolID === "edit") { + output.description = `${output.description}\n\nHashline: Accepts refs copied from read...` + } +} +``` + +Only `output.description` is modified. `output.parameters` (or equivalent schema field) is never touched. + +The `tool.execute.before` hook does the actual translation: + +```typescript +"tool.execute.before": async (input, output) => { + if (isNativeEditTool(name)) { + const translatedArgs = await translateHashlineEditArgs(sanitizedArgs, input, config) + if (translatedArgs) { + output.args = translatedArgs // { filePath, oldString, newString } + return + } + } +} +``` + +This converts hashline args into native args, but only runs after schema validation has already passed. + +### Result + +- Read annotations work (the `tool.execute.after` hook transforms output, no schema issue). +- Edit translation is effectively unreachable when OpenCode enforces strict schema validation on tool inputs. +- The LLM sees the description hint but cannot act on it because the schema rejects the args. + +## Fix + +The `tool.definition` hook should extend the Edit tool's parameter schema to include the hashline fields (`operations`, `operation`, `startRef`, `endRef`, `ref`, `fileRev`, `expectedFileHash`, `safeReapply`, `replacement`, `content`) and relax `required` so that either `oldString`/`newString` or hashline-style args are accepted. This would let the args pass schema validation and reach the `tool.execute.before` translation hook. diff --git a/specs/pr-replace-edit-tool.md b/specs/pr-replace-edit-tool.md new file mode 100644 index 0000000..f3596a1 --- /dev/null +++ b/specs/pr-replace-edit-tool.md @@ -0,0 +1,64 @@ +# Replace built-in edit tool with custom hashline edit tool + +## Problem + +The hashline plugin annotates Read tool output with `#HL` line refs (e.g. `12#A3F#9BC`) and `REV` tokens so the LLM can make precise, hash-verified edits. Previously, we tried to extend the native Edit tool's JSON Schema at runtime via the `tool.definition` hook, adding `operations`/`fileRev`/`safeReapply` fields alongside the existing `oldString`/`newString`. + +This approach failed. Session export inspection confirmed that **every edit call used `oldString`/`newString`** -- the schema extension either didn't propagate to the LLM or wasn't persuasive enough to override trained behavior. The hashline refs were generated but never consumed. + +## Solution + +Replace the built-in `edit` tool entirely with a custom tool (`.opencode/tools/edit.ts`) that **only accepts hashline-style args**. Per the OpenCode docs: "If a custom tool uses the same name as a built-in tool, the custom tool takes precedence." + +The custom tool schema has no `oldString`/`newString`. The LLM must use `operations[]` with hash-anchored refs from read output. This matches the approach used by [oh-my-openagent](https://github.com/code-yeongyu/oh-my-openagent), which successfully steers LLMs toward hash-anchored edits by removing the native fallback path. + +## Changes + +### Created +- **`.opencode/tools/edit.ts`** -- Custom edit tool with hashline-native schema (`filePath`, `fileRev?`, `operations[]`). Calls `runHashlineOperationsDetailed` with `dryRun: false` to apply edits directly. Includes a concise tool description with workflow, rules, operation types, examples, and error recovery. + +### Modified +- **`.opencode/plugins/hashline-hooks.ts`** -- Removed ~190 lines of dead code: + - Removed the `tool.definition` hook's edit schema extension block + - Removed `translateHashlineEditArgs` and all supporting functions (`hasHashlineEditShape`, `toHashlineOperations`, `firstString`, `firstBoolean`, `isNativeEditTool`) + - Simplified `tool.execute.before` to only strip hashline prefixes from content fields (write, patch, apply_patch still need this) + - Cleaned up unused imports (`mapOperationInput`, `runHashlineOperationsDetailed`, `HashlineOperationInput`) +- **`.opencode/plugins/hashline-routing.ts`** -- Removed `"edit"` from the known-tools set and removed all edit-specific snake_case-to-camelCase arg normalization (the custom tool defines its own schema) +- **`test/hashline-hardening.test.mjs`** -- Updated tests to verify the hook no longer modifies the edit tool's schema or description + +### Removed +- **`.opencode/tools/resolve-hash-edit.ts`** -- Deleted (backup in `tools_disabled/`). This MCP tool was a workaround that translated hashline operations to `oldString`/`newString` in a separate tool call. Redundant now that the edit tool handles hashline operations directly. + +## What stays the same + +- **Read tool** -- Still uses the `tool.execute.after` hook to annotate output with `#HL` refs. Not replaced. +- **Write/patch/apply_patch tools** -- Still native, still get hashline prefix stripping via `tool.execute.before` +- **System prompt injection** -- `experimental.chat.system.transform` hook still injects hashline workflow guidance +- **hashline-core.ts** -- Core engine unchanged +- **Ref format** -- Still `LINE#HASH#ANCHOR` (3-4 hex chars), more collision-resistant than alternatives + +## How to verify + +See `specs/verify-hashline-edit-schema.md` for the full test procedure. Quick version: + +1. Start a new OpenCode session +2. Ask the LLM to read a file, then edit it +3. Export the session and inspect edit tool calls: + +```bash +opencode export > session.json +node -e " +const d = require('./session.json'); +for (const m of d.messages) { + for (const p of (m.parts || [])) { + if (p.type === 'tool' && p.tool === 'edit') { + const input = p.state?.input || {}; + const style = 'operations' in input ? 'HASHLINE' : 'NATIVE'; + console.log(style, Object.keys(input)); + } + } +} +" +``` + +All edit calls should print `HASHLINE`. diff --git a/specs/verify-hashline-edit-schema.md b/specs/verify-hashline-edit-schema.md new file mode 100644 index 0000000..552c585 --- /dev/null +++ b/specs/verify-hashline-edit-schema.md @@ -0,0 +1,148 @@ +# Verify: Custom Edit Tool Uses Hashline Args + +The built-in `edit` tool has been replaced by a custom tool (`.opencode/tools/edit.ts`) that **only** accepts hashline-style args (`filePath`, `fileRev`, `operations[]`). There is no `oldString`/`newString` path. This spec verifies the custom tool works end-to-end in a new session. + +## Background + +The hashline plugin now works as follows: + +1. **Read**: `tool.execute.after` hook annotates output with `#HL` refs and a `REV:` token (unchanged). +2. **Edit**: A custom tool registered as `edit` in `.opencode/tools/edit.ts` replaces the built-in. It accepts only `{ filePath, fileRev?, operations[] }` and calls `runHashlineOperationsDetailed` directly. No `oldString`/`newString` schema exists. + +The old approach (extending the native edit schema via `tool.definition` hook) was removed because the schema extension never reached the LLM -- confirmed by session export inspection. + +## What to verify + +1. The LLM's edit calls contain `operations` (not `oldString`/`newString`). +2. The edit executes successfully and the file is modified. +3. Hash mismatch errors are reported clearly when refs are stale. + +## Test procedure + +### 1. Create a test file + +Create `testdata/verify-edit.txt` with content: + +``` +alpha +bravo +charlie +delta +echo +foxtrot +``` + +### 2. Read the file + +Ask the LLM to read `testdata/verify-edit.txt`. The output should include `#HL` refs: + +``` +#HL REV:XXXXXXXX +#HL 1#XXX#XXX|alpha +#HL 2#XXX#XXX|bravo +... +``` + +Confirm the refs and REV token are present. + +### 3. Edit using hashline refs + +Ask the LLM to replace the line `bravo` with `bravo-replaced`. Since the edit tool only accepts hashline args, the LLM must send something like: + +```json +{ + "filePath": "testdata/verify-edit.txt", + "fileRev": "", + "operations": [ + { "op": "replace", "ref": "", "content": "bravo-replaced" } + ] +} +``` + +This should execute successfully. + +### 4. Verify the edit worked + +Read the file again. Confirm `bravo` was replaced with `bravo-replaced`. + +### 5. Test a second edit (re-read required) + +Ask the LLM to also replace `delta` with `delta-modified`. The LLM should re-read (since refs are stale after the first edit), get fresh refs, and submit a new edit call. + +## How to inspect raw LLM tool call args + +### Option A: Export the session (after the fact) + +```bash +# List sessions to find the ID +opencode session list --format json + +# Export the session +opencode export > session.json + +# Inspect edit tool calls +node -e " +const d = require('./session.json'); +let editNum = 0; +for (const m of d.messages) { + for (const p of (m.parts || [])) { + if (p.type === 'tool' && p.tool === 'edit') { + editNum++; + const input = p.state?.input || {}; + const hasOps = 'operations' in input; + const hasOld = 'oldString' in input; + const style = hasOps ? 'HASHLINE' : (hasOld ? 'NATIVE (BAD)' : 'UNKNOWN'); + console.log('Edit #' + editNum + ': ' + style); + console.log(' Keys:', Object.keys(input)); + console.log(' Input:', JSON.stringify(input).slice(0, 400)); + console.log(); + } + } +} +console.log('Total edit calls:', editNum); +" +``` + +Every edit call should print `HASHLINE`. If any prints `NATIVE (BAD)`, the custom tool is not being used. + +### Option B: Use `opencode run --format json` + +```bash +opencode run --format json \ + "Read testdata/verify-edit.txt, then replace 'bravo' with 'bravo-modified'" \ + 2>&1 | node -e " +const rl = require('readline').createInterface({ input: process.stdin }); +rl.on('line', line => { + try { + const e = JSON.parse(line); + if (e.type === 'tool' || (e.properties?.tool === 'edit')) { + console.log(JSON.stringify(e, null, 2)); + } + } catch {} +}); +" +``` + +### Option C: Share the session + +Type `/share` in the TUI after the edit. The shared session URL shows tool call arguments. + +## Expected results + +- **All** edit calls must use hashline-style args (`operations` + optional `fileRev`). There is no `oldString`/`newString` fallback in the custom tool schema. +- If the edit tool is called but fails with a schema validation error mentioning `oldString` or `newString`, it means the custom tool did NOT replace the built-in. Check that `.opencode/tools/edit.ts` exists and exports a default tool. +- If the edit tool receives `operations` but the edit fails with a hash mismatch, the LLM used stale refs. This is expected behavior -- the error message should guide re-reading. + +## Troubleshooting + +| Symptom | Cause | Fix | +|---------|-------|-----| +| Edit calls still show `oldString`/`newString` | Custom tool not loaded | Verify `.opencode/tools/edit.ts` exists with `export default tool(...)`. Restart opencode. | +| Schema error on edit call | Custom tool schema mismatch | Check that the tool schema matches what the LLM sends. | +| `resolve-hash-edit` tool still appears | Old MCP tool not removed | Delete `.opencode/tools/resolve-hash-edit.ts` (backup is in `tools_disabled/`). | +| Hash mismatch error | LLM used stale refs after a prior edit | Expected. The LLM should re-read before the next edit. | +| Edit succeeds but no diff shown in TUI | Custom tool output format differs from built-in | The custom tool returns a string like `Updated (+N -N)`. The TUI may not show a diff panel for custom tools. | + +## Cleanup + +Delete `testdata/verify-edit.txt` after testing if it was created for this purpose. diff --git a/test/hashline-hardening.test.mjs b/test/hashline-hardening.test.mjs index 0efc1eb..5b06e82 100644 --- a/test/hashline-hardening.test.mjs +++ b/test/hashline-hardening.test.mjs @@ -211,14 +211,65 @@ test("tool descriptions guide agents toward batched edit workflows", async () => assert.match(readOutput.description, /canonical #HL refs plus a REV token/i) assert.match(readOutput.description, /plan all same-file changes before calling edit/i) - const editOutput = { description: "native edit", parameters: {} } - await definition?.({ toolID: "edit" }, editOutput) - assert.match(editOutput.description, /Accepts refs copied from read/i) - assert.match(editOutput.description, /Prefer one batched call per file/i) - assert.match(editOutput.description, /operations:\[\{ op, ref\|startRef\/endRef, content\? \}\]/i) + // The edit tool is now a custom tool replacement (in .opencode/tools/edit.ts) + // so the tool.definition hook no longer modifies its description or schema. const writeOutput = { description: "native write", parameters: {} } await definition?.({ toolID: "write" }, writeOutput) assert.match(writeOutput.description, /Use write for new files or full rewrites/i) assert.match(writeOutput.description, /Prefer edit for targeted existing-file changes/i) }) + +test("edit tool definition hook no longer modifies edit schema (custom tool replaces it)", async () => { + const hooks = makeHooks() + const definition = hooks["tool.definition"] + + assert.equal(typeof definition, "function") + + const editOutput = { + description: "native edit", + parameters: { + type: "object", + properties: { + filePath: { type: "string" }, + oldString: { type: "string" }, + newString: { type: "string" }, + replaceAll: { type: "boolean" }, + }, + required: ["filePath", "oldString", "newString"], + }, + } + + await definition?.({ toolID: "edit" }, editOutput) + + const props = editOutput.parameters.properties + + // The hook should NOT add hashline fields to the edit schema anymore + assert.equal(props.operations, undefined, "operations should not be injected") + assert.equal(props.fileRev, undefined, "fileRev should not be injected") + assert.equal(props.safeReapply, undefined, "safeReapply should not be injected") + + // Description should not be modified for edit tool + assert.equal(editOutput.description, "native edit", "edit description should not be modified by hook") + + // required should remain untouched + const required = editOutput.parameters.required + assert.ok(required.includes("oldString"), "oldString should still be required (unchanged)") + assert.ok(required.includes("newString"), "newString should still be required (unchanged)") +}) + +test("non-edit tools do not get schema modifications", async () => { + const hooks = makeHooks() + const definition = hooks["tool.definition"] + + assert.equal(typeof definition, "function") + + const readOutput = { + description: "native read", + parameters: { type: "object", properties: {}, required: [] }, + } + await definition?.({ toolID: "read" }, readOutput) + + assert.equal(readOutput.parameters.properties.operations, undefined, "read tool should not get operations property") + assert.equal(readOutput.parameters.properties.fileRev, undefined, "read tool should not get fileRev property") +}) diff --git a/testdata/proof.txt b/testdata/proof.txt deleted file mode 100644 index de07f07..0000000 --- a/testdata/proof.txt +++ /dev/null @@ -1,10 +0,0 @@ -# New Test File -This file was created by the hashline plugin. - -Features demonstrated: -- Read -- Edit -- Patch -- Write - -Status: All operations successful! \ No newline at end of file diff --git a/testdata/sample.txt b/testdata/sample.txt deleted file mode 100644 index 3cc64f7..0000000 --- a/testdata/sample.txt +++ /dev/null @@ -1,4 +0,0 @@ -alpha -BETA -gamma -delta