
    sjEe             	          U d Z ddlZddlZddlZddlZddlZddlZddlZddlZddl	m
Z
mZ ddlmZmZ ddlmZmZmZmZmZ ddlmZ ddlmZ ddlmZmZmZmZ  e ej                               Z! ee!          Z" ee!          Z# ej$        d	          Z% ej$        d
          Z&dedefdZ'dedee         fdZ(dededefdZ)dZ*dede+ee,f         fdZ-dee         de,fdZ.dede,fdZ/e G d d                      Z0e G d d                      Z1e G d d                      Z2e G d d                      Z3e G d d                       Z4e G d! d"                      Z5e G d# d$                      Z6 ej$        d%          Z7d&e6de+eee         f         fd'Z8d(ede+eef         fd)Z9 ej$        d*          Z:d+ede+ee;ef         dz  fd,Z<d-Z=e+e>d.<   de?defd/Z@dee?         d0e;defd1ZA G d2 d3e
          ZBh d4ZCd5d6d7d8d9d:ZD eEh d;          ZFd<d=d>d?ZGd@ed(ede,fdAZHdBede+e,ef         fdCZIdBede+e,ef         fdDZJdBede+e,ef         fdEZKdBede+e,ef         fdFZLeLeIeJeJeKdGZM eEh dH          ZNdIZOdIZPdJZQdKZRdIZSdZTdLZUdMZVdNedOe;de;fdPZWeReSfdQedRede+e;e;f         fdSZXeTeUfdQedRede+e;e;f         fdTZY ej$        dU          ZZdVede,fdWZ[dXee         de,fdYZ\d&e4dVede4fdZZ] G d[ d\eB          Z^dS )]a\  
File Operations Module

Provides file manipulation capabilities (read, write, patch, search) that work
across all terminal backends (local, docker, ssh, singularity, modal, daytona, vercel_sandbox).

The key insight is that all file operations can be expressed as shell commands,
so we wrap the terminal backend's execute() interface to provide a unified file API.

Usage:
    from tools.file_operations import ShellFileOperations
    from tools.terminal_tool import _active_environments
    
    # Get file operations for a terminal environment
    file_ops = ShellFileOperations(terminal_env)
    
    # Read a file
    result = file_ops.read_file("/path/to/file.py")
    
    # Write a file
    result = file_ops.write_file("/path/to/new.py", "print('hello')")
    
    # Search for content
    result = file_ops.search("TODO", path=".", file_glob="*.py")
    N)ABCabstractmethod)	dataclassfield)OptionalListDictAnyClassVar)Path)BINARY_EXTENSIONS)build_write_denied_pathsbuild_write_denied_prefixesget_write_denied_erroris_write_deniedz!\x1b\][^\x07\x1b]*(?:\x07|\x1b\\)z+'?\x07?__HERMES_FENCE_[A-Za-z0-9]+__\x07?'?textreturnc                 t   | s| S g }|                      d          D ]}d|v pd|v }t                              d|          }t                              d|          }|                    dd          }|r|                    d          dk    rr|                    |           d                    |          S )z;Strip leaked terminal fence wrappers from file read output.Tkeepends__HERMES_FENCE_z] z'
	 )
splitlines_OSC_SEQUENCE_REsub_FENCE_MARKER_REreplacestripappendjoin)r   cleaned_lineslinehad_terminal_wrappercleaneds        9/home/agent/.hermes/hermes-agent/tools/file_operations.py_strip_terminal_fence_leaksr'   A   s     !M.. & &0D8KGtO"&&r400"&&r733//&"-- 	GMM*$=$=$C$CW%%%%77=!!!    samplec                 :    | sdS | dd         }d|v rdS d|v rdS dS )u"  Return the dominant line ending in ``sample`` or None if undetermined.

    Looks at the first few line breaks and picks ``\r\n`` if any are
    present (Windows / DOS), otherwise ``\n`` (Unix).  Returns ``None``
    for empty / single-line content where we can't tell.  Used to
    preserve the file's original line endings across write_file and
    patch operations — without this the agent's bare-LF tool args
    silently normalize Windows-line-ending files, and patch produces
    mixed endings when only a substituted region changes.
    Ni   

 )r)   heads     r&   _detect_line_endingr/   R   s?      t%4%=D~~vt||t4r(   targetc                     |                      dd                               dd          }|dk    r|S |dk    r|                     dd          S | S )a5  Convert all line endings in ``text`` to ``target`` (``\n`` or ``\r\n``).

    Idempotent: ``_normalize_line_endings(_normalize_line_endings(x, "\r\n"), "\r\n") == _normalize_line_endings(x, "\r\n")``.
    Strips lone ``\r`` characters as well, so mixed-ending content is
    homogenized in a single pass.
    r+   r,   )r   )r   r0   lf_normalizeds      r&   _normalize_line_endingsr4   h   s\     LL..66tTBBM~~$$T6222Kr(   u   ﻿c                 ~    | r8|                      t                    r| t          t                    d         dfS | dfS )zReturn (text-without-leading-BOM, had_bom).

    Only a single leading BOM is stripped; a BOM appearing mid-content is
    left alone (it's legitimate data there, not a file marker).
    NTF)
startswith	_UTF8_BOMlenr   s    r&   
_strip_bomr:      sC      +	** +C	NNOO$d**;r(   c                 T    t          |           o|                     t                    S )z)True if ``text`` begins with a UTF-8 BOM.)boolr6   r7   r9   s    r&   _has_bomr=      s    ::4$//)444r(   pathc                      t          |           S )z.Return True if path is on the write deny list.)_shared_is_write_deniedr>   s    r&   _is_write_deniedrB      s    "4(((r(   c                   $   e Zd ZU dZdZeed<   dZeed<   dZ	eed<   dZ
eed<   d	Zee         ed
<   dZeed<   dZeed<   d	Zee         ed<   d	Zee         ed<   d	Zee         ed<   d	Zee         ed<    ee          Zee         ed<   defdZd	S )
ReadResultzResult from reading a file.r   contentr   total_lines	file_sizeF	truncatedNhint	is_binaryis_imagebase64_content	mime_type
dimensionserrordefault_factorysimilar_filesr   c                 H    d | j                                         D             S )Nc                 *    i | ]\  }}||g k    ||S Nr-   .0kvs      r&   
<dictcomp>z&ReadResult.to_dict.<locals>.<dictcomp>   s(    TTTA!-AQSGG1GGGr(   __dict__itemsselfs    r&   to_dictzReadResult.to_dict   s$    TT!4!4!6!6TTTTr(   )__name__
__module____qualname____doc__rE   str__annotations__rF   intrG   rH   r<   rI   r   rJ   rK   rL   rM   rN   rO   r   listrR   r   dictr`   r-   r(   r&   rD   rD      s#        %%GSKIsItD(3-ItHd$(NHSM(((#Ix}### $J$$$E8C=$uT:::M49:::U U U U U U Ur(   rD   c                       e Zd ZU dZdZeed<   dZeed<   dZ	e
e         ed<   dZe
eeef                  ed<   dZe
e         ed	<   dZe
e         ed
<   dZe
e         ed<   defdZdS )WriteResultzResult from writing a file.r   bytes_writtenFdirs_createdNverifiedlintlsp_diagnosticsrO   warningr   c                 H    d | j                                         D             S )Nc                     i | ]
\  }}|||S rU   r-   rV   s      r&   rZ   z'WriteResult.to_dict.<locals>.<dictcomp>   s    HHHA!-1---r(   r[   r^   s    r&   r`   zWriteResult.to_dict   s$    HH!4!4!6!6HHHHr(   )ra   rb   rc   rd   rl   rg   rf   rm   r<   rn   r   ro   r	   re   r
   rp   rO   rq   ri   r`   r-   r(   r&   rk   rk      s         %%M3L$
  $Hhtn###%)D(4S>
"))) &*OXc])))E8C=!GXc]!!!I I I I I I Ir(   rk   c                   L   e Zd ZU dZdZeed<   dZeed<    e	e
          Zee         ed<    e	e
          Zee         ed<    e	e
          Zee         ed	<   d
Zeeeef                  ed<   d
Zee         ed<   d
Zee         ed<   dZeed<   d
Zee         ed<   defdZd
S )PatchResultzResult from patching a file.Fsuccessr   diffrP   files_modifiedfiles_createdfiles_deletedNro   rp   rO   	no_changenoter   c                 @   d| j         i}| j        rd|d<   | j        r
| j        |d<   | j        r
| j        |d<   | j        r
| j        |d<   | j        r
| j        |d<   | j        r
| j        |d<   | j        r
| j        |d	<   | j        r
| j        |d
<   | j	        r
| j	        |d<   |S )Nrv   Tr{   r|   rw   rx   ry   rz   ro   rp   rO   )
rv   r{   r|   rw   rx   ry   rz   ro   rp   rO   r_   results     r&   r`   zPatchResult.to_dict   s    "+T\!:> 	'"&F;9 	'!YF6N9 	'!YF6N 	;'+':F#$ 	9&*&8F?# 	9&*&8F?#9 	'!YF6N 	=(,(<F$%: 	)"jF7Or(   )ra   rb   rc   rd   rv   r<   rf   rw   re   r   rh   rx   r   ry   rz   ro   r   r	   r
   rp   rO   r{   r|   ri   r`   r-   r(   r&   ru   ru      s"        &&GTD#NNN %d ; ; ;NDI;;;$uT:::M49:::$uT:::M49:::%)D(4S>
")))%)OXc])))E8C= ItD(3-      r(   ru   c                   @    e Zd ZU dZeed<   eed<   eed<   dZeed<   dS )SearchMatchzA single search match.r>   line_numberrE   g        mtimeN)	ra   rb   rc   rd   re   rf   rg   r   floatr-   r(   r&   r   r      sF           
IIILLLE5r(   r   c                   P   e Zd ZU dZ ee          Zee         e	d<    ee          Z
ee         e	d<    ee          Zeeef         e	d<   dZee	d<   dZee	d	<   d
Zee         e	d<   d
Zee         e	d<   d
Zee         e	d<   dZee         e	d<   dee         fdZddedefdZd
S )SearchResultzResult from searching.rP   matchesfilescountsr   total_countFrH   Nlimit_reasonrq   rO      _DENSIFY_MIN_MATCHESr   c                 L   t          | j                  | j        k     rdS g }d}| j        D ]e}|j        |k    r!|                    |j                   |j        }|                    d|j         d|j                                                    fd                    |          S )uK  Render content-mode matches as a compact, path-grouped text block.

        The verbose form repeats the ``{"path","line","content"}`` keys and the
        full path string for every match. This groups consecutive matches by
        path (path printed once, then ``  <line>: <content>`` rows), which is
        lossless — every path, line number, and content byte is preserved — and
        readable by the model without any decode step.

        Returns ``None`` when densification is not worthwhile (too few matches),
        so the caller falls back to the verbose array.
        Nz  : r,   )	r8   r   r   r>   r    r   rE   rstripr!   )r_   linescurrent_pathms       r&   _densify_matcheszSearchResult._densify_matches  s     t|t8884 &* 	E 	EAv%%QV$$$ v LLCamCCqy/?/?/A/ACCDDDDyyr(   densifyc                 Z   d| j         i}| j        r9|r|                                 nd }|d|d<   ||d<   nd | j        D             |d<   | j        r
| j        |d<   | j        r
| j        |d<   | j        rd	|d
<   | j        r
| j        |d<   | j        r
| j        |d<   | j        r
| j        |d<   |S )Nr   ztpath-grouped: each file path on its own line, followed by indented '<line>: <content>' rows for matches in that filematches_formatmatches_textc                 8    g | ]}|j         |j        |j        d S ))r>   r#   rE   r>   r   rE   )rW   r   s     r&   
<listcomp>z(SearchResult.to_dict.<locals>.<listcomp>:  s8     % % % VQ]qyQQ% % %r(   r   r   r   TrH   r   rq   rO   )	r   r   r   r   r   rH   r   rq   rO   )r_   r   r   denses       r&   r`   zSearchResult.to_dict-  s   %2D4D$E< 	/6@D))+++DE Q '( */~&&% %!\% % %y! : 	)"jF7O; 	+#{F8> 	'"&F; 	7%)%6F>"< 	- $F9: 	)"jF7Or(   F)ra   rb   rc   rd   r   rh   r   r   r   rf   r   re   ri   r   r	   rg   r   rH   r<   r   r   rq   rO   r   r   r   r`   r-   r(   r&   r   r      s=          !&t!<!<!<GT+<<<uT222E49222"U4888FDcN888KIt"&L(3-&&&!GXc]!!!E8C=
 +,(3-+++ (3-        8 t       r(   r   c                   X    e Zd ZU dZdZeed<   dZeed<   dZe	ed<   dZ
e	ed<   d	efd
ZdS )
LintResultzResult from linting a file.Trv   Fskippedr   outputmessager   c                 p    | j         r
d| j        dS | j        rdnd| j        d}| j        r
| j        |d<   |S )Nr   )statusr   okrO   )r   r   r   )r   r   rv   r   r~   s     r&   r`   zLintResult.to_dictU  sR    < 	B'DLAAA$(L=DDgUU< 	- $F9r(   N)ra   rb   rc   rd   rv   r<   rf   r   r   re   r   ri   r`   r-   r(   r&   r   r   M  sw         %%GTGTFCGS      r(   r   c                   0    e Zd ZU dZdZeed<   dZeed<   dS )ExecuteResultz&Result from executing a shell command.r   stdoutr   	exit_codeN)	ra   rb   rc   rd   r   re   rf   r   rg   r-   r(   r&   r   r   ^  s8         00FCIsr(   r   z'\n?\[Command timed out after \d+s\]\s*$r   c                 n    | j         dk    r"t                              d| j                  dfS | j        dfS )zIReturn stdout cleaned for parsing and a limit reason for search timeouts.|   r   search_timeoutN)r   _SEARCH_TIMEOUT_MARKER_REr   r   )r   s    r&   _search_stdout_and_limitr   h  s;    3(,,R??AQQQ=$r(   r   c                    g }g }|                      d          D ]}|                                s|                                }|                    d          s|                    d          r|                    |           k|dk    st
                              |          r|                    |           |                    |           d                    |          d                    |          fS )uC  Separate rg/grep diagnostic lines from real match output.

    ``_exec`` runs commands with ``stderr=subprocess.STDOUT``, so error and
    warning text from ``rg``/``grep`` is interleaved with match lines in a
    single stream. Diagnostics must not be parsed as matches, and on a hard
    failure they are the error message to surface.

    Returns ``(diagnostics, payload)`` where ``payload`` contains only lines
    that look like real search output — a match line (``file:line:content``),
    a files-only path, a count line, or a context line/separator. Everything
    else (tool-prefixed errors, rg's multi-line ``regex parse error`` block
    with its indented carets, blank lines) is folded into ``diagnostics``.

    Classifying by *shape* rather than by error prefix is what lets the
    exit-2 guard distinguish a pure failure (no usable payload → surface the
    error) from a partial failure (some files matched, one was unreadable →
    keep the matches). It also means error text can never be mis-parsed as a
    match, a latent bug that predates the exit-code fix.
    r,   zrg: zgrep: --)splitr   lstripr6   r    _SEARCH_OUTPUT_REmatchr!   )r   diagnosticspayloadr#   strippeds        r&   _split_tool_diagnosticsr   o  s    (  KGT"" % %zz|| 	 ;;==v&& 	(*=*=h*G*G 	t$$$ 4<<,22488<NN4    t$$$$99[!!499W#5#555r(   z0^([A-Za-z]:)?[^\s:][^\n]*?[:\-]\d|^[^\s:][^\s]*$r#   c                    | r| dk    rdS d}t          j        d|           D ]}|}|dS | d|                                         }|sdS |t          |                    d                    | |                                d         fS )a]  Parse grep/rg context output in ``path-line-content`` format.

    Context lines are ambiguous because filenames may legitimately contain
    ``-<digits>-`` segments. Prefer the rightmost numeric separator so a path
    like ``dir/file-12-name.py-8-context`` resolves to
    ``dir/file-12-name.py`` line ``8`` instead of truncating at ``file``.
    r   Nz-(\d+)-   )refinditerstartrg   groupend)r#   r   	candidater>   s       r&   _parse_search_context_liner     s      44<<tE[T22  	}tD tU[[^^$$d599;;<<&888r(   ))s   PNG

