@sema-agent/core
Version:
Stateless, task-oriented AI agent core
72 lines (71 loc) • 4.93 kB
TypeScript
/**
* 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;