
    epj             	          d 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                      Z,e	 G d d                      Z-e	 G d d                      Z.e	 G d d                      Z/e	 G d d                       Z0e	 G d! d"                      Z1e	 G d# d$                      Z2 ej         d%          Z3d&e2de'eee         f         fd'Z4d(ede'eef         fd)Z5 ej         d*          Z6d+ede'ee7ef         dz  fd,Z8 G d- d.e          Z9h d/Z:d0d1d2d3d4d5Z; e<h d6          Z=d7d8d9d:Z>d;ed(ede(fd<Z?d=ede'e(ef         fd>Z@d=ede'e(ef         fd?ZAd=ede'e(ef         fd@ZBd=ede'e(ef         fdAZCeCe@eAeAeBdBZD e<h dC          ZEdDZFdDZGdEZHdFZIdDZJdZKdGZLdHedIe7de7fdJZMeIeJfdKedLede'e7e7f         fdMZNeKeLfdKedLede'e7e7f         fdNZO ej         dO          ZPdPede(fdQZQdRee         de(fdSZRd&e0dPede0fdTZS G dU dVe9          ZTdS )Wa\  
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        ;/home/thesage/.hermes/hermes-agent/tools/file_operations.py_strip_terminal_fence_leaksr'   =   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/   N   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   d   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_deniedrA      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__rD   str__annotations__rE   intrF   rG   r<   rH   r   rI   rJ   rK   rL   rM   rN   r   listrQ   r   dictr_   r-   r(   r&   rC   rC      s#        %%GSKIsItD(3-ItHd$(NHSM(((#Ix}### $J$$$E8C=$uT:::M49:::U U U U U U Ur(   rC   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_diagnosticsrN   warningr   c                 H    d | j                                         D             S )Nc                     i | ]
\  }}|||S rT   r-   rU   s      r&   rY   z'WriteResult.to_dict.<locals>.<dictcomp>   s    HHHA!-1---r(   rZ   r]   s    r&   r_   zWriteResult.to_dict   s$    HH!4!4!6!6HHHHr(   )r`   ra   rb   rc   rk   rf   re   rl   r<   rm   r   rn   r	   rd   r
   ro   rN   rp   rh   r_   r-   r(   r&   rj   rj      s         %%M3L$
  $Hhtn###%)D(4S>
"))) &*OXc])))E8C=!GXc]!!!I I I I I I Ir(   rj   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   diffrO   files_modifiedfiles_createdfiles_deletedNrn   ro   rN   	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 )Nru   Trz   r{   rv   rw   rx   ry   rn   ro   rN   )
ru   rz   r{   rv   rw   rx   ry   rn   ro   rN   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(   )r`   ra   rb   rc   ru   r<   re   rv   rd   r   rg   rw   r   rx   ry   rn   r   r	   r
   ro   rN   rz   r{   rh   r_   r-   r(   r&   rt   rt      s"        &&GTD#NNN %d ; ; ;NDI;;;$uT:::M49:::$uT:::M49:::%)D(4S>
")))%)OXc])))E8C= ItD(3-      r(   rt   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_numberrD   g        mtimeN)	r`   ra   rb   rc   rd   re   rf   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.rO   matchesfilescountsr   total_countFrG   Nlimit_reasonrp   rN      _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   rD   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#   rD   r>   r   rD   )rV   r   s     r&   
<listcomp>z(SearchResult.to_dict.<locals>.<listcomp>6  s8     % % % VQ]qyQQ% % %r(   r   r   r   TrG   r   rp   rN   )	r   r   r   r   r   rG   r   rp   rN   )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)r`   ra   rb   rc   r   rg   r   r   r   re   r   rd   rh   r   r	   rf   r   rG   r<   r   r   rp   rN   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.Tru   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   okrN   )r   r   r   )r   r   ru   r   r}   s     r&   r_   zLintResult.to_dictQ  sR    < 	B'DLAAA$(L=DDgUU< 	- $F9r(   N)r`   ra   rb   rc   ru   r<   re   r   r   rd   r   rh   r_   r-   r(   r&   r   r   I  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)	r`   ra   rb   rc   r   rd   re   r   rf   r-   r(   r&   r   r   Z  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   d  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   k  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startrf   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(   c                   z   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	eded
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(   rD   c                     dS )z8Write content to a file, creating directories as needed.Nr-   )r^   r>   rD   s      r&   
write_filezFileOperations.write_file  r   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  s	     	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 backendrN   )rj   r   r^   r>   r   s      r&   delete_pathzFileOperations.delete_path  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(   .N2   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   r   r   rD   Nr   r   rD   r   )r`   ra   rb   rc   r   rd   rf   rC   r   r   rj   r   r<   rt   r   r   r   r   r   r   r   r   r-   r(   r&   r   r     s?       JJ c 3 3 *    ^ # *    ^ s S [    ^ */ # 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 rT   r-   )rV   plowers     r&   	<genexpr>z._looks_like_linter_unusable.<locals>.<genexpr>f  s'      ,,aqEz,,,,,,r(   )_LINTER_UNUSABLE_PATTERNSgetr   any)r   r   patternsr   s      @r&   _looks_like_linter_unusabler  Y  sU     ),,X66H uLLNNE,,,,8,,,,,,r(   rD   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typer`   )rD   _jsones      r&   _lint_json_inprocr  i  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  r`   )rD   _yaml_eventr  s       r&   _lint_yaml_inprocr  u  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 }n(# t          $ r 	 ddl}n# t          $ r Y Y dS w xY wY nw xY w	 |                    |            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  r  Fr   )tomllibr  tomlir
  r  r  r`   )rD   _tomlr  s      r&   _lint_toml_inprocr"    s    $ $ $ $	$!!!!! 	$ 	$ 	$####	$ "!$1Gx 1 1 1a)00Q0000000001sA    
,,
&,&,,A 
A6A1+A61A6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  r`   r  r  )rD   _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   valuedefaultc                 T    	 t          |           S # t          t          f$ r |cY S w xY w)z8Best-effort integer coercion for tool pagination inputs.)rf   	TypeError
ValueError)r-  r.  s     r&   _coerce_intr2    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_limitsr4  maxr2  DEFAULT_READ_OFFSETDEFAULT_READ_LIMITmin)r   r   r4  	max_linesnormalized_offsetnormalized_limits         r&   normalize_read_paginationr=    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   )r6  r2  DEFAULT_SEARCH_OFFSETDEFAULT_SEARCH_LIMIT)r   r   r;  r<  s       r&   normalize_search_paginationrA    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_newlinerD    s*     7?Ld#;#B#B7#K#KLLLr(   rN   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_errorrG    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   rD  rN   rG  rp   )r~   r   s     r&   )_maybe_warn_line_oriented_newline_patternrI    s_    Q&@&I&I| ;FLII FL	P N Mr(   c                   H   e Zd ZdZdOdefdZ	 	 dPdededededef
d	Zd
ede	fdZ
dOdedede	fdZdede	fdZdQd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ddfdZdOdedee         dee         fdZdOdedee         de	fdZdedededefdZdRded!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dSded(e	defd)Zded(e	defd*Zd+ed,edefd-Zdededefd.Z	 dSded/ed0ed1e	def
d2Z d3edefd4Z!dOdedee         de"fd5Z#	 dOdedee         d6ee         de"fd7Z$de	fd8Z%d9ede	fd:Z&dede	fd;Z'deddfd<Z(ddd=dedee         d6ee         defd>Z)	 	 	 dTdBededCedDee         d"ed!edEedFede*fdGZ+dBededCedDee         d"ed!edEedFedee*         fdHZ,dBededDee         dee         fdIZ-dBeded"ed!ede*f
dJZ.dBeded"ed!ede*f
dKZ/dBededDee         d"ed!edEedFede*fdLZ0dBededDee         d"ed!edEedFede*fdMZ1dBededDee         d"ed!edEedFede*fdNZ2dS )U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.
        rL  Nconfig/)envgetattrrL  _command_cache)r^   terminal_envrL  s      r&   __init__zShellFileOperations.__init__0  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|}t	          |                    dd          |                    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.
        rV  NrW  rL  r   r   
