
    Uj;              	         d Z ddlmZ ddlZddlZddlZddlZddlZddl	Zddl
ZddlmZ ddlmZ ddlmZ  ee      j#                         j$                  j$                  Z eej(                  j+                  d eej$                  dz                    Zej(                  j+                  d	d
      Z eej(                  j+                  d e ej2                         dz  dz  dz  dz                    ZdZd#dZd$dZ	 	 d%	 	 	 	 	 d&dZd'dZd(dZ d)dZ! e"d      Z# e"d      Z$d*dZ%d+dZ&d'dZ'd*dZ(d,d-dZ)d*dZ*d,d.dZ+d*d Z,d'd!Z-d" Z.y)/u~  Storage HUD — a fast, lazy file browser over Supabase Storage.

The bucket 'jarvis-files' (on Ahmed's Supabase) is the WRITABLE shared file store
that any Jarvis / Claude on any device can read, save, and delete from. This
module is the engine side of the HUD's "pull up the storage" panel.

DESIGN RULE (the whole point): the LIST is METADATA-ONLY. `list()` / `show()`
fetch names + type + size + updated timestamp and NOTHING ELSE — no file bodies.
A file is downloaded ONLY when Ahmed actually opens it (`open_file` / `download`),
and even then it's cached to disk and re-fetched ONLY when the remote copy is
newer. This is deliberately unlike the memory graph, which was slow because it
bulk-loaded everything.

Ports the proven REST logic from jarvis-tools-service/storage.py (stdlib urllib,
no new deps). Endpoints under {SUPABASE_URL}/storage/v1/...:
  POST /object/list/{bucket}   → metadata list (name, updated_at, metadata.size…)
  GET  /object/{bucket}/{name} → the file body (only on open)
  POST /object/{bucket}/{name} → upload (x-upsert)
  DELETE /object/{bucket}/{name}

Creds come from the process env, else the Ultron repo's .env (the single source
of truth — SUPABASE_URL + SUPABASE_SECRET, the service_role key). MCP_STORAGE=0
disables the whole thing.

Engine → HUD event (voice.events.emit):
  storage {files:[{name,type,size,updated}]}   — open / refresh the panel
    )annotationsN)datetime)Path)emit
ULTRON_ENVz.envSTORAGE_BUCKETzjarvis-filesJARVIS_STORAGE_CACHELibraryzApplication SupportJarvisstoragez.emptyFolderPlaceholderc                 2   t         j                  j                  dd      } t         j                  j                  dd      }| r|st        j	                         r	 t        j                         j                         D ]r  }|j                         }|j                  d      sd|vr)|j                  d      \  }}}|j                         |j                         }}|dk(  r| s|} h|dk(  sn|rq|}t 	 | r|sy| j                  d      |fS # t        $ r Y #w xY w)zG(base_url, service_key) from env, falling back to the Ultron repo .env.SUPABASE_URL SUPABASE_SECRET#=N/)osenvironget_ULTRON_ENVexists	read_text
splitlinesstrip
startswith	partition	Exceptionrstrip)urlkeylinek_vs         C/Users/ahmed/devFolder/Ultron/claude-voice/voice/storage_control.py_credsr'   ;   s    
**..
,C
**..*B
/CC[//1	#--/::<zz|??3'3d?..-1awwy!'')1&sC++CC = C::c?C	  		s   BD
 *D
 -D
 
	DDc                 b    t         j                  j                  dd      dk(  ryt               duS )zBOn when the Supabase service creds are available and not disabled.MCP_STORAGE10FN)r   r   r   r'        r&   enabledr.   R   s)    	zz~~mS)S084r-   c           
        t               }|st        d      |\  }}t        j                  j	                  | d| ||       }|j                  dd|        |j                  d|       |xs i j                         D ]  \  }	}
|j                  |	|
        	 t        j                  j                  ||      5 }|j                         cd d d        S # 1 sw Y   y xY w# t        j                  j                  $ rA}t        d|j                   d	|j                         d d
 j                  dd             d }~ww xY w)Nz/storage not configured (no SUPABASE_URL/SECRET)z/storage/v1)datamethodAuthorizationzBearer apikey)timeoutzHTTP z:    utf-8ignore)r'   RuntimeErrorurllibrequestRequest
add_headeritemsurlopenreaderror	HTTPErrorcodedecode)r1   pathbodyheadersr4   credsbaser!   reqr#   r%   res                r&   _dorL   ]   s&   HELMMID#
..
 
 D6TF!;$(. ! 0CNN?gcUO4NN8S!B%%'1q! (J^^##C#9Q668 :99<<!! JU166("ffhtn33GXFGI J 	JJs6   !C! ;C	C! CC! C! !D?><D::D?c                    ddl } t        ddt         | j                  ddddd	d
      j	                         ddi      }| j                  |      }|D cg c]-  }|j                  d      r|j                  d      t        k7  r|/ c}S c c}w )uA   Raw Supabase list rows (METADATA ONLY — never the file bodies).r   NPOSTz/object/list/r   i  nameasc)columnorder)prefixlimitsortByContent-Typezapplication/json)jsonrL   BUCKETdumpsencodeloadsr   _PLACEHOLDER)rW   r0   r=   is       r&   	_raw_listr^   q   s    vvh/zzR$06%GI "FH 23	5D
 JJtE @u!uuV}v,!> u @ @ @s   2B
c                   t        |       j                  j                  d      j                         }|r|S |xs i j	                  d      xs t        j                  |       d   xs d}d|v r|j                  d      d   S dS )zA short type tag for the row icon: the extension if any, else the mime
    subtype (e.g. 'pdf', 'png', 'plain'). Lowercased, no dot..mimetyper   r   r   )r   suffixlstriplowerr   	mimetypes
guess_typesplit)rO   metaextmimes       r&   _kindrl   ~   s{     t*


"
"3
'
-
-
/C

JBJ'N9+?+?+Ea+HNBD"%+4::c?2525r-   c                    | sy	 t        j                  | j                  dd            j                         S # t        $ r Y yw xY w)znParse an ISO-8601 timestamp (with 'Z' / fractional seconds) to epoch
    seconds. 0.0 on anything unparseable.g        Zz+00:00)r   fromisoformatreplace	timestampr   )isos    r&   _epochrs      sF     %%ckk#x&@AKKMM s   28 	AAzCabcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789.-() 0123456789abcdefc                    t        |       j                         j                  d      } dj                  d | j	                  d      D              S )Nr   r   c              3  `   K   | ]&  }t        |      t        v rt        |      nd |d ( yw)r$   02xN)chr_KEEP).0bs     r&   	<genexpr>z_enc_key.<locals>.<genexpr>   s3      20q !Vu_3q6AaW+=0s   ,.r6   )strr   rd   joinrZ   rO   s    r&   _enc_keyr      sD    t9??##C(D77 2 KK02 2 2r-   c                `   t               }dt        |       }}||k  r| |   dk(  rN|dz   |k  rF| |dz      t        v r8| |dz      t        v r*|j                  t	        | |dz   |dz    d             |dz  }n%|j                  t        | |         dz         |dz  }||k  r|j                  dd	      S )
Nr   r$                  r6   rp   )	bytearraylen_HEXappendintordrC   )r!   r{   r]   ns       r&   	_dec_namer      s    Ac#hqA
a%q6S=QUaZC!H,<QqSTAQHHSQqS1r*+FAHHSQ[4'(FA a% 88GY''r-   c            
        g } t               D ]  }|j                  d      xs i }|j                  d      }||j                  d      }t        |d         }| j                  |t	        ||      t        |t        t        f      rt        |      nd|j                  d      xs |j                  d      xs dd	        | S )
zdMetadata-only listing: [{name, type, size, updated}] sorted by name.
    NEVER fetches file content.metadatasizecontentLengthrO   r   
updated_atlastModifiedr   )rO   typer   updated)r^   r   r   r   rl   
isinstancer   float)outr]   ri   r   displays        r&   listr      s     C[uuZ &Bxx<88O,DAfI&

'4(!+D3,!?CIQuu\*Ldhh~.FL"	
 	  Jr-   c                    t        |       }t               D ]U  }|j                  d      |k(  s|j                  d      xs i }|j                  d      xs |j                  d      xs dc S  y)z<The remote updated_at for one file (from the metadata list).rO   r   r   r   r   )r   r^   r   )rO   r!   r]   ri   s       r&   _remote_updatedr      sa    
4.C[55=C55$*D55&H$((>*BHbH  r-   c                   t        |       j                         j                  d      } | st        d      t        | z  }||n
t        |       }t        |      }|j                         r-|r+|j                         j                  |dz
  k\  rt        |      S t        ddt         dt        j                  j                  t        |       d             }|j                   j#                  dd       |j%                  |       |r$	 t'        j(                  |||f       t        |      S t        |      S # t*        $ r Y t        |      S w xY w)	zDownload ONE file to the local cache and return its path. Re-downloads
    only when the remote copy is newer than the cached one (compared via the
    remote updated_at vs the cached file's mtime). This is the ONLY path that
    ever pulls bytes.r   zdownload: need a file namer   GET/object/safeT)parentsexist_ok)r}   r   rd   
ValueErrorCACHEr   rs   is_filestatst_mtimerL   rX   r9   parsequoter   parentmkdirwrite_bytesr   utimeOSError)rO   r   dest
remote_iso	remote_tsr0   s         r&   downloadr      s$   
 t9??##C(D5664<D#/_T5JJz"I||~)		(<(<	A(M4yu&,,*<*<Xd^RU*<*V)WXYDKKdT2T	HHTIy12 t93t9  	t9	s   D3 3	E	E	c                \   t        |       }	 t        j                  dk(  rt        j                  d|g       |S t
        j                  dk(  rt        j                  |       |S t        j                  d|g       	 |S # t        $ r%}t        dt        |      dd         Y d}~|S d}~ww xY w)zsDownload (cached) then open the file with the OS default app so Ahmed can
    view/edit it. Returns the local path.darwinopenntzxdg-openz  [storage] open failed: Nx   )r   sysplatform
subprocessPopenr   rO   	startfiler   printr}   )rO   rD   rK   s      r&   	open_filer      s     D>D:<<8#fd^, K WW_LL
 K j$/0 K  :)#a&#,899K:s"   *A= (A= #A= =	B+B&&B+c                   t         j                  j                  t        |       j	                               }|rt         j                  j                  |      st        d|      |r(t        |      j	                         j                  d      ndxs t         j                  j                  |      }t        j                  |      d   xs d}t        |d      5 }|j                         }ddd       t        dd	t         dt        j                   j#                  t%        |      d
       |dd       |S # 1 sw Y   NxY w)zvUpload a LOCAL file into the bucket (upsert). `name` defaults to the
    file's own basename. Returns the stored name.zno file at r   r   r   zapplication/octet-streamrbNrN   r   r   true)rV   zx-upsert)r   rD   
expanduserr}   r   isfileFileNotFoundErrorrd   basenamerf   rg   r   r?   rL   rX   r9   r   r   r   )
local_pathrO   lprk   frE   s         r&   uploadr      s     
		C
O113	4BRWW^^B'+bV 455-1CIOO$$S)rRbgg>N>Nr>RD%a(F,FD	b$1vvx 
(6(!FLL$6$6x~C$6$P#QRTX624K	 
s   D99Ec                @   t        |       j                         j                  d      } | st        d      t	        ddt
         dt        j                  j                  t        |       d              	 t        | z  j                  d       | S # t        $ r Y | S w xY w)z8Remove a file from the bucket. Returns the deleted name.r   zdelete: need a file nameDELETEr   r   T)
missing_ok)r}   r   rd   r   rL   rX   r9   r   r   r   r   unlinkr   r   s    r&   deleter     s    t9??##C(D344HVHAfll&8&8$c&8&R%STU	. K  Ks   5B 	BBc                     	 t               } t        d|        | S # t        $ r3}t        dt	        |      dd         t        dg        g cY d}~S d}~ww xY w)u   List (metadata only) and push a `storage` event so the HUD opens/refreshes
    the file panel. Returns the same list for the caller. Never raises — a viewer
    refresh must not crash the assistant.r   )filesz"  [storage] panel refresh failed: Nr   )r   r   r   r   r}   )r   rK   s     r&   showr     sV    Ye$ 23q6$3<.ABYb!	s    	A(AAAc                    dd l ddlm} m} dd | ddi       dfd       } | ddd	t        i      dfd
       } | ddt        t        d      dfd       } | ddd	t        i      dfd       } |dd||||g      S )Nr   )toolcreate_sdk_mcp_serverc                    dd| dgiS )Ncontenttext)r   r   r,   )msgs    r&   _textzbuild_server.<locals>._text-  s    VS9:;;r-   storage_showuB  Pull up Ahmed's file storage on the HUD and list what's there. Use for 'pull up the storage', 'show my files', 'open storage', 'what files do I have'. Opens a fast panel (names + types only — nothing is downloaded) and returns the list. He can then open/delete files there, or you can with storage_open / storage_delete.c                t  K   	 j                  t               d {   }|s d      S |D cg c]'  }d|d    |j                  d      r	d|d    dndz   ) }} t        |       dd	j	                  |      z         S 7 fc c}w # t
        $ r!} d
t        |      d d        cY d }~S d }~ww xY ww)NzStorage is empty.z- rO   r   z  (z bytes)r   z# file(s) in storage (panel is up):

zstorage_show failed:    )	to_threadr   r   r   r~   r   r}   )argsr   r   linesrK   r   asyncios        r&   r   z"build_server.<locals>.storage_show0  s     
	A!++D11E011 $%# !F)%45EE&M#ai[0rK#  % CJ<'KL99U+, - - 2%
  	A0Q?@@	As_   B8B BB B8B ,B'B B8B B 	B5B0*B5+B80B55B8storage_openu   Open ONE stored file by name — downloads it (cached; only re-fetched if it changed) and opens it in its default app so Ahmed can view or edit it. `name` is the exact file name from storage_show.rO   c                &  K   t        | j                  dd            j                         }|s d      S 	 j                  t        |       d {     d| d      S 7 # t
        $ r!} dt        |      d d        cY d }~S d }~ww xY ww)NrO   r   z&storage_open failed: need a file name.zOpened r`   zstorage_open failed: r   )r}   r   r   r   r   r   r   rO   rK   r   r   s      r&   r   z"build_server.<locals>.storage_openC  s      488FB'(..0ABB	A##It44474&*++ 5 	A0Q?@@	AsF   4BA$ A"A$ !B"A$ $	B-B	BB	BBstorage_uploadzUpload a LOCAL file (by absolute path on this Mac) into Ahmed's storage so it syncs everywhere. `local_path` = the path; `name` = optional name to store it as (defaults to the file's own name). Refreshes the HUD panel.)r   rO   c           
       K   t        | j                  dd            j                         }|s d      S 	 j                  t        |t        | j                  dd            j                         xs d        d {   }j                  t
               d {     d| d      S 7 -7 # t        $ r!} dt        |      d d        cY d }~S d }~ww xY ww)	Nr   r   z)storage_upload failed: need a local_path.rO   z	Uploaded z to storage.zstorage_upload failed: r   )r}   r   r   r   r   r   r   )r   r   rO   rK   r   r   s       r&   r   z$build_server.<locals>.storage_uploadR  s      ,+,224DEE	C **C 45;;=EG GD##D)))9TF,788G) 	C23q6$3<.ABB	CsY   4CAB/ =B+>B/ B-B/ *C+B/ -B/ /	C8CCCCCstorage_deletezXDelete a file from Ahmed's storage by name (from storage_show). Refreshes the HUD panel.c                d  K   t        | j                  dd            j                         }|s d      S 	 j                  t        |       d {    j                  t
               d {     d| d      S 7 -7 # t        $ r!} dt        |      d d        cY d }~S d }~ww xY ww)NrO   r   z(storage_delete failed: need a file name.zDeleted z from storage.zstorage_delete failed: r   )r}   r   r   r   r   r   r   r   s      r&   r   z$build_server.<locals>.storage_deletec  s      488FB'(..0CDD	C##FD111##D)))8D6899 2) 	C23q6$3<.ABB	CsX   4B0B A?B .B/B >B0?B B 	B-B("B-#B0(B--B0r   z1.0.0)rO   versiontools)r   r}   returndict)r   r   r   r   )r   claude_agent_sdkr   r   r}   )r   r   r   r   r   r   r   r   s         @@r&   build_serverr   (  s    << 
.; =?@A@A 
.F 3-	
A
A 

% 69#&F	H

CH

C 

%'-sm5	C5	C !\>>J r-   )r   ztuple[str, str] | None)r   bool)NN   )r1   r}   rD   r}   rE   zbytes | NonerF   zdict | Noner4   r   r   bytes)r   z
list[dict])rO   r}   ri   r   r   r}   )rr   r}   r   r   )rO   r}   r   r}   )r!   r}   r   r}   )N)rO   r}   r   
str | Noner   r}   )r   r}   rO   r   r   r}   )/__doc__
__future__r   rf   r   r   r   urllib.errorr9   urllib.parseurllib.requestr   pathlibr   voice.eventsr   __file__resolver   _PROJECTr   r   r}   r   rX   homer   r\   r'   r.   rL   r^   rl   rs   	frozensetry   r   r   r   r   r   r   r   r   r   r   r   r,   r-   r&   <module>r     sk  6 #  	  
      >!!#**112::>>,"%hoo&>"?A B	(.	9 	RZZ^^			i"77(BYNOQ 	R ) .  6:46JJ.1J;@J(
@6  	I	K#$2
("&0 $Lr-   