+
    BSj5                    (  a  0 t $ R t^ RIHt ^ RIt^ RIt^ RIt^ RIHtHtH	t	 ^ RI
Ht / tR]R&   . tR]R&   R	 R
 ltR R ltR&R R lltR R ltR R ltR&R R lltR R ltR R ltR R ltR R ltR R ltR R  ltR! R" ltR# R$ ltR% tR# )'uL  Interactive compose/confirm cards for email + calendar.

Instead of firing an email or booking an event blind, Jarvis opens a *draft
card* on the HUD (like the image popup) that Ahmed can eyeball and hand-edit —
fix a misheard address, tweak the subject — then Send or Cancel. Either side
can drive it:

  Jarvis (voice)  -> email_compose / event_compose  (open a card)
                     compose_update                 (change a field)
                     compose_send / compose_cancel  (act on it)
  Ahmed  (HUD)    -> edits a field, clicks Send / Cancel / ✕
                     -> HUD writes control/card_action.json
                     -> ControlWatcher applies it and drops a note in the
                        master's inbox so Jarvis follows up in his own words.

The draft lives HERE, in the engine process, shared by the MCP tools (below)
and the ControlWatcher. Field edits Ahmed types are stored silently so a later
spoken "just send it" sends HIS corrected version. Only Send/Cancel/Close make
Jarvis speak — otherwise he'd chatter while Ahmed types.

Engine → HUD events (via voice.events.emit):
  compose     {draft_id, to, cc, subject, body, attachments[]}
  event_card  {draft_id, title, start, end, attendees, meet, context[]}
  card_close  {draft_id}
Set MCP_COMPOSE=0 to disable (Jarvis falls back to direct email_send).
)annotationsN)datetime	timedeltatimezone)emitzdict[str, dict]_DRAFTS	list[str]_ORDERc                   V ^8  d   QhRR/# )   returnbool )formats   "voice/compose.py__annotate__r   *   s     = = =    c                     \         P                  P                  R R4      R8X  d   R# ^ RIHp Hp V P                  4       ;'       g    VP                  4       # )MCP_COMPOSE10Femail_controlgcal_control)osenvirongetvoicer   r   enabledr   s     r   r   r   *   s?    	zz~~mS)S01  "<<l&:&:&<<r   c                   V ^8  d   QhRR/# )r   r   strr   )r   s   "r   r   r   1   s          r   c                 N    \         P                  ! 4       P                  R ,          # ):N   N)uuiduuid4hexr   r   r   _new_idr&   1   s    ::<Br   c                    V ^8  d   QhRRRR/# )r   kindz
str | Noner   r   )r   s   "r   r   r   5   s      J * r   c                    \        \        4       F8  p\        P                  V4      pV'       g   K"  V e   VR,          V 8X  g   K6  Vu # 	  R# )z7Most recent open draft id, optionally filtered by kind.Nr(   )reversedr	   r   r   )r(   didds   &  r   	latest_idr-   5   s=    KK1$,!F)t"3J   r   c                    V ^8  d   QhRRRR/# r   draft_idr    r   dict | Noner   )r   s   "r   r   r   >   s     ! ! ! !r   c                ,    \         P                  V 4      # N)r   r   r0   s   &r   	get_draftr5   >   s    ;;x  r   c                   V ^8  d   QhRR/# )r   r   r   r   )r   s   "r   r   r   D   s      y r   c                   \        V \        \        34      '       dK   V  Uu. uF=  p\        V4      P	                  4       '       g   K$  \        V4      P	                  4       NK?  	  up# \        V \        4      '       dG   V P                  R4       Uu. uF*  qP	                  4       '       g   K  VP	                  4       NK,  	  up# . # u upi u upi )zNormalize an attachments value (list of paths, or a comma-separated
string) into a clean list of path strings. Anything else -> [].,)
isinstancelisttupler    stripsplit)valps   & r   _parse_attachmentsr@   D   s     #e}%%(+>1s1v||~A>>#s#&99S>?>aWWY		>??I ??s   !CCC
,C
c          
     ,    V ^8  d   QhRRRRRRRRRR/# )r   tor    ccsubjectbodyr   r   )r   s   "r   r   r   N   s0      3 C # S ,/r   c                    \        4       pR RRRV RVRVRVR\        V4      //\        V&   \        P	                  V4       \        V4       V# )r(   emailfieldsrB   rC   rD   rE   attachments)r&   r@   r   r	   append