returncoder   )r   r   )rQ  rP  rL  executer   r   )r^   rU  rL  rV  rW  kwargseffective_cwdr~   s           r&   _execzShellFileOperations._execS  s    "  	( 'F9!#-F<  Iwtx==I!!'GG}GGG::h++jjq11
 
 
 	
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)rR  r]  r   r   )r^   r^  r~   s      r&   _has_commandz ShellFileOperations._has_commands  s[    d)))ZZ Qc Q Q QRRF'-}':':'<'<'ED$"3''r(   r>   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   �Ni  c              3   H   K   | ]}t          |          d k     |dvdV  dS )    z
	r   N)ord)rV   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   sumr9  r8   )r^   r>   rb  extnon_printables        r&   _is_likely_binaryz%ShellFileOperations._is_likely_binaryz  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   )rh  r>   ri  r   IMAGE_EXTENSIONS)r^   r>   rk  s      r&   	_is_imagezShellFileOperations._is_image  s4    gt$$Q'--//&&&r(   r   rD   
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]|)r5  rs  r   	enumerater8   r    r!   )	r^   rD   rq  rs  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   NrO  z[a-zA-Z0-9._-]+zecho ~)	r6   r]  r   r   r   findr   	fullmatchr8   )
r^   r>   r~   homerest	slash_idxusernameexpand_result	user_homesuffixs
             r&   _expand_pathz ShellFileOperations._expand_path  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'z'"'"')tools.environments.localr  r   )r^   r  r  s      r&   _escape_shell_argz%ShellFileOperations._escape_shell_arg  sC     	=<<<<<oc""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.

        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; 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)rW  )r  rh  r>   dirnamer]  )r^   r>   rD   q_pathparentq_parenttmplscripts           r&   _atomic_writez!ShellFileOperations._atomic_write  s     ''--&&-#))&11 %%&:;;0%% %%% % % (,,/ 	> zz&Wz555r(   pre_contentc                     |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  r]  r   r   r^   r>   r  head_cmdhead_results        r&   _detect_file_line_endingz,ShellFileOperations._detect_file_line_ending9  sx      	4&{333 N4#9#9$#?#?MMMjj** A%%[-?%4";#5666r(   c                     |t          |          S d|                     |           d}|                     |          }|j        dk    s|j        sdS t          |j                  S )aL  Whether the file on disk starts with a UTF-8 BOM.

        Uses ``pre_content`` if we already read the file (zero extra exec
        calls); otherwise issues a tiny ``head -c 3`` to sample just the
        marker. A missing/empty file returns False (new writes get no BOM
        unless the caller explicitly includes one).
        Nz
head -c 3 r  r   F)r=   r  r]  r   r   r  s        r&   _file_has_bomz!ShellFileOperations._file_has_bomN  ss     "K(((J 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_linesrv   s          r&   _unified_diffz!ShellFileOperations._unified_diff_  sn    **D*99	**D*99	#y$(__"??
 
 

 wwt}}r(   r   r   r   c           	      D   |                      |          }t          ||          \  }}d|                     |           d}|                     |          }|j        dk    r|                     |          S t          |j                  }	 t          |	                                          }n# t          $ r d}Y nw xY w|t          k    r	 |                     |          rt          dd|d          S d|                     |           d}|                     |          }	t          |	j                  }
|                     ||
          rt          d|d	          S ||z   d
z
  }d| d| d|                     |           }|                     |          }|j        dk    rt          d|j                   S t          |j                  }|d
k    rt          |          \  }}d|                     |           }|                     |          }t          |j                  }	 t          |	                                          }n# t          $ r d}Y nw xY w||k    }d}|rd|d
z    d| d| d| d	}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
        wc -c < r  r   TzImage file detected. Automatically redirected to vision_analyze tool. Use vision_analyze with this file path to inspect the image contents.)rJ   rI   rF   rH   head -c 1000 zUBinary file - cannot display as text. Use appropriate tools to handle this file type.rI   rF   rN   r   zsed -n ',zp' Failed to read file: r   zwc -l < NzUse offset=z to continue reading (showing -z of z lines))rD   rE   rF   rG   rH   )r  r=  r  r]  r   _suggest_similar_filesr'   r   rf   r   r1  MAX_FILE_SIZErp  rC   rm  r:   ry  )r^   r>   r   r   stat_cmdstat_resultstat_outputrF   
sample_cmdsample_resultsample_outputend_lineread_cmdread_resultread_output_wc_cmd	wc_result	wc_outputrE   rG   rH   s                         r&   r   zShellFileOperations.read_filen  s9      &&1&%@@ Id44T::HHHjj** A%%..t4441+2DEE	K--//00II 	 	 	III	 }$$ >>$ 		#\    PT%;%;D%A%AOOO


:..3M4HII!!$66 	#m    E>A%RfRRxRRD4J4J44P4PRR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**;??#
 
 
 	
s$   !B. .B=<B=*!I II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    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   ls -1  2>/dev/null | head -50r,   d   Z   F   <      (   g?   c                     | d          S )Nr   r-   )xs    r&   <lambda>z<ShellFileOperations._suggest_similar_files.<locals>.<lambda>  s    1Q4% r(   )keyc                     g | ]\  }}|S r-   r-   )rV   r  fps      r&   r   z>ShellFileOperations._suggest_similar_files.<locals>.<listcomp>  s    ...%!R2...r(   Nr   zFile not found: )rN   rQ   )rh  r>   r  basenameri  r   r  r]  r   r   r   r   r6   r8   setr6  r    r!   sortrC   )r^   r>   dir_pathr  basename_no_extrk  
lower_namels_cmd	ls_result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 "199MM5"',,x*C*C"DEEE(((..6"1":...+T++!
 
 
 	
r(   c                 ~   |                      |          }d|                     |           d}|                     |          }|j        dk    r|                     |          S t          |j                  }	 t          |                                          }n# t          $ r d}Y nw xY w| 
                    |          rt          dd|          S |                     d|                     |           d          }t          |j                  }|                     ||          rt          d|d          S |                     d	|                     |                     }|j        dk    rt          d
|j                   S t          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  r  r   T)rJ   rI   rF   r  u'   Binary file — cannot display as text.r  cat r  r   )rD   rF   )r  r  r]  r   r  r'   r   rf   r   r1  rp  rC   rm  r:   )r^   r>   r  r  r  rF   r  r  
cat_resultraw_contentr  s              r&   r   z!ShellFileOperations.read_file_raw  s      &&Hd44T::HHHjj** A%%..t4441+2DEE	K--//00II 	 	 	III	>>$ 	RttyQQQQ

#]43I3I$3O3O#]#]#]^^3M4HII!!$66 	)?    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   9!B B*)B*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).
        r  r  r   s      r&   r   zShellFileOperations.delete_path4  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)
zpython3 -c r   python3r   z
python -c zFailed to delete r   zunknown error)	r  r   rj   r<   r]  r  r   r   r   )r^   r>   r   deniedsnippetr~   s         r&   r  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.Mover  r   zmv  r   zFailed to move z -> r   )r  r   rj   r]  r  r   r   )r^   r   r   r   r  r~   s         r&   r   zShellFileOperations.move_fileh  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           	      2   |                      |          }t          |          }|rt          |          S t          j                            |          d                                         }|t          v rt          	                    |          nd}|0 ||          \  }}|s |dk    rt          d| d| d| d          S d}|t          v p| 
                    |          }	|	rGd	|                     |           d
}
|                     |
          }|j        dk    r|j        r|j        }|                     ||          }|dk    rt!          |d          }|                     ||          rt%          |          s
t&          |z   }|                     |           t          j                            |          }d}|r:d|                     |           }|                     |          }|j        dk    rd}|                     ||          }|j        dk    rt          d|j                   S d|                     |           d
}|                     |          }	 t/          |j                                                  }n2# t2          $ r% t5          |                    d                    }Y nw xY wd}	 d|                     |           d
}|                     |          }|j        dk    r|j                                        r|j                                                                        d         }t;          j        |                    dd                                                    }||k    }|st          d| d          S n# t@          $ 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

        Returns:
            WriteResult with bytes written, lint summary, or error.
        r   r   Nr  zRefusing to write 'z': candidate content fails z syntax validation (zC). The file was NOT created or modified. Fix the content and retry.r  r  r   r+   Fz	mkdir -p TzFailed to write file: r  zutf-8z
sha256sum surrogatepass#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)rk   rl   rm   rn   ro   )&r  r   rj   rh  r>   ri  r   _FAIL_CLOSED_INPROC_EXTSLINTERS_INPROCr   _lsp_handles_extensionr  r]  r   r   r  r4   r  r=   r7   _snapshot_lsp_baseliner  r  rf   r   r1  r8   encoder   hashlibsha256	hexdigestr  _check_lint_deltaru   r   _maybe_lsp_diagnosticsr_   )r^   r>   rD   r  rk  inproc_linter_ok	_lint_errr  want_prer  r  original_endingr  rl   	mkdir_cmdmkdir_resultwrite_resultr  r  rk   content_verifiedhash_cmdhash_resultdisk_shaexpected_shalint_resultro   blocks                                r&   r   zShellFileOperations.write_file{  s   @   && (-- 	-V,,,,6 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 	1
 Id44T::HHHH**X..K$))k.@))0 77kJJf$$-gv>>G dK00 	*'9J9J 	*')G 	##D))) && 	$DD$:$:6$B$BDDI::i00L%**#" ))$88!Q&&%Sl>Q%S%STTTT Id44T::HHHjj**	9 2 8 8 : :;;MM 	9 	9 	9w 7 788MMM	9 ,0	$ND$:$:4$@$@NNNH**X..K$))k.@.F.F.H.H)&-3355;;==a@&~gnnWo.V.VWWaacc#+|#; ' &)$ ) ) )     	$ 	$ 	$#	$ ,,T{Y`,aa *. 	(+"5 	(//+G 0  E  ("''%%*5?$$&&&4+
 
 
 	
s%   #&J
 
,J98J9?CN N('N(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.)ru   rz   r{   z'Could not find match for old_string in )format_no_match_hintzFailed to write changes: z2Post-write verification failed: could not re-read r+   r,   r2   r  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.r  N)ru   rv   rw   rn   ro   )r  r   rt   r  r]  r   r   r:   tools.fuzzy_matchr  r  r  r  r/   r4   r   rN   r   r8   r  r  r_   ro   )r^   r>   r   r   r   r  r  r  rD   r  r  r  match_count	_strategyrN   r  err_msgr  file_endingr  
verify_cmdverify_result_verify_bomless_verify_stdout_normalized_new_content_normalizedrv   r  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[99 	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   .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_parserr  r  rt   )r^   r   r  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 |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   ru   r   zNo linter for z fileszLSP server handles u    — shell linter skippedz not availablez{file}r  rV  )
strip_ansic              3   f   K   | ],}|                                 |                                 V  -d S rT   r   rV   lns     r&   r   z2ShellFileOperations._check_lint.<locals>.<genexpr>V  s7      IIbhhjjIIIIIIIr(   x   z not usable:    )rh  r>   ri  r   r  r   r  r]  r   r   r   LINTERS_SHELL_LINTER_LSP_REDUNDANT_lsp_will_handler   ra  r   r  tools.ansi_stripr%  r   nextr   )r^   r>   rD   rk  inprocr  r  r   err
linter_cmdr   r^  r~   r%  r%   
first_lines                   r&   _check_lintzShellFileOperations._check_lint  s   ( 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 ---$2G2G2M2M-LcLLL   
 S\
##%%a(  ** 	Qdx4O4O4OPPPP   4+A+A$+G+GHH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(   r  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).
        )rD   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'  )rV   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.)ru   r   r   zLNew lint errors introduced by this edit (pre-existing errors filtered out):
r,   r#  )r5  ru   r   r   r   r   r!   )r^   r>   r  r  postpre
post_linesr:  s          @r&   r  z%ShellFileOperations._check_lint_deltac  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.
        rP  NFr   )LocalEnvironment)rQ  r  r?  r  
isinstance)r^   rP  r?  s      r&   _lsp_local_onlyz#ShellFileOperations._lsp_local_only  sn     dE4((; 5	AAAAAAA 	 	 	55	#/000s    
,,rk  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.serversrC  r  r   
extensions)r^   rk  rC  	ext_lowersrvs        r&   r  z*ShellFileOperations._lsp_handles_extension  s      	5	1111111 	 	 	55	IIKK	 	 	CCN**tt +us    
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)rA  	agent.lsprJ  r  r<   enabled_forr^   r>   rJ  svcs       r&   r.  z$ShellFileOperations._lsp_will_handle  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   rI  )rA  rK  rJ  r  snapshot_baselinerM  s       r&   r  z*ShellFileOperations._snapshot_lsp_baseline  s     ##%% 	F	------+--CC 	 	 	FF	;F	!!$''''' 	 	 	DD	s   ) 
77A 
A$#A$r  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   rI  N)build_line_shiftT)delta
line_shift)report_for_filetruncatez)LSP diagnostics introduced by this edit:
)rA  rK  rJ  r  rL  agent.lsp.range_shiftrR  get_diagnostics_syncagent.lsp.reporterrU  rV  )r^   r>   r  r  rJ  rN  rT  rR  r   rU  rV  r  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 nor`  r  z 2>/dev/null | head -20r   r,      zSimilar paths: , r   z. rN   r   r   )rA  r  r]  r  r   _try_multi_path_searchrh  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_query
hint_partsparent_checkr  lower_q
candidatesentryles                       r&   r   zShellFileOperations.searchc  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   )rV   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^  z (+z more))r   r8   r  r]  r  r   r    r   rb  rc  rN   r   extendr   r   updater   rG   r!   rp   )r^   r   r>   r0   r   r   r   r   r   partsexistingmissingr   expandedchkmergedr   r{   s                     r&   ra  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 |rd|                     |           nd}|                     d| d|                     |           d|                     |           dd	          }d
}d
}|j        pd                                                                D ]F}|                    d          \  }	}
}|                                r|t          |          z  }|dz  }G|d
k    r	d| d| dS |                     d| d|                     |           d|                     |           dd	          }d
}d
}|j        pd                                                                D ]F}|                    d          \  }	}
}|                                r|t          |          z  }|dz  }G|d
k    r	d| d| dS t          j
        d|          r|                     d| d|                     |           d|                     |           dd	          }t          d |j        pd                                                                D                       }|d
k    r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.
        rgNz --glob r   zrg -i --count-matchesr  r  r  r$  r   :r   z0 exact matches, but z case-insensitive match(es) in u/    file(s) — the pattern's casing may be wrong.z'rg --hidden --no-ignore --count-matchesz 0 matches in visible files, but z match(es) in us    hidden or gitignored file(s) — these are excluded by default. Search the hidden path explicitly to include them.z[.\[\](){}?*+^$\\|]zrg -F --count-matchesc              3      K   | ]Y}|                     d           d                                         /t          |                     d           d                   V  ZdS )r{  r  N)
rpartitionisdigitrf   )rV   r#   s     r&   r   z8ShellFileOperations._zero_match_probe.<locals>.<genexpr>	  sr        ??3''*2244DOOC((+,,     r(   z0 regex matches, but uy    literal match(es) — the pattern contains regex metacharacters that likely need escaping (or pass a simpler substring).)ra  r  r]  r   r   r   r}  r~  rf   r   r   rj  )r^   r   r>   r   	glob_exprprobeci_totalci_filesr#   _p_sepnhiddenh_totalh_filesfixedf_totals                    r&   _zero_match_probez%ShellFileOperations._zero_match_probe  sw      && 	4FOWBt55i@@BBBUW	

&I & &%%g..& &151G1G1M1M& & & 	  
 
 \'R..00;;== 	 	D//#..KBayy{{ CFF"Aa<<P P PP P P &i & &%%g..& &151G1G1M1M& & & 	  
 
 ](b//11<<>> 	 	D//#..KBayy{{ 3q66!1Q;;Q7 Q QQ Q Q
 9+W55 	JJ*	 * *))'22* *595K5KD5Q5Q* * * 	   E   "\/R6688CCEE    G
 {{>G > > >
 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**/rO  c              3   H   K   | ]}|d vo|                     d          V  dS >   ..r   r   Nr6   rV   parts     r&   r   z4ShellFileOperations._search_files.<locals>.<genexpr>/	  sL       '
 '
 #<(<(<'
 '
 '
 '
 '
 '
r(   rz  r|  zFile search requires 'rg' (ripgrep) or 'find'. Install ripgrep for best results: https://github.com/BurntSushi/ripgrep#installationr   z-not -path '*/.*'r   r  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>o	  s8      ^^Dt;.G4??33G3G^^^^^^r(   r   r   rG   r   )r6   r   r   r  rs  ra  _search_files_rgr   r  r]  r   r   r8   r   r~  r    resolverelative_tor1  r<   )r^   r   r>   r   r   search_patternsearch_roothas_hidden_path_ancestorhidden_excludehidden_filter_exprpagination_exprr^  r~   r   r   
cmd_simpler   r#   rs  normalized_rootfiltered_files	file_path	rel_partss                          r&   rb  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.
        rO  *zrg --files --sortr=modified -g r  z 2>/dev/null | head -n r  r$  c                     g | ]}||S r-   r-   rV   r  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]  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++' '$' ' 	
 J337??@@ 4 4T : :@@@	 		E 		E+!7!7!E!E + +))$//+ +(+ + 
 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).Frz  TgrepzqContent search requires ripgrep (rg) or grep. Install ripgrep: https://github.com/BurntSushi/ripgrep#installationr   r   Nr  )ra  _search_with_rg_search_with_grepr   rN   r   r   r   r   r  r  rp   rI  )r^   r   r>   r   r   r   r   r   used_rgr~   rH   s              r&   rc  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|                    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Search using ripgrep.)rz  z--line-numberz--no-headingz--with-filenamerF  r   -Cz--glob
files_only-lcount-cr+  rt  r.   -nset -o pipefail; r  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   rG   r   rp   r{  r   r   r   rG   r   ^([A-Za-z]:)?(.*?):(\d+):(.*)$r   r   r^       r   )r   r   rG   r   rp   )rD  r    rq  rd   r  r!   r]  r   r   r   r   r   r   r   r8   r<   rsplitrf   r1  rj  valuesr   compiler   r   r   r   )r^   r   r>   r   r   r   r   r   	cmd_parts	multiliner  r^  r~   r   r   r   r   	error_msg_ml_noter  totalr  r   r#   rs  	_match_rer   r   parseds                                r&   r  z#ShellFileOperations._search_with_rg	  s@    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5666
 /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                 \	   ddg}|                     d           |dk    r$|                    dt          |          g           |r*|                    d|                     |          g           |dk    r|                     d           n|d	k    r|                     d
           |                     |                     |                     |                     |                     |                     ||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|                     t/          |                    d          pd|                    d          z   t!          |                    d                     |                    d!          d"d#         $                     |dk    rPt3          |          }|r?|                     t/          |d         |d         |d         d"d#         $                     t          |          }||||z            }t          |||||z   k    pt          |          |%          S )&zFallback search using grep.r  z-rnHz--exclude-dir='.*'r   r  z	--includer  r  r  r  r+  rt  r.   r  r  r  r  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  Nr  r   )r   r   rG   r   )r    rq  rd   r  r!   r]  r   r   r   r   r   r   r   r8   r<   r  rf   r1  rj  r  r   r  r   r   r   r   )r^   r   r>   r   r   r   r   r   r  r  r^  r~   r   r   r   r   r  r  r  r  r   r#   rs  r  r   r   r  s                              r&   r  z%ShellFileOperations._search_with_grep]
  s    V$	 	-... 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//55666 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   6K
K"!K"rT   )NNN)r   r   r   r   )3r`   ra   rb   rc   rd   rT  rf   r   r]  r<   ra  rm  rp  ry  r  r  r  r   r  r  r  rC   r   r  r   rj   r   r   r  r   r   rt   r   r   r   r5  r  rA  r  r.  r  r  r   r   ra  r  rb  r  rc  r  r  r-   r(   r&   rK  rK  (  s3        !2 !2# !2 !2 !2 !2F CG $
 
S 
s 
C 

)6
 
 
 
@( ( ( ( ( ( c 3 $    :'c 'd ' ' ' '
# # ## #c # # # #4# # # # # #J7S 7S 7 7 7 7"K6# K6 K6 K6 K6 K6 K6Z7 7S 7x} 7X`adXe 7 7 7 7*, ,# ,HSM ,T , , , ,"	 	3 	# 	RU 	 	 	 	_
 _
c _
3 _
3 _
* _
 _
 _
 _
B2
3 2
: 2
 2
 2
 2
h%
# %
* %
 %
 %
 %
N: : : : : :> > > > > > > >*3 *4 *K * * * *XS s {    &_
s _
S _
[ _
 _
 _
 _
L +0K
 K
# K
3 K
C K
#'K
4?K
 K
 K
 K
Zs {    <V
 V
 V
hsm V
z V
 V
 V
 V
r 9=S
 S
c S
 S
(0S
AKS
 S
 S
 S
j1 1 1 1 1*# $    . 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H HC H%-c]H7?}H H H HT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Ks K# K(3- K"K,/K>AKLOKT`K K K KZy yC yHSM y!$y.1y@CyNQyVby y y y y yr(   rK  )Urc   rh  r   r  r  abcr   r   dataclassesr   r   typingr   r   r	   r
   r   pathlibr   tools.binary_extensionsr   agent.file_safetyr   r   r   r   r@   rd   r~  _HOMEWRITE_DENIED_PATHSWRITE_DENIED_PREFIXESr  r   r   r'   r/   r4   r7   tupler<   r:   r=   rA   rC   rj   rt   r   r   r   r   r   r   r   r   rf   r   r   ro  r,  	frozensetr-  r   r  r  r  r"  r(  r  r  	MAX_LINESMAX_LINE_LENGTHr  r7  r8  r?  r@  r2  r=  rA  rC  rD  rG  rI  rK  r-   r(   r&   <module>r     s   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:; ; ; ; ;S ; ; ;F 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  	    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     n n n n n. n n n n nr(   