UNPKG

@sema-agent/core

Version:

Stateless, task-oriented AI agent core

72 lines (71 loc) • 4.93 kB
/** * File encoding + line-ending detection for the Read/Edit/Write band, ported from CC's * `utils/fileRead.ts` mechanism and hardened where CC is lossy: * * - BOM detection: `FF FE` → utf16le, `EF BB BF` → utf8-with-BOM, else plain utf8. Detection is * BOM-only; UTF-16BE / heuristic sniffing is deliberately NOT done — a wrong guess silently * corrupts, and the BOM-less case stays a refusal at the Read layer. * - The MODEL-FACING text is BOM-stripped and CRLF-normalized (what the model quotes in `old_string` * must match what Read showed it). CRLF-normalize is parity on both of CC's read lanes; BOM-strip * is parity on CC's READ lane only — CC's EDIT/WRITE lane hands the model text with the leading * U+FEFF still in it and strips only inside its staleness/hash coordinate. sema strips in ONE * shared decoder for every lane, so the model-facing view and the hash coordinate cannot drift * apart: a registered delta, not parity. * - Write-back re-encodes with the ORIGINAL encoding, PRESERVES the BOM (CC drops it on Write — a * registered deliberate improvement), and restores the file's dominant line endings for Edit * (Write keeps the model's endings as-is, CC decision). */ /** Detected byte-level encoding of a text file (BOM-driven; utf8 is the no-BOM default). */ export interface DetectedFileEncoding { encoding: "utf8" | "utf16le"; hadBom: boolean; } export type DetectedLineEndings = "CRLF" | "LF"; export interface DecodedTextFile { /** Model-facing text: BOM stripped, `\r\n` normalized to `\n`. The staleness-hash coordinate. */ text: string; encoding: DetectedFileEncoding; /** Dominant line ending of the ORIGINAL bytes (majority vote; CC's counting rule). * Deliberately NOT per-line lossless: a mixed-endings file is unified to the dominant ending on * the first edit (accepted non-goal — CC behaves the same way). * The SAMPLE is a registered delta: CC votes on the first 4096 characters only, while * {@link detectLineEndings} below votes on the whole file. The verdict decides what an Edit WRITES * BACK across the entire file, so sampling a prefix is exactly the case where the cheap answer * flips the remaining 99% of the lines. */ endings: DetectedLineEndings; /** A utf16le body with an ODD byte count is TRUNCATED/corrupt — decoding would silently drop the * dangling byte and a later write-back would destroy it permanently. Flagged so the tool layer * fails closed instead of "repairing" the file. */ malformed?: true; } /** BOM-only encoding detection (CC `detectEncodingForResolvedPath` parity — no content heuristics). */ export declare function detectFileEncoding(bytes: Uint8Array): DetectedFileEncoding; /** Decode file bytes into the model-facing normalized form + the metadata needed to write back losslessly. */ export declare function decodeTextBytes(bytes: Uint8Array): DecodedTextFile; /** * Re-encode text for writing back to a file that was decoded with {@link decodeTextBytes}. * * @param text normalized (`\n`) text when `endings` is CRLF/LF (Edit path — restore the file's * dominant endings); pass `endings:"preserve"` to write the text's OWN line endings untouched * (Write path — CC decision: the model sent explicit endings in `content` and meant them). * @returns a plain string for the no-BOM utf8 + LF-or-preserve fast path, else the exact bytes * (BOM + encoded body). */ export declare function encodeTextForFile(text: string, encoding: DetectedFileEncoding, endings: DetectedLineEndings | "preserve"): string | Uint8Array; /** Normalize model-supplied match/replacement text the same way the file text was normalized * (CC FileEditTool parity: `old_string`/`new_string` are CRLF-normalized before matching). * Deliberately does NOT strip a leading U+FEFF: an `old_string` starting with one may be quoting * genuine file content — use {@link normalizeFileText} only where the string stands for a whole file. */ export declare function normalizeEditText(s: string): string; /** Split a leading U+FEFF off MODEL-SUPPLIED whole-file text (CC parity). The BOM is a file-level * byte marker, not text: decodeTextBytes strips it on the way in, so it can never live in the * normalized coordinate. Only for strings that stand for a WHOLE file (Write `content`, * readFileState seeding) — a decoded file text's leading U+FEFF is genuine content and must stay. */ export declare function splitLeadingBom(text: string): { hadBom: boolean; text: string; }; /** CC parity (BOM-strip + CRLF-normalize): THE coordinate every readFileState hash lives in * (Read/Edit/Write staleness all hash `decodeTextBytes(...).text`). A hash taken on raw model content * with a leading U+FEFF can never agree with any later re-read. */ export declare function normalizeFileText(s: string): string;