_emit_card)rB   rC   rD   rE   rI   r+   s   &&&&& r   create_email_draftrL   N   sV    
)CGXb$Iw)+608 9GCL MM#sOJr   c               4    V ^8  d   QhRRRRRRRRRRRR	R
R/# )r   titler    startend	attendeesmeetr   contextr   r   r   )r   s   "r   r   r   Y   s@     	 	c 	# 	C 	C 	!	,5	:=	r   c                    \        4       pR RRRV RVRVRVR\        V4      /RT;'       g    . /\        V&   \        P	                  V4       \        V4       V# )	r(   eventrH   rN   rO   rP   rQ   rR   rS   )r&   r   r   r	   rJ   rK   )rN   rO   rP   rQ   rR   rS   r+   s   &&&&&& r   create_event_draftrV   Y   sa    
)CGXsYT
04 	7==b"GCL MM#sOJr   c                    V ^8  d   QhRRRR/# )r   r0   r    r   Noner   )r   s   "r   r   r   e   s     + + + +r   c                P   \         P                  V 4      pV'       g   R # VR,          pVR,          R8X  dv   \        RYP                  RR4      VP                  RR4      VP                  RR4      VP                  R	R4      \        VP                  R
4      ;'       g    . 4      R7       R # \        RWP                  RR4      VP                  RR4      VP                  RR4      VP                  RR4      \	        VP                  R4      4      VP                  R. 4      R7       R # )NrH   r(   rG   composerB    rC   rD   rE   rI   )r0   rB   rC   rD   rE   rI   
event_cardrN   rO   rP   rQ   rR   rS   )r0   rN   rO   rP   rQ   rR   rS   )r   r   r   r:   r   )r0   r,   fs   &  r   rK   rK   e   s    HA	(AyGYeeD"odBy")=%%#aeeM288b9	;
 	\HEE'24F55"%155+;uu["-Dv4GUU9b)	+r   c               (    V ^8  d   QhRRRRRRRR/# )	r   r0   r    rH   dictreemitr   r   r1   r   )r   s   "r   r   r   x   s(     
 
3 
 
