
    z4UjS                    p   d Z ddlmZ ddlZddlZddlmZ  ee      j                         j                  j                  dz  Z
dZ	 e
j                         r: ee
      ej                  vr"ej                  j                  d ee
             ddlZddZddZdddZdd	Zddd
Zd Zy# e$ r dZY #w xY w)u/
  Asana project management — in-process MCP so Jarvis runs Ahmed's projects by voice.

THIN WRAPPER — deliberately. All the REST logic lives in ONE place,
`jarvis-tools-service/asana.py`, which this module imports and re-exposes as
in-process SDK-MCP tools. The hosted connector (mcp_server.py) wraps the SAME
functions. So voice Jarvis and the claude.ai connector have IDENTICAL Asana
capability, and one bug fix fixes both. Never re-implement a call here.

Asana (app.asana.com) is where Ahmed's PROJECT work lives: projects, their tasks,
due dates, assignees and comment threads. Distinct from mcp__tasks__* (his
personal to-do list on the Railway brain).

Auth: ASANA_PAT in .env, sent as `Authorization: Bearer <pat>`. MCP_ASANA=0
disables the whole server; a missing jarvis-tools-service dir just disables it
too (enabled() returns False — the engine keeps booting). Optional
ASANA_WORKSPACE (gid or name) pins the workspace. No extra deps — stdlib urllib.

Everything a tool returns is SPOKEN ALOUD, so results are short plain prose /
plain "- name — due …" lines. Never markdown, never a JSON dump, never a
traceback: failures come back as one human sentence. (The one exception is
asana_api, the escape hatch, whose raw JSON is for the model, not for speech.)

Tools (Claude sees them as mcp__asana__<name>):
  asana_workspaces      list workspaces (and the default one)
  asana_projects        list projects, optional name filter, optional archived
  asana_project_create  new project (team needed only in an organization)
  asana_project_update  rename / re-date / re-own / ARCHIVE (reversible)
  asana_project_members list, add or remove the people on a project
  asana_project_status  post a status update (on_track / at_risk / …)
  asana_task            FULL detail of one task (notes, section, subtasks, comments)
  asana_tasks           open (or completed) tasks: a project, a section, or his own
  asana_task_create     new task (project / section / parent / assignee / due date)
  asana_task_update     rename, re-note, re-date, re-assign, (un)complete
  asana_task_move       to another project and/or section (add + remove, a real move)
  asana_task_complete   shortcut for "mark it done"
  asana_task_delete     delete a task (recoverable for 30 days)
  asana_task_people     assignee, followers, tags
  asana_subtasks        list or create subtasks
  asana_comment         post a comment
  asana_comments        read the comment thread
  asana_search          find tasks (premium search, typeahead fallback)
  asana_api             escape hatch — any Asana REST endpoint
    )annotationsN)Pathzjarvis-tools-servicec                     t         j                  j                  dd      dk(  ryt        t              xr t	        j
                         S )zJOn when the shared module imported and a Personal Access Token is present.	MCP_ASANA10F)osenvirongetboolasana
configured     A/Users/ahmed/devFolder/Ultron/claude-voice/voice/asana_control.pyenabledr   ?   s4    	zz~~k3'3.;-5++--r   c                $    ddt        |       dgiS )Ncontenttext)typer   )str)msgs    r   _textr   F   s    S:;<<r   c                \    t        | j                  ||      xs |      j                         S N)r   r   strip)argskeydefaults      r   _sr    J   s&    txxW%017799r   c                    | j                  |      }||dk(  ryt        |t              r|S t        |      j	                         j                         dv S )uK   Tri-state: absent/'' → None, else true/false. (MCP may hand us a string.)N )r   trueyesydonecomplete)r   
isinstancer   r   r   lower)r   r   vs      r   _br+   N   sK    AyAG!Tq6<<>!%RRRr   c                    i }| j                         D ]&  \  }}t        |t              rd|fn|\  }}||d||<   ( d|t        |xs g       dS )u  A FULL JSON Schema for @tool.

    Why not the {"name": str} shorthand the SDK also accepts: it marks EVERY
    declared parameter as required, so `asana_projects` (all-optional) would be
    rejected with "'query' is a required property". A full schema (type +
    properties) is passed through verbatim, so we control `required` — and each
    property carries its own description, which is what the model actually reads.

    props: {name: "description"} → string, or {name: ("integer", "description")}.
    string)r   descriptionobject)r   
propertiesrequired)itemsr(   r   list)propsr1   outkr*   kinddescs          r   _schemar9   X   s[     C1&0C&8h]a
dt4A  CT(.b=QRRr   c                 |   dd l ddlm} m} dfd |ddt	        i             dfd       } |ddt	        d	d
d            dfd       } |ddt	        dddddddg            dfd       } |ddt	        ddddddddg            dfd        } |d!d"t	        dd#d$d%d&dg            dfd'       } |d(d)t	        dd*d+d,d-dd.g            dfd/       } |d0d1t	        d2d3id2g            dfd4       } |d5d6t	        d7d8d9d:d;            dfd<       }	 |d=d>t	        d?d@dAdBdCdDddEdg            dfdF       }
 |dGdHt	        d3ddddIdJdKdLd2g            dfdM       } |dNdOt	        d3dPdQdRdSd2g            dfdT       } |dUdVt	        d2d3id2g            dfdW       } |dXdYt	        d2d3id2g            dfdZ       } |d[d\t	        d3dJd]d^d_d`dad2g            dfdb       } |dcddt	        dedfdgd2g            dfdh       } |didjt	        d3dkdld2d.g            dfdm       } |dndot	        d3dpdqd2g            dfdr       } |dsdtt	        dudvdwdddxdy            dfdz       } |d{d|t	        d}d~dddddg            dfd       } | dd||||||||	|
||||||||||g      S )Nr   )create_sdk_mcp_servertoolc                ^   K   t         j                  | g|i | d{         S 7 w)u   Every asana.* function is blocking and NEVER raises — it returns a
        sentence. Run it off the event loop and speak whatever comes back.N)r   	to_thread)fnakwasyncios      r   _runzbuild_server.<locals>._runs   s2      ,7,,R:!:r::;;:s   -+	-asana_workspacesu   List the Asana workspaces this account can see (and which one the other asana tools default to). Rarely needed — only for 'which Asana am I on' or when a project can't be found.c                L   K    t         j                         d {   S 7 wr   )r   
workspacesr   rC   s    r   rD   z&build_server.<locals>.asana_workspacesx   s      
 %**++++s   $"$asana_projectszList Ahmed's Asana projects. `query` optionally narrows by name ('redesign', 'client'). `archived`='true' lists archived ones instead. Use for 'what projects do I have', 'what's in Asana'.z(a word or two to narrow the list by name)booleanz%true = list ARCHIVED projects instead)queryarchivedc           
        K    t         j                  t        | d      t        t	        | d                   d {   S 7 w)NrJ   rK   )r   projectsr    r   r+   rG   s    r   rH   z$build_server.<locals>.asana_projects   s>      %.."T7*;r$
346 6 	6 6s   9AAAasana_project_createzCreate an Asana project. `name` required. `team` is only needed if the workspace is an ORGANIZATION (the tool tells you, and lists the teams, if so). Optional `notes`, `due_on` (YYYY-MM-DD), `privacy` (public_to_workspace | private_to_team | private).zthe project namez(only needed in an ORGANIZATION workspacezdescription of the projectz
YYYY-MM-DDz/public_to_workspace | private_to_team | private)nameteamnotesdue_onprivacyrO   c                   K    t         j                  t        | d      t        | d      t        | d      t        | d      t        | d             d {   S 7 w)NrO   rP   rQ   rR   rS   )r   project_creater    rG   s    r   rN   z*build_server.<locals>.asana_project_create   sZ      %..40@"T6BRT7+Rh-?D)ATV V 	V Vs   AAAAasana_project_updateu1  Change an Asana project (`project` = its name or gid): `name` (rename), `notes`, `due_on`, `owner`, or `archive`='true' to ARCHIVE it (hides it, keeps everything, reversible with archive='false'). Archiving is how you 'get rid of' a project — deleting one is NOT offered here because it can't be undone.zthe project's name or gidza new name (rename)znew descriptionz new owner: a name, email or 'me')rI   z%true = archive it, false = un-archive)projectrO   rQ   rR   ownerarchiverW   c                   K    t         j                  t        | d      t        | d      t        | d      t        | d      t        | d      t        | d             d {   S 7 w)NrW   rO   rQ   rR   rX   rY   )r   project_updater    r+   rG   s    r   rV   z*build_server.<locals>.asana_project_update   s`      %..40CRfEUT7+Rh-?D'ART9-/ / 	/ /   AA&A$ A&asana_project_membersu  Who's on a project — and invite/remove them ('add Sara to the launch project'). `add` / `remove` = comma-separated emails, names or gids ('me' works). `access_level` (admin|editor|commenter|viewer) sets their rights when adding. No add/remove = just list the members. NOTE: this shares an existing project with someone already in the workspace; inviting a brand-new person to Asana itself is asana_api POST /workspaces/{gid}/addUser.z9comma-separated people to add (emails, names, gids, 'me')z comma-separated people to removez#admin | editor | commenter | viewer)rW   addremoveaccess_levelc           
        K    t         j                  t        | d      t        | d      t        | d      t        | d             d {   S 7 w)NrW   r^   r_   r`   )r   project_membersr    rG   s    r   r]   z+build_server.<locals>.asana_project_members   sP      %//D)1DbuoT8,b~.FH H 	H H   AA	A
Aasana_project_statuszPost a status update on a project so the team sees where it stands. `text` = the update. `status_type` = on_track (default), at_risk, off_track, on_hold, or complete. `title` optional.zthe status update itselfz=on_track (default) | at_risk | off_track | on_hold | completezoptional headline)rW   r   status_typetitler   c           
        K    t         j                  t        | d      t        | d      t        | dd      t        | d             d {   S 7 w)NrW   r   re   on_trackrf   )r   project_statusr    rG   s    r   rd   z*build_server.<locals>.asana_project_status   sS      %..40CRfEUT=*=r$?PR R 	R R   AA
AA
asana_taskzFULL detail of ONE Asana task (`task` = its name or gid): notes, assignee, due date, project, section, tags, followers, subtasks and the latest comments. Use it for 'tell me about that task', 'what's the story on X', or before changing something.taskzthe task's name or gidc                b   K    t         j                  t        | d             d {   S 7 wNrl   )r   rl   r    rG   s    r   rk   z build_server.<locals>.asana_task   s'      %**bv&67777   %/-/asana_taskszList Asana tasks. `project` (name or gid) = that project's tasks; add `section` for one board column; no project = Ahmed's OWN tasks ('what's on my plate'). Open tasks only unless `completed`='true'. `limit` defaults to 50.z0a project name or gid; empty = Ahmed's own tasksz"a board column inside that project)rI   z#true = list COMPLETED tasks instead)integerz max tasks to return (default 50))rW   section	completedlimitc                   K   t        | d      } t        j                  || t        | d      t        t	        | d            t        | j                  d      xs d             d {   S 7 w)NrW   rr   rs   rt   2   )r    r   tasksr   r+   intr   )r   projrC   s     r   rp   z!build_server.<locals>.asana_tasks   sf      $	"%++tXr$	7Jr$45dhhw/5268 8 	8 8s   A%A/(A-)A/asana_task_createu  Create an Asana task. `name` (short, imperative) is required. `project` = the project name/gid to file it under (without one it lands in his My Tasks). `section` = the board column inside that project. `parent` = another task, to make this a SUBTASK of it. `notes` = detail. `assignee` = a person's name/email, or 'me' (the default). `due_on` = YYYY-MM-DD — resolve 'tomorrow' / 'next Thursday' to a real date yourself (Asia/Riyadh).z!the task name (short, imperative)z$project name or gid to file it underz board column inside that projectu+   another task — makes this a SUBTASK of itzdetail / descriptionz+a person's name or email, or 'me' (default))rO   rW   rr   parentrQ   assigneerR   c                   K    t         j                  t        | d      t        | d      t        | d      t        | dd      t        | d      t        | d      t        | d             d {   S 7 w)	NrO   rQ   rW   r|   merR   rr   r{   )r   task_creater    rG   s    r   rz   z'build_server.<locals>.asana_task_create   so       %++Rf-=r$?PT9-r$
D/IT8,by.A2dHCUW W 	W Ws   A(A2+A0,A2asana_task_updateu  Change an existing Asana task (`task` = its gid or name, matched fuzzily). Set only what changes: `name` (rename), `notes`, `due_on` (YYYY-MM-DD), `start_on` (needs a due date too, and a PAID Asana plan — it fails on his free one), `assignee` (a name or 'me'), `complete` ('true' to close, 'false' to reopen). Use for 'push that to Friday', 'give it to Sara', 'reopen the launch task'.zYYYY-MM-DD (paid plans only)z!a person's name or email, or 'me')rI   ztrue = close it, false = reopen)rl   rO   rQ   rR   start_onr|   r'   c                   K    t         j                  t        | d      t        | d      t        | d      t        | d      t        | d      t        | d      t        | d             d {   S 7 w)Nrl   rO   rQ   rR   r   r|   r'   )r   task_updater    r+   rG   s    r   r   z'build_server.<locals>.asana_task_update   sm      %++Rf-=r$?OT7+Rh-?D*AUT:.40DF F 	F Fs   A'A1*A/+A1asana_task_moveuk  Move a task to another `project` and/or `section` (board column). This is a REAL move — the task is taken out of the project it was in (Asana would otherwise leave it in both). `section` alone moves it between columns of its current project. `remove_from` optionally names just one project to drop. Use for 'move it to Done', 'that belongs on the client board'.zthe project to move it INTOzthe board column to put it inz+optionally, the one project to drop it from)rl   rW   rr   remove_fromc           
        K    t         j                  t        | d      t        | d      t        | d      t        | d             d {   S 7 w)Nrl   rW   rr   r   )r   	task_mover    rG   s    r   r   z%build_server.<locals>.asana_task_move  sO      %//2dF+;Ri=PT9-r$/FH H 	H Hrc   asana_task_completez{Mark an Asana task done. `task` = its gid or name. Use for 'tick off the landing page task', 'that Asana task is finished'.c                b   K    t         j                  t        | d             d {   S 7 wrn   )r   task_completer    rG   s    r   r   z)build_server.<locals>.asana_task_complete  s)     
 %--r$/?@@@@ro   asana_task_deleteu   Delete an Asana task ('remove that task', 'get rid of it'). It goes to Asana's Deleted Items and can be restored for 30 days, so it's safe — but only do it when he clearly means DELETE, not 'mark it done' (asana_task_complete).c                b   K    t         j                  t        | d             d {   S 7 wrn   )r   task_deleter    rG   s    r   r   z'build_server.<locals>.asana_task_delete  s)      %++Rf-=>>>>ro   asana_task_peopleu  Who and what is on a task: set the `assignee`, add/remove followers (`add_followers` / `remove_followers`, comma-separated names, emails or 'me'), add/remove tags (`add_tag` / `remove_tag` — a new tag is created if it doesn't exist). With no changes it just reports who's on it.z*comma-separated people to add as followersz#comma-separated people to un-followz%comma-separated tags (created if new)zcomma-separated tags to remove)rl   r|   add_followersremove_followersadd_tag
remove_tagc                   K    t         j                  t        | d      t        | d      t        | d      t        | d      t        | d      t        | d             d {   S 7 w)Nrl   r|   r   r   r   r   )r   task_peopler    rG   s    r   r   z'build_server.<locals>.asana_task_people#  se      %++Rf-=r$
?ST?3R>P5QT9-r$/EG G 	G Gr\   asana_subtaskszList a task's subtasks, or create them: `add` = comma-separated subtask names ('draft the copy, get sign-off'). Use for 'break that task down'.zthe parent task's name or gidz'comma-separated subtask names to create)rl   r^   c                x   K    t         j                  t        | d      t        | d             d {   S 7 w)Nrl   r^   )r   subtasksr    rG   s    r   r   z$build_server.<locals>.asana_subtasks4  s/      %.."T6*:BtUOLLLL   0:8:asana_commentzAdd a comment to an Asana task so the team sees it. `task` = its gid or name, `text` = the comment. Use for 'comment on the design task that the client approved'.zthe comment)rl   r   c                x   K    t         j                  t        | d      t        | d             d {   S 7 w)Nrl   r   )r   commentr    rG   s    r   r   z#build_server.<locals>.asana_comment=  s0      %--D&)92dF;KLLLLr   asana_commentsu   READ the comment thread on a task (system events are skipped) — 'what did they say on that task', 'any update from the team'. `limit` = how many of the latest to show (default 10).)rq   z#how many of the latest (default 10))rl   rt   c           	        K    t         j                  t        | d      t        | j	                  d      xs d             d {   S 7 w)Nrl   rt   
   )r   commentsr    rx   r   rG   s    r   r   z$build_server.<locals>.asana_commentsF  sE      %.."T6*:dhhw/5268 8 	8 8s   AAA
Aasana_searchu  Find Asana tasks when you don't know where they live ('find the invoice task', 'anything about the Riyadh launch'). `query` = a word or two; optional filters `project`, `assignee` ('me' or a name), `due_before` / `due_after` (YYYY-MM-DD), `completed` ('true'/'false'). On a free Asana plan it falls back to name matching (descriptions aren't searched), and the index lags up to a minute — for a task you JUST created use asana_task / asana_tasks instead.za word or two to look forz"limit to one project (name or gid)z'me' or a person's name)rI   z#true = only done, false = only open)rJ   rW   r|   
due_before	due_afterrs   c                   K    t         j                  t        | d      t        | d      t        | d      t        | d      t        | d      t        | d             d {   S 7 w)NrJ   rW   r|   r   r   rs   )r   searchr    r+   rG   s    r   r   z"build_server.<locals>.asana_searchQ  sb      %,,4(92dI;NT:.40FT;/D+1FH H 	H Hr\   	asana_apiu  Escape hatch — call ANY Asana REST endpoint (base https://app.asana.com/api/1.0) for things the other asana tools don't cover: sections, tags, teams, attachments, dependencies, templates, goals, portfolios, custom fields, webhooks, inviting a NEW person to the workspace, deleting a project. `method` = GET/POST/PUT/DELETE, `path` = everything after /api/1.0 (e.g. '/projects/123/sections'). Do NOT wrap `body_json` in {"data":…} — that's added for you. IMPORTANT: Asana returns only gid/name unless you ask for fields, so pass e.g. params_json={"opt_fields":"name,due_on,assignee.name","limit":50}. Returns raw JSON — summarise it, never read it out.zGET | POST | PUT | DELETEz6everything after /api/1.0, e.g. /projects/123/sectionsu+   JSON body, WITHOUT the {"data":…} wrapperu)   JSON query params — put opt_fields here)methodpath	body_jsonparams_jsonr   r   c           
        K    t         j                  t        | dd      t        | d      t        | d      t        | d             d {   S 7 w)Nr   GETr   r   r   )r   rawr    rG   s    r   r   zbuild_server.<locals>.asana_apic  sQ     " %))Rh%>4@PT;/D-1HJ J 	J Jrj   r   z2.0.0)rO   versiontools)returndict)r   r   r   r   )rB   claude_agent_sdkr;   r<   r9   )r;   r<   rD   rH   rN   rV   r]   rd   rk   rp   rz   r   r   r   r   r   r   r   r   r   r   rC   rB   s                        @@r   build_serverr   n   s8   <<
 

./6r{<,	<, 

B FSU V	W6W6 

 ? -E8)O	Q
 (
V
V 

 
 90;L)4VRT +	
 /
 /
 

!' 9U?#HJ +	 H H 

 ? 95"a/1 f%	'	(R(R 
,1 634vh
?A8A8 
- PB RKM NO8O8 

X >D@J2L)+ (WW
 

J 30;L)7UBM	O
 (FF
 

Q
 3;="OQ (	
H
H 

?
634vh
?AA	AA 

# 634vh
?A?A? 

O 3B$P'LE!AC (GG
 

T
:CE(MM 
/ 3]K6"$	%M%M 

- 3NP(	88 
.. 7B8!-L R	T UVHVH
 
+	B 8S O"MO V$	&' J!' J !g1E#%:<P;(9;L 35F .-Y( r   )r   r   )r   r   r   r   )r"   )r   r   r   r   r   r   r   r   )r   r   r   r   r   )r4   r   r1   zlist[str] | Noner   r   )__doc__
__future__r   r	   syspathlibr   __file__resolver{   _SERVICEr   is_dirr   r   insert	Exceptionr   r   r    r+   r9   r   r   r   r   <module>r      s   *V # 	 
  >!!#**114JJx=(HHOOAs8}-
.=:SS,Qg  Es   A
B+ +B54B5