/** * Edit-approval interception decision logic. * * Pure functions only: given the plugin settings, the tool call, and the * target file's current state, decide whether the call may proceed untouched * (`pass`) or must go through the approval panel (`ask` with a reason that * carries the tool/file header plus the rendered line diff). * * @module dsh-edit-approval/guard */ import { type DiffLine } from './diff.ts'; /** * The default intercepted-tool list lives in `./config.ts` beside the schema * defaults it feeds, so the document's defaults have one home. */ /** Fully resolved plugin settings (schema defaults + row config + user layer). */ export interface EditApprovalSettings { /** Master switch; off means edits execute without asking. */ enabled: boolean; /** Whitelist of intercepted tool names. */ tools: readonly string[]; /** Ask only when the change touches at least this many lines. */ minDiffLines: number; /** Whether creating a new file asks for approval. */ includeCreate: boolean; /** Whether clearing/emptying a file asks for approval. */ includeDelete: boolean; } export type EditOperation = 'create' | 'modify' | 'delete'; /** One intercepted edit: current state plus the proposed new content. */ export interface EditTarget { /** Tool name that initiated the edit. */ readonly toolName: string; /** Display path from the tool args (`file_path` / `path`). */ readonly filePath: string; readonly operation: EditOperation; /** Current file content; '' when the file does not exist. */ readonly current: string; /** Proposed content after the edit. */ readonly proposed: string; /** Best-effort preview note (e.g. a non-unique replacement target). */ readonly note?: string; } /** Decision returned to the `tools/pre-execute` waterfall. */ export type GuardResult = { readonly kind: 'pass'; } | { readonly kind: 'ask'; readonly reason: string; }; export interface GuardInput { readonly settings: EditApprovalSettings; readonly toolName: string; readonly args: Readonly>; /** Current file content; '' for absent files. */ readonly current: string; /** Whether the target file currently exists. */ readonly exists: boolean; } /** Extract the target path from a write-family tool call, or undefined. */ export declare function targetPathOf(toolName: string, args: Readonly>): string | undefined; /** Number of non-overlapping occurrences of `needle` in `haystack`. */ export declare function countOccurrences(haystack: string, needle: string): number; /** * Compute the proposed content for one intercepted call, mirroring the * tool's own semantics (single unique replace for `edit`/`str_replace`, * insert-after-line for `insert`, full text for `write`/`create`). * * Returns the current content unchanged (possibly with a `note`) when the * edit cannot be previewed or would fail — the diff is then empty and the * guard passes, letting the tool report the error itself. */ export declare function proposeContent(toolName: string, args: Readonly>, current: string): { proposed: string; note?: string; }; /** Build the approval reason: a header line plus the rendered diff. */ export declare function formatReason(toolName: string, filePath: string, operation: EditOperation, diff: readonly DiffLine[], note?: string): string; /** * Decide whether one write-family call must be approved. * * `pass` covers every case that should not block the call: disabled, tool * not whitelisted, non-edit `str_replace_editor` commands, no-op edits, and * edits below `minDiffLines`. `ask` carries the reason the approval panel * headlines. */ export declare function decideApproval(input: GuardInput): GuardResult;