zPNG image data)s   zJPEG image data)s   GIF87aGIF image data)s   GIF89ar   )s   RIFFz$RIFF container (WAV/AVI/WebP family))s   %PDF-zPDF document)s   PKz$ZIP archive (also docx/xlsx/jar/apk))s   PKzZIP archive (empty))s   zgzip compressed data)s   BZhzbzip2 compressed data)s   7zXZ zxz compressed data)s   7z'z7-Zip archive)s   ELFzELF executable)s   MZzWindows PE executable)s   zMach-O executable (64-bit))s   z$Mach-O universal binary / Java class)s   SQLite format 3 zSQLite database)s   OggSzOgg container)s   fLaCz
FLAC audio)s   ID3zMP3 audio (ID3 tag))s      z$ISO media container (MP4/MOV family))s   BMzBMP image data)s   II* zTIFF image data (little-endian))s   MM *zTIFF image data (big-endian)_MAGIC_SIGNATURESc                     | sdS t           D ]B\  }}|                     |          r(|                    d          r| dd         dk    r>|c S CdS )a%  Best-effort human name for binary content from its magic bytes.

    Returns e.g. ``"PNG image data"`` or ``"unknown binary"``. Never raises.
    The ISO-media entry additionally checks for ``ftyp`` at offset 4, since
    the leading size field alone (three NULs) is too weak a signature.
    zunknown binaryz	ISO media      s   ftyp)r   r6   )r)   prefixnames      r&   identify_binary_bytesr     sz       )  V$$ 	{++ qsw0F0FKKK	 r(   rG   c                     t          | pd          }|dk    r
|dz  dd}n|dk    r
|dz  dd}n| d}d| d	| d
S )a  One-line answer for the binary-file refusal.

    Naming the dead end: "Binary file" alone sends the model hunting for
    'appropriate tools' that may not exist in its toolset. Naming the TYPE
    ("PNG image data, 4.1 KB") answers what-is-this in a single read.
    r(   i   z.1fz MBi   z KBz byteszBinary file (, u   ) — cannot display as text.)r   )r)   rG   kindsizes       r&   describe_binary_filer     s     !3//DK{+4444	d		d"++++###F4FF4FFFFr(   c                      e Zd ZdZed&dedededefd            Zededefd	            Z	d'dede
e         defdZe	 d'dedede
e         defd            Ze	 d(dededededef
d            Zededefd            Zededefd            Zd(dededefdZedededefd            Ze	 	 	 d)d eded!ed"e
e         deded#ed$edefd%            Zd
S )*FileOperationsz@Abstract interface for file operations across terminal backends.r     r>   offsetlimitr   c                     dS )z$Read a file with pagination support.Nr-   )r_   r>   r   r   s       r&   	read_filezFileOperations.read_file  	     	r(   c                     dS )a
  Read the complete file content as a plain string.

        No pagination, no line-number prefixes, no per-line truncation.
        Returns ReadResult with .content = full file text, .error set on
        failure. Always reads to EOF regardless of file size.
        Nr-   r_   r>   s     r&   read_file_rawzFileOperations.read_file_raw  s	     	r(   N	max_bytesc                 "    t          d          S )zCRead complete binary content as base64 across the backend boundary.z1Binary reads are not implemented for this backendrO   rD   )r_   r>   r   s      r&   read_file_byteszFileOperations.read_file_bytes  s     STTTTr(   rE   pre_contentc                     dS )z8Write content to a file, creating directories as needed.Nr-   )r_   r>   rE   r   s       r&   
write_filezFileOperations.write_file  	     	r(   F
old_string
new_stringreplace_allc                     dS )z,Replace text in a file using fuzzy matching.Nr-   )r_   r>   r   r   r   s        r&   patch_replacezFileOperations.patch_replace!  r   r(   patch_contentc                     dS )zApply a V4A format patch.Nr-   )r_   r   s     r&   	patch_v4azFileOperations.patch_v4a'  r   r(   c                     dS )z>Delete a file. Returns WriteResult with .error set on failure.Nr-   r   s     r&   delete_filezFileOperations.delete_file,  r   r(   	recursivec                 P    |rt          d          S |                     |          S )zCross-platform delete that handles files and (with recursive=True)
        directory trees. Default implementation delegates to ``delete_file``
        for the non-recursive case; backends with native recursive support
        should override.
        z1Recursive delete not implemented for this backendr   )rk   r   r_   r>   r   s      r&   delete_pathzFileOperations.delete_path1  s3      	Z%XYYYY%%%r(   srcdstc                     dS )zSMove/rename a file from src to dst. Returns WriteResult with .error set on failure.Nr-   )r_   r   r   s      r&   	move_filezFileOperations.move_file;  r   r(   .2   r   patternr0   	file_globoutput_modecontextc	                     dS )zSearch for content or files.Nr-   )	r_   r   r>   r0   r   r   r   r   r   s	            r&   searchzFileOperations.search@  s	    
 	r(   r   r   rU   r   r   rE   Nr   r   rE   r   )ra   rb   rc   rd   r   re   rg   rD   r   r   r   r   rk   r   r<   ru   r   r   r   r   r   r   r   r-   r(   r&   r   r     s       JJ c 3 3 *    ^ # *    ^U UC UHSM UZ U U U U 04 s S  (9D   ^
 */ # 3 C #'4?   ^
 s {    ^      ^& & & & & & & & S s {    ^ BKOP<= c  C "3-7:IL69BN   ^  r(   r   >   .bmp.gif.ico.jpg.png.jpeg.webpz python -m py_compile {file} 2>&1znode --check {file} 2>&1znpx tsc --noEmit {file} 2>&1zgo vet {file} 2>&1zrustfmt --check {file} 2>&1).pyz.js.ts.go.rs>   r  r	  r  )z/this is not the tsc command you are looking forz%could not determine executable to runznot found in npm registry)zno input filename givenzerror: not a workspace)zcannot find packagezgo: cannot find main module)npxrustfmtgobase_cmdc                     t                               |           }|sdS |                                t          fd|D                       S )aU  Return True iff ``output`` from ``base_cmd`` indicates the linter
    itself couldn't run (a tooling gap), as opposed to a real lint error
    in the file being checked.

    ``base_cmd`` is the first word of the linter command line (``npx``,
    ``rustfmt``, ``go``, ...).  ``output`` is the stdout/stderr captured
    from running it.
    Fc              3       K   | ]}|v V  	d S rU   r-   )rW   plowers     r&   	<genexpr>z._looks_like_linter_unusable.<locals>.<genexpr>  s'      ,,aqEz,,,,,,r(   )_LINTER_UNUSABLE_PATTERNSgetr  any)r  r   patternsr  s      @r&   _looks_like_linter_unusabler    sU     ),,X66H uLLNNE,,,,8,,,,,,r(   rE   c           	          ddl }	 |                    |            dS # |j        $ r'}dd|j         d|j         d|j         dfcY d}~S d}~wt          $ r%}dt          |          j         d	| fcY d}~S d}~ww xY w)
z;In-process JSON syntax check.  Returns (ok, error_message).r   NTr   FzJSONDecodeError:  (line 	, column )r   )	jsonloadsJSONDecodeErrormsglinenocolno	Exceptiontypera   )rE   _jsones      r&   _lint_json_inprocr'    s    1Gx  V V VU!%UUUU17UUUUUUUUUU 1 1 1a)00Q0000000001s,    
A;A	A;	A;A60A;6A;c                     	 ddl }n# t          $ r Y dS w xY w	 |                    |           D ]}dS # |j        $ r}dd| fcY d}~S d}~wt          $ r%}dt          |          j         d| fcY d}~S d}~ww xY w)uw  In-process YAML syntax check.  Returns (ok, error_message).

    Skipped gracefully if PyYAML isn't installed — YAML parsing is optional.

    Deliberately a *syntax-only* scan (``yaml.parse``), not ``safe_load``:
    loading rejects perfectly valid YAML that merely isn't a single plain
    document — multi-document streams (``---``-separated Kubernetes
    manifests raise ``ComposerError``) and application-defined tags
    (CloudFormation ``!Sub``/``!Ref``, Ansible ``!vault`` raise
    ``ConstructorError``).  Those are content conventions for whatever
    consumes the file, not syntax errors, and this linter's verdict is
    used as a fail-closed WRITE gate in ``write_file`` — a false positive
    here refuses a legitimate write outright.  ``yaml.parse`` still
    catches real scanner/parser failures (unclosed quotes, bad
    indentation, tab-mangled block maps).
    r   N)T__SKIP__r  FzYAMLError: r   )yamlImportErrorparse	YAMLErrorr#  r$  ra   )rE   _yaml_eventr&  s       r&   _lint_yaml_inprocr0    s    "        1kk'** 	 	Fx? ( ( ('A''''''''' 1 1 1a)00Q0000000001s8    
3 
A;A	A;	A;A60A;6A;c                     ddl }	 |                    |            dS # t          $ r%}dt          |          j         d| fcY d}~S d}~ww xY w)z<In-process TOML syntax check (stdlib tomllib, Python 3.11+).r   Nr  Fr   )tomllibr  r#  r$  ra   )rE   _tomlr&  s      r&   _lint_toml_inprocr4    sz    1Gx 1 1 1a)00Q0000000001s    
AAAAc                 >   ddl }	 |                    |            dS # t          $ rH}|j        rd|j         d|j         dnd}dt          |          j         d	|j         | fcY d}~S d}~wt          $ r%}dt          |          j         d	| fcY d}~S d}~ww xY w)
u   In-process Python syntax check via ast.parse.

    Catches SyntaxError, IndentationError, and everything else the
    ast module rejects — matching py_compile's scope but with no
    subprocess overhead and no dependency on a ``python`` in PATH.
    r   Nr  r  r  r  r   Fr   )	astr,  SyntaxErrorr!  r   r$  ra   r   r#  )rE   _astr&  locs       r&   _lint_python_inprocr:    s     1

