/** * Hoisted tool overrides — replace Pi's built-in read/write/edit/grep with * AFT-backed Rust implementations. Registering a tool with the same name as * a built-in replaces the built-in entirely. * * Each tool provides: * - `promptSnippet` / `promptGuidelines`: teach the model our argument shape * in Pi's system prompt (Pi's built-ins use generic one-liners otherwise). * - `renderCall` / `renderResult` for `write` and `edit`: without these, * Pi's ToolExecutionComponent falls back to the *built-in* renderer for * same-named tools, which reads `path` and `edits[]` and can garble output * when a renderer does not match the registered argument shape (issue #15). * - Structured `details: { diff, firstChangedLine }` so the rendered diff * also ends up in the agent's message stream, matching Pi's convention. * * `read` and `grep` keep the default text-only result rendering because our * payload (`path`, `pattern`) already aligns with Pi's built-in arg shape. */ import { type AgentToolResult, type ExtensionAPI, type Theme } from "@earendil-works/pi-coding-agent"; import { type Component, Text } from "@earendil-works/pi-tui"; import type { PluginContext } from "../types.js"; import { type RenderResultOptionsLike } from "./render-helpers.js"; /** Reuse the user-tier gh_read description gate across every GitHub-capable tool. */ export declare function whenGhReadEnabled(enabled: boolean, description: string): string; /** * Local shape for Pi's render context — the real type is exposed by * `@earendil-works/pi-coding-agent`'s internals but not publicly exported. * We only read `args`, `lastComponent`, and `isError` here; everything else is ignored. */ interface RenderContextLike { args: unknown; lastComponent: Component | undefined; isError: boolean; } /** * Enforce AFT's `restrict_to_project_root` isolation for an out-of-root target. * * Pi has no host-level permission/allow-list system to bubble to. So the knob * is binary: when `restrict_to_project_root` is false (Pi default) the path is * allowed (Rust accepts it); when true, the path is hard-blocked with a clear, * actionable error — never a prompt. A per-call grant could never override the * Rust-side boundary anyway, which is exactly the issue #125 footgun this * avoids. The thrown error surfaces as the tool result (Pi's user surface). */ export declare function assertExternalDirectoryPermission(extCtx: { cwd: string; }, target: string, options?: { restrictToProjectRoot?: boolean; serverValidatedRead?: boolean; }): Promise; export interface ToolSurfaceFlags { /** True keeps AFT on host-native names; false registers aft_ alternatives. */ hoistBuiltinTools?: boolean; hoistRead: boolean; hoistWrite: boolean; hoistEdit: boolean; hoistGrep: boolean; /** * Mirrors the user's `restrict_to_project_root` AFT config (Pi default * `false`). When false, the user has explicitly opted into "no * restriction" — Pi has no host-level external_directory allow-list, so * a `ui.confirm` prompt has no policy to consult and would only annoy * the user. When true, Rust hard-rejects out-of-root paths before the * plugin layer sees them anyway, so the prompt is also unreachable. We * pass this through so `assertExternalDirectoryPermission` can skip the * prompt in the false case (the common one) and the helper stays in * place as a safety net for unusual contexts that opt into restriction * but still want a chance to allow a one-off external write. */ restrictToProjectRoot: boolean; } /** Details surfaced to both renderer and agent message stream. */ interface FileMutationDetails { diff?: string; firstChangedLine?: number; additions: number; deletions: number; replacements?: number; editsApplied?: number; diagnostics?: unknown[]; /** * True when Rust returned `diff.truncated = true` — the before/after strings * were omitted because the file exceeded the diff size cap, so we have no * line-level diff to render. Both the agent-facing text and the TUI renderer * surface this explicitly rather than silently showing a summary. */ truncated?: boolean; /** * Whether AFT's auto-formatter ran on the post-write content. Mirrors the * `data.formatted` field from the Rust write/edit response. When true, * the file content on disk is what the formatter produced; when false, * `formatSkippedReason` explains why. */ formatted?: boolean; /** * Reason the formatter was skipped, when `formatted=false`. One of the * documented values from `crates/aft/src/format.rs::auto_format`: * `"unsupported_language"`, `"no_formatter_configured"`, * `"formatter_not_installed"`, `"formatter_excluded_path"`, `"timeout"`, * `"error"`. Pi agents read this to decide whether to retry, fix config, * or accept the unformatted result. */ formatSkippedReason?: string; /** * v0.27.1: Rust returns `no_op: true` when the post-write file content * is byte-identical to the pre-write state. This separates "matched but * produced no change" from a real `+0/-0` failure mode in the UI. * See GitHub #45. */ noOp?: boolean; } export declare function registerHoistedTools(pi: ExtensionAPI, ctx: PluginContext, surface: ToolSurfaceFlags): void; /** * Shape a bridge mutation response into an `AgentToolResult` Pi can render. * Exported for unit tests covering truncation, diagnostics, and batch-edit * summaries without spinning up a real bridge. */ export declare function buildMutationResult(response: Record): AgentToolResult; export declare function renderMutationCall(toolName: "write" | "edit", filePath: string | undefined, theme: Theme, context: RenderContextLike): Text; export declare function renderMutationResult(result: AgentToolResult, theme: Theme, context: RenderContextLike, options?: RenderResultOptionsLike): Component; /** Resolve a path argument to an absolute path if it exists, decoding file: * URLs and expanding `~`. */ export declare function resolvePathArg(cwd: string, path: string): Promise; /** * Brace-aware split for OpenCode-style include args. * * Accepts: * - "*.ts,*.tsx" (comma-separated includes) * - "**\/*.{vue,ts,tsx}" (single glob with brace alternation) * - "*.ts,**\/*.{vue,tsx}" (mix of both) * * A naive split-by-`,` would chop `*.{vue,ts}` into `*.{vue` + `ts}`, * which then fails downstream globbing with * `unclosed alternate group; missing '}'`. */ export declare function splitIncludeGlobs(include: string): string[]; /** * Build the navigation footer for a `read` response. * * The pure clamping/range logic lives in aft-bridge. Pi keeps the * host-specific parameter hint (`offset/limit`) here so existing agent-facing * output stays byte-for-byte identical. */ export declare function formatReadFooter(agentSpecifiedRange: boolean, data: Record): string; export {}; //# sourceMappingURL=hoisted.d.ts.map