From 9ab7fc9758eabfa329e6842a4a13e6896be14a41 Mon Sep 17 00:00:00 2001 From: JRedeker Date: Wed, 22 Jul 2026 23:19:05 -0400 Subject: [PATCH] fix(plugin): export only default from entry module (OpenCode 1.18.4) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit OpenCode 1.18.4's loader invokes EVERY function-valued export of a plugin entry module as a plugin factory (iterates Object.values(entryModule) and calls each with PluginInput). index.ts also exported scrubSecrets and callMorphApply, which OpenCode invoked as bogus factories. They return a string / result object rather than throwing, so this produced no load error — but it silently registered garbage plugins alongside the real one. Fix (matches the toolbox plugin fixes): entry module exports exactly one thing — default. All implementation + named test exports move to impl.ts via git mv (no logic change); index.ts becomes a thin shim importing impl.ts and re-exporting default. Repoint src/execute.ts + index.test.ts imports to impl.js; add impl.ts to package.json files. 137/137 tests pass; typecheck clean; entry verified to export exactly [default]. --- impl.ts | 398 +++++++++++++++++++++++++++++++++++++++++++++++ index.test.ts | 6 +- index.ts | 410 ++----------------------------------------------- package.json | 1 + src/execute.ts | 4 +- 5 files changed, 419 insertions(+), 400 deletions(-) create mode 100644 impl.ts diff --git a/impl.ts b/impl.ts new file mode 100644 index 0000000..56e3284 --- /dev/null +++ b/impl.ts @@ -0,0 +1,398 @@ +/** + * OpenCode Morph Fast Apply Plugin + * + * Integrates Morph's Fast Apply API for 10x faster code editing. + * Uses lazy edit markers (// ... existing code ...) for partial file updates. + * + * @see https://docs.morphllm.com/quickstart + */ + +import { type Plugin, tool } from "@opencode-ai/plugin"; +import { + ALLOW_READONLY_AGENTS, + EXISTING_CODE_MARKER, + MORPH_API_KEY, + MORPH_API_URL, + MORPH_MODEL, + MORPH_TIMEOUT, + PLUGIN_VERSION, + READONLY_AGENTS, +} from "./src/constants.js"; +import { countChanges, generateUnifiedDiff } from "./src/diff.js"; +import { findDroppedIdentifiers } from "./src/imports.js"; +import { normalizeCodeEditInput } from "./src/normalize.js"; +import { resolveTargetPath } from "./src/path-confinement.js"; +import { executeMorphEdit } from "./src/execute.js"; + +/** + * Stable failure-kind classification for API/guard/write failures. + * These strings are safe to log and contain no secrets. + */ +export type FailureKind = + | "api_timeout" + | "api_http_error" + | "api_parse_error" + | "api_request_failed" + | "api_empty_response" + | "missing_api_key"; + +/** + * Remove secrets from a message before returning or logging it. + */ +export function scrubSecrets(message: unknown, apiKey?: unknown): string { + let result: string; + if (typeof message === "string") { + result = message; + } else if (message instanceof Error) { + result = message.message; + } else { + try { + result = String(message ?? ""); + } catch { + result = ""; + } + } + if (typeof apiKey === "string" && apiKey.length > 0) { + result = result.split(apiKey).join("***REDACTED***"); + } + // Scrub generic Bearer tokens + result = result.replace( + /Bearer\s+[A-Za-z0-9_.-]{10,}/gi, + "Bearer ***REDACTED***", + ); + return result; +} + +export interface CallMorphApplyResult { + success: boolean; + content?: string; + error?: string; + kind?: FailureKind; +} + +/** + * Call Morph's Apply API to merge code edits. + * + * The AbortController timeout stays active through the full response lifecycle + * (fetch, error body read, and JSON body parse) so slow body streams are also + * bounded. + */ +export async function callMorphApply( + originalCode: string, + codeEdit: string, + instructions: string, + options?: { + timeout?: number; + apiKey?: string; + apiUrl?: string; + model?: string; + }, +): Promise { + const { + timeout = MORPH_TIMEOUT, + apiKey = MORPH_API_KEY, + apiUrl = MORPH_API_URL, + model = MORPH_MODEL, + } = options || {}; + + if (!apiKey) { + return { + success: false, + error: + "MORPH_API_KEY not set. Get one at https://morphllm.com/dashboard/api-keys", + kind: "missing_api_key", + }; + } + + const controller = new AbortController(); + const timeoutId = setTimeout(() => controller.abort(), timeout); + + try { + const response = await fetch(`${apiUrl}/v1/chat/completions`, { + method: "POST", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${apiKey}`, + }, + body: JSON.stringify({ + model, + messages: [ + { + role: "user", + content: `${instructions}\n${originalCode}\n${codeEdit}`, + }, + ], + temperature: 0, + }), + signal: controller.signal, + }); + + if (!response.ok) { + const errorText = await response.text(); + clearTimeout(timeoutId); + return { + success: false, + error: scrubSecrets( + `Morph API error (${response.status}): ${errorText}`, + apiKey, + ), + kind: "api_http_error", + }; + } + + let result: { choices: Array<{ message: { content: string } }> }; + try { + result = (await response.json()) as { + choices: Array<{ message: { content: string } }>; + }; + } catch (parseErr) { + clearTimeout(timeoutId); + // A timeout that fires while the response body is being read/parsed + // surfaces here as an AbortError. Classify it as a timeout rather than a + // parse error so failures are diagnosed correctly. + const e = parseErr as Error; + if (e?.name === "AbortError" || controller.signal.aborted) { + return { + success: false, + error: `Morph API timeout after ${timeout}ms`, + kind: "api_timeout", + }; + } + return { + success: false, + error: "Morph API returned invalid JSON", + kind: "api_parse_error", + }; + } + + clearTimeout(timeoutId); + const mergedCode = result.choices?.[0]?.message?.content; + + if (!mergedCode) { + return { + success: false, + error: "Morph API returned empty response", + kind: "api_empty_response", + }; + } + + return { + success: true, + content: mergedCode, + }; + } catch (err) { + clearTimeout(timeoutId); + const error = err as Error; + if (error.name === "AbortError") { + return { + success: false, + error: `Morph API timeout after ${timeout}ms`, + kind: "api_timeout", + }; + } + return { + success: false, + error: scrubSecrets(`Morph API request failed: ${error.message}`, apiKey), + kind: "api_request_failed", + }; + } +} + +const MorphFastApply: Plugin = async ({ directory, client }) => { + /** + * Helper for structured logging with stderr fallback + */ + const log = async ( + level: "debug" | "info" | "warn" | "error", + message: string, + ) => { + try { + await client.app.log({ + body: { + service: "morph-fast-apply", + level, + message, + }, + }); + } catch { + // Fallback to stderr if SDK logging fails + process.stderr.write(`[morph-fast-apply] ${message}\n`); + } + }; + + // Log plugin initialization status + if (!MORPH_API_KEY) { + await log( + "warn", + "MORPH_API_KEY not set - morph_edit tool will be disabled", + ); + } else { + await log("info", `Plugin loaded with model: ${MORPH_MODEL}`); + } + + return { + tool: { + /** + * morph_edit - Fast code editing using Morph's Apply API + * + * Use this tool for efficient partial file edits. It's 10x faster than + * traditional edit tools for large files and complex changes. + * + * Uses "// ... existing code ..." markers to represent unchanged sections. + */ + morph_edit: tool({ + description: `Edit existing files using partial code snippets with "// ... existing code ..." markers. Morph's AI merges your changes into the full file. + +WHEN TO USE morph_edit vs edit: +- morph_edit: large files (300+ lines), multiple scattered changes, complex refactoring, whitespace-sensitive edits +- native edit: small exact string replacements, simple renames, single-line fixes (faster, no API call) +- native write: creating new files from scratch + +FORMAT — use "// ... existing code ..." to represent unchanged sections: +// ... existing code ... +FIRST_EDIT +// ... existing code ... +SECOND_EDIT +// ... existing code ... + +CRITICAL RULES: +- ALWAYS wrap changes with markers at start AND end (omitting markers DELETES surrounding code) +- Include 1-2 unique context lines around each edit to anchor the location precisely +- Write a specific 'instructions' param: "I am adding X to function Y" not "update code" +- Preserve exact indentation +- For deletions: show surrounding context, omit the deleted lines +- Batch multiple edits to the same file in one call + +DISAMBIGUATION — when a file has repeated patterns, include enough unique context: + BAD: just "return result;" (matches many places) + GOOD: include the unique function signature above it + +NATIVE EDIT RECOVERY — when a native edit reports an unmatched or ambiguous exact target: +- Re-read the target file before any retry. +- Do not repeat the unchanged failed edit input. +- Make at most one corrected native edit only when the re-read proves the change is still small and exact. +- Otherwise use morph_edit for the contextual repair (multi-line, scattered, whitespace-sensitive, or broader anchoring). +- Use write only for new-file or intentional full-file replacement. + +This recovery is separate from the Morph API-error/timeout fallback below, which still recovers to native edit. + +FALLBACK: If morph_edit fails (API error, timeout), use the native 'edit' tool with exact oldString/newString matching.`, + + args: { + target_filepath: tool.schema + .string() + .describe("Path of the file to modify"), + instructions: tool.schema + .string() + .describe( + "Brief first-person description of what you're changing. Used to disambiguate uncertainty in the edit.", + ), + code_edit: tool.schema + .string() + .describe( + 'The code changes wrapped with "// ... existing code ..." markers for unchanged sections', + ), + }, + + async execute(args, context) { + return executeMorphEdit(args, { + log, + directory, + context, + now: () => Date.now(), + readFile: async (filepath) => { + const file = Bun.file(filepath); + const exists = await file.exists(); + return { + exists, + text: exists ? await file.text() : "", + }; + }, + writeFile: async (filepath, content) => { + await Bun.write(filepath, content); + }, + callMorphApply, + resolveTargetPath, + normalizeCodeEditInput, + findDroppedIdentifiers, + generateUnifiedDiff, + countChanges, + constants: { + MORPH_API_KEY, + ALLOW_READONLY_AGENTS, + READONLY_AGENTS, + EXISTING_CODE_MARKER, + PLUGIN_VERSION, + MORPH_MODEL, + }, + }); + }, + }), + }, + + /** + * Customize tool output display in TUI + */ + "tool.execute.after": async (input, output) => { + if (input.tool === "morph_edit") { + // Parse output to build a branded title + const fileMatch = output.output.match(/Applied edit to (.+?)\n/); + const statsMatch = output.output.match(/\+(\d+) -(\d+) lines/); + const timingMatch = output.output.match(/\| (\d+)ms/); + const createdMatch = output.output.match(/Created new file: (.+?)\n/); + const linesMatch = output.output.match(/Lines: (\d+)/); + const errorMatch = output.output.match(/^Error:/); + const blockedMatch = output.output.match(/not available in (.+?) mode/); + const apiFailMatch = output.output.match(/^Morph API failed:/); + const unsafeMatch = output.output.match( + /^Morph API produced unsafe output for (.+?)\./, + ); + const truncationMatch = output.output.match( + /^Morph API produced a potentially destructive merge for (.+?)\./, + ); + const droppedImportsMatch = output.output.match( + /^Morph API produced a merge with missing imports for (.+?)\./, + ); + + if (createdMatch) { + // New file created + const lines = linesMatch?.[1] || "?"; + output.title = `Morph: ${createdMatch[1]} (new, ${lines} lines)`; + } else if (fileMatch && statsMatch) { + // Successful edit + const timing = timingMatch ? ` (${timingMatch[1]}ms)` : ""; + output.title = `Morph: ${fileMatch[1]} +${statsMatch[1]}/-${statsMatch[2]}${timing}`; + } else if (unsafeMatch) { + // Post-merge guard: marker leakage + output.title = `Morph: blocked (marker leakage) ${unsafeMatch[1]}`; + } else if (truncationMatch) { + // Post-merge guard: catastrophic truncation + output.title = `Morph: blocked (truncation) ${truncationMatch[1]}`; + } else if (droppedImportsMatch) { + // Post-merge guard: dropped import identifiers + output.title = `Morph: blocked (dropped imports) ${droppedImportsMatch[1]}`; + } else if (blockedMatch) { + // Blocked by readonly agent + output.title = `Morph: blocked (${blockedMatch[1]} mode)`; + } else if (apiFailMatch) { + // API failure + output.title = `Morph: API failed`; + } else if (errorMatch) { + // Other error + output.title = `Morph: failed`; + } + + // Add structured metadata for potential future TUI enhancements + output.metadata = { + ...output.metadata, + provider: "morph", + version: PLUGIN_VERSION, + model: MORPH_MODEL, + }; + } + }, + }; +}; + +// Default export for OpenCode plugin loader +export default MorphFastApply; diff --git a/index.test.ts b/index.test.ts index 9aa2374..6e2d6b5 100644 --- a/index.test.ts +++ b/index.test.ts @@ -13,7 +13,7 @@ import { type ExecuteMorphEditRuntime, } from "./src/execute.js"; import { generateUnifiedDiff, countChanges } from "./src/diff.js"; -import { callMorphApply, scrubSecrets } from "./index.js"; +import { callMorphApply, scrubSecrets } from "./impl.js"; describe("EXISTING_CODE_MARKER", () => { test("is the canonical marker string", () => { @@ -164,7 +164,9 @@ describe("native edit recovery policy", () => { readFileSync(join(import.meta.dir, relativePath), "utf-8"); test("embedded morph_edit description states the recovery policy and preserves the API fallback", () => { - const content = readSurface("index.ts"); + // The morph_edit tool definition (and its description prose) lives in + // impl.ts; index.ts is a thin entry shim exporting only default. + const content = readSurface("impl.ts"); for (const anchor of RECOVERY_ANCHORS) { expect(content).toContain(anchor); } diff --git a/index.ts b/index.ts index 56e3284..310d6e8 100644 --- a/index.ts +++ b/index.ts @@ -1,398 +1,16 @@ -/** - * OpenCode Morph Fast Apply Plugin - * - * Integrates Morph's Fast Apply API for 10x faster code editing. - * Uses lazy edit markers (// ... existing code ...) for partial file updates. - * - * @see https://docs.morphllm.com/quickstart - */ +// opencode-morph-fast-apply — plugin ENTRY module. +// +// OpenCode 1.18.4+ invokes EVERY function-valued export of a plugin entry +// module as a plugin factory: its loader iterates Object.values(entryModule) +// and calls each export with the PluginInput. This module previously also +// exported helper functions (scrubSecrets, callMorphApply), which OpenCode +// then invoked as bogus factories — non-throwing here (they returned a string / +// result object), so it produced no load error, but it silently registered +// garbage "plugins" alongside the real one. +// +// Therefore the entry module exports EXACTLY ONE thing: default. All +// implementation and its named (test) exports live in impl.ts, pulled in as a +// normal transitive import — OpenCode only inspects the entry file's exports. +import MorphFastApply from "./impl.js"; -import { type Plugin, tool } from "@opencode-ai/plugin"; -import { - ALLOW_READONLY_AGENTS, - EXISTING_CODE_MARKER, - MORPH_API_KEY, - MORPH_API_URL, - MORPH_MODEL, - MORPH_TIMEOUT, - PLUGIN_VERSION, - READONLY_AGENTS, -} from "./src/constants.js"; -import { countChanges, generateUnifiedDiff } from "./src/diff.js"; -import { findDroppedIdentifiers } from "./src/imports.js"; -import { normalizeCodeEditInput } from "./src/normalize.js"; -import { resolveTargetPath } from "./src/path-confinement.js"; -import { executeMorphEdit } from "./src/execute.js"; - -/** - * Stable failure-kind classification for API/guard/write failures. - * These strings are safe to log and contain no secrets. - */ -export type FailureKind = - | "api_timeout" - | "api_http_error" - | "api_parse_error" - | "api_request_failed" - | "api_empty_response" - | "missing_api_key"; - -/** - * Remove secrets from a message before returning or logging it. - */ -export function scrubSecrets(message: unknown, apiKey?: unknown): string { - let result: string; - if (typeof message === "string") { - result = message; - } else if (message instanceof Error) { - result = message.message; - } else { - try { - result = String(message ?? ""); - } catch { - result = ""; - } - } - if (typeof apiKey === "string" && apiKey.length > 0) { - result = result.split(apiKey).join("***REDACTED***"); - } - // Scrub generic Bearer tokens - result = result.replace( - /Bearer\s+[A-Za-z0-9_.-]{10,}/gi, - "Bearer ***REDACTED***", - ); - return result; -} - -export interface CallMorphApplyResult { - success: boolean; - content?: string; - error?: string; - kind?: FailureKind; -} - -/** - * Call Morph's Apply API to merge code edits. - * - * The AbortController timeout stays active through the full response lifecycle - * (fetch, error body read, and JSON body parse) so slow body streams are also - * bounded. - */ -export async function callMorphApply( - originalCode: string, - codeEdit: string, - instructions: string, - options?: { - timeout?: number; - apiKey?: string; - apiUrl?: string; - model?: string; - }, -): Promise { - const { - timeout = MORPH_TIMEOUT, - apiKey = MORPH_API_KEY, - apiUrl = MORPH_API_URL, - model = MORPH_MODEL, - } = options || {}; - - if (!apiKey) { - return { - success: false, - error: - "MORPH_API_KEY not set. Get one at https://morphllm.com/dashboard/api-keys", - kind: "missing_api_key", - }; - } - - const controller = new AbortController(); - const timeoutId = setTimeout(() => controller.abort(), timeout); - - try { - const response = await fetch(`${apiUrl}/v1/chat/completions`, { - method: "POST", - headers: { - "Content-Type": "application/json", - Authorization: `Bearer ${apiKey}`, - }, - body: JSON.stringify({ - model, - messages: [ - { - role: "user", - content: `${instructions}\n${originalCode}\n${codeEdit}`, - }, - ], - temperature: 0, - }), - signal: controller.signal, - }); - - if (!response.ok) { - const errorText = await response.text(); - clearTimeout(timeoutId); - return { - success: false, - error: scrubSecrets( - `Morph API error (${response.status}): ${errorText}`, - apiKey, - ), - kind: "api_http_error", - }; - } - - let result: { choices: Array<{ message: { content: string } }> }; - try { - result = (await response.json()) as { - choices: Array<{ message: { content: string } }>; - }; - } catch (parseErr) { - clearTimeout(timeoutId); - // A timeout that fires while the response body is being read/parsed - // surfaces here as an AbortError. Classify it as a timeout rather than a - // parse error so failures are diagnosed correctly. - const e = parseErr as Error; - if (e?.name === "AbortError" || controller.signal.aborted) { - return { - success: false, - error: `Morph API timeout after ${timeout}ms`, - kind: "api_timeout", - }; - } - return { - success: false, - error: "Morph API returned invalid JSON", - kind: "api_parse_error", - }; - } - - clearTimeout(timeoutId); - const mergedCode = result.choices?.[0]?.message?.content; - - if (!mergedCode) { - return { - success: false, - error: "Morph API returned empty response", - kind: "api_empty_response", - }; - } - - return { - success: true, - content: mergedCode, - }; - } catch (err) { - clearTimeout(timeoutId); - const error = err as Error; - if (error.name === "AbortError") { - return { - success: false, - error: `Morph API timeout after ${timeout}ms`, - kind: "api_timeout", - }; - } - return { - success: false, - error: scrubSecrets(`Morph API request failed: ${error.message}`, apiKey), - kind: "api_request_failed", - }; - } -} - -const MorphFastApply: Plugin = async ({ directory, client }) => { - /** - * Helper for structured logging with stderr fallback - */ - const log = async ( - level: "debug" | "info" | "warn" | "error", - message: string, - ) => { - try { - await client.app.log({ - body: { - service: "morph-fast-apply", - level, - message, - }, - }); - } catch { - // Fallback to stderr if SDK logging fails - process.stderr.write(`[morph-fast-apply] ${message}\n`); - } - }; - - // Log plugin initialization status - if (!MORPH_API_KEY) { - await log( - "warn", - "MORPH_API_KEY not set - morph_edit tool will be disabled", - ); - } else { - await log("info", `Plugin loaded with model: ${MORPH_MODEL}`); - } - - return { - tool: { - /** - * morph_edit - Fast code editing using Morph's Apply API - * - * Use this tool for efficient partial file edits. It's 10x faster than - * traditional edit tools for large files and complex changes. - * - * Uses "// ... existing code ..." markers to represent unchanged sections. - */ - morph_edit: tool({ - description: `Edit existing files using partial code snippets with "// ... existing code ..." markers. Morph's AI merges your changes into the full file. - -WHEN TO USE morph_edit vs edit: -- morph_edit: large files (300+ lines), multiple scattered changes, complex refactoring, whitespace-sensitive edits -- native edit: small exact string replacements, simple renames, single-line fixes (faster, no API call) -- native write: creating new files from scratch - -FORMAT — use "// ... existing code ..." to represent unchanged sections: -// ... existing code ... -FIRST_EDIT -// ... existing code ... -SECOND_EDIT -// ... existing code ... - -CRITICAL RULES: -- ALWAYS wrap changes with markers at start AND end (omitting markers DELETES surrounding code) -- Include 1-2 unique context lines around each edit to anchor the location precisely -- Write a specific 'instructions' param: "I am adding X to function Y" not "update code" -- Preserve exact indentation -- For deletions: show surrounding context, omit the deleted lines -- Batch multiple edits to the same file in one call - -DISAMBIGUATION — when a file has repeated patterns, include enough unique context: - BAD: just "return result;" (matches many places) - GOOD: include the unique function signature above it - -NATIVE EDIT RECOVERY — when a native edit reports an unmatched or ambiguous exact target: -- Re-read the target file before any retry. -- Do not repeat the unchanged failed edit input. -- Make at most one corrected native edit only when the re-read proves the change is still small and exact. -- Otherwise use morph_edit for the contextual repair (multi-line, scattered, whitespace-sensitive, or broader anchoring). -- Use write only for new-file or intentional full-file replacement. - -This recovery is separate from the Morph API-error/timeout fallback below, which still recovers to native edit. - -FALLBACK: If morph_edit fails (API error, timeout), use the native 'edit' tool with exact oldString/newString matching.`, - - args: { - target_filepath: tool.schema - .string() - .describe("Path of the file to modify"), - instructions: tool.schema - .string() - .describe( - "Brief first-person description of what you're changing. Used to disambiguate uncertainty in the edit.", - ), - code_edit: tool.schema - .string() - .describe( - 'The code changes wrapped with "// ... existing code ..." markers for unchanged sections', - ), - }, - - async execute(args, context) { - return executeMorphEdit(args, { - log, - directory, - context, - now: () => Date.now(), - readFile: async (filepath) => { - const file = Bun.file(filepath); - const exists = await file.exists(); - return { - exists, - text: exists ? await file.text() : "", - }; - }, - writeFile: async (filepath, content) => { - await Bun.write(filepath, content); - }, - callMorphApply, - resolveTargetPath, - normalizeCodeEditInput, - findDroppedIdentifiers, - generateUnifiedDiff, - countChanges, - constants: { - MORPH_API_KEY, - ALLOW_READONLY_AGENTS, - READONLY_AGENTS, - EXISTING_CODE_MARKER, - PLUGIN_VERSION, - MORPH_MODEL, - }, - }); - }, - }), - }, - - /** - * Customize tool output display in TUI - */ - "tool.execute.after": async (input, output) => { - if (input.tool === "morph_edit") { - // Parse output to build a branded title - const fileMatch = output.output.match(/Applied edit to (.+?)\n/); - const statsMatch = output.output.match(/\+(\d+) -(\d+) lines/); - const timingMatch = output.output.match(/\| (\d+)ms/); - const createdMatch = output.output.match(/Created new file: (.+?)\n/); - const linesMatch = output.output.match(/Lines: (\d+)/); - const errorMatch = output.output.match(/^Error:/); - const blockedMatch = output.output.match(/not available in (.+?) mode/); - const apiFailMatch = output.output.match(/^Morph API failed:/); - const unsafeMatch = output.output.match( - /^Morph API produced unsafe output for (.+?)\./, - ); - const truncationMatch = output.output.match( - /^Morph API produced a potentially destructive merge for (.+?)\./, - ); - const droppedImportsMatch = output.output.match( - /^Morph API produced a merge with missing imports for (.+?)\./, - ); - - if (createdMatch) { - // New file created - const lines = linesMatch?.[1] || "?"; - output.title = `Morph: ${createdMatch[1]} (new, ${lines} lines)`; - } else if (fileMatch && statsMatch) { - // Successful edit - const timing = timingMatch ? ` (${timingMatch[1]}ms)` : ""; - output.title = `Morph: ${fileMatch[1]} +${statsMatch[1]}/-${statsMatch[2]}${timing}`; - } else if (unsafeMatch) { - // Post-merge guard: marker leakage - output.title = `Morph: blocked (marker leakage) ${unsafeMatch[1]}`; - } else if (truncationMatch) { - // Post-merge guard: catastrophic truncation - output.title = `Morph: blocked (truncation) ${truncationMatch[1]}`; - } else if (droppedImportsMatch) { - // Post-merge guard: dropped import identifiers - output.title = `Morph: blocked (dropped imports) ${droppedImportsMatch[1]}`; - } else if (blockedMatch) { - // Blocked by readonly agent - output.title = `Morph: blocked (${blockedMatch[1]} mode)`; - } else if (apiFailMatch) { - // API failure - output.title = `Morph: API failed`; - } else if (errorMatch) { - // Other error - output.title = `Morph: failed`; - } - - // Add structured metadata for potential future TUI enhancements - output.metadata = { - ...output.metadata, - provider: "morph", - version: PLUGIN_VERSION, - model: MORPH_MODEL, - }; - } - }, - }; -}; - -// Default export for OpenCode plugin loader export default MorphFastApply; diff --git a/package.json b/package.json index 7f773cb..e800686 100644 --- a/package.json +++ b/package.json @@ -10,6 +10,7 @@ }, "files": [ "index.ts", + "impl.ts", "src/", "README.md", "LICENSE", diff --git a/src/execute.ts b/src/execute.ts index ea5c4c6..03953a0 100644 --- a/src/execute.ts +++ b/src/execute.ts @@ -1,6 +1,6 @@ import type { ResolveTargetPathResult } from "./path-confinement.js"; -import type { FailureKind } from "../index.js"; -import { scrubSecrets } from "../index.js"; +import type { FailureKind } from "../impl.js"; +import { scrubSecrets } from "../impl.js"; export interface ExecuteMorphEditArgs { target_filepath: string;