7x : : ::;(J666186666a)99QU9C999999999 1 1 1a)00Q0000000001s,    
B=A*$B*B7BBB)r  .json.yaml.yml.toml>   r=  r;  r>  r<  r   i   r   r   __hermes_not_regular__valuedefaultc                 T    	 t          |           S # t          t          f$ r |cY S w xY w)z8Best-effort integer coercion for tool pagination inputs.)rg   	TypeError
ValueError)r@  rA  s     r&   _coerce_intrE  %  s<    5zzz"   s    ''r   r   c                     ddl m}  |            }t          dt          | t                              }t          |t
                    }t          dt          ||                    }||fS )a  Return safe read_file pagination bounds.

    Tool schemas declare minimum/maximum values, but not every caller or
    provider enforces schemas before dispatch. Clamp here so invalid values
    cannot leak into sed ranges like ``0,-1p``.

    The upper bound on ``limit`` comes from ``tool_output.max_lines`` in
    config.yaml (defaults to the module-level ``MAX_LINES`` constant).
    r   )get_max_linesr   )tools.tool_output_limitsrG  maxrE  DEFAULT_READ_OFFSETDEFAULT_READ_LIMITmin)r   r   rG  	max_linesnormalized_offsetnormalized_limits         r&   normalize_read_paginationrP  -  st     766666IA{63FGGHH"5*<==1c"2I>>??...r(   c                     t          dt          | t                              }t          dt          |t                              }||fS )zCReturn safe search pagination bounds for shell head/tail pipelines.r   r   )rI  rE  DEFAULT_SEARCH_OFFSETDEFAULT_SEARCH_LIMIT)r   r   rN  rO  s       r&   normalize_search_paginationrT  @  sF     A{63HIIJJ1k%1EFFGG...r(   z(?<!\\)(?:\\\\)*\\nr   c                 X    d| v p&t          t                              |                     S )a  Return True when a content-search regex tries to match a newline.

    ``search_files`` runs rg/grep in line-oriented mode, not rg
    ``-U``/``--multiline`` mode, so newline regexes cannot match across
    lines.  Detect both a literal newline already decoded into the tool
    argument and a regex ``
`` escape (odd number of backslashes before
    ``n``).  Even backslashes, e.g. ``\n``, mean a literal backslash+n
    search and should not warn.
    r,   )r<   _REGEX_NEWLINE_ESCAPE_REr   )r   s    r&   _pattern_has_regex_newlinerW  K  s*     7?Ld#;#B#B7#K#KLLLr(   rO   c                     | sdS d| v od| v S )z@Return True for rg's hard error when multiline mode is required.Fzliteral "\n" is not allowed--multiliner-   r   s    r&   _is_line_oriented_newline_errorrZ  X  s$     u+u4O%9OOr(   c                     | j         dk    st          |          s| S | j        rt          | j                  s| S d| _        d| _        | S )zHAttach a newline-regex warning only when search found no usable results.r   Na  0 results found. Note: search_files content search is line-oriented and does not run ripgrep with -U/--multiline, so `\n` in the regex does not match line breaks. Use context=N to inspect neighboring lines, or escape as `\\n` when searching for a literal backslash+n.)r   rW  rO   rZ  rq   )r   r   s     r&   )_maybe_warn_line_oriented_newline_patternr\  _  s_    Q&@&I&I| ;FLII FL	P N Mr(   c                   @   e Zd ZdZd`defdZ	 	 dadededededef
d	Zd
ede	fdZ
dbdedefdZedede	fd            Zd`dedede	fdZdede	fdZdcdededefdZdedefdZdedefdZdedefdZdededdfdZd`dedee         dee         fd Zd`dedee         de	fd!Zd"ed#ed$edefd%Zdedefd&Zededefd'            Zd(Zd)Zded*ed+ed,edd-f
d.Zddded*ed+edefd0Z dedee         fd1Z!dedefd2Z"dedefd3Z#d`ded4ee         defd5Z$dede%fd6Z&deded8e	de%fd9Z'ded8e	de%fd:Z(d;ed<ede%fd=Z)	 d`dededee         de%fd>Z*	 deded?ed@edAe	de+f
dBZ,dCede+fdDZ-d`dedee         de.fdEZ/	 d`dedee         dFee         de.fdGZ0de	fdHZ1dIede	fdJZ2dede	fdKZ3dede	fdLZ4deddfdMZ5dddNdedee         dFee         defdOZ6	 	 	 dfdSededTedUee         d+ed*edVedWede7fdXZ8dSededTedUee         d+ed*edVedWedee7         fdYZ9dSededUee         dee         fdZZ:dSeded+ed*ede7f
d[Z;dSeded+ed*ede7f
d\Z<dSededUee         d+ed*edVedWede7fd]Z=dSededUee         d+ed*edVedWede7fd^Z>dSededUee         d+ed*edVedWede7fd_Z?dS )gShellFileOperationsz
    File operations implemented via shell commands.
    
    Works with ANY terminal backend that has execute(command, cwd) method.
    This includes local, docker, singularity, ssh, modal, and daytona environments.
    Ncwdc                     || _         |p2t          |dd          p!t          t          |dd          dd          pd| _        i | _        dS )u  
        Initialize file operations with a terminal environment.

        Args:
            terminal_env: Any object with execute(command, cwd) method.
                         Returns {"output": str, "returncode": int}
            cwd: Optional explicit fallback cwd when the terminal env has
                 no cwd attribute (rare — most backends track cwd live).

        Note:
            Every _exec() call prefers the LIVE ``terminal_env.cwd`` over
            ``self.cwd`` so ``cd`` commands run via the terminal tool are
            picked up immediately.  ``self.cwd`` is only used as a fallback
            when the env has no cwd at all — it is NOT the authoritative
            cwd, despite being settable at init time.

            Historical bug (fixed): prior versions of this class used the
            init-time cwd for every _exec() call, which caused relative
            paths passed to patch/read/write to target the wrong directory
            after the user ran ``cd`` in the terminal.  Patches would
            claim success and return a plausible diff but land in the
            original directory, producing apparent silent failures.
        r_  Nconfig/)envgetattrr_  _command_cache)r_   terminal_envr_  s      r&   __init__zShellFileOperations.__init__w  se    0  
  V',t<< V7<4@@%NNVRU 	 02r(   commandtimeout
stdin_datar   c                 :   i }|r||d<   |||d<   |pt          | j        dd          p| j        } | j        j        |fd|i|}|                    dd          }|                    d          r|dk    rd}t          |                    d	d
          |          S )u  Execute command via terminal backend.

        Args:
            stdin_data: If provided, piped to the process's stdin instead of
                        embedding in the command string. Bypasses ARG_MAX.

        Cwd resolution order (critical — see class docstring):
          1. Explicit ``cwd`` arg (if provided)
          2. Live ``self.env.cwd`` (tracks ``cd`` commands run via terminal)
          3. Init-time ``self.cwd`` (fallback when env has no cwd attribute)

        This ordering ensures relative paths in file operations follow the
        terminal's current directory — not the directory this file_ops was
        originally created in.  See test_file_ops_cwd_tracking.py.
        ri  Nrj  r_  
returncoder   stdin_errorr   r   r   )r   r   )rd  rc  r_  executer  r   )	r_   rh  r_  ri  rj  kwargseffective_cwdr   r   s	            r&   _execzShellFileOperations._exec  s    "  	( 'F9!#-F<  Iwtx==I!!'GG}GGGJJ|Q//	
 ::m$$ 	aI::h++
 
 
 	