d 
{ 
r   c                    \         P                  V 4      pV'       g   R# VR,          P                  VP                  4        UUu/ uF  w  rEVf   K  WEbK  	  upp4       V'       d   \	        V 4       V# u uppi )zMerge `fields` into the draft. reemit=True refreshes the HUD card
(use when JARVIS edits); False stores silently (use for Ahmed's own
typing, which the HUD already shows).NrH   )r   r   updateitemsrK   )r0   rH   r`   r,   kvs   &&&   r   update_draftrf   x   s]     	HAhKI1IJ8H Js   A5
A5
c                    V ^8  d   QhRRRR/# r/   r   )r   s   "r   r   r      s      3 ; r   c                    \         P                  V R 4      pV \        9   d   \        P                  V 4       V'       d   \	        RV R7       V# )N
card_closer4   )r   popr	   remover   )r0   r,   s   & r   cancel_draftrl      s8    Hd#A6h\H-Hr   c                    V ^8  d   QhRRRR/# )r   r0   r    r   ztuple[bool, str]r   )r   s   "r   r   r      s     % %s %'7 %r   c           
     .  "   \         P                  V 4      pV'       g   R# VR,          p VR,          R8X  Ed   ^ RIHp \	        VP                  R4      4      pV'       do   \
        P                  ! VP                  VP                  RR4      VP                  R	R4      VP                  R
R4      VP                  RR4      V4      G Rj  xL
 pM\
        P                  ! VP                  VP                  RR4      VP                  R	R4      VP                  R
R4      VP                  RR4      4      G Rj  xL
 pM#\
        P                  ! \        V4      G Rj  xL
 p\        V 4       RV3#  L L9 L  \         d   pRRT 23u Rp?# Rp?ii ; i5i)zcSend the email / create the event for this draft, then close the card.
Returns (ok, human_summary).FrH   r(   rG   r   rI   rB   r[   rD   rE   rC   NTzfailed: )Fzthat draft is no longer open.)r   r   r   r   r@   asyncio	to_thread_send_create_eventrl   	Exception)r0   r,   r]   r   attsoutes   &      r   
send_draftrx      sU     	HA55	(A%V9+%aeeM&:;D#--!''tREE)R(!%%*;EE$OT+ +
 $--!''tREE)R(!%%*;QUU4_N N  ))-;;CXSy+
N <  %n$$%sl   )FBE6 E0A,E6 8E29#E6 E4E6 /F0E6 2E6 4E6 6FFFFFFc                    V ^8  d   QhRRRR/# )r   r]   r_   r   r    r   )r   s   "r   r   r      s     A AT Ac Ar   c           	        ^ RI Hp VP                  4       pV P                  RR4      pTP	                  Y0P                  R4      ;'       g    R^4      p\        V P                  RR4      4      P                  R4       Uu. uF+  pVP                  4       '       g   K  VP                  4       NK-  	  ppRV P                  R	R4      ;'       g    R
RRVRVP                  /RRVRVP                  //pV'       d   V Uu. uF  pRV/NK	  	  upVR&   RRRTRV'       d   RMR/pV P                  R4      '       d0   RR\        P                  ! 4       P                  RRR///VR&   ^VR&   VP                  4       P                  ! R%/ VB P                  4       p	V P                  R4      '       d"   V	P                  RR4      p
RVR,           RV
 2# V'       d   R R!P                  V4       2MRpR"VR,           R#V V R$2# u upi u upi )&u:   Blocking calendar insert — called via asyncio.to_thread.r   rO   r[   rP   NrQ   r8   summaryrN   
(untitled)dateTimetimeZonerG   
calendarIdprimaryrE   sendUpdatesallnonerR   createRequest	requestIdconferenceSolutionKeytypehangoutsMeetconferenceDataconferenceDataVersionhangoutLinkz	(no link)z	Meeting 'z' booked. Meet link: u    — invited , zEvent 'z' booked for .r   )r   r   _servicer   	_end_fromr    r=   r<   _TZr#   r$   r%   eventsinsertexecutejoin)r]   r   svcrO   rP   rw   rQ   rE   kwevlinkwhos   &           r   rs   rs      s   "



!CEE'2E

 
 e(<(<b
AC$'k2(>$?$E$Ec$J $JqGGI $JI  	155"%55*eZ1A1AB
C\-=-=>D
 3<=9agq\9=[
	64
)
9BuuV}}"1))#fn%=4? "@ '("#				"r	"	*	*	,BuuV}}vvm[14	?++@GG4=M$))I./
02CT)_%]5'#a@@+ >s    G>G>=Hc                    V ^8  d   QhRRRR/# )r   	start_isor    r   r   r   )r   s   "r   r   r      s      # ) r   c           	         ^ RI Hp \        P                  ! V 4      P	                  4       p TP                  4       p\        TP                  TP                  TP                  \        P                  R7      P                  4       p\        TP                  TP                  TP                  \        P                  R7      \        ^R7      ,           P                  4       pTP                  4       P                  RYERR^R7      P!                  4       p. pTP#                  R. 4       F  pTP#                  R	/ 4      P#                  R
4      ;'       g#    TP#                  R	/ 4      P#                  RR4      p	RT	9   d
   T	R,          MRp
TP%                  T
 RTP#                  RR4       24       K  	  T#   \
         d    . u # i ; i  \
         d    . u # i ; i)zDEvents on the same day as `start_iso`, for the card's context strip.r{   )tzinfo)daysr   T	startTime)r   timeMintimeMaxsingleEventsorderBy
maxResultsrc   rO   r~   date?T:      Nzall-dayz  r|   r}   )r   r   r   fromisoformatr   rt   r   yearmonthdayr   utc	isoformatr   r   r:   r   r   rJ   )r   r   r   r   tmintmaxresrv   r   whenhhmms   &          r   surrounding_eventsr      s   &$$Y/446##%#))SWW'||--6Y[ 	399cgghllK#$%.Y[ 	jjl ${r   CCJ79 	 '''2&B66'2&**:6 5 5w#''4 "%+4;9DJJ$r"&&L"A!BCD	 '
 
#  	$  	s*   *F2 D#G A G 2GGGGc                    V ^8  d   QhRRRR/# )r   msgr    r   r_   r   )r   s   "r   r   r      s     8 8s 8t 8r   c                    R RRRV /./# )contentr   textr   )r   s   &r   _textr      s    5677r   c                     ^ RI Hp Hp V ! RRR\        R\        R\        R\        R\        /4      R	 R
 l4       pV ! RRR\        R\        R\        R\        R\        /4      R R l4       pV ! RRR\        R\        R\        R\        R\        R\        R\        R\        R\        R\        R\        /4      R R l4       pV ! RRR\        /4      R R l4       pV ! RRR\        /4      R R  l4       pV! R!R"W#WEV.R#7      # )$z,Build the in-process ``compose`` MCP server.)toolcreate_sdk_mcp_serveremail_composeu  Open an editable EMAIL card on Ahmed's HUD for him to review before sending (preferred over email_send when Ahmed is present). Returns a draft_id. Then either he clicks Send, or he tells you to send and you call compose_send. `to`/`cc` are comma-separated addresses. `attachments` = optional file paths (comma-separated) to stage on the card — Ahmed sees them and can add/remove before it sends.rB   rD   rE   rC   rI   c                    V ^8  d   QhRRRR/# r   argsr_   r   r   )r   s   "r   r   "build_server.<locals>.__annotate__   s     
/ 
/$ 
/4 
/r   c                  "   ^ RI Hp VP                  4       '       g   \        R4      # \	        \        V P                  RR4      4      P                  4       \        V P                  RR4      4      P                  4       \        V P                  RR4      4      \        V P                  RR4      4      V P                  R4      4      p\        R	V R
V R24      # 5i)    ro   zemail is not configured.rB   r[   rC   rD   rE   rI   zDraft zU is on Ahmed's screen for review. Wait for him to Send/Cancel, or call compose_send('z') if he tells you to send it.)r   r   r   r   rL   r    r   r<   )r   r   r+   s   &  r   r   #build_server.<locals>.email_compose   s      	($$&&344 r"#))+S$1C-D-J-J-LB'(#dhhvr.B*CHH]#% vcU #BBE G-. / 	/s   CCevent_composea  Open an editable CALENDAR event card on Ahmed's HUD to confirm before booking. Shows his other events that day for context. Times are local ISO like '2026-07-08T15:00:00'. Set meet=true to attach a Google Meet link. Returns a draft_id; book it with compose_send.rN   rO   rP   rQ   rR   c                    V ^8  d   QhRRRR/# r   r   )r   s   "r   r   r     s     N N$ N4 Nr   c                  "   ^ RI Hp VP                  4       '       g   \        R4      # \	        V P                  RR4      4      P                  4       pV'       g   \        R4      # \        P                  ! \        V4      G Rj  xL
 p\        \	        V P                  RR4      4      V\	        V P                  RR4      4      P                  4       \	        V P                  R	R4      4      P                  4       \        V P                  R
R4      4      V4      pV'       d   R\        V4       R2MRp\        RV RV RV R24      #  L5i)r   r{   zcalendar is not configured.rO   r[   z$event_compose failed: no start time.NrN   rP   rQ   rR   Fz He already has z event(s) that day.zEvent draft z! is on Ahmed's screen for review.z Book it with compose_send('z') when he confirms.)r   r   r   r   r    r   r<   rp   rq   r   rV   r   len)r   r   rO   ctxr+   notes   &     r   r   #build_server.<locals>.event_compose  s&     	'##%%677DHHWb)*002?@@%%&8%@@ "%&#$**,b)*002&%()3	0
 DG!#c(+>?B|C5(I$ P336%7KM N 	N As   BE	ECE	compose_updateu$  Change a field on an open draft card (email or event) and refresh it on Ahmed's screen — e.g. he says 'change the subject' or 'add Elie to CC'. Pass draft_id (omit = most recent) and only the fields to change. `attachments` (comma-separated file paths) REPLACES the draft's attachment list.r0   c                    V ^8  d   QhRRRR/# r   r   )r   s   "r   r   r   %  s     
R 
R4 
RD 
Rr   c                  "   \        V P                  R R4      4      P                  4       ;'       g    \        4       pV'       d   \	        V4      '       g   \        R4      # R Uu/ uF#  pW 9   g   K  W,          R8w  g   K  W V,          bK%  	  ppV P                  R4      R9  d   \        V R,          4      VR&   \        WRR7       \        RT RRP                  V4      ;'       g    R	 R
24      # u upi 5i)r0   r[   zno open draft to update.rI   T)r`   zUpdated draft z: r   z	(nothing)r   )	rB   rC   rD   rE   rN   rO   rP   rQ   rR   )Nr[   )	r    r   r<   r-   r5   r   r@   rf   r   )r   r+   rd   rH   s   &   r   r   $build_server.<locals>.compose_update  s      $((:r*+113BBy{)C..344(H( "#+,9 9=B !!W*( 	 H 88M"*4$6tM7J$KF=!S.~cU"TYYv->-M-M+,NaPQQHs%   AC9 C9&C43
C4C4A*C9compose_sendzSend the email / book the event for an open draft card, using its CURRENT values (including any edits Ahmed typed). Pass draft_id (omit = most recent). Closes the card.c                    V ^8  d   QhRRRR/# r   r   )r   s   "r   r   r   6  s       $ r   c                  "   \        V P                  R R4      4      P                  4       ;'       g    \        4       pV'       d   \	        V4      '       g   \        R4      # \        V4      G Rj  xL
 w  r#\        V4      #  L5i)r0   r[   zno open draft to send.N)r    r   r<   r-   r5   r   rx   )r   r+   okrv   s   &   r   r   "build_server.<locals>.compose_send1  sc      $((:r*+113BBy{)C..122"3'Sz (s   AB*B0B1Bcompose_cancelzODiscard an open draft card without sending. Pass draft_id (omit = most recent).c                    V ^8  d   QhRRRR/# r   r   )r   s   "r   r   r   @  s     0 04 0D 0r   c                   "   \        V P                  R R4      4      P                  4       ;'       g    \        4       pV'       d   \	        V4      '       g   \        R4      # \        V4       \        RV R24      # 5i)r0   r[   zno open draft to cancel.zDiscarded draft r   )r    r   r<   r-   r5   r   rl   )r   r+   s   & r   r   $build_server.<locals>.compose_cancel=  sc      $((:r*+113BBy{)C..344S'uA.//s   AA<6A<rZ   z1.0.0)nameversiontools)claude_agent_sdkr   r   r    r   )r   r   r   r   r   r   r   s          r   build_serverr      sN   <	/M ifc4# 
/ 
/ 
/M C#uc;4NN" 

%
 sD#tS)S&#C#uc;4-.
R.
R 
.3 s
	

 

"$.#46060 !^   r   r3   )__conditional_annotations____doc__
__future__r   rp   r   r#   r   r   r   voice.eventsr   r   __annotations__r	   r   r&   r-   r5   r@   rL   rV   rK   rf   rl   rx   rs   r   r   r   )r   s   @r   <module>r      s   6 #  	  2 2   	 = !	+&
%8A<:8_ r   