r(   cmdc                     || j         vr>|                     d| d          }|j                                        dk    | j         |<   | j         |         S )z6Check if a command exists in the environment (cached).zcommand -v z >/dev/null 2>&1 && echo 'yes'yes)re  rq  r   r   )r_   rr  r   s      r&   _has_commandz ShellFileOperations._has_command  s[    d)))ZZ Qc Q Q QRRF'-}':':'<'<'ED$"3''r(     r>   lengthc                    |                      d| d|                     |           d          }|j        dk    rdS t          |j                  }d                    |                                          }|sdS t          j        d|          sdS 	 t          j
        |d	
          S # t          j        t          f$ r Y dS w xY w)u-  Fetch the first ``length`` raw bytes of a file through the terminal.

        File operations run through a terminal backend (possibly remote), so
        raw bytes cannot cross the transport directly — the terminal decodes
        stdout with ``errors="replace"`` and manufactures U+FFFD at every
        byte it cannot decode, including a multibyte character cut in half by
        ``head -c``. Wrapping the sample in base64 lets the original bytes
        survive the transport, so binary detection can happen at the byte
        layer where it is well-defined (#80308 and friends).

        Returns the sample bytes, or ``None`` when the transport could not
        produce clean base64 (exotic shells without ``base64``); callers fall
        back to the legacy text-sample heuristic in that case.
        zhead -c  z 2>/dev/null | base64r   Nr   r(   z[A-Za-z0-9+/]+={0,2}Tvalidate)rq  _escape_shell_argr   r'   r   r!   r   r   	fullmatchbase64	b64decodebinasciiErrorrD  )r_   r>   rw  r   encodeds        r&   _sample_file_bytesz&ShellFileOperations._sample_file_bytes  s     SvSS 6 6t < <SSS
 
 q  4-fm<<'''--//** 	3|3W== 	4	#Gd;;;;
+ 	 	 	44	s   B* *CCr)   c                    | sdS d| v rdS 	 |                      d           dS # t          $ r_}|j        t          |           dz
  k    r9	 | d|j                                      d           Y d}~dS # t          $ r Y nw xY wY d}~dS d}~ww xY w)u  Byte-layer binary detection (the boundary for the #80308 class).

        Contract: a file is text when its sample is valid UTF-8, allowing one
        incomplete multibyte sequence at the very end (an artifact of cutting
        the sample at a byte boundary, not a property of the file). Anything
        else — NUL bytes, mid-stream invalid UTF-8 such as latin-1 or true
        binaries — stays read-only, preserving the anti-mojibake guarantee
        the old U+FFFD check existed for: a read→edit→write round-trip must
        never rewrite undecodable bytes with replacement characters.

        A file that legitimately *contains* U+FFFD (EF BF BD — e.g. logs of
        lossy output) is valid UTF-8 and reads as text; the old text-layer
        check misclassified it because it could not tell a stored replacement
        character from a transport-manufactured one.
        F    Tutf-8   N)decodeUnicodeDecodeErrorr   r8   )r)   excs     r&   _is_likely_binary_bytesz+ShellFileOperations._is_likely_binary_bytes  s    "  	5f4	MM'"""5! 		 		 		 yCKK!O++;SY;'..w777 55555)   D44444		s2   # 
BB	"A11
A>;B=A>>BBcontent_samplec                 4   t           j                            |          d                                         }|t          v rdS |rSd|dd         v rdS t          d |dd         D                       }|t          t          |          d          z  dk    S dS )	z
        Check if a file is likely binary.
        
        Uses extension check (fast) + content analysis (fallback).
        r   Tu   �Nrv  c              3   H   K   | ]}t          |          d k     |dvdV  dS )    z
	r   N)ord)rW   cs     r&   r  z8ShellFileOperations._is_likely_binary.<locals>.<genexpr>!  sJ        E  Ea"%a&&2++!82C2C !"2C2C2C2C E  Er(   g333333?F)osr>   splitextr  r   sumrL  r8   )r_   r>   r  extnon_printables        r&   _is_likely_binaryz%ShellFileOperations._is_likely_binary
  s     gt$$Q'--//###4  	I >%4%000t  E  E>%4%+@  E  E  E E EM 3s>':':D#A#AADHHur(   c                     t           j                            |          d                                         }|t          v S )z2Check if file is an image we can return as base64.r   )r  r>   r  r  IMAGE_EXTENSIONS)r_   r>   r  s      r&   	_is_imagezShellFileOperations._is_image'  s4    gt$$Q'--//&&&r(   r   rE   
start_linec                    ddl m}  |            }|                    d          }g }t          ||          D ]?\  }}t	          |          |k    r|d|         dz   }|                    | d|            @d                    |          S )uJ  Add line numbers to content in ``LINE_NUM|CONTENT`` format.

        The gutter uses a compact ``<n>|`` prefix (e.g. ``34|foo``) rather
        than a fixed-width zero/space-padded one (``    34|foo``). The
        padding was pure token overhead: on dense source the padded gutter
        cost ~48% more tokens than the bare content and ~16% more than the
        compact form, because the leading spaces + zero-padding tokenize
        into extra tokens on every single line. An A/B (Sonnet 4.6, 2
        passes) showed the compact gutter matches the padded gutter on
        line-reference / patch / value-lookup / structure tasks (4/4 both),
        while dropping line numbers entirely regressed line-referencing
        (the model hand-counted and was off-by-one, 3/4) — so we keep the
        numbers, just not the padding.
        r   get_max_line_lengthr,   )r   Nz... [truncated]|)rH  r  r   	enumerater8   r    r!   )	r_   rE   r  r  max_line_lengthr   numberedir#   s	            r&   _add_line_numbersz%ShellFileOperations._add_line_numbers,  s     	A@@@@@--//d## j999 	+ 	+GAt4yy?**,_,-0AAOOqMM4MM****yy"""r(   c                    |s|S |                     d          r9|                     d          }|j        dk    r|j                                        r|j                                        }|dk    r|S |                     d          r||dd         z   S |dd         }|                    d          }|dk    r
|d|         n|}|rt          j        d|          rt|                     d	|           }|j        dk    rQ|j                                        r8|j                                        }|dt          |          z   d         }	||	z   S |S )
z
        Expand shell-style paths like ~ and ~user to absolute paths.
        
        This must be done BEFORE shell escaping, since ~ doesn't expand
        inside single quotes.
        ~z
echo $HOMEr   z~/r   Nrb  z[a-zA-Z0-9._-]+zecho ~)	r6   rq  r   r   r   findr   r}  r8   )
r_   r>   r   homerest	slash_idxusernameexpand_result	user_homesuffixs
             r&   _expand_pathz ShellFileOperations._expand_pathF  sr     	K ??3 	2ZZ--F1$$)<)<)>)>$}**,,3;;K__T** +$qrr(?* ABBx IIcNN	/8A~~4

++4 2-? J J 2 %)JJ/B/B/B$C$CM$.!338L8R8R8T8T3$1$8$>$>$@$@	!%a#h--&7&8&8!9(611r(   argc                 \    ddl m}  ||          }d|                    dd          z   dz   S )a4  Escape a string for safe use in shell commands.

        On Windows native drive paths (``C:\Users\x`` / ``C:/Users/x``)
        and mixed MSYS leftovers (``/c/Users\x``) are rewritten to the
        Git Bash ``/c/Users/x`` form via ``_bash_safe_path``: bash eats
        backslashes and MSYS otherwise mangles drive paths into the
        ``Directory \drivers\etc does not exist`` failure class. Reuses
        the env-layer translator so shell file ops and the terminal ``cd``
        agree on the path form. No-op off Windows and for plain POSIX paths.
        r   )_bash_safe_path''"'"')tools.environments.localr  r   )r_   r  r  s      r&   r|  z%ShellFileOperations._escape_shell_argk  sC     	=<<<<<oc""S[[i000366r(   c                     ddl m}m} |r!|r ||                              dd          }d|                    dd          z   dz   S )u  Escape a path argument destined for a NATIVE Windows binary.

        ``_escape_shell_arg`` rewrites Windows paths to the Git Bash MSYS
        form (``/c/Users/x``) so bash builtins resolve them. But native
        Windows binaries invoked from that bash (ripgrep installed via
        winget/cargo/choco, native git, etc.) do not understand ``/c/...``
        paths — and Hermes disables MSYS argument conversion for its bash
        subprocesses (``MSYS_NO_PATHCONV=1`` / ``MSYS2_ARG_CONV_EXCL=*``,
        see ``_apply_windows_msys_bash_env_defaults``), so nothing ever
        translates the MSYS form back. The native tool then fails with
        ``The system cannot find the path specified. (os error 3)``.

        The forward-slash native form (``C:/Users/x``) is the one spelling
        every layer accepts: bash passes it through untouched (it is not an
        absolute POSIX path, so no conversion applies even without the
        opt-outs), and Windows APIs treat ``/`` and ``\`` as equivalent
        separators. MSYS builds of the same tools accept it too, so this is
        safe regardless of which flavor of the binary is installed.

        On non-Windows hosts this is exactly ``_escape_shell_arg``.
        r   )_IS_WINDOWS_msys_to_windows_path\rb  r  r  )r  r  r  r   )r_   r  r  r  s       r&   _escape_native_tool_argz+ShellFileOperations._escape_native_tool_arg|  sl    , 	POOOOOOO 	@3 	@'',,44T3??CS[[i000366r(   r   c                    |                      |          }t          j                            |          pd}|                      |          }|                      d          }d| d| d|z   dz   }|                     ||          S )a  Write ``content`` to ``path`` atomically via temp-file + rename.

        Streams ``content`` over stdin into a temp file in the SAME
        directory as ``path`` (so the final ``mv`` is a real rename on the
        same filesystem, not a non-atomic cross-device copy), preserves the
        existing file's mode if it exists, then renames over the target.
        On any failure the temp file is removed so we never leak a partial
        ``.hermes-tmp`` file next to the user's data, and the original file
        is left untouched. Content rides stdin so there is no ARG_MAX limit.

        ``mkdir -p`` for the parent directory is folded into this script
        (one fewer subprocess vs. a separate ``mkdir -p`` call).

        Returns an :class:`ExecuteResult`; ``exit_code == 0`` means the file
        was swapped into place atomically. A non-zero exit means nothing was
        renamed and the original (if any) is intact.
        r   z.hermes-tmp.XXXXXXz
set -e; d=z; t=z; if [ -L "$t" ]; then rt="$(readlink -f "$t" 2>/dev/null || realpath "$t" 2>/dev/null || true)"; [ -n "$rt" ] && { t="$rt"; d="$(dirname "$t")"; }; fi; mkdir -p "$d"; tmp="$(mktemp -p "$d" a   2>/dev/null || mktemp "$d/.hermes-tmp.$$.XXXXXX" 2>/dev/null || { tmp="$d/.hermes-tmp.$$"; : > "$tmp" && echo "$tmp"; })"; [ -n "$tmp" ] || { echo "atomic write: could not create temp file" >&2; exit 1; }; trap 'rm -f \"$tmp\"' EXIT; if [ -e "$t" ]; then m="$(stat -c%a "$t" 2>/dev/null || stat -f%Lp "$t" 2>/dev/null || true)"; [ -n "$m" ] && chmod "$m" "$tmp" 2>/dev/null || true; fi; cat > "$tmp"; if [ ! -e "$t" ]; then chmod "=rw" "$tmp" 2>/dev/null || true; fi; mv -f "$tmp" "$t"; trap - EXIT)rj  )r|  r  r>   dirnamerq  )r_   r>   rE   q_pathparentq_parenttmplscripts           r&   _atomic_writez!ShellFileOperations._atomic_write  s    $ ''--&&-#))&11 %%&:;;6%% %%% % %" (,#,"/#! 	H zz&Wz555r(   r   c                     |rt          |          S d|                     |           d}|                     |          }|j        dk    s|j        sdS t          |j                  S )u  Detect the dominant line ending of a file on disk.

        If ``pre_content`` is already available (we just read the file
        for lint/LSP purposes), inspect that — zero extra exec calls.
        Otherwise issue a tiny ``head -c 4096`` to sample the first 4KB.

        Returns ``"\r\n"`` for CRLF (Windows), ``"\n"`` for LF (Unix),
        or ``None`` if undetermined (new file, empty file, single-line
        file with no line break in the first chunk).
        zhead -c 4096  2>/dev/nullr   N)r/   r|  rq  r   r   r_   r>   r   head_cmdhead_results        r&   _detect_file_line_endingz,ShellFileOperations._detect_file_line_ending  sx      	4&{333 N4#9#9$#?#?MMMjj** A%%[-?%4";#5666r(   c                     d|                      |           d}|                     |          }|j        dk    s|j        sdS t	          |j                  S )u0  Whether the file on disk starts with a UTF-8 BOM.

        Always probes the first 3 bytes on disk — do NOT trust
        ``pre_content`` for BOM detection because the most common
        provider (``read_file_raw``) deliberately strips BOMs so the
        agent never sees U+FEFF glyphs.  Passing BOM-stripped content
        through ``pre_content`` would cause a false-negative and
        silently remove the marker on rewrite.

        A missing/empty file returns False (new writes get no BOM
        unless the caller explicitly includes one).
        z
head -c 3 r  r   F)r|  rq  r   r   r=   r  s        r&   _file_has_bomz!ShellFileOperations._file_has_bom  s`     K 6 6t < <JJJjj** A%%[-?%5*+++r(   old_contentnew_contentfilenamec                     |                     d          }|                     d          }t          j        ||d| d|           }d                    |          S )z2Generate unified diff between old and new content.Tr   za/zb/)fromfiletofiler   )r   difflibunified_diffr!   )r_   r  r  r  	old_lines	new_linesrw   s          r&   _unified_diffz!ShellFileOperations._unified_diff  sn    **D*99	**D*99	#y$(__"??
 
 

 wwt}}r(   c           	      T    |                      |          }d| d| d| dt           d	S )u  Byte size of a regular file, without opening one that never ends.

        ``wc -c < path`` opens the path. On a FIFO with no writer, a socket,
        or a character device like /dev/zero that never reaches EOF, that
        read blocks forever — and the read helpers pass no timeout to
        :meth:`_exec`, so the turn wedges until the process is killed. The
        device blocklist in ``tools/file_tools.py`` cannot cover this: it
        matches literal ``/dev/*`` names, while a FIFO is a file *type* and
        can sit at any path.

        ``[ -f ]`` is a stat, not an open — it answers exactly the question
        the size probe needs (regular file, symlinks followed) without
        touching the contents. Non-regular paths that exist report the
        sentinel so callers can say so instead of claiming the file is
        missing; a genuinely absent path still exits non-zero.
        zif [ -f z ]; then wc -c < z 2>/dev/null; elif [ -e z ]; then echo z; else exit 1; fi)r|  NOT_REGULAR_SENTINEL)r_   r>   r  s      r&   _size_probe_cmdz#ShellFileOperations._size_probe_cmd(  sX    " $$T**s  S   ,@  	
r(   c                 *    t          d|  d          S )z5Error for a path that exists but would block if read.zCannot read 'z`': not a regular file (directory, FIFO, socket, or device). Reading it could block indefinitely.r   r   rA   s    r&   _not_regular_errorz&ShellFileOperations._not_regular_error@  s5     K K K K
 
 
 	
r(   i   i   r   r   rG   zOptional[ReadResult]c                    t           j                            |          d                                         }|t          v rdS || j        k    rdS d|dt          |           dt          |           d| j         d| j         d}|                     d	| 	                    |                     }|j
        d
k    r6d|j        pdv r+|                     d| 	                    |                     }t          |j        pd          }|                    d          }	|j
        d
k    s|	d
k     rdS ||	t          d          z   d                                         }
	 t!          j        d|
v r|
                    dd          d
         n|
          }|d         }t          |d                   }t'          |                    dd                    }n# t*          t,          t.          f$ r Y dS w xY w||z   dz
  }||k    }d|                                 dg}|r%|                    d|dz    d| d| d| d	           t5          |                     ||          |||d                    |                    S )al  Attempt to read ``path`` as UTF-16 text, transcoded to UTF-8.

        Returns a populated ``ReadResult`` when the file is UTF-16 (BOM or
        zero-byte parity heuristic), else ``None`` so the caller falls back
        to the binary-file error. Files over 10 MiB are not rescued.
        ``path`` must already be expanded (caller ran ``_expand_path``).
        r   Nzimport sys, json, os
p = z

offset = z	
limit = z
MAX = z

SAMPLE = af  
try:
    size = os.path.getsize(p)
    if size > MAX:
        print('HERMES_UTF16:NO'); sys.exit(0)
    with open(p, 'rb') as f:
        data = f.read()
    sample = data[:SAMPLE]
    enc = None
    if sample[:2] == b'\xfe\xff':
        enc = 'utf-16-be'
    elif sample[:2] == b'\xff\xfe':
        enc = 'utf-16-le'
    else:
        odd = sum(1 for i in range(1, len(sample), 2) if sample[i] == 0)
        even = sum(1 for i in range(0, len(sample), 2) if sample[i] == 0)
        if even == 0 and odd >= 2:
            enc = 'utf-16-le'
        elif odd == 0 and even >= 2:
            enc = 'utf-16-be'
    if enc is None:
        print('HERMES_UTF16:NO'); sys.exit(0)
    text = data.decode(enc, 'replace')
    if text[:1] == '\ufeff':
        text = text[1:]
    text = text.replace('\r\n', '\n')
    lines = text.split('\n')
    total = len(lines)
    sel = lines[offset - 1: offset - 1 + limit]
    out = {'total_lines': total, 'encoding': enc,
           'content': '\n'.join(sel)}
    print('HERMES_UTF16:OK')
    print(json.dumps(out, ensure_ascii=True))
except Exception:
    print('HERMES_UTF16:NO'); sys.exit(0)
python3 -c r   python3r   
python -c zHERMES_UTF16:OKr,   rE   rF   encodingzutf-16zTranscoded from zP to UTF-8 for display. Text edits via patch/write_file would re-encode as UTF-8.Use offset= to continue reading (showing - of  lines)ry  rE   rF   rG   rH   rI   )r  r>   r  r  r   _UTF16_MAX_BYTESrg   _UTF16_SAMPLE_BYTESrq  r|  r   r   r'   r  r8   r   r  r  r   re   r  rD  KeyErrorrC  upperr    rD   r  r!   )r_   r>   r   r   rG   r  snippetr   r   markerr   datarE   rF   r  end_linerH   
hint_partss                     r&   _try_read_utf16z#ShellFileOperations._try_read_utf16V  s    gt$$Q'--//###4t,,,4':': ':F': ': 5zz': ': *	': ':
 0': ': ': 	V K$*@*@*I*IKKLLq  Y6=3FB%G%GZZ NT-C-CG-L-L N NOOF,V]-@bAA.//q  FQJJ4#&7"8"8899:@@BB	:47??gmmD!44Q77PWXXD9oGd=122K488J99::HHHi0 	 	 	44	 E>A%(*	R)9)9 R R R S
 	Hhl H H"H H%-H H3>H H H   **7F;;#*%%
 
 
 	
s   A4G G-,G-r   c                 @	   |                      |          }t          ||          \  }}|                     |                     |                    }|j        dk    ri|                     |          }|=|                     |||          }d| d| d}|j        r| d|j         n||_        |S |                     |          S t          |j
                  }|                                t          k    r|                     |          S 	 t          |                                          }	n# t          $ r d}	Y nw xY w|	t           k    r	 |                     |          rt%          dd|	d	
          S |                     |          }
|
Vt(          j                            |          d                                         t0          v }|p|                     |
          }nXd|                     |           d}|                     |          }t          |j
                  }|                     ||          }|r<|                     ||||	          }||S t%          d|	t;          |
|	                    S ddlm} d |            z  dz   }||z   dz
  }d| d| d|                     |           d| }|                     |          }|j        dk    rt%          d|j
                   S t          |j
                  }|dk    rtA          |          \  }}d|                     |           }|                     |          }t          |j
                  }	 t          |                                          }n# t          $ r d}Y nw xY w||k    }d}|rd|dz    d| d| d| d	}|s|!                    d          rod|                     |           d}|                     |          }t          |j
                  } |j        dk    r"|                                 d k    r
|dd!         }|	dk    rt%          d"ddd#$          S ||cxk    rdk    r n nt%          d"||	d%| d&| d'| d($          S t%          | "                    ||          ||	||)          S )*a  
        Read a file with pagination, binary detection, and line numbers.
        
        Args:
            path: File path (absolute or relative to cwd)
            offset: Line number to start from (1-indexed, default 1)
            limit: Maximum lines to return (default 500, max 2000)
        
        Returns:
            ReadResult with content, metadata, or error info
        r   N)r   r   zNote: 'zD' not found byte-for-byte; resolved to the unicode-equivalent file 'zM' (invisible encoding difference: NFC/NFD or special space/quote characters).ry  TzImage file detected. Automatically redirected to vision_analyze tool. Use vision_analyze with this file path to inspect the image contents.)rK   rJ   rG   rI   r   head -c 1000 r  rJ   rG   rO   r  r   zsed -n ',zp' z | cut -b1-Failed to read file: r   zwc -l < r  r  r  r  r  r,   z
tail -c 1 z | wc -l0r   zFile is empty (0 bytes).)rE   rF   rG   rI   zNote: offset z  is beyond the end of the file (z$ lines total). Retry with offset <= r   r  )#r  rP  rq  r  r   _unicode_variant_matchr   rI   _suggest_similar_filesr'   r   r   r  r  rg   rD  MAX_FILE_SIZEr  rD   r  r  r>   r  r  r   r  r|  r  r  r   rH  r  r:   endswithr  )!r_   r>   r   r   stat_resultvariantr   r|   stat_outputrG   sample_bytes
ext_binaryrJ   
sample_cmdsample_resultsample_outpututf16_resultr  line_clamp_bytesr  read_cmdread_resultread_output_wc_cmd	wc_result	wc_outputrF   rH   rI   tail_cmdtail_resulttail_outputs!                                    r&   r   zShellFileOperations.read_file  s      &&1&%@@ jj!5!5d!;!;<< A%% 11$77G"eLL#d # #4;# # #  :@N55555$..t4441+2DEE"666**4000	K--//00II 	 	 	III	 }$$ >>$ 		#\    ..t44#))$//288::>OOJ"Pd&B&B<&P&PIIS)?)?)E)ESSSJ JJz22M78LMMM..t]CCI 	  //feYOOL'###*<CC   < 	A@@@@@22444q8E>A%-v - - - -T-C-CD-I-I - -*- - 	 jj** A%%$PK<N$P$PQQQQ1+2DEE Q;;'44NK ;D22488::JJv&&	/	0@AA		ioo//00KK 	 	 	KKK	  (*	 	yxAxxVxxV^xxdoxxxD  	/[11$77 	/JD$:$:4$@$@JJJH**X..K5k6HIIK$))k.?.?.A.AS.H.H)#2#. >>/	    K####!#####'#&F & &#& &"& & &	 	 	 	 **;??#
 
 
 	
s$   !D. .D=<D=!M> >NNc                 t   t           j                            |          pd}t           j                            |          sdS dt          dt          fd           d|                     |           d}|                     |          }|j        dk    s|j        	                                sdS fd	t          |j                                                  D             }t          |          d
k    r8|dk    sd|v r&t           j                            ||d                   n|d         S dS )u  Find an existing file whose name is unicode-equivalent to ``path``.

        macOS names screenshots with a NARROW NO-BREAK SPACE (U+202F) before
        AM/PM, stores names NFD-decomposed, and Finder renames turn ' into
        ’ — all invisible in rendered text. Compare directory entries
        under a normalization that erases exactly those differences and
        return the on-disk spelling when exactly one entry matches.
        r   Nr   r   c                 l    t          j        d|           }dD ]\  }}|                    ||          }|S )NNFC))u    ry  )    ry  )u   ’r  )u   ‘r  )unicodedata	normalizer   )r   outr   r   s       r&   _canonz:ShellFileOperations._unicode_variant_match.<locals>._canon  sG     't44C , ,S kk#s++Jr(   ls -1 r  r   c                 B    g | ]}|r|k    
 |          k    |S r-   r-   )rW   entryr  r  r0   s     r&   r   z>ShellFileOperations._unicode_variant_match.<locals>.<listcomp>  sJ     
 
 

 (**vve}}/F/F /F/F/Fr(   r   rb  )r  r>   r  basenamere   r|  rq  r   r   r   r'   r   r8   r!   )	r_   r>   dir_pathls_cmd	ls_result
candidatesr  r  r0   s	         @@@r&   r  z*ShellFileOperations._unicode_variant_matchu  sb    7??4((/C7##D)) 	4	 	 	 	 	 	 !!H$00::HHHJJv&&	!##9+;+A+A+C+C#4
 
 
 
 
 
4Y5EFFQQSS
 
 

 z??a<DOOsVZ{{27<<*Q-888`jkl`mmtr(   c                    t           j                            |          pd}t           j                            |          }t           j                            |          d         }t           j                            |          d                                         }|                                }d|                     |           d}|                     |          }g }	|j        dk    r|j	        
                                r|j	        
                                                    d          D ]}
|
s|
                                }d}||k    rd}n:t           j                            |
          d                                         |                                k    rd}n|                    |          s|                    |          rd	}n||v rd
}n||v rt          |          dk    rd}n|rt           j                            |
          d                                         |k    r_t          |          t          |          z  }t          |          t          t          |          t          |                    dz  k    rd}|dk    r.t!          j        d||                                          dk    rd}|dk    r5|	                    |t           j                            ||
          f           |	                    d            d |	dd         D             }t-          d| |          S )z;Suggest similar files when the requested file is not found.r   r   r   r   2>/dev/null | head -50r,   d   Z   F   <      (   g?   Ng?r   c                     | d          S )Nr   r-   )xs    r&   <lambda>z<ShellFileOperations._suggest_similar_files.<locals>.<lambda>  s    1Q4% r(   )keyc                     g | ]\  }}|S r-   r-   )rW   r  fps      r&   r   z>ShellFileOperations._suggest_similar_files.<locals>.<listcomp>  s    ...%!R2...r(   r   File not found: )rO   rR   )r  r>   r  r  r  r  r|  rq  r   r   r   r   r6   r8   setrI  r  SequenceMatcherratior    r!   sortrD   )r_   r>   r  r  basename_no_extr  
lower_namer  r  scoredflfscorecommonsimilars                  r&   r  z*ShellFileOperations._suggest_similar_files  s   7??4((/C7##D))'**844Q7gx((+1133^^%%
 T$00::SSSJJv&&	!##	(8(>(>(@(@#%++--33D99 #F #F WWYY ##EEW%%a((+11337L7L7N7NNNEE]]:.. #*2G2G2K2K #EE2%%EE:%%#b''A++EE #RW--a00399;;sBB __s2ww6F6{{c#j//3r77&C&Cc&III " A::'"9*b# #%''S#! #! E199MM5"',,x*C*C"DEEE(((..6"1":...+T++!
 
 
 	
r(   c                    |                      |          }|                     |                     |                    }|j        dk    r|                     |          S t          |j                  }|                                t          k    r| 	                    |          S 	 t          |                                          }n# t          $ r d}Y nw xY w|                     |          rt          dd|          S |                     |          }|Vt          j                            |          d                                         t&          v }|p|                     |          }nV|                     d|                     |           d          }t          |j                  }	|                     ||	          }|r t          d|t/          ||                    S |                     d	|                     |                     }
|
j        dk    rt          d
|
j                   S t1          t          |
j                            \  }}t          ||          S )zRead the complete file content as a plain string.

        No pagination, no line-number prefixes, no per-line truncation.
        Uses cat so the full file is returned regardless of size.
        r   T)rK   rJ   rG   Nr   r  r  r  cat r  r   )rE   rG   )r  rq  r  r   r  r'   r   r   r  r  rg   rD  r  rD   r  r  r>   r  r  r   r  r|  r  r   r:   )r_   r>   r  r  rG   r  r  rJ   r  r  
cat_resultraw_contentr  s                r&   r   z!ShellFileOperations.read_file_raw  ss      &&jj!5!5d!;!;<< A%%..t4441+2DEE"666**4000	K--//00II 	 	 	III	>>$ 	RttyQQQQ..t44#))$//288::>OOJ"Pd&B&B<&P&PII JJ'at7M7Md7S7S'a'a'abbM78LMMM..t]CCI 	)*<CC    ZZ Et'='=d'C'C E EFF
1$$$OJ<M$O$OPPPP $$?
@Q$R$RSSQ
 
 
 	
s   %!C CCr   c                    |                      |          }|                     |                     |                    }|j        dk    rt	          d|           S t          |j                  }|                                t          k    r| 	                    |          S 	 t          |                                          }n## t          $ r t	          d|           cY S w xY w| ||k    rt	          |d|dd|dd	
          S |                     d|                     |                     }|j        dk    rt	          d|j                   S d                    t          |j                                                            }	 t          j        |d           n4# t          t          j        j        f$ r t	          d|           cY S w xY wt	          ||d          S )z9Read binary-safe bytes from any shell-backed environment.r   r.  r   zCould not determine file size: NzFile is too large (r  z bytes, limit is r  )rG   rO   z	base64 < zFailed to read binary file: r   Trz  z*Backend returned invalid binary data for: )rL   rG   rJ   )r  rq  r  r   rD   r'   r   r   r  r  rg   rD  r|  r!   r   r~  r  r  r  )r_   r>   r   r  r  rG   r  compacts           r&   r   z#ShellFileOperations.read_file_bytes
  sC     &&jj!5!5d!;!;<< A%%$=t$=$=>>>>1+2DEE"666**4000	NK--//00II 	N 	N 	N$Ld$L$LMMMMMM	N Y%:%:#XIXXX)XXXX   
 **G)?)?)E)EGGHH!!$S7>$S$STTTT''5gnEEKKMMNN	YWt44444FO12 	Y 	Y 	Y$WQU$W$WXXXXXX	Y"
 
 
 	
s$   #!C C%$C%F) ).GGc                 0    |                      |d          S )u(  Delete a single file.

        Cross-platform: runs via ``python -c`` against the terminal env's
        Python so it works on Windows shells (``cmd.exe``/PowerShell) that
        don't ship ``rm``. Directories are rejected here — use
        ``delete_path(recursive=True)`` for trees.
        Fr   _python_deleter   s     r&   r   zShellFileOperations.delete_file+  s     ""45"999r(   Fr   c                 0    |                      ||          S )a  Cross-platform delete that handles files and (with recursive=True)
        directory trees. Always preferred over emitting ``rm -rf`` /
        ``Remove-Item -Recurse`` directly so the same tool call works on
        every backend (local / docker / ssh / Windows).
        rB  rC  r   s      r&   r   zShellFileOperations.delete_path5  s     ""49"===r(   c                 
   |                      |          }t          |d          }|rt          |          S d|dt          |          d}|                     d|                     |                     }|j        dk    r6d	|j        pd
v r+|                     d|                     |                     }|j        dk    r1t          d| d|j        pd
                                pd           S t                      S )NDeleteverbr   z-import shutil, pathlib, sys
p = pathlib.Path(z)
recursive = aI  
try:
    if p.is_dir() and not p.is_symlink():
        if recursive:
            shutil.rmtree(p)
        else:
            print('is a directory: ' + str(p), file=sys.stderr); sys.exit(2)
    else:
        p.unlink()
except FileNotFoundError:
    pass
except Exception as exc:
    print(str(exc), file=sys.stderr); sys.exit(1)
r  r   r  r   r  zFailed to delete r   zunknown error)	r  r   rk   r<   rq  r|  r   r   r   )r_   r>   r   deniedr  r   s         r&   rD  z"ShellFileOperations._python_delete=  s<     &&'8<<< 	-V,,,,B $B B	??B B B 	, K$*@*@*I*IKKLL q  Y6=3FB%G%GZZ NT-C-CG-L-L N NOOFq  %s%s%s&-BUSUA\A\A^A^Aqbq%s%stttt}}r(   r   r   c                    |                      |          }|                      |          }||fD ]'}t          |d          }|rt          |          c S (|                     d|                     |           d|                     |                     }|j        dk    rt          d| d| d	|j                   S t                      S )
zMove a file via mv.MoverH  r   zmv ry  r   zFailed to move z -> r   )r  r   rk   rq  r|  r   r   )r_   r   r   r  rJ  r   s         r&   r   zShellFileOperations.move_filei  s    $$$$s 	1 	1A+AF;;;F 1"0000001M$((--MM0F0Fs0K0KMM
 
 q  %Vs%V%V%V%Vv}%V%VWWWW}}r(   c           	         |                      |          }t          |          }|rt          |          S t          j        d|          }|r*t          d| d|                    d          d          S t          j                            |          d         	                                }|t          v rt                              |          nd}|0 ||          \  }}	|s |	d	k    rt          d| d
| d|	 d          S |t          v p|                     |          }
|
rJ|nGd|                     |           d}|                     |          }|j        dk    r|j        r|j        }|                     ||          }|dk    rt'          |d          }|                     ||          rt+          |          s
t,          |z   }|                     |           t          j                            |          }t3          |          }	 |                    dd          }n.# t6          $ r!}t          d| d| d          cY d}~S d}~ww xY w|                     ||          }|j        dk    rt          d|j                   S t;          |          }d}	 d|                     |           d}|                     |          }|j        dk    r|j                                        rs|j                                                                        d         }tA          j!        |          "                                }||k    }|st          d| d          S n# tF          $ r d}Y nw xY w| $                    |||          }d}|j%        s|j&        r| '                    |||          }|r|}t          ||||r|(                                nd|          S )u  
        Write content to a file, creating parent directories as needed.

        Pipes content through stdin to avoid OS ARG_MAX limits on large
        files. The content never appears in the shell command string —
        only the file path does.

        Before anything touches disk, a fail-closed syntax gate runs
        against the CANDIDATE content: if ``path``'s extension is in
        ``_FAIL_CLOSED_INPROC_EXTS`` (JSON/YAML/TOML — structured data
        formats where a parse failure always means corruption) and the
        candidate content doesn't parse, the write is refused outright.
        No temp file, no rename, nothing on disk changes.

        After a write that clears the gate, runs a post-first / pre-lazy
        lint check via ``_check_lint_delta()``.  If the new content is
        clean, the lint call is O(one parse).  If the new content has
        errors the gate didn't already catch (i.e. errors from a linter
        outside ``_FAIL_CLOSED_INPROC_EXTS``, such as Python), the
        pre-write content is linted too and only errors newly introduced
        by this write are surfaced — pre-existing problems are filtered
        out so the agent isn't distracted chasing them.

        Args:
            path: File path to write
            content: Content to write
            pre_content: Pre-edit file content if the caller already has it
                (e.g. patch_replace read the file for fuzzy matching).
                When provided, skips a redundant ``cat`` subprocess to
                re-read the file for lint baseline / line-ending
                detection. BOM detection always probes disk (the most
                common provider — ``read_file_raw`` — strips BOMs, so
                trusting ``pre_content`` for BOM would cause false
                negatives and silent marker loss on rewrite). When
                None, reads from disk as before.

        Returns:
            WriteResult with bytes written, lint summary, or error.
        r   z[\ud800-\udc7f\udd00-\udfff]zRefusing to write 'z0': content contains a lone surrogate character (r   zH) that cannot be encoded as UTF-8. The file was NOT created or modified.r   Nr)  z': candidate content fails z syntax validation (zC). The file was NOT created or modified. Fix the content and retry.r<  r  r+   r  surrogateescapezFailed to write file: z
sha256sum #Post-write verification failed for u{   : on-disk content hash differs from the intended write. The write did not persist correctly — re-read the file and retry.r   post_content)rl   rm   rn   ro   rp   ))r  r   rk   r   r   r   r  r>   r  r  _FAIL_CLOSED_INPROC_EXTSLINTERS_INPROCr  _lsp_handles_extensionr|  rq  r   r   r  r4   r  r=   r7   _snapshot_lsp_baseliner  r<   encodeUnicodeEncodeErrorr  r8   r   r   hashlibsha256	hexdigestr#  _check_lint_deltarv   r   _maybe_lsp_diagnosticsr`   )r_   r>   rE   r   rJ  r   r  inproc_linter_ok	_lint_errwant_prer  r  original_endingr  rm   content_bytesr  write_resultrl   content_verifiedhash_cmdhash_resultdisk_shaexpected_shalint_resultrp   blocks                               r&   r   zShellFileOperations.write_file|  s   T   && (-- 	-V,,,, I5w?? 	N$ N N,-GGAJJN N N   B gt$$Q'--//36:R3R3R**3///X\$*]733NC 9
22"Nd N NN N4=N N N   . .(LD,G,G,L,L 	5&  M$"8"8">">LLL"jj22(A--+2D-"-"4K 77kJJf$$-gv>>G dK00 	*'9J9J 	*')G 	##D))) &&F||,		#NN74EFFMM! 	 	 	C$ C C,/C C C        	 ))$88!Q&&%Sl>Q%S%STTTT M** ,0	$ND$:$:4$@$@NNNH**X..K$))k.@.F.F.H.H)&-3355;;==a@&~m<<FFHH#+|#; ' &)$ ) ) )     	$ 	$ 	$#	$ ,,T{Y`,aa *. 	(+"5 	(//+G 0  E  ("''%%*5?$$&&&4+
 
 
 	
s1   H& &
I0IIICM% %M43M4r   r   r   c           
         |                      |          }t          |          }|rt          |          S d|                     |           d}|                     |          }|j        dk    rt          d|           S |j        }|}	t          |          \  }}
ddlm	}  |||||          \  }}}}|s|dk    riddlm
}  ||||          rt          ddd	| d
          S |pd| }	 ddlm} | |||||          z  }n# t          $ r Y nw xY wt          |          S t          |          }|rt          ||          }|                     |||	          }|j        rt          d|j                   S d|                     |           d}|                     |          }|j        dk    rt          d|           S t          |j                  \  }}
|                    dd                              dd          }|                    dd                              dd          }||k    r4t          d| dt%          |           dt%          |           d          S |                     |||          }|                     |||          }t          d||g|r|                                nd|j                  S )ai  
        Replace text in a file using fuzzy matching.

        Args:
            path: File path to modify
            old_string: Text to find (must be unique unless replace_all=True)
            new_string: Replacement text
            replace_all: If True, replace all occurrences

        Returns:
            PatchResult with diff and lint results
        r   r<  r  r   r  )fuzzy_find_and_replace)is_already_appliedTuT   File already contains the target text — the edit appears to be already applied to z0. No write performed; do not re-send this patch.)rv   r{   r|   z'Could not find match for old_string in )format_no_match_hint)r   zFailed to write changes: z2Post-write verification failed: could not re-read r+   r,   r2   rO  z5: on-disk content differs from intended write (wrote z chars, read back zb chars after normalizing line endings). The patch did not persist. Re-read the file and try again.rP  N)rv   rw   rx   ro   rp   )r  r   ru   r|  rq  r   r   r:   tools.fuzzy_matchrl  rm  rn  r#  r/   r4   r   rO   r   r8   r  r[  r`   rp   )r_   r>   r   r   r   rJ  r  r  rE   r>  r  rl  r  match_count	_strategyrO   rm  err_msgrn  file_endingrc  
verify_cmdverify_result_verify_bomless_verify_stdout_normalized_new_content_normalizedrw   ri  s                               r&   r   z!ShellFileOperations.patch_replace  s      && (-- 	-V,,,, E$0066DDDjj** A%%%CT%C%CDDDD$   ((
 	=<<<<<5K5KZ[6
 6
2[)U  	.K1$$ =<<<<<!!':zBB 	" "@<@@ @ @	    OOOOGBBBBBB//jRYZZZ   W---- *'22 	L1+{KKK
 t[3> ' @ @ 	W%UAS%U%UVVVV GD22488FFF


:.."a''%`Z^%`%`aaaa ((<==$3$;$;FD$I$I$Q$QRVX\$]$]!"-"5"5fd"C"C"K"KDRV"W"W$(???Md M M566M M 011M M M    !!';==
 ,,TwU`,aa 6*5?$$&&&4 )8
 
 
 	
s   0D 
DDr   c                 t    ddl m}m}  ||          \  }}|rt          d|           S  |||           }|S )a  
        Apply a V4A format patch.
        
        V4A format:
            *** Begin Patch
            *** Update File: path/to/file.py
            @@ context hint @@
             context line
            -removed line
            +added line
            *** End Patch
        
        Args:
            patch_content: V4A format patch string
        
        Returns:
            PatchResult with changes made
        r   )parse_v4a_patchapply_v4a_operationszFailed to parse patch: r   )tools.patch_parserrz  r{  ru   )r_   r   rz  r{  
operationsparse_errorr   s          r&   r   zShellFileOperations.patch_v4a 	  sm    ( 	MLLLLLLL"1/-"@"@
K 	N%L{%L%LMMMM &%j$77r(   c                    t           j                            |          d                                         }t                              |          }||Ud|                     |           d}|                     |          }|j        dk    rt          dd| d	          S |j
        } ||          \  }}|d
k    rt          dd| d	          S t          ||rdn|          S |t          vrt          dd| d	          S |dk    r&|                     |          rt          dd	          S |t          v r*|                     |          rt          dd| d	          S t          |         }	|	                                d         }
|                     |
          st          d|
 d	          S |	                    d|                     |                    }|                     |d          }|j        dk    rt'          |
|j
                  rzddlm}  ||j
                                                  }t/          d |                                D             |dd                   }t          d|
 d|dd          	          S t          |j        dk    |j
                                        r|j
                                        nd          S )u8  
        Run syntax check on a file after editing.

        Prefers the in-process linter for structured formats (JSON, YAML,
        TOML) when possible — those parse via the Python stdlib in
        microseconds and don't require a subprocess.  Falls back to the
        shell linter table for compiled/type-checked languages
        (py_compile, node --check, tsc, go vet, rustfmt).

        Args:
            path: File path (used to select the linter + for shell invocation).
            content: Optional file content.  If provided AND an in-process
                     linter matches the extension, we lint the content
                     directly without re-reading the file from disk.  Ignored
                     for shell linters.

        Returns:
            LintResult with status and any errors.
        r   Nr<  r  r   TzFailed to read z	 for lint)r   r   r)  zNo linter available for z (missing dependency)r   rv   r   zNo linter for z filesr  u   Project tsconfig.json detected — per-file tsc skipped (single-file tsc can't resolve project aliases/globals; use the LSP tier or `tsc -p tsconfig.json` for real diagnostics).zLSP server handles u    — shell linter skippedz not availablez{file}r'  ri  )
strip_ansic              3   f   K   | ],}|                                 |                                 V  -d S rU   r   rW   lns     r&   r  z2ShellFileOperations._check_lint.<locals>.<genexpr>	  s7      IIbhhjjIIIIIIIr(   x   z not usable:    )r  r>   r  r  rS  r  r|  rq  r   r   r   LINTERS_has_ancestor_tsconfig_SHELL_LINTER_LSP_REDUNDANT_lsp_will_handler   ru  r   r  r  tools.ansi_stripr  r   nextr   )r_   r>   rE   r  inprocr  r  r   err
linter_cmdr  rr  r   r  r%   
first_lines                   r&   _check_lintzShellFileOperations._check_lint>	  sP   ( gt$$Q'--//  ##C((L$"8"8">">LLL"jj22(A--%d<]d<]<]<]^^^^%,fWooGBj  !$8mSV8m8m8mnnnnbr1BsCCCC gd4PS4P4P4PQQQQ %<<D77==<$   " ---$2G2G2M2M-LcLLL   
 S\
##%%a(  ** 	Qdx4O4O4OPPPP   4+G+G+M+MNNC,,q  %@6=%Y%Y  433333 j//5577GIIg&8&8&:&:III J #DD*TcT2BDD   
 $),2M,?,?,A,AI6=&&(((r
 
 
 	
r(   rQ  c                    |                      ||          }|j        s|j        r|S ||S |                      ||          }|j        s|j        s|j        s|S d |j                                        D             fd|j                                        D             }|st          d|j        d          S t          ddd	                    |          z   
          S )u\  
        Run post-write syntax lint with pre-write baseline comparison.

        Two-tier strategy:

        1. **Syntax check** (in-process or shell-based, microseconds).
           Catches the bug class that motivated this layer: corrupt
           writes, mashed quotes, truncated output.  Hot path.

        2. **Delta refinement against pre-write content** when the
           syntax tier reports errors.  Filter out errors that already
           existed pre-edit so the agent isn't distracted by inherited
           state.

        Semantic diagnostics from the LSP layer are fetched separately
        via :meth:`_maybe_lsp_diagnostics` and surfaced in the
        ``lsp_diagnostics`` field on :class:`WriteResult` /
        :class:`PatchResult`.  Keeping the two channels separate lets
        the agent (and any downstream parsers) read syntax errors and
        semantic errors as independent signals.

        Args:
            path: File path (for linter selection).
            pre_content: File content BEFORE the write.  Pass None for new
                         files or when the pre-state isn't available — the
                         delta refinement is skipped and all post errors
                         are returned.
            post_content: File content AFTER the write.  Optional; if None,
                          the shell linter reads from disk (same as
                          _check_lint).

        Returns:
            LintResult.  ``output`` contains either the full post-lint
            errors (no pre-state) or just the new-error lines (delta
            refinement applied).
        )rE   Nc                 ^    h | ]*}|                                 |                                 +S r-   r  r  s     r&   	<setcomp>z8ShellFileOperations._check_lint_delta.<locals>.<setcomp>	  s-    PPPBRXXZZPRXXZZPPPr(   c                 h    g | ].}|                                 |                                 v,|/S r-   r  )rW   r  	pre_liness     r&   r   z9ShellFileOperations._check_lint_delta.<locals>.<listcomp>	  s9    jjjRrxxzzjbhhjj`iNiNibNiNiNir(   Fu^   Pre-existing lint errors — this edit didn't introduce new ones but the file is still broken.)rv   r   r   zLNew lint errors introduced by this edit (pre-existing errors filtered out):
r,   r  )r  rv   r   r   r   r   r!   )r_   r>   r   rQ  postpre
post_linesr  s          @r&   r[  z%ShellFileOperations._check_lint_delta	  s-   L l;; < 	4< 	K Kt[99; 	#+ 	SZ 	 K QP#**?*?*A*APPP	jjjj4;#9#9#;#;jjj
 	 {x    8:>))J:O:OP
 
 
 	
r(   c                 ~    t          | dd          }|dS 	 ddlm} n# t          $ r Y dS w xY wt	          ||          S )u}  Return True iff this FileOperations is wired to a local backend.

        LSP servers run on the host process — they need access to the
        files they're linting.  Remote/sandboxed backends (Docker,
        Modal, SSH, Daytona) keep files inside the sandbox where the
        host-side LSP server can't reach them, so we skip the LSP
        path for those entirely.
        rc  NFr   )LocalEnvironment)rd  r  r  r#  
isinstance)r_   rc  r  s      r&   _lsp_local_onlyz#ShellFileOperations._lsp_local_only
  sn     dE4((; 5	AAAAAAA 	 	 	55	#/000s    
,,r  c                     |sdS 	 ddl m} n# t          $ r Y dS w xY w|                                }|D ]}||j        v r dS dS )u  Return True iff some registered LSP server claims this extension.

        Used to decide whether to capture pre-write content for the
        line-shift map.  Capturing is cheap (one ``cat`` on the host)
        but pointless if no LSP would ever look at the file.

        Safe to call on remote backends — the registry is purely
        in-process metadata; we still gate the actual LSP path on
        :meth:`_lsp_local_only`.
        Fr   )SERVERST)agent.lsp.serversr  r#  r  
extensions)r_   r  r  	ext_lowersrvs        r&   rT  z*ShellFileOperations._lsp_handles_extension
  s      	5	1111111 	 	 	55	IIKK	 	 	CCN**tt +us    
c                    |                                  sdS 	 t          j                            t          j                            |                    }	 t          j                            t          j                            |d                    rdS t          j                            |          }||k    rdS |}i# t          $ r Y dS w xY w)u  True iff a tsconfig.json exists in *path*'s directory or any ancestor.

        A single-file ``tsc`` invocation can't read that config, so its
        diagnostics for such a file are pure noise (unresolved aliases /
        ambient globals). Used by :meth:`_check_lint` to skip the per-file
        shell tsc for project TypeScript files.

        Best-effort and local-host only: a host-side ``os.path`` walk. On a
        remote/sandboxed backend the project tree isn't on this host, so the
        walk returns False and the shell linter runs exactly as before — never
        suppress lint based on a probe that couldn't answer.
        FTztsconfig.json)r  r  r>   r  abspathisfiler!   r#  )r_   r>   dr  s       r&   r  z*ShellFileOperations._has_ancestor_tsconfig1
  s     ##%% 	5
	 5 566A7>>"',,q/"B"BCC  4++Q;; 5  	 	 	55	s   A:B> %B> ;B> >
CCc                 
   |                                  sdS 	 ddlm} n# t          $ r Y dS w xY w	  |            }n# t          $ r Y dS w xY w|dS 	 t	          |                    |                    S # t          $ r Y dS w xY w)u!  Return True iff the LSP service is active AND will lint this file.

        Stronger than :meth:`_lsp_handles_extension` — that one only checks
        the static server registry.  This one additionally requires the
        LSP service to be configured/enabled and the file to pass
        :meth:`agent.lsp.manager.LSPService.enabled_for` (which gates on
        workspace detection, disabled-server set, and the broken-pair
        short-circuit).

        Used by :meth:`_check_lint` to decide whether to skip the per-file
        shell linter for extensions in ``_SHELL_LINTER_LSP_REDUNDANT``.

        Best-effort: any failure path returns False so the shell linter
        runs as before — never suppress lint based on an LSP probe that
        couldn't actually answer the question.
        Fr   get_service)r  	agent.lspr  r#  r<   enabled_forr_   r>   r  svcs       r&   r  z$ShellFileOperations._lsp_will_handleL
  s    " ##%% 	5	------- 	 	 	55		+--CC 	 	 	55	;5	--... 	 	 	55	s-    
--
< 
A
	A
!A4 4
BBc                     |                                  sdS 	 ddlm}  |            }n# t          $ r Y dS w xY w|dS 	 |                    |           dS # t          $ r Y dS w xY w)uL  Capture pre-edit LSP diagnostics so the post-write delta is correct.

        Best-effort.  Silent on every failure path — LSP is an
        enrichment layer and must never break a write.

        Skipped entirely on non-local backends (Docker, Modal, SSH,
        etc.) — the server can't see files inside the sandbox.
        Nr   r  )r  r  r  r#  snapshot_baseliner  s       r&   rU  z*ShellFileOperations._snapshot_lsp_baselinen
  s     ##%% 	F	------+--CC 	 	 	FF	;F	!!$''''' 	 	 	DD	s   ) 
77A 
A$#A$rP  c                   |                                  sdS 	 ddlm} n# t          $ r Y dS w xY w	  |            }n# t          $ r Y dS w xY w||                    |          sdS d}|.|,||k    r&	 ddlm}  |||          }n# t          $ r d}Y nw xY w	 |                    |d|          }n# t          $ r Y dS w xY w|sdS 	 ddlm	}	m
}
  |	||          }|sdS  |
d	|z             S # t          $ r Y dS w xY w)
u  Best-effort LSP semantic diagnostics for ``path``.

        Returns a formatted ``<diagnostics>`` block, or empty string
        when LSP is unavailable / disabled / produced no errors.

        When both ``pre_content`` and ``post_content`` are provided,
        a line-shift map is built and passed to the LSPService so
        baseline diagnostics are remapped into post-edit coordinates
        before the set-difference.  Without this, edits that delete
        or insert lines surface every pre-existing diagnostic below
        the edit point as "introduced by this edit".

        Wraps everything in a try/except so a misbehaving LSP server
        can't break a write.  This intentionally swallows all errors
        — the calling tier already returned a clean syntax result, so
        ``""`` here just means "no extra info to add".

        Skipped entirely on non-local backends (Docker, Modal, SSH,
        etc.) — same reasoning as ``_snapshot_lsp_baseline``.
        r   r   r  N)build_line_shiftT)delta
line_shift)report_for_filetruncatez)LSP diagnostics introduced by this edit:
)r  r  r  r#  r  agent.lsp.range_shiftr  get_diagnostics_syncagent.lsp.reporterr  r  )r_   r>   r   rQ  r  r  r  r  r   r  r  rj  s               r&   r\  z*ShellFileOperations._maybe_lsp_diagnostics
  s   6 ##%% 	2	------- 	 	 	22		+--CC 	 	 	22	;cood33;2
 
"|'?KS_D_D_"BBBBBB--k<HH

 " " "!


"	224tPZ2[[KK 	 	 	22	 	2	DDDDDDDD#OD+66E r8H5PQQQ 	 	 	22	sW    
--
< 
A
	A
3B BBB2 2
C ?C C.  C. .
C<;C<r   r   r   r   r0   r   r   r   c	           
      *   t          ||          \  }}|                     |          }|                     d|                     |           d          }	d|	j        v r|                     ||||||||          }
|
|
S t          j                            |          pd}t          j        	                    |          }d| g}|                     d|                     |           d          }d	|j        v r=|r:|                     d
|                     |           d          }|j
        dk    r|j                                        r|                                }g }|j                                                            d          D ]q}|s|                                }||v s!||v s|                    |dd                   r3|                    t          j                            ||                     r|r3|                    dd                    |dd                   z              t#          d                    |          d          S |dk    r|                     ||||          S |                     |||||||          S )a\  
        Search for content or files.
        
        Args:
            pattern: Regex (for content) or glob pattern (for files)
            path: Directory/file to search (default: cwd)
            target: "content" (grep) or "files" (glob)
            file_glob: File pattern filter for content search (e.g., "*.py")
            limit: Max results (default 50)
            offset: Skip first N results
            output_mode: "content", "files_only", or "count"
            context: Lines of context around matches
        
        Returns:
            SearchResult with matches or file list
        test -e ! && echo exists || echo not_found	not_foundNr   zPath not found: ztest -d z && echo yes || echo nort  r  z 2>/dev/null | head -20r   r,   r  zSimilar paths: r   r   z. rO   r   r   )rT  r  rq  r|  r   _try_multi_path_searchr  r>   r  r  r   r   r  r   r6   r    r!   r   _search_files_search_content)r_   r   r>   r0   r   r   r   r   r   checkmultir  basename_queryr  parent_checkr  lower_qr  r  les                       r&   r   zShellFileOperations.search
  s   & 4FEBB   && 

ed&<&<T&B&Beeeff%,&&
 //vy%g E  W__T**1cFW--d33N3T334J::R411&99RRR L ++++ JJTT33F;;TTT 	 &!++	0@0F0F0H0H+,2244G!#J!*!1!7!7!9!9!?!?!E!E K K$ %$"[[]]"b==B'MMR]]7SUTUSU;=W=WM&--bgll65.I.IJJJ! "))-		*RaR.0I0II    ii
++   
 W%%gtUFCCC''y%(3W> > >r(   c	           
      D   d |                     d          D             }	t          |	          dk     rdS g g }}
|	D ]c}|                     |          }|                     d|                     |           d          }d|j        v r|
n|                    |           d|
sdS t                      }|
D ]}|dk    r|                     ||||          }n| 	                    |||||||          }|j
        rD|j                            |j                   |j                            |j                   |j                            |j                   |xj        |j        z  c_        |j        p|j        |_        |j        d|         |_        |j        d|         |_        d	t          |	           d
t          |
           d}|rO|dd                    |dd                   z   z  }t          |          dk    r|dt          |          dz
   dz  }||_        |S )a  Recover a not-found ``path`` that is really several paths in one string.

        Production trajectories show models passing "dir1 dir2 dir3" (or
        comma-separated lists) as ``path``. Split on whitespace/commas; when
        at least one candidate exists and at least two candidates were given,
        search every existing path, merge results, and note skipped parts.
        Returns None when this doesn't look like a multi-path string.
        c                 h    g | ]/}|                                 D ]}|                                |0S r-   )r   r   )rW   chunkr  s      r&   r   z>ShellFileOperations._try_multi_path_search.<locals>.<listcomp>  s:    TTTuTTA!''))TTTTTr(   r  r%  Nr  r  existsr   zpath contained z entries; searched z that existz; skipped missing: r   r   (+ more))r   r8   r  rq  r|  r   r    r   r  r  rO   r   extendr   r   updater   rH   r!   rq   )r_   r   r>   r0   r   r   r   r   r   partsexistingmissingr  expandedchkmergedr   r|   s                     r&   r  z*ShellFileOperations._try_multi_path_search  s`    UT

3TTTu::>>4' 	O 	OA((++H**^411(;;^^^ C "SZ//XXWDDXNNNN 	4 	A 	AA  ((!UFCC**7Ay%+6A Ay N!!#+...L	***M  ,,,#/1%/@3=F/|FUF+ZUZZHZZZ 	7)DIIgbqbk,B,BBBD7||a6c'llQ.6666r(   c           
         |                      d          sdS dt          fd}ddt          dt          fd}|rd	|                     |           nd
}|                     d| d|                     |           d|                     |           dd          } ||j                  \  }}	|dk    r"d| dt          |	           d ||	           dS |                     d| d|                     |           d|                     |           dd          }
 ||
j                  \  }}|dk    r"d| dt          |           d ||           dS t          j	        d|          r|                     d| d|                     |           d|                     |           dd          } ||j                  \  }}|dk    r"d| dt          |           d ||           dS dS ) a  Return a hint for a 0-match content search, or None.

        13.9% of production content searches return zero matches and give
        the model nothing to steer by. Run ONE cheap case-insensitive count
        probe; if it hits, say so. If the pattern contains regex
        metacharacters, also probe it as a fixed string. Bounded: two rg
        invocations max, count-only output.
        rgNr   c                    d}g }| pd                                                                 D ]V}|                    d          \  }}}|                                r'|t	          |          z  }|                    |           W||fS )z3Parse ``path:count`` lines from rg --count-matches.r   r   :)r   r   
rpartitionisdigitrg   r    )r   totalper_filer#   r  _sepns          r&   _tallyz5ShellFileOperations._zero_match_probe.<locals>._tallyQ  s    EH2,,..99;; ' '!__S11
499;; 'SVVOEOOA&&&(?"r(   r   capr   c                     d                     | d |                   }t          |           |z
  }||dk    rd| dndz   S )Nr   r   r  r  r   )r!   r8   )r  r  shownextras       r&   _paths_notez:ShellFileOperations._zero_match_probe.<locals>._paths_note\  sO    IIhttn--EMMC'E5199/%////"EEr(   z --glob r   zrg -i --count-matchesry  r   r'  r  r   z0 exact matches, but z case-insensitive match(es) in z
 file(s): u'    — the pattern's casing may be wrong.z'rg --hidden --no-ignore --count-matchesz 0 matches in visible files, but z match(es) in z hidden or gitignored file(s): u#    — these are excluded by default.z[.\[\](){}?*+^$\\|]zrg -F --count-matchesz0 regex matches, but z literal match(es) in ug    — the pattern contains regex metacharacters that likely need escaping (or pass a simpler substring).)r   )
ru  re   rg   r|  rq  r  r   r8   r   r   )r_   r   r>   r   r  r  	glob_exprprobeci_totalci_pathshiddenh_totalh_pathsfixedf_totalf_pathss                   r&   _zero_match_probez%ShellFileOperations._zero_match_probeD  s)      && 	4		#3 		# 		# 		# 		#	F 	Fs 	F3 	F 	F 	F 	F
 GPWBt55i@@BBBUW	

&I & &%%g..& &151M1Md1S1S& & & 	  
 
 $VEL11(a<<5 5 5(mm5 5/:{8/D/D5 5 5 &i & &%%g..& &151M1Md1S1S& & & 	  
 
 "6&-00Q;;M7 M Mw<<M M;w''M M M
 9+W55 	JJ*	 * *))'22* *595Q5QRV5W5W* * * 	   E  &vel33GW{{>G > >7||> >/:{7/C/C> > > tr(   c                 ~   |                     d          sd|vr|}n|                    d          d         }t          |          }t          d |j        D                       }|                     d          r|                     ||||          S |                     d          st          d          S |sd	nd
}|rd| nd
}	d
}
|sd|dz    d| }
d|                     |           |	 d|                     |           d|
 }| 	                    |d          }t          |          \  }}|                                s^|s\d|                     |           |	 d|                     |           d|
 }| 	                    |d          }t          |          \  }}g }|                                                    d          D ]}|s|                    dd          }t          |          dk    rJ|d                             dd
                                          r|                    |d                    x|                    |           |r|                                }g }|D ]}	 t          |                                                              |          j        }n$# t$          $ r t          |          j        }Y nw xY wt          d |D                       r{|                    |           ||||z            }t          |t          |          t'          |          |          S )z-Search for files by name pattern (glob-like).z**/rb  r  c              3   H   K   | ]}|d vo|                     d          V  dS >   ..r   r   Nr6   rW   parts     r&   r  z4ShellFileOperations._search_files.<locals>.<genexpr>  sL       '
 '
 #<(<(<'
 '
 '
 '
 '
 '
r(   r  r  zFile search requires 'rg' (ripgrep) or 'find'. Install ripgrep for best results: https://github.com/BurntSushi/ripgrep#installationr   z-not -path '*/.*'r   ry  z | tail -n +r   z | head -n zfind z -type f -name z* -printf '%T@ %p\n' 2>/dev/null | sort -rnr$  r  z 2>/dev/null | sort -rnr,   r%  r   r   c              3   H   K   | ]}|d vo|                     d          V  dS r  r  r  s     r&   r  z4ShellFileOperations._search_files.<locals>.<genexpr>  s8      ^^Dt;.G4??33G3G^^^^^^r(   r   r   rH   r   )r6   r   r   r  r  ru  _search_files_rgr   r|  rq  r   r   r8   r   r  r    resolverelative_torD  r<   )r_   r   r>   r   r   search_patternsearch_roothas_hidden_path_ancestorhidden_excludehidden_filter_exprpagination_exprrr  r   r   r   
cmd_simpler   r#   r  normalized_rootfiltered_files	file_path	rel_partss                          r&   r  z!ShellFileOperations._search_files  s    !!%(( 	4S-?-?$NN$]]3//3N4jj#& '
 '
#)'
 '
 '
 $
 $
  T"" 	N((ufMMM   (( 	K    5MT,,RT5CK1111
 ' 	LKVaZKKEKKOMd,,T22 M4F M MW[WmWmn|W}W} M M;JM M C,,7??||~~ 	Dl 	DC!7!7!=!= C?Q C Cbfbxbx  zH  cI  cI C C1@C CJZZ
BZ77F#;F#C#C FLLLNN((.. 	# 	#D JJsA&&E5zzQ58#3#3C#<#<#D#D#F#FU1X&&&&T""""
 $ 	:)1133ON" 1 1	6 $Y 7 7 9 9 E Eo V V \II! 6 6 6 $Y 5III6^^T]^^^^^ %%i0000"6&5.#89E E

<((%	
 
 
 	
s   79J11KKc                    d|vr|                     d          sd| }n|}||z   }d|                     |           d|                     |           d| }|                     |d          }t	          |          \  }	}
d |	                                                    d	          D             }|s|
sd
|                     |           d|                     |           d| }|                     |d          }t	          |          \  }	}
d |	                                                    d	          D             }||||z            }t          |t          |          t          |          |k    pt          |
          |
          S )ad  Search for files by name using ripgrep's --files mode.

        rg --files respects .gitignore and excludes hidden directories by
        default, and uses parallel directory traversal for ~200x speedup
        over find on wide trees.  Results are sorted by modification time
        (most recently edited first) when rg >= 13.0 supports --sortr.
        rb  *zrg --files --sortr=modified -g ry  z 2>/dev/null | head -n r$  r  c                     g | ]}||S r-   r-   rW   r6  s     r&   r   z8ShellFileOperations._search_files_rg.<locals>.<listcomp>  s    @@@1a@Q@@@r(   r,   zrg --files -g c                     g | ]}||S r-   r-   r  s     r&   r   z8ShellFileOperations._search_files_rg.<locals>.<listcomp>
      DDDq!DDDDr(   r  )
r6   r|  r  rq  r   r   r   r   r8   r<   )r_   r   r>   r   r   glob_patternfetch_limit
cmd_sortedr   r   r   	all_files	cmd_plainpages                 r&   r  z$ShellFileOperations._search_files_rg  s    gg&8&8&=&=(w==LL"Lfn'd.D.D\.R.R ' '++D11' '$' ' 	
 J337??@@ 4 4T : :@@@	 		E 		E+!7!7!E!E + +//55+ +(+ + 
 ZZ	2Z66F#;F#C#C FLDDFLLNN$8$8$>$>DDDI./I)nn3ItL7I7I%	
 
 
 	
r(   c           	         d}|                      d          rd}|                     |||||||          }	nA|                      d          r|                     |||||||          }	nt          d          S |	j        sg|	j        dk    r\|	j        sU|	j        sN|	j        sG	 | 	                    |||          }
n# t          $ r d}
Y nw xY w|
r|	j        s|
n|	j         d	|
 |	_        |r|	S t          |	|          S )
z,Search for content inside files (grep-like).Fr  TgrepzqContent search requires ripgrep (rg) or grep. Install ripgrep: https://github.com/BurntSushi/ripgrep#installationr   r   Nry  )ru  _search_with_rg_search_with_grepr   rO   r   r   r   r   r  r#  rq   r\  )r_   r   r>   r   r   r   r   r   used_rgr   rI   s              r&   r  z#ShellFileOperations._search_content  sn    T"" 	G))'4E6*5w@ @FFv&& 	++GT9eV,7B BFF  \     	\!3q!8!8 "9/5| "9DJM "9--gtYGG    \-3^![FNA[A[UYA[A[  	M8IIIs   B7 7CCc                 	   g d}t          |          }	|	r|                    d           |dk    r$|                    dt          |          g           |r*|                    d|                     |          g           |dk    r|                    d           n|dk    r|                    d	           |                    |                     |                     |                    |                     |                     |dk    r||z   d
z   n||z   }
|                    dddt          |
          g           dd                    |          z   }|                     |d          }t          |          \  }}t          |          \  }}|j
        dk    rW|                                sC|                                p|j                                        pd}t          d| d          S |}|	rdnd}|dk    rnd |                                                    d          D             }t          |          }||||z            }t          ||t!          |          ||          S |dk    ri }|                                                    d          D ]_}d|v rY|                    dd          }t          |          dk    r0	 t%          |d                   ||d         <   O# t&          $ r Y [w xY w`t          |t)          |                                          t!          |          |          S t-          j        d          }g }|                                                    d          D ]}|r|dk    r|                    |          }|r|                    t3          |                    d          pd |                    d          z   t%          |                    d!                    |                    d"          dd#         $                     |dk    rPt7          |          }|r?|                    t3          |d         |d         |d         dd#         $                     t          |          }||||z            }t          |||||z   k    pt!          |          ||%          S )&zSearch using ripgrep.)r  z--line-numberz--no-headingz--with-filenamerY  r   -Cz--glob
files_only-lcount-cr  r  r.   -nset -o pipefail; ry  r$  r  r%  Search errorSearch failed: r  ut   Pattern contains \n — multiline mode (-U) was enabled automatically so the regex can match across line boundaries.Nc                     g | ]}||S r-   r-   r  s     r&   r   z7ShellFileOperations._search_with_rg.<locals>.<listcomp>  r  r(   r,   )r   r   rH   r   rq   r  r   r   r   rH   r   ^([A-Za-z]:)?(.*?):(\d+):(.*)$r   r   r  r     r   )r   r   rH   r   rq   )rW  r    r  re   r|  r  r!   rq  r   r   r   r   r   r   r   r8   r<   rsplitrg   rD  r  valuesr   compiler   r   r   r   )r_   r   r>   r   r   r   r   r   	cmd_parts	multiliner  rr  r   r   r   r   r   	error_msg_ml_noter  r  r  r   r#   r  	_match_rer   r   parseds                                r&   r  z#ShellFileOperations._search_with_rg;  sB    ONN	 /w77	 	,]+++ Q;;dCLL1222  	Lh(>(>y(I(IJKKK ,&&T""""G##T""" 	//88999 	55d;;<<<
 /6kkefns**uv~#vtS-=-=>??? "CHHY$7$77C,,7??  7v>>W q   #))++Vv}/B/B/D/DVI&C	&C&CQRSSSS  != =  	
 ,&&DDFLLNN$8$8$>$>DDDI	NNEVFUN23D!|,,)     G##F,,T22 ! !$;; KKQ//E5zzQ!/258}}F58,,) ! ! ! D!00|,,)	    
#DEEIG,,T22   tt|| OOD)) NN;ggajj.B!''!**<$'

OO !

4C4 0$ $ $   
  Q;;7==F {!'(.q	$*1IdsdO( ( (    LLE6&5.01D!&5.0FD4F4F)    s   K00
K=<K=c                 X
   ddg}|                     d           |dk    r$|                    dt          |          g           |r*|                    d|                     |          g           |dk    r|                     d           n|d	k    r|                     d
           |                     |                     |                     |                    d          p!t          t          j        d|                    }	|	r|                     |          }
nB|                    d          r
|dd         n|}d}
|dvr|
d|                     |           z  }
|                     |
           ||z   |dk    rdndz   }|                    dddt          |          g           dd                    |          z   }| 	                    |d          }t          |          \  }}t          |          \  }}|j        dk    rW|                                sC|                                p|j                                        pd}t          d| d          S |}|dk    rmd |                                                    d          D             }t#          |          }||||z            }t          ||t          |          |           S |d	k    ri }|                                                    d          D ]_}d!|v rY|                    d!d"          }t#          |          dk    r0	 t'          |d"                   ||d         <   O# t(          $ r Y [w xY w`t          |t+          |                                          t          |          |#          S t          j        d$          }g }|                                                    d          D ]}|r|d%k    r|                    |          }|r|                     t1          |                    d"          pd&|                    d          z   t'          |                    d'                    |                    d(          dd)         *                     |dk    rPt5          |          }|r?|                     t1          |d         |d"         |d         dd)         *                     t#          |          }||||z            }t          |||||z   k    pt          |          |+          S ),zFallback search using grep.r  z-rnHEz--exclude-dir='.*'r   r  z	--includer  r  r  r   )rb  z\\z^[A-Za-z]:[\\/]z./r%  Nz"$PWD">   r   r   rb  r  r  r.   r!  r"  ry  r$  r  r#  r$  r  c                     g | ]}||S r-   r-   r  s     r&   r   z9ShellFileOperations._search_with_grep.<locals>.<listcomp>  r  r(   r,   r  r  r   r&  r'  r   r   r  r   r(  r   )r   r   rH   r   )r    r  re   r|  r6   r<   r   r   r!   rq  r   r   r   r   r   r   r   r8   r)  rg   rD  r  r*  r+  r   r   r   )r_   r   r>   r   r   r   r   r   r,  is_absoluter   relative_pathr  rr  r   r   r   r   r   r.  r  r  r  r   r#   r  r0  r   r   r1  s                                 r&   r  z%ShellFileOperations._search_with_grep  s    W%	 	-... Q;;dCLL1222  	Ok4+A+A)+L+LMNNN ,&&T""""G##T""" 	//88999oom44 
H'..9
 9
  	K0066KK(,(=(=GDHH4M"KI--J4#9#9-#H#HJJJ%%% fnw{{B#vtS-=-=>??? "CHHY$7$77C,,7??  7v>>W q   #))++Vv}/B/B/D/DVI&C	&C&CQRSSSS,&&DDFLLNN$8$8$>$>DDDI	NNEVFUN23D!|,,)	    G##F,,T22 ! !$;; KKQ//E5zzQ!/258}}F58,,) ! ! ! D!00|,,)	    
#DEEIG,,T22   tt||OOD)) NN;ggajj.B!''!**<$'

OO !

4C4 0$ $ $   
 Q;;7==F {!'(.q	$*1IdsdO( ( (    LLE6&5.01D!&5.0FD4F4F)	   s   4M
M M rU   )NNN)rv  )r   r   r   r   )@ra   rb   rc   rd   re   rg  rg   r   rq  r<   ru  r  staticmethodbytesr  r  r  r  r  r|  r  r  r   r  r  r  r  rD   r  r  r  r  r   r  r  r   r   rk   r   r   rD  r   r   ru   r   r   r   r  r[  r  rT  r  r  rU  r\  r   r   r  r  r  r  r  r  r  r-   r(   r&   r^  r^  o  s	        !2 !2# !2 !2 !2 !2F CG $%
 %
S %
s %
C %
%
)6%
 %
 %
 %
N( ( ( ( ( ( s C    >    $       \ D c 3 $    :'c 'd ' ' ' '
# # ## #c # # # #4# # # # # #J7S 7S 7 7 7 7"73 73 7 7 7 78V6# V6 V6 V6 V6 V6 V6p7 7S 7x} 7X`adXe 7 7 7 7*, ,# ,HSM ,T , , , ,(	 	3 	# 	RU 	 	 	 	
C 
C 
 
 
 
0 
 
 
 
 
 \
$ (\
C \
 \
S \
#&\
+A\
 \
 \
 \
|
 
c 
3 
3 
* 
 
 
 
B*3 *8C= * * * *X9
3 9
: 9
 9
 9
 9
v,
# ,
* ,
 ,
 ,
 ,
\
 
C 
HSM 
Z 
 
 
 
B: : : : : :> > > > > > > >*3 *4 *K * * * *XS s {    ( 15K
 K
s K
S K
 (K
9DK
 K
 K
 K
d +0Q
 Q
# Q
3 Q
C Q
#'Q
4?Q
 Q
 Q
 Q
fs {    <p
 p
 p
hsm p
z p
 p
 p
 p
f 9=S
 S
c S
 S
(0S
AKS
 S
 S
 S
j1 1 1 1 1*# $    .3 4    6 S  T        D3 4    6 &*&*@ @ @@ c]	@
 sm@ 
@ @ @ @L CLOP<=F> F>c F> F>C F>"3-F>7:F>ILF>F>69F>BNF> F> F> F>P/c / /c /*23-/@C/MP/,//:=/BJ<BX/ / / /bK KC K%-c]K7?}K K K KZT
S T
 T
C T
 T
Q] T
 T
 T
 T
l,
 ,
3 ,
s ,
C ,
T` ,
 ,
 ,
 ,
\$Js $J# $J(3- $J"$J,/$J>A$JLO$JT`$J $J $J $JLNs N# N(3- N"N,/N>ANLONT`N N N N`G GC GHSM G!$G.1G@CGNQGVbG G G G G Gr(   r^  )_rd   r~  r  r  r   r  rX  r  r  abcr   r   dataclassesr   r   typingr   r   r	   r
   r   pathlibr   tools.binary_extensionsr   agent.file_safetyr   r   r   r   r@   re   r  _HOMEWRITE_DENIED_PATHSWRITE_DENIED_PREFIXESr+  r   r   r'   r/   r4   r7   tupler<   r:   r=   rB   rD   rk   ru   r   r   r   r   r   r   r   r   rg   r   r   rf   r7  r   r   r   r  r  	frozensetr  r  r  r'  r0  r4  r:  rS  rR  	MAX_LINESMAX_LINE_LENGTHr  rJ  rK  rR  rS  r  rE  rP  rT  rV  rW  rZ  r\  r^  r-   r(   r&   <module>rE     sd    4   				 				        # # # # # # # # ( ( ( ( ( ( ( ( 6 6 6 6 6 6 6 6 6 6 6 6 6 6       5 5 5 5 5 5            	IDIKK--e44 33E::  2:BCC 2:LMM "c "c " " " ""     ,# s s    : 	S U39-    58C= 5T 5 5 5 5
)3 )4 ) ) ) ) U U U U U U U U& I I I I I I I I0 % % % % % % % %P         I I I I I I I IX                  'BJ'QRR ] uS(3-=O7P    .6C .6E#s(O .6 .6 .6 .6l BJRSS 9S 9U3S=-AD-H 9 9 9 9: 5   <% C    "G% GS GS G G G G"@ @ @ @ @S @ @ @P NMM  .%)( X (i(=(=(=>> 
  .-# -s -t - - - - 	1s 	1uT3Y'7 	1 	1 	1 	11s 1uT3Y'7 1 1 1 1@1s 1uT3Y'7 1 1 1 11 1tSy)9 1 1 1 10  * %9%H%H%HII  	     0 s S S     -@+=/ /c /%(/BGS// / / /& /D-A/ / /'*/FKCQTHo/ / / / &2:&<== 
M 
M 
M 
M 
M 
MP8C= PT P P P Pl S Ua     c' c' c' c' c'. c' c' c' c' c'r(   