@agentdeploymentco/bishop 0.3.4 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/README.md +90 -9
  2. package/dist/cli.js +19 -17
  3. package/dist/config.js +4 -4
  4. package/dist/db.js +57 -11
  5. package/dist/files.js +2 -2
  6. package/dist/gmail/context.js +1 -0
  7. package/dist/gmail/create.js +1 -1
  8. package/dist/gmail/history.js +1 -1
  9. package/dist/gmail/interface.js +2 -2
  10. package/dist/harness/claude.js +6 -5
  11. package/dist/harness/codex.js +5 -5
  12. package/dist/harness/create.js +1 -1
  13. package/dist/harness/principal.js +14 -10
  14. package/dist/ids.js +1 -1
  15. package/dist/interface/authorize.js +1 -1
  16. package/dist/interface/context.js +1 -0
  17. package/dist/nono-setup.js +7 -0
  18. package/dist/outgoing.js +2 -2
  19. package/dist/principals.js +6 -0
  20. package/dist/sandbox.js +20 -0
  21. package/dist/schedule/cli.js +7 -7
  22. package/dist/schedule/prompt.js +2 -2
  23. package/dist/schedule/runner.js +3 -3
  24. package/dist/schedule/store.js +2 -2
  25. package/dist/sessions/store.js +13 -7
  26. package/dist/slack/app.js +2 -2
  27. package/dist/slack/context.js +1 -0
  28. package/dist/slack/handlers.js +1 -1
  29. package/dist/slack/history.js +1 -1
  30. package/dist/slack/interface.js +2 -2
  31. package/dist/slack/members.js +1 -1
  32. package/dist/slack/principal.js +1 -0
  33. package/dist/slack/users.js +1 -1
  34. package/dist/teams/context.js +1 -0
  35. package/dist/teams/create.js +2 -2
  36. package/dist/teams/fake.js +1 -1
  37. package/dist/teams/graph.js +1 -1
  38. package/dist/teams/history.js +1 -1
  39. package/dist/teams/interface.js +3 -3
  40. package/dist/teams/principal.js +1 -0
  41. package/dist/teams/provision.js +1 -1
  42. package/dist/teams/setup.js +39 -39
  43. package/dist/tools/files.js +2 -2
  44. package/dist/tools/schedules.js +5 -5
  45. package/dist/tools/server.js +1 -1
  46. package/dist/transcript.js +3 -0
  47. package/dist/turn.js +3 -1
  48. package/dist/worktrees.js +3 -3
  49. package/package.json +13 -8
@@ -1,2 +1,2 @@
1
- import{runCommand as C}from"../commands/run.js";import{commandLine as L}from"../commands/syntax.js";import{etiquetteNote as O,threadParticipants as E}from"../interface/etiquette.js";import{threadVerbosity as H}from"../interface/verbosity.js";import{log as f}from"../log.js";import{createSlackApp as F}from"./app.js";import{createAuthorizer as K}from"./authorizer.js";import{botTurnCap as _}from"./bots.js";import{deliverToSlack as U,describeSlackHandle as z,parseSlackHandle as b,resolveSlackDestination as R,slackHandle as W}from"./destination.js";import{describeSlackFile as j,parseSlackFile as q,slackAttachment as v,slackFilePayload as G}from"./files.js";import{registerHandlers as V}from"./handlers.js";import{landedMidThread as Y,threadHistory as $}from"./history.js";import{linkMentions as I,mentionableName as S,renderMentions as T}from"./mentions.js";import{createPresence as J}from"./presence.js";import{say as N}from"./say.js";import{threadKeyString as Q}from"./thread.js";import{userDirectory as X}from"./users.js";const Z={name:"slack",guidance:["This conversation is a Slack thread. Each thing you write is posted as its own message while","you work, and the tools you call show as a card between them, so someone is watching the","whole turn. Say what you are doing as you go, and keep it to a line or two. Your last message","is the answer to what was asked. Slack renders standard markdown, tables included, so write","normally and keep it short.","People and other agents are named to you as @name. Write @name to mention one and Bishop","makes it a real Slack mention; a name it cannot match to one account stays plain text, so","use the name you were given. @here, @channel, and @everyone are never mentions, whoever","writes them.","A thread can hold a conversation between other people that has nothing to do with you.",'Bishop marks a message you may leave alone with a <thread> tag carrying reply="optional":',"other people are talking here and this one did not name you, so answer it only if you have","something worth adding. To stay out of it, say <no-output> and nothing else, and Bishop","posts nothing at all. Every message without that tag is yours to answer."].join(`
2
- `)},ee=3e3;async function te(a,i,s=ee){const l=await ae(a.botUserId,i,s),r=S(l)??a.username;if(!r)return;const m=[a.appName,l.realName].filter(h=>!!h?.trim()&&h?.toLowerCase()!==r.toLowerCase());return{address:`@${r}`,...m.length>0?{aka:[...new Set(m)]}:{}}}async function ae(a,i,s){const l={userId:a};let r;const m=new Promise(h=>{r=setTimeout(()=>h(l),s)});try{return await Promise.race([i.lookup(a).catch(()=>l),m])}finally{clearTimeout(r)}}async function be(a){const{app:i,identity:s}=await F(a.credentials),l=await K(i.client,a.allow),r=X(i.client,{self:s.botUserId}),m=_(a.maxBotTurns),h=E(),g=H(a.meta,"slack",a.verbosity),x={verbosity:g,model:a.model,configured:a.configuredModel??{},stop:a.stop},c=await te(s,r),y={...Z,...c?{self:c}:{}};c?f.info({address:c.address,aka:c.aka},"the agent's name in Slack"):f.warn("could not read the bot's own name; the agent will not know what it is called");const k=new Map,M=async({key:e,keyString:t,thread:o,text:w},d)=>{const n=L(w);if(!n)return!1;const p=C(n,{thread:o},x);try{await N(d.client,{channel:e.channelId,threadTs:e.threadTs},p),k.get(t)?.interrupted()}catch(u){f.warn({thread:t,err:u},"couldn't answer a command")}return!0};async function B(e){const t=await r.lookup(e.userId),o=S(t);return{id:e.userId,...o?{displayName:o}:{},...t.realName?{realName:t.realName}:{},...t.email?{email:t.email}:{},...e.isBot?{isBot:!0}:{}}}function P(e,t){const o=j(t);if(!o.unavailable)return a.attachments.remember(e,y.name,{id:o.id,name:o.name,payload:G(t)})}async function A(e,t){return t.files.length===0?[]:Promise.all(t.files.map(async o=>{const w=P(e.id,o),d=await a.files.save(e.dirKey,v(o,a.credentials.botToken));return w?{...d,ref:w}:d}))}function D(e,t){const o=T(e.messages[0]?.text??"",r),w=Y(e),d=O({participants:e.participants,mentioned:e.messages.some(n=>n.mentionsAgent)});return{thread:e.thread,isNewThread:e.isNewThread,origin:y,...d?{note:d}:{},...w?{history:()=>$({client:t.client,key:e.key,before:w,allows:n=>l.allows(n),answersBots:m.answersBots,self:{botUserId:s.botUserId,...s.botId?{botId:s.botId}:{},...c?{botName:c.address}:{}},users:r,remember:n=>a.attachments.remember(e.thread.id,y.name,n),saw:(n,p)=>h.saw(e.keyString,n,p)})}:{},async messages(){return Promise.all(e.messages.map(async(n,p)=>{const u=await A(e.thread,n);return{principal:await B(n),text:p===0?await o:await T(n.text,r),...u.length>0?{files:u}:{}}}))},presence:()=>{const n=J(e,t,{outgoing:p=>I(p,r),title:()=>o,verbosity:()=>g.of(e.thread.id).level,...d?{mayDecline:!0}:{},...a.taskIntervalMs===void 0?{}:{taskIntervalMs:a.taskIntervalMs}});return k.set(e.keyString,n),{...n,finish:async()=>{try{await n.finish()}finally{k.delete(e.keyString)}}}},async notify(n){await N(t.client,{channel:e.channelId,threadTs:e.threadTs},n)},async destination(){const n=W({channel:e.channelId,threadTs:e.threadTs});return{interface:"slack",handle:n,label:await z(i.client,n)}}}}return V(i,{identity:s,...c?{botName:c.address}:{},threads:a.threads,events:a.events,run:(e,t)=>a.run(D(e,t)),stop:a.stop,allows:e=>l.allows(e),bots:m,participants:h,command:M}),{name:"slack",async start(){await i.start(),s.scopes.includes("files:read")||f.warn("this Slack app has no files:read scope, so it cannot open files people share; reinstall it to grant one"),s.scopes.includes("files:write")||f.warn("this Slack app has no files:write scope, so it cannot send files back; reinstall it to grant one"),r.prime(),f.info("listening on Slack via Socket Mode")},async stop(){l.stop(),await i.stop()},allows:e=>l.allows(e),resolveDestination:(e,t)=>R(i.client,e,{...t?.optedInOnly?{allows:o=>l.allows(o)}:{}}),sameConversation:(e,t)=>b(e).channel===b(t).channel,deliver:async e=>{const t=await U(i.client,{...e,text:await I(e.text,r)});return t?{threadKey:Q({teamId:s.teamId,channelId:t.channel,threadTs:t.threadTs})}:{}},attachment:e=>{const t=q(e);return t?v(t,a.credentials.botToken):void 0}}}export{Z as SLACK_ORIGIN,be as createSlackInterface,te as slackIdentity};
1
+ import{runCommand as O}from"../commands/run.js";import{commandLine as C}from"../commands/syntax.js";import{etiquetteNote as L,threadParticipants as E}from"../interface/etiquette.js";import{threadVerbosity as H}from"../interface/verbosity.js";import{log as f}from"../log.js";import{createSlackApp as U}from"./app.js";import{createAuthorizer as F}from"./authorizer.js";import{botTurnCap as K}from"./bots.js";import{slackObserver as _}from"./context.js";import{deliverToSlack as z,describeSlackHandle as R,parseSlackHandle as I,resolveSlackDestination as W,slackHandle as j}from"./destination.js";import{describeSlackFile as q,parseSlackFile as G,slackAttachment as S,slackFilePayload as V}from"./files.js";import{registerHandlers as Y}from"./handlers.js";import{landedMidThread as $,threadHistory as J}from"./history.js";import{linkMentions as T,mentionableName as Q,renderMentions as x}from"./mentions.js";import{createPresence as X}from"./presence.js";import{slackSighting as Z}from"./principal.js";import{say as M}from"./say.js";import{threadKeyString as ee}from"./thread.js";import{userDirectory as te}from"./users.js";const ae={name:"slack",guidance:["This conversation is a Slack thread. Each thing you write is posted as its own message while","you work, and the tools you call show as a card between them, so someone is watching the","whole turn. Say what you are doing as you go, and keep it to a line or two. Your last message","is the answer to what was asked. Slack renders standard markdown, tables included, so write","normally and keep it short.","People and other agents are named to you as @name. Write @name to mention one and Bishop","makes it a real Slack mention; a name it cannot match to one account stays plain text, so","use the name you were given. @here, @channel, and @everyone are never mentions, whoever","writes them.","A thread can hold a conversation between other people that has nothing to do with you.",'Bishop marks a message you may leave alone with a <thread> tag carrying reply="optional":',"other people are talking here and this one did not name you, so answer it only if you have","something worth adding. To stay out of it, say <no-output> and nothing else, and Bishop","posts nothing at all. Every message without that tag is yours to answer."].join(`
2
+ `)},ne=3e3;async function oe(t,i,o=ne){const l=await re(t.botUserId,i,o),r=Q(l)??t.username;if(!r)return;const h=[t.appName,l.realName].filter(w=>!!w?.trim()&&w?.toLowerCase()!==r.toLowerCase());return{address:`@${r}`,...h.length>0?{aka:[...new Set(h)]}:{}}}async function re(t,i,o){const l={userId:t};let r;const h=new Promise(w=>{r=setTimeout(()=>w(l),o)});try{return await Promise.race([i.lookup(t).catch(()=>l),h])}finally{clearTimeout(r)}}async function xe(t){const{app:i,identity:o}=await U(t.credentials),l=await F(i.client,t.allow),r=te(i.client,{self:o.botUserId}),h=K(t.maxBotTurns),w=E(),b=_({client:i.client,users:r,home:o,self:o.botUserId}),v=H(t.meta,"slack",t.verbosity),N={verbosity:v,model:t.model,configured:t.configuredModel??{},stop:t.stop},d=await oe(o,r),u={...ae,...d?{self:d}:{}};d?f.info({address:d.address,aka:d.aka},"the agent's name in Slack"):f.warn("could not read the bot's own name; the agent will not know what it is called");const k=new Map,P=async({key:e,keyString:a,thread:s,text:m},c)=>{const n=C(m);if(!n)return!1;const p=O(n,{thread:s},N);try{await M(c.client,{channel:e.channelId,threadTs:e.threadTs},p),k.get(a)?.interrupted()}catch(y){f.warn({thread:a,err:y},"couldn't answer a command")}return!0};async function g(e,a,s){const m=await r.lookup(e),c=Z(m,a,o);return t.principals.see(u.name,s?{...c,self:s}:c)}g(o.botUserId,!0,!0).catch(e=>{f.warn({err:e},"could not record the bot's own account")});function B(e,a){const s=q(a);if(!s.unavailable)return t.attachments.remember(e,u.name,{id:s.id,name:s.name,payload:V(a)})}async function D(e,a){return a.files.length===0?[]:Promise.all(a.files.map(async s=>{const m=B(e.id,s),c=await t.files.save(e.dirKey,S(s,t.credentials.botToken));return m?{...c,ref:m}:c}))}function A(e,a){const s=x(e.messages[0]?.text??"",r),m=$(e),c=L({participants:e.participants,mentioned:e.messages.some(n=>n.mentionsAgent)});return{thread:e.thread,isNewThread:e.isNewThread,origin:u,...c?{note:c}:{},...m?{history:()=>J({client:a.client,key:e.key,before:m,allows:n=>l.allows(n),answersBots:h.answersBots,self:{botUserId:o.botUserId,...o.botId?{botId:o.botId}:{},...d?{botName:d.address}:{}},users:r,principal:g,remember:n=>t.attachments.remember(e.thread.id,u.name,n),saw:(n,p)=>w.saw(e.keyString,n,p)})}:{},observe:()=>b.observe({channelId:e.channelId,direct:e.isDirectMessage,speaker:e.messages.at(-1)?.userId??""}),async messages(){return Promise.all(e.messages.map(async(n,p)=>{const y=await D(e.thread,n);return{principal:await g(n.userId,n.isBot),externalId:n.ts,text:p===0?await s:await x(n.text,r),...y.length>0?{files:y}:{}}}))},presence:()=>{const n=X(e,a,{outgoing:p=>T(p,r),title:()=>s,verbosity:()=>v.of(e.thread.id).level,...c?{mayDecline:!0}:{},...t.taskIntervalMs===void 0?{}:{taskIntervalMs:t.taskIntervalMs}});return k.set(e.keyString,n),{...n,finish:async()=>{try{await n.finish()}finally{k.delete(e.keyString)}}}},async notify(n){await M(a.client,{channel:e.channelId,threadTs:e.threadTs},n)},async destination(){const n=j({channel:e.channelId,threadTs:e.threadTs});return{interface:"slack",handle:n,label:await R(i.client,n)}}}}return Y(i,{identity:o,...d?{botName:d.address}:{},threads:t.threads,events:t.events,run:(e,a)=>t.run(A(e,a)),stop:t.stop,allows:e=>l.allows(e),bots:h,participants:w,command:P,changed:e=>b.changed(e)}),{name:"slack",async start(){await i.start(),o.scopes.includes("files:read")||f.warn("this Slack app has no files:read scope, so it cannot open files people share; reinstall it to grant one"),o.scopes.includes("files:write")||f.warn("this Slack app has no files:write scope, so it cannot send files back; reinstall it to grant one"),r.prime(),f.info("listening on Slack via Socket Mode")},async stop(){l.stop(),await i.stop()},allows:e=>l.allows(e.externalId),resolveDestination:(e,a)=>W(i.client,e,{...a?.optedInOnly?{allows:s=>l.allows(s)}:{}}),sameConversation:(e,a)=>I(e).channel===I(a).channel,deliver:async e=>{const a=await z(i.client,{...e,text:await T(e.text,r)});return a?{threadKey:ee({teamId:o.teamId,channelId:a.channel,threadTs:a.threadTs})}:{}},attachment:e=>{const a=G(e);return a?S(a,t.credentials.botToken):void 0}}}export{ae as SLACK_ORIGIN,xe as createSlackInterface,oe as slackIdentity};
@@ -1 +1 @@
1
- import{log as n}from"../log.js";function a(e){const s=e.profile?.display_name?.trim(),o=e.profile?.real_name?.trim(),t=e.profile?.email?.trim(),r=e.name?.trim();return{userId:e.id??"",...s?{displayName:s}:{},...o?{realName:o}:{},...t?{email:t}:{},...r?{handle:r}:{}}}const l="USLACKBOT";function u(e){return!!e.id&&!e.deleted&&e.id!==l}async function c(e,s=()=>!0){const o=[];let t;do{const r=await e.users.list({limit:200,...t?{cursor:t}:{}});for(const i of r.members??[])i.id&&s(i)&&o.push(a(i));t=r.response_metadata?.next_cursor||void 0}while(t);return n.debug({users:o.length},"paged the workspace member list"),o}export{u as isMentionable,c as listMembers,a as toUser};
1
+ import{log as a}from"../log.js";function d(t){const n=t.profile?.display_name?.trim(),s=t.profile?.real_name?.trim(),e=t.profile?.email?.trim(),r=t.name?.trim(),i=t.enterprise_user?.enterprise_id,o=t.is_restricted||t.is_ultra_restricted||t.is_stranger;return{userId:t.id??"",...n?{displayName:n}:{},...s?{realName:s}:{},...e?{email:e}:{},...r?{handle:r}:{},...t.team_id?{teamId:t.team_id}:{},...i?{enterpriseId:i}:{},...o?{outsider:!0}:{}}}const l="USLACKBOT";function c(t){return!!t.id&&!t.deleted&&t.id!==l}async function p(t,n=()=>!0){const s=[];let e;do{const r=await t.users.list({limit:200,...e?{cursor:e}:{}});for(const i of r.members??[])i.id&&n(i)&&s.push(d(i));e=r.response_metadata?.next_cursor||void 0}while(e);return a.debug({users:s.length},"paged the workspace member list"),s}export{c as isMentionable,p as listMembers,d as toUser};
@@ -0,0 +1 @@
1
+ import{mentionableName as r}from"./mentions.js";function l(n,e){return n.outsider?"external":n.teamId?n.teamId===e.teamId||n.enterpriseId&&n.enterpriseId===e.enterpriseId?"internal":"external":"unknown"}function d(n,e,a){const t=r(n),i=n.realName??n.displayName;return{externalId:n.userId,...t?{handle:t}:{},...i?{name:i}:{},...n.email?{email:n.email}:{},kind:e?"bot":"person",affiliation:l(n,a)}}export{l as affiliationOf,d as slackSighting};
@@ -1 +1 @@
1
- import{log as a}from"../log.js";import{isMentionable as D,listMembers as M,toUser as b}from"./members.js";const _=3600*1e3,N=2e3,x=60*1e3;function l(i){return i?.trim().toLowerCase()||void 0}function L(i,s){return l(i.handle)===s||l(i.displayName)===s||l(i.realName)===s}function k(i,s={}){const{ttlMs:f=_,deadlineMs:p=N,self:g}=s,c=new Map;let o,m;const u=new Map;async function w(e){let t={userId:e};try{const r=(await i.users.info({user:e})).user;if(r){const h=r.real_name?.trim();t={...b(r),userId:e,...!r.profile?.real_name?.trim()&&h?{realName:h}:{}}}}catch(n){a.debug({userId:e,err:n},"could not resolve the Slack user")}return c.set(e,{user:t,expires:Date.now()+f}),t}function d(){return m??=M(i,e=>D(e)&&e.id!==g).then(e=>{o={users:e,expires:Date.now()+f}}).catch(e=>{o={users:[],expires:Date.now()+x},a.debug({err:e},"could not list the workspace members")}).finally(()=>{m=void 0}),m}async function y(){if(o&&o.expires>Date.now())return o.users;const e=d();let t;const n=new Promise(r=>{t=setTimeout(r,p)});return await Promise.race([e,n]),clearTimeout(t),o?o.users:(a.debug("the member list is still loading; leaving this name unlinked"),[])}return{get size(){return c.size},prime(){d()},async lookup(e){if(!e)return{userId:e};const t=c.get(e);if(t&&t.expires>Date.now())return t.user;const n=u.get(e);if(n)return n;const r=w(e).finally(()=>u.delete(e));return u.set(e,r),r},async findByName(e){const t=l(e.replace(/^@+/,""));if(!t)return;const n=(await y()).filter(r=>L(r,t));if(n.length===1)return n[0];n.length>1&&a.debug({name:t,hits:n.length},"a written name matched more than one user")}}}export{k as userDirectory};
1
+ import{log as a}from"../log.js";import{isMentionable as D,listMembers as M,toUser as b}from"./members.js";const _=3600*1e3,N=2e3,x=60*1e3;function l(i){return i?.trim().toLowerCase()||void 0}function L(i,o){return l(i.handle)===o||l(i.displayName)===o||l(i.realName)===o}function k(i,o={}){const{ttlMs:f=_,deadlineMs:p=N,self:w}=o,c=new Map;let s,u;const m=new Map;async function y(e){let t={userId:e};try{const r=(await i.users.info({user:e})).user;if(r){const g=r.real_name?.trim();t={...b(r),userId:e,...!r.profile?.real_name?.trim()&&g?{realName:g}:{}}}}catch(n){a.debug({userId:e,err:n},"could not resolve the Slack user")}return c.set(e,{user:t,expires:Date.now()+f}),t}function d(){return u??=M(i,e=>D(e)&&e.id!==w).then(e=>{s={users:e,expires:Date.now()+f}}).catch(e=>{s={users:[],expires:Date.now()+x},a.debug({err:e},"could not list the workspace members")}).finally(()=>{u=void 0}),u}async function h(){if(s&&s.expires>Date.now())return s.users;const e=d();let t;const n=new Promise(r=>{t=setTimeout(r,p)});return await Promise.race([e,n]),clearTimeout(t),s?s.users:(a.debug("the member list is still loading; leaving this name unlinked"),[])}return{get size(){return c.size},prime(){d()},async roster(){const e=await h();return e.length>0?e:void 0},async lookup(e){if(!e)return{userId:e};const t=c.get(e);if(t&&t.expires>Date.now())return t.user;const n=m.get(e);if(n)return n;const r=y(e).finally(()=>m.delete(e));return m.set(e,r),r},async findByName(e){const t=l(e.replace(/^@+/,""));if(!t)return;const n=(await h()).filter(r=>L(r,t));if(n.length===1)return n[0];n.length>1&&a.debug({name:t,hits:n.length},"a written name matched more than one user")}}}export{k as userDirectory};
@@ -0,0 +1 @@
1
+ import{audienceOf as d,observations as b}from"../interface/context.js";import{log as c}from"../log.js";import{affiliationOf as k}from"./principal.js";import{baseConversation as u}from"./thread.js";function v(i,a){return i==="private"||i==="shared"?"group":i!=="standard"?"unknown":a==="public"?"public":a==="private"||a==="hiddenmembership"?"group":"unknown"}function C(i){const{api:a,tenantId:m}=i,s=b({...i.ttlMs?{ttlMs:i.ttlMs}:{},...i.now?{now:i.now}:{}}),f=new Map;async function l(r){try{const{members:t,complete:e}=await a.members(r);if(!e)return c.debug({conversation:r},"a conversation too big to check everyone in; its audience is unknown"),"unknown";const o=t.filter(n=>!n.id.startsWith("28:"));return d(o.map(n=>k(n,m)))}catch(t){return c.debug({conversation:r,err:t},"could not list a Teams roster"),"unknown"}}async function p(r,t){const[e,o]=await Promise.all([t?a.channelInfo(t):void 0,l(r)]),n=e?.membershipType?.toLowerCase();return{location:{externalId:r,kind:"channel",...e?.name?{name:e.name}:{}},visibility:v(n,e?.teamVisibility?.toLowerCase()),audience:o==="external"||n==="standard"||n==="private"?o:"unknown"}}return{async observe({conversationId:r,scope:t,channel:e,speaker:o}){const n=u(r);return t==="personal"?{location:{externalId:n,kind:"personal"},visibility:"one-to-one",audience:d([o])}:t==="groupChat"?s.get(n,async()=>({location:{externalId:n,kind:"groupChat"},visibility:"group",audience:await l(n)})):(e&&f.set(n,e.teamId),s.get(n,()=>p(n,e)))},changed({conversationId:r,channelId:t,teamId:e}){const o=new Set([u(r)]);if(t&&o.add(u(t)),e)for(const[n,g]of f)g===e&&o.add(n);c.debug({conversations:[...o]},"a Teams conversation changed; reading it again");for(const n of o)s.changed(n)}}}export{C as teamsObserver};
@@ -1,2 +1,2 @@
1
- import{randomUUID as x}from"node:crypto";import{readFile as I}from"node:fs/promises";import{App as $}from"@microsoft/teams.apps";import{BishopError as S}from"../errors.js";import{log as h}from"../log.js";import{retry as g}from"../retry.js";import{teamsRetryable as y}from"./api.js";import{teamsAllowList as M}from"./authorize.js";import{DOWNLOAD_TIMEOUT_MS as U,teamsFilesOf as C}from"./files.js";import{teamsGraph as F}from"./graph.js";import{createTeamsInterface as k}from"./interface.js";import{teamsLogger as v}from"./logger.js";import{createListener as R}from"./server.js";import{graphAddressOf as O}from"./thread.js";const q=3978;async function B(e){const t=async(s,c)=>{try{return await I(s,"utf8")}catch(d){throw new S(`Couldn't read the TLS ${c} at ${s}: ${d.message}`,"ConfigError")}};return{cert:await t(e.cert,"certificate"),key:await t(e.key,"key")}}function p(e,t=[]){return{type:"message",text:e,...t.length>0?{attachments:t}:{}}}const j="application/vnd.microsoft.teams.card.file.consent",A="application/vnd.microsoft.teams.card.file.info";function E(e){if(!e||typeof e!="object")return;const t=e.ref;return typeof t=="string"?t:void 0}function N(e){const t={description:`Bishop wants to send ${e.name}.`,sizeInBytes:e.size,acceptContext:{ref:e.ref},declineContext:{ref:e.ref}};return{contentType:j,name:e.name,content:t}}function _(e){return{id:x(),contentType:A,contentUrl:e.webUrl,name:e.name,content:{uniqueId:e.uniqueId,fileType:e.fileType}}}async function D(e,t){const s=t.uploadInfo?.uploadUrl;if(!s)throw new Error("Teams returned no upload URL for the accepted file");const c=T(s),d=new Uint8Array(await I(e.path)),l=Math.max(0,d.byteLength-1),w=await fetch(c,{method:"PUT",headers:{"content-type":"application/octet-stream","content-length":String(d.byteLength),"content-range":`bytes 0-${l}/${d.byteLength}`},body:d});if(!w.ok)throw new Error(`Teams returned ${w.status} while receiving ${e.name}`)}async function ce(e){const t=M(e.config?.allow),s=R({port:e.config?.port??q,...e.config?.host?{host:e.config.host}:{},...e.config?.tls?{tls:await B(e.config.tls)}:{}}),c=new $({clientId:e.credentials.clientId,clientSecret:e.credentials.clientSecret,tenantId:e.credentials.tenantId,httpServerAdapter:s,logger:v()}),d=F(e.credentials),l=new Map,b=k({api:{async send(r,n,o){const a=o?.files??[];let i=n,u=[];o?.channel&&a.length>0?(u=(await Promise.all(a.map(m=>d.uploadChannelFile(o.channel,m)))).map(_),i=[n,...u.map(m=>`<attachment id="${m.id}"></attachment>`)].filter(Boolean).join(`
2
- `)):a.length>0&&(u=a.map(f=>(l.set(f.ref,f),N(f))));try{const f=await g(()=>c.send(r,p(i,u)),{what:"teams send",retryable:y});return f?.id?{id:f.id}:void 0}catch(f){for(const m of a)l.delete(m.ref);throw f}},async update(r,n,o){await g(()=>c.api.conversations.updateActivity(r,n,p(o)),{what:"teams update",retryable:y})},async member(r,n){return await g(()=>c.api.conversations.getMemberById(r,n),{what:"teams roster",retryable:y})},async sharedFiles(r){const n=O(r);return n?d.files(n):[]},async history(r){const n=O(r);if(n)return d.conversation(n)},async fetchFile(r){const n=await fetch(T(r),{signal:AbortSignal.timeout(U)});if(!n.ok)throw new Error(`the download returned ${n.status}`);if(!n.body)throw new Error("the download sent no body");return n.body},async fetchAttachment(r){const n=T(r),o=new URL(c.api.serviceUrl);if(n.host!==o.host)throw h.warn({host:n.host,service:o.host},"refusing to send the app credential to a host that is not the Teams service"),new Error("the image is not hosted on the Teams service");const a=await c.api.http.get(n.toString(),{responseType:"arraybuffer",timeout:U});return P(a.data)},async fetchShared(r){return d.fetchShared(r)}},allow:t,...e.config?.verbosity?{verbosity:e.config.verbosity}:{},listener:s,meta:e.meta,model:e.model,...e.configuredModel?{configuredModel:e.configuredModel}:{},threads:e.threads,events:e.events,files:e.files,attachments:e.attachments,run:e.run,stop:e.stop});c.on("message",async({activity:r})=>{await b.handle(G(r))}),c.on("file.consent.accept",async({activity:r,send:n})=>(await L(r.value,n),{status:200})),c.on("file.consent.decline",async({activity:r,send:n})=>{const o=E(r.value.context),a=o?l.get(o):void 0;return o&&l.delete(o),a?(await n(p(`${a.name} was not sent because the file-consent request was declined.`)),{status:200}):(h.warn("received a Teams file-consent decline for an unknown file"),{status:200})});async function L(r,n){const o=E(r.context),a=o?l.get(o):void 0;if(o&&l.delete(o),!a){h.warn("received a Teams file-consent acceptance for an unknown file");return}try{await D(a,r);const i=r.uploadInfo;if(!i?.contentUrl||!i.uniqueId)throw new Error("Teams returned incomplete file information after the upload");await n(p(`${a.name} was sent.`,[{contentType:A,contentUrl:i.contentUrl,name:i.name??a.name,content:{uniqueId:i.uniqueId,fileType:i.fileType??a.mimeType}}]))}catch(i){h.warn({err:i,file:a.name},"could not finish a Teams file upload"),await n(p(`Could not send ${a.name}: ${i.message}`))}}return await c.initialize(),h.info({endpoint:c.server.messagingEndpoint},"the Teams app is mounted"),b}function T(e){const t=new URL(e);if(t.protocol!=="https:")throw new Error("the download URL is not HTTPS");return t}function P(e){if(e instanceof Uint8Array)return e;if(e instanceof ArrayBuffer)return new Uint8Array(e);throw new Error("the download came back as something other than bytes")}function z(e){const t=e.recipient?.id,s=t?e.stripMentionsText?.({accountId:t}):e;return(s?.stripMentionsText?.({tagOnly:!0})??s)?.text??e.text??""}function G(e){const t=W(e.channelData);return{id:e.id,conversationId:e.conversation.id,conversationType:typeof e.conversation.conversationType=="string"?e.conversation.conversationType:"channel",text:z(e),...H(e.attachments),mentioned:e.isRecipientMentioned?.()===!0,from:{id:e.from.id,...e.from.aadObjectId?{aadObjectId:e.from.aadObjectId}:{},...e.from.name?{name:e.from.name}:{}},...e.recipient?.id?{recipientId:e.recipient.id}:{},...e.recipient?.name?{recipientName:e.recipient.name}:{},files:C(e.attachments),...t?{channel:t}:{}}}function H(e){const t=e?.find(s=>s.contentType?.toLowerCase()==="text/html"&&typeof s.content=="string")?.content;return typeof t=="string"?{html:t}:{}}function W(e){const t=e?.team?.aadGroupId,s=e?.channel?.id;if(!(typeof t!="string"||typeof s!="string"))return{teamId:t,channelId:s}}export{ce as createTeams,G as inboundOf};
1
+ import{randomUUID as x}from"node:crypto";import{readFile as I}from"node:fs/promises";import{App as M}from"@microsoft/teams.apps";import{BishopError as $}from"../errors.js";import{log as h}from"../log.js";import{retry as w}from"../retry.js";import{teamsRetryable as g}from"./api.js";import{teamsAllowList as S}from"./authorize.js";import{DOWNLOAD_TIMEOUT_MS as U,teamsFilesOf as v}from"./files.js";import{teamsGraph as C}from"./graph.js";import{createTeamsInterface as k}from"./interface.js";import{teamsLogger as F}from"./logger.js";import{createListener as R}from"./server.js";import{graphAddressOf as E}from"./thread.js";const B=3978,q=500;async function D(e){const r=async(s,c)=>{try{return await I(s,"utf8")}catch(i){throw new $(`Couldn't read the TLS ${c} at ${s}: ${i.message}`,"ConfigError")}};return{cert:await r(e.cert,"certificate"),key:await r(e.key,"key")}}function p(e,r=[]){return{type:"message",text:e,...r.length>0?{attachments:r}:{}}}const P="application/vnd.microsoft.teams.card.file.consent",A="application/vnd.microsoft.teams.card.file.info";function O(e){if(!e||typeof e!="object")return;const r=e.ref;return typeof r=="string"?r:void 0}function _(e){const r={description:`Bishop wants to send ${e.name}.`,sizeInBytes:e.size,acceptContext:{ref:e.ref},declineContext:{ref:e.ref}};return{contentType:P,name:e.name,content:r}}function j(e){return{id:x(),contentType:A,contentUrl:e.webUrl,name:e.name,content:{uniqueId:e.uniqueId,fileType:e.fileType}}}async function N(e,r){const s=r.uploadInfo?.uploadUrl;if(!s)throw new Error("Teams returned no upload URL for the accepted file");const c=T(s),i=new Uint8Array(await I(e.path)),l=Math.max(0,i.byteLength-1),y=await fetch(c,{method:"PUT",headers:{"content-type":"application/octet-stream","content-length":String(i.byteLength),"content-range":`bytes 0-${l}/${i.byteLength}`},body:i});if(!y.ok)throw new Error(`Teams returned ${y.status} while receiving ${e.name}`)}async function ie(e){const r=S(e.config?.allow),s=R({port:e.config?.port??B,...e.config?.host?{host:e.config.host}:{},...e.config?.tls?{tls:await D(e.config.tls)}:{}}),c=new M({clientId:e.credentials.clientId,clientSecret:e.credentials.clientSecret,tenantId:e.credentials.tenantId,httpServerAdapter:s,logger:F()}),i=C(e.credentials),l=new Map,b=k({api:{async send(t,n,o){const a=o?.files??[];let d=n,u=[];o?.channel&&a.length>0?(u=(await Promise.all(a.map(m=>i.uploadChannelFile(o.channel,m)))).map(j),d=[n,...u.map(m=>`<attachment id="${m.id}"></attachment>`)].filter(Boolean).join(`
2
+ `)):a.length>0&&(u=a.map(f=>(l.set(f.ref,f),_(f))));try{const f=await w(()=>c.send(t,p(d,u)),{what:"teams send",retryable:g});return f?.id?{id:f.id}:void 0}catch(f){for(const m of a)l.delete(m.ref);throw f}},async update(t,n,o){await w(()=>c.api.conversations.updateActivity(t,n,p(o)),{what:"teams update",retryable:g})},async members(t){const n=await w(()=>c.api.conversations.getPagedMembers(t,q),{what:"teams roster page",retryable:g});return{members:n.members,complete:!n.continuationToken}},async channelInfo(t){return i.channel(t)},async member(t,n){return await w(()=>c.api.conversations.getMemberById(t,n),{what:"teams roster",retryable:g})},async sharedFiles(t){const n=E(t);return n?i.files(n):[]},async history(t){const n=E(t);if(n)return i.conversation(n)},async fetchFile(t){const n=await fetch(T(t),{signal:AbortSignal.timeout(U)});if(!n.ok)throw new Error(`the download returned ${n.status}`);if(!n.body)throw new Error("the download sent no body");return n.body},async fetchAttachment(t){const n=T(t),o=new URL(c.api.serviceUrl);if(n.host!==o.host)throw h.warn({host:n.host,service:o.host},"refusing to send the app credential to a host that is not the Teams service"),new Error("the image is not hosted on the Teams service");const a=await c.api.http.get(n.toString(),{responseType:"arraybuffer",timeout:U});return G(a.data)},async fetchShared(t){return i.fetchShared(t)}},allow:r,...e.config?.verbosity?{verbosity:e.config.verbosity}:{},listener:s,meta:e.meta,model:e.model,...e.configuredModel?{configuredModel:e.configuredModel}:{},threads:e.threads,events:e.events,files:e.files,attachments:e.attachments,principals:e.principals,tenantId:e.credentials.tenantId,run:e.run,stop:e.stop});c.on("message",async({activity:t})=>{await b.handle(H(t))}),c.on("conversationUpdate",async({activity:t})=>{const n=t.channelData?.channel?.id,o=t.channelData?.team?.aadGroupId;b.changed({conversationId:t.conversation.id,...typeof n=="string"?{channelId:n}:{},...typeof o=="string"?{teamId:o}:{}})}),c.on("file.consent.accept",async({activity:t,send:n})=>(await L(t.value,n),{status:200})),c.on("file.consent.decline",async({activity:t,send:n})=>{const o=O(t.value.context),a=o?l.get(o):void 0;return o&&l.delete(o),a?(await n(p(`${a.name} was not sent because the file-consent request was declined.`)),{status:200}):(h.warn("received a Teams file-consent decline for an unknown file"),{status:200})});async function L(t,n){const o=O(t.context),a=o?l.get(o):void 0;if(o&&l.delete(o),!a){h.warn("received a Teams file-consent acceptance for an unknown file");return}try{await N(a,t);const d=t.uploadInfo;if(!d?.contentUrl||!d.uniqueId)throw new Error("Teams returned incomplete file information after the upload");await n(p(`${a.name} was sent.`,[{contentType:A,contentUrl:d.contentUrl,name:d.name??a.name,content:{uniqueId:d.uniqueId,fileType:d.fileType??a.mimeType}}]))}catch(d){h.warn({err:d,file:a.name},"could not finish a Teams file upload"),await n(p(`Could not send ${a.name}: ${d.message}`))}}return await c.initialize(),h.info({endpoint:c.server.messagingEndpoint},"the Teams app is mounted"),b}function T(e){const r=new URL(e);if(r.protocol!=="https:")throw new Error("the download URL is not HTTPS");return r}function G(e){if(e instanceof Uint8Array)return e;if(e instanceof ArrayBuffer)return new Uint8Array(e);throw new Error("the download came back as something other than bytes")}function z(e){const r=e.recipient?.id,s=r?e.stripMentionsText?.({accountId:r}):e;return(s?.stripMentionsText?.({tagOnly:!0})??s)?.text??e.text??""}function H(e){const r=J(e.channelData);return{id:e.id,conversationId:e.conversation.id,conversationType:typeof e.conversation.conversationType=="string"?e.conversation.conversationType:"channel",text:z(e),...W(e.attachments),mentioned:e.isRecipientMentioned?.()===!0,from:{id:e.from.id,...e.from.aadObjectId?{aadObjectId:e.from.aadObjectId}:{},...e.from.name?{name:e.from.name}:{}},...e.recipient?.id?{recipientId:e.recipient.id}:{},...e.recipient?.name?{recipientName:e.recipient.name}:{},files:v(e.attachments),...r?{channel:r}:{}}}function W(e){const r=e?.find(s=>s.contentType?.toLowerCase()==="text/html"&&typeof s.content=="string")?.content;return typeof r=="string"?{html:r}:{}}function J(e){const r=e?.team?.aadGroupId,s=e?.channel?.id;if(!(typeof r!="string"||typeof s!="string"))return{teamId:r,channelId:s}}export{ie as createTeams,H as inboundOf};
@@ -1 +1 @@
1
- import{FILE_DOWNLOAD_INFO as F,GRAPH_REFERENCE as E}from"./files.js";function M(){const n=[],p=[],u=[],l=[],h=[],f=[],m=new Map,y=new Map,w=new Map,g=new Map;let a,i,d,b=!1,x=1;const c=(e,t)=>{l.push({url:e,credential:t});const o=g.get(e);if(o===void 0)throw new Error(`nothing hosted at ${e}`);return new TextEncoder().encode(o)};return{sent:n,edits:p,rosterCalls:u,fetched:l,sharedCalls:h,historyCalls:f,addMember(e,t){w.set(`${e}:${t.id}`,t)},addBytes(e,t){g.set(e,t)},addShared(e,t){y.set(e,t)},addHistory(e,t){m.set(e,t)},failNextSend(e=new Error("Teams is down")){a=e},failNextUpdate(e=new Error("Teams is down")){i=e},sendAnonymously(){b=!0},failMember(e=new Error("no roster access")){d=e},async send(e,t,o){if(a){const s=a;throw a=void 0,s}const r=`activity-${x++}`;return n.push({conversationId:e,text:t,id:r,...o?.files?.length?{files:o.files}:{}}),b?void 0:{id:r}},async update(e,t,o){if(i){const s=i;throw i=void 0,s}p.push({conversationId:e,activityId:t,text:o});const r=n.find(s=>s.id===t);if(!r)throw new Error(`no activity ${t} to update`);r.text=o},async member(e,t){if(u.push(e),d)throw d;return w.get(`${e}:${t}`)},async sharedFiles(e){return e.conversationType==="personal"?[]:(h.push(e.id),y.get(e.id)??[])},async history(e){if(e.conversationType==="personal")return;f.push(e.id);const t=m.get(e.conversationId);if(!t)return;const o=r=>Number(r.id)<Number(e.id);return{...t,messages:t.messages.filter(o)}},async fetchFile(e){return c(e,"none")},async fetchAttachment(e){return c(e,"bot")},async fetchShared(e){return c(e,"graph")}}}function _(n={}){return{contentType:F,name:"doc.txt",uniqueId:"1150D938-8870-4044-9F2C-5BBDEBA70C9D",contentUrl:"https://contoso.sharepoint.com/personal/jerry_contoso_com/Documents/doc.txt",etag:"123",downloadUrl:"https://download.contoso.com/doc.txt?tempauth=abc",...n}}function N(n={}){return{contentType:E,name:"deploy.log",uniqueId:"fe1a6fcf-0000-4000-8000-00000000000b",etag:'"{FE1A6FCF-0000-4000-8000-00000000000B},2"',contentUrl:"https://contoso.sharepoint.com/sites/ops/Shared Documents/General/deploy.log",size:24,mimeType:"text/plain",...n}}function v(n={}){return{contentType:"image/png",contentUrl:"https://smba.trafficmanager.net/amer/v3/attachments/img-1/views/original",...n}}function C(n={}){return{id:"1700000000001",conversationId:"19:chat@thread.v2",conversationType:"personal",text:"what is the deploy status",mentioned:!1,recipientId:"28:the-agent",recipientName:"bishop",files:[],...n,from:{id:"29:bot-scoped-id",aadObjectId:"00000000-0000-0000-0000-00000000000a",name:"Hayes",...n.from}}}function D(n={}){return{id:"1700000000000",at:17e11,user:{id:"00000000-0000-0000-0000-00000000000b",name:"Kim"},html:"<p>the deploy is stuck on step three</p>",appMentions:[],files:[],...n}}export{_ as aFile,D as aGraphMessage,C as aMessage,N as aSharedFile,v as anImage,M as fakeTeams};
1
+ import{FILE_DOWNLOAD_INFO as T,GRAPH_REFERENCE as M}from"./files.js";function _(){const n=[],l=[],u=[],h=[],f=[],m=[],y=[],w=new Map,g=new Map,b=new Map,d=new Map,x=new Map;let i,c,s,F=!1,E=1;const p=(e,t)=>{h.push({url:e,credential:t});const r=x.get(e);if(r===void 0)throw new Error(`nothing hosted at ${e}`);return new TextEncoder().encode(r)};return{sent:n,edits:l,rosterCalls:u,fetched:h,sharedCalls:f,historyCalls:m,listCalls:y,addMember(e,t){d.set(`${e}:${t.id}`,t)},addChannel(e,t){w.set(e,t)},addBytes(e,t){x.set(e,t)},addShared(e,t){b.set(e,t)},addHistory(e,t){g.set(e,t)},failNextSend(e=new Error("Teams is down")){i=e},failNextUpdate(e=new Error("Teams is down")){c=e},sendAnonymously(){F=!0},failMember(e=new Error("no roster access")){s=e},async send(e,t,r){if(i){const a=i;throw i=void 0,a}const o=`activity-${E++}`;return n.push({conversationId:e,text:t,id:o,...r?.files?.length?{files:r.files}:{}}),F?void 0:{id:o}},async update(e,t,r){if(c){const a=c;throw c=void 0,a}l.push({conversationId:e,activityId:t,text:r});const o=n.find(a=>a.id===t);if(!o)throw new Error(`no activity ${t} to update`);o.text=r},async member(e,t){if(u.push(e),s)throw s;return d.get(`${e}:${t}`)},async members(e){if(y.push(e),s)throw s;const t=`${e}:`;return{members:[...d].filter(([o])=>o.startsWith(t)).map(([,o])=>o),complete:!0}},async channelInfo({channelId:e}){return w.get(e)},async sharedFiles(e){return e.conversationType==="personal"?[]:(f.push(e.id),b.get(e.id)??[])},async history(e){if(e.conversationType==="personal")return;m.push(e.id);const t=g.get(e.conversationId);if(!t)return;const r=o=>Number(o.id)<Number(e.id);return{...t,messages:t.messages.filter(r)}},async fetchFile(e){return p(e,"none")},async fetchAttachment(e){return p(e,"bot")},async fetchShared(e){return p(e,"graph")}}}function N(n={}){return{contentType:T,name:"doc.txt",uniqueId:"1150D938-8870-4044-9F2C-5BBDEBA70C9D",contentUrl:"https://contoso.sharepoint.com/personal/jerry_contoso_com/Documents/doc.txt",etag:"123",downloadUrl:"https://download.contoso.com/doc.txt?tempauth=abc",...n}}function $(n={}){return{contentType:M,name:"deploy.log",uniqueId:"fe1a6fcf-0000-4000-8000-00000000000b",etag:'"{FE1A6FCF-0000-4000-8000-00000000000B},2"',contentUrl:"https://contoso.sharepoint.com/sites/ops/Shared Documents/General/deploy.log",size:24,mimeType:"text/plain",...n}}function D(n={}){return{contentType:"image/png",contentUrl:"https://smba.trafficmanager.net/amer/v3/attachments/img-1/views/original",...n}}function I(n={}){return{id:"1700000000001",conversationId:"19:chat@thread.v2",conversationType:"personal",text:"what is the deploy status",mentioned:!1,recipientId:"28:the-agent",recipientName:"bishop",files:[],...n,from:{id:"29:bot-scoped-id",aadObjectId:"00000000-0000-0000-0000-00000000000a",name:"Hayes",...n.from}}}function v(n={}){return{id:"1700000000000",at:17e11,user:{id:"00000000-0000-0000-0000-00000000000b",name:"Kim"},html:"<p>the deploy is stuck on step three</p>",appMentions:[],files:[],...n}}export{N as aFile,v as aGraphMessage,I as aMessage,$ as aSharedFile,D as anImage,_ as fakeTeams};
@@ -1 +1 @@
1
- import{readFile as P}from"node:fs/promises";import{log as p}from"../log.js";import{retry as b}from"../retry.js";import{DOWNLOAD_TIMEOUT_MS as A,GRAPH_REFERENCE as _}from"./files.js";const g="https://graph.microsoft.com/v1.0",v="https://login.microsoftonline.com",N=1e4,O=2,x=1e3,L=2e4,E=50,C=3,B=10,F=6e4,z=2,q=1e3,H=6e4;class l extends Error{status;retryAfterMs;constructor(i,c,u){super(i),this.status=c,this.retryAfterMs=u,this.name="GraphError"}}function S(e){const{status:i,retryAfterMs:c}=e??{};return typeof i!="number"?!1:i===0?!0:i===429?c===void 0?!0:{retryAfterMs:c}:i>=500}function I(e){const i=e.headers.get("retry-after");if(i===null)return;const c=Number(i);return Number.isFinite(c)&&c>=0?c*1e3:void 0}function U(e){return`u!${Buffer.from(e,"utf8").toString("base64url")}`}function Y(e){if(e.kind==="chat")return`/chats/${e.chatId}/messages/${e.messageId}`;const i=`/teams/${e.teamId}/channels/${e.channelId}/messages`;return e.replyToId&&e.replyToId!==e.messageId?`${i}/${e.replyToId}/replies/${e.messageId}`:`${i}/${e.messageId}`}function M(e){if(!(e.contentType!==_||!e.contentUrl))return{contentType:_,contentUrl:e.contentUrl,...e.id?{uniqueId:e.id}:{},...e.name?{name:e.name}:{}}}function j(e){if(!e.id||e.messageType&&e.messageType!=="message"||e.deletedDateTime)return;const i=e.createdDateTime?Date.parse(e.createdDateTime):Number.NaN,c=e.from?.user,u=e.from?.application,y=e.body?.content??"";return{id:e.id,...Number.isFinite(i)?{at:i}:{},...c?.id?{user:{id:c.id,...c.displayName?{name:c.displayName}:{}}}:{},...u?.id?{app:{id:u.id}}:{},html:e.body?.contentType==="text"?y.replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;"):y,appMentions:(e.mentions??[]).flatMap(m=>{const $=m.mentioned?.application?.id;return $&&m.id!==void 0?[{id:String(m.id),appId:$}]:[]}),files:(e.attachments??[]).flatMap(m=>M(m)??[])}}function G(e,i){return/^\d+$/.test(e)&&/^\d+$/.test(i)?Number(e)<Number(i):e!==i}function D(e){return[...e].sort((i,c)=>(i.at??0)-(c.at??0)||(G(i.id,c.id)?-1:1))}function Q(e){let i;async function c(o){if(i&&i.expiresAt>Date.now())return i.token;const r=await fetch(`${v}/${e.tenantId}/oauth2/v2.0/token`,{method:"POST",headers:{"content-type":"application/x-www-form-urlencoded"},body:new URLSearchParams({client_id:e.clientId,client_secret:e.clientSecret,grant_type:"client_credentials",scope:"https://graph.microsoft.com/.default"}),signal:o}).catch(n=>{throw new l(`could not reach Entra: ${n.message}`,0)});if(!r.ok)throw new l(`Entra refused the app credentials with ${r.status}`,r.status,I(r));const t=await r.json();if(!t.access_token)throw new l("Entra returned no access token",0);const s=(t.expires_in??3600)*1e3;return i={token:t.access_token,expiresAt:Date.now()+s-H},i.token}async function u(o,r,t){return b(async()=>{const s=await fetch(o.startsWith(`${g}/`)?o:`${g}${o}`,{headers:{authorization:`Bearer ${await c(t)}`},signal:t}).catch(n=>{throw new l(`could not reach Graph: ${n.message}`,0)});if(!s.ok)throw new l(`Graph answered ${r} with ${s.status}`,s.status,I(s));return await s.json()},{what:`graph ${r}`,retryable:S,attempts:O,maxDelayMs:x})}async function y(o,r,t,s){return b(async()=>{const n=await fetch(`${g}${o}`,{method:"PUT",headers:{authorization:`Bearer ${await c(s)}`,"content-type":"application/octet-stream","content-length":String(t.byteLength)},body:t,signal:s}).catch(a=>{throw new l(`could not reach Graph: ${a.message}`,0)});if(!n.ok)throw new l(`Graph answered ${r} with ${n.status}`,n.status,I(n));return await n.json()},{what:`graph ${r}`,retryable:S,attempts:z,maxDelayMs:q})}async function m(o,r,t,s){try{const a=(await u(`/drives/${encodeURIComponent(o)}/items/${encodeURIComponent(r)}?$select=sharepointIds`,"the SharePoint identifiers of an uploaded file",s)).sharepointIds?.listItemUniqueId;if(a)return a}catch(n){p.debug({err:n,itemId:r},"could not read the SharePoint ID of an uploaded file")}return t?.match(/\{([0-9a-fA-F-]{36})\}/)?.[1]}async function $(o,r){const t=await u(`/teams/${encodeURIComponent(o.teamId)}/channels/${encodeURIComponent(o.channelId)}/filesFolder`,"a channel files folder",r);if(!t.id||!t.parentReference?.driveId)throw new l("Graph returned an incomplete channel files folder",0);return{driveId:t.parentReference.driveId,folderId:t.id}}async function R(o,r){try{return await u(`/shares/${U(o)}/driveItem`,"a shared file",r)}catch(t){const s=t instanceof l?t.status:0;return p.warn({err:t,status:s},"could not read a file shared in a Teams conversation"),s===401||s===403?{refused:"Bishop is not allowed to read files in this Microsoft 365 tenant"}:void 0}}async function T(o,r,t,s){const n=[];let a=o;try{for(let f=0;a&&f<r;f+=1){const d=await u(a,t,s);n.push(...d.value??[]);const h=d["@odata.nextLink"];if(a=h?.startsWith(`${g}/`)?h:void 0,h&&!a)return p.warn({what:t},"Graph offered a next page somewhere other than Graph"),{raw:n,complete:!1,failed:!1}}return{raw:n,complete:!a,failed:!1}}catch(f){return p.warn({err:f,what:t,read:n.length},"could not read the whole conversation"),{raw:n,complete:!1,failed:!0}}}return{async conversation(o){const r=AbortSignal.timeout(L),t=d=>{const h=new Set;return d.flatMap(k=>{const w=j(k);return!w||!G(w.id,o.messageId)||h.has(w.id)?[]:(h.add(w.id),[w])})};if(o.kind==="chat"){const d=await T(`/chats/${o.chatId}/messages?$top=${E}&$orderby=createdDateTime desc`,C,"a chat",r);return d.failed&&d.raw.length===0?void 0:{messages:D(t(d.raw)),complete:d.complete}}const s=o.replyToId;if(!s||s===o.messageId)return{messages:[],complete:!0};const n=`/teams/${o.teamId}/channels/${o.channelId}/messages/${s}`,[a,f]=await Promise.all([u(n,"the message that opened a thread",r).then(d=>d,d=>{p.warn({err:d},"could not read the message that opened a Teams thread")}),T(`${n}/replies?$top=${E}`,B,"a thread",r)]);if(!(!a&&f.failed&&f.raw.length===0))return{messages:D(t([...a?[a]:[],...f.raw])),complete:!!a&&f.complete}},async files(o){const r=AbortSignal.timeout(N);let t;try{t=await u(Y(o),"a message",r)}catch(n){return p.warn({err:n,address:o.kind},"could not read a Teams message from Graph"),[]}const s=(t.attachments??[]).flatMap(n=>M(n)??[]);return Promise.all(s.map(async n=>{const a=await R(n.contentUrl,r);return a?"refused"in a?{...n,unavailable:a.refused}:{...n,name:n.name??a.name,...a.eTag?{etag:a.eTag}:{},...a.size===void 0?{}:{size:a.size},...a.file?.mimeType?{mimeType:a.file.mimeType}:{}}:n}))},async fetchShared(o){const r=AbortSignal.timeout(A),t=await fetch(`${g}/shares/${U(o)}/driveItem/content`,{headers:{authorization:`Bearer ${await c(r)}`},redirect:"manual",signal:r});if(t.status>=300&&t.status<400){const s=t.headers.get("location");if(!s)throw new Error("Graph redirected the download to nowhere");const n=await fetch(s,{signal:AbortSignal.timeout(A)});if(!n.ok)throw new Error(`the download returned ${n.status}`);if(!n.body)throw new Error("the download sent no body");return n.body}if(!t.ok)throw new Error(`Graph answered the download with ${t.status}`);if(!t.body)throw new Error("the download sent no body");return t.body},async uploadChannelFile(o,r){const t=AbortSignal.timeout(F),s=new Uint8Array(await P(r.path)),n=await $(o,t),a=await y(`/drives/${encodeURIComponent(n.driveId)}/items/${encodeURIComponent(n.folderId)}:/${encodeURIComponent(r.name)}:/content`,`the file ${r.name} in a Teams channel`,s,t);if(!a.id||!a.webUrl)throw new l(`Graph returned an incomplete upload for ${r.name}`,0);return{id:a.id,uniqueId:await m(n.driveId,a.id,a.eTag,t),name:a.name??r.name,webUrl:a.webUrl,fileType:a.file?.mimeType??r.mimeType}}}}export{S as graphRetryable,Q as teamsGraph};
1
+ import{readFile as O}from"node:fs/promises";import{log as h}from"../log.js";import{retry as _}from"../retry.js";import{DOWNLOAD_TIMEOUT_MS as S,GRAPH_REFERENCE as E}from"./files.js";const w="https://graph.microsoft.com/v1.0",x="https://login.microsoftonline.com",U=1e4,C=2,L=1e3,B=2e4,G=50,F=3,z=10,q=6e4,H=2,Y=1e3,j=6e4;class m extends Error{status;retryAfterMs;constructor(i,c,u){super(i),this.status=c,this.retryAfterMs=u,this.name="GraphError"}}function R(e){const{status:i,retryAfterMs:c}=e??{};return typeof i!="number"?!1:i===0?!0:i===429?c===void 0?!0:{retryAfterMs:c}:i>=500}function I(e){const i=e.headers.get("retry-after");if(i===null)return;const c=Number(i);return Number.isFinite(c)&&c>=0?c*1e3:void 0}function M(e){return`u!${Buffer.from(e,"utf8").toString("base64url")}`}function W(e){if(e.kind==="chat")return`/chats/${e.chatId}/messages/${e.messageId}`;const i=`/teams/${e.teamId}/channels/${e.channelId}/messages`;return e.replyToId&&e.replyToId!==e.messageId?`${i}/${e.replyToId}/replies/${e.messageId}`:`${i}/${e.messageId}`}function D(e){if(!(e.contentType!==E||!e.contentUrl))return{contentType:E,contentUrl:e.contentUrl,...e.id?{uniqueId:e.id}:{},...e.name?{name:e.name}:{}}}function X(e){if(!e.id||e.messageType&&e.messageType!=="message"||e.deletedDateTime)return;const i=e.createdDateTime?Date.parse(e.createdDateTime):Number.NaN,c=e.from?.user,u=e.from?.application,g=e.body?.content??"";return{id:e.id,...Number.isFinite(i)?{at:i}:{},...c?.id?{user:{id:c.id,...c.displayName?{name:c.displayName}:{}}}:{},...u?.id?{app:{id:u.id}}:{},html:e.body?.contentType==="text"?g.replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;"):g,appMentions:(e.mentions??[]).flatMap(p=>{const $=p.mentioned?.application?.id;return $&&p.id!==void 0?[{id:String(p.id),appId:$}]:[]}),files:(e.attachments??[]).flatMap(p=>D(p)??[])}}function k(e,i){return/^\d+$/.test(e)&&/^\d+$/.test(i)?Number(e)<Number(i):e!==i}function v(e){return[...e].sort((i,c)=>(i.at??0)-(c.at??0)||(k(i.id,c.id)?-1:1))}function Z(e){let i;async function c(o){if(i&&i.expiresAt>Date.now())return i.token;const r=await fetch(`${x}/${e.tenantId}/oauth2/v2.0/token`,{method:"POST",headers:{"content-type":"application/x-www-form-urlencoded"},body:new URLSearchParams({client_id:e.clientId,client_secret:e.clientSecret,grant_type:"client_credentials",scope:"https://graph.microsoft.com/.default"}),signal:o}).catch(n=>{throw new m(`could not reach Entra: ${n.message}`,0)});if(!r.ok)throw new m(`Entra refused the app credentials with ${r.status}`,r.status,I(r));const t=await r.json();if(!t.access_token)throw new m("Entra returned no access token",0);const s=(t.expires_in??3600)*1e3;return i={token:t.access_token,expiresAt:Date.now()+s-j},i.token}async function u(o,r,t){return _(async()=>{const s=await fetch(o.startsWith(`${w}/`)?o:`${w}${o}`,{headers:{authorization:`Bearer ${await c(t)}`},signal:t}).catch(n=>{throw new m(`could not reach Graph: ${n.message}`,0)});if(!s.ok)throw new m(`Graph answered ${r} with ${s.status}`,s.status,I(s));return await s.json()},{what:`graph ${r}`,retryable:R,attempts:C,maxDelayMs:L})}async function g(o,r,t,s){return _(async()=>{const n=await fetch(`${w}${o}`,{method:"PUT",headers:{authorization:`Bearer ${await c(s)}`,"content-type":"application/octet-stream","content-length":String(t.byteLength)},body:t,signal:s}).catch(a=>{throw new m(`could not reach Graph: ${a.message}`,0)});if(!n.ok)throw new m(`Graph answered ${r} with ${n.status}`,n.status,I(n));return await n.json()},{what:`graph ${r}`,retryable:R,attempts:H,maxDelayMs:Y})}async function p(o,r,t,s){try{const a=(await u(`/drives/${encodeURIComponent(o)}/items/${encodeURIComponent(r)}?$select=sharepointIds`,"the SharePoint identifiers of an uploaded file",s)).sharepointIds?.listItemUniqueId;if(a)return a}catch(n){h.debug({err:n,itemId:r},"could not read the SharePoint ID of an uploaded file")}return t?.match(/\{([0-9a-fA-F-]{36})\}/)?.[1]}async function $(o,r){const t=await u(`/teams/${encodeURIComponent(o.teamId)}/channels/${encodeURIComponent(o.channelId)}/filesFolder`,"a channel files folder",r);if(!t.id||!t.parentReference?.driveId)throw new m("Graph returned an incomplete channel files folder",0);return{driveId:t.parentReference.driveId,folderId:t.id}}async function P(o,r){try{return await u(`/shares/${M(o)}/driveItem`,"a shared file",r)}catch(t){const s=t instanceof m?t.status:0;return h.warn({err:t,status:s},"could not read a file shared in a Teams conversation"),s===401||s===403?{refused:"Bishop is not allowed to read files in this Microsoft 365 tenant"}:void 0}}async function T(o,r,t,s){const n=[];let a=o;try{for(let l=0;a&&l<r;l+=1){const d=await u(a,t,s);n.push(...d.value??[]);const f=d["@odata.nextLink"];if(a=f?.startsWith(`${w}/`)?f:void 0,f&&!a)return h.warn({what:t},"Graph offered a next page somewhere other than Graph"),{raw:n,complete:!1,failed:!1}}return{raw:n,complete:!a,failed:!1}}catch(l){return h.warn({err:l,what:t,read:n.length},"could not read the whole conversation"),{raw:n,complete:!1,failed:!0}}}const b=new Set;async function A(o,r,t,s,n){try{return await u(o,r,n)}catch(a){const l=`${t} ${s}`;a.status===403&&!b.has(l)?(b.add(l),h.warn({team:t,permission:s},"this team never consented to a permission the agent is told about its channels with; they are reported as unknown until the app is reinstalled there")):h.debug({err:a,team:t},`could not read ${r}`);return}}return{async channel({teamId:o,channelId:r}){const t=AbortSignal.timeout(U),s=encodeURIComponent(o),[n,a]=await Promise.all([A(`/teams/${s}/channels/${encodeURIComponent(r)}?$select=displayName,membershipType`,"a channel",o,"ChannelSettings.Read.Group",t),A(`/teams/${s}?$select=visibility`,"a team",o,"TeamSettings.Read.Group",t)]);if(n)return{...n.displayName?{name:n.displayName}:{},...n.membershipType?{membershipType:n.membershipType}:{},...a?.visibility?{teamVisibility:a.visibility}:{}}},async conversation(o){const r=AbortSignal.timeout(B),t=d=>{const f=new Set;return d.flatMap(N=>{const y=X(N);return!y||!k(y.id,o.messageId)||f.has(y.id)?[]:(f.add(y.id),[y])})};if(o.kind==="chat"){const d=await T(`/chats/${o.chatId}/messages?$top=${G}&$orderby=createdDateTime desc`,F,"a chat",r);return d.failed&&d.raw.length===0?void 0:{messages:v(t(d.raw)),complete:d.complete}}const s=o.replyToId;if(!s||s===o.messageId)return{messages:[],complete:!0};const n=`/teams/${o.teamId}/channels/${o.channelId}/messages/${s}`,[a,l]=await Promise.all([u(n,"the message that opened a thread",r).then(d=>d,d=>{h.warn({err:d},"could not read the message that opened a Teams thread")}),T(`${n}/replies?$top=${G}`,z,"a thread",r)]);if(!(!a&&l.failed&&l.raw.length===0))return{messages:v(t([...a?[a]:[],...l.raw])),complete:!!a&&l.complete}},async files(o){const r=AbortSignal.timeout(U);let t;try{t=await u(W(o),"a message",r)}catch(n){return h.warn({err:n,address:o.kind},"could not read a Teams message from Graph"),[]}const s=(t.attachments??[]).flatMap(n=>D(n)??[]);return Promise.all(s.map(async n=>{const a=await P(n.contentUrl,r);return a?"refused"in a?{...n,unavailable:a.refused}:{...n,name:n.name??a.name,...a.eTag?{etag:a.eTag}:{},...a.size===void 0?{}:{size:a.size},...a.file?.mimeType?{mimeType:a.file.mimeType}:{}}:n}))},async fetchShared(o){const r=AbortSignal.timeout(S),t=await fetch(`${w}/shares/${M(o)}/driveItem/content`,{headers:{authorization:`Bearer ${await c(r)}`},redirect:"manual",signal:r});if(t.status>=300&&t.status<400){const s=t.headers.get("location");if(!s)throw new Error("Graph redirected the download to nowhere");const n=await fetch(s,{signal:AbortSignal.timeout(S)});if(!n.ok)throw new Error(`the download returned ${n.status}`);if(!n.body)throw new Error("the download sent no body");return n.body}if(!t.ok)throw new Error(`Graph answered the download with ${t.status}`);if(!t.body)throw new Error("the download sent no body");return t.body},async uploadChannelFile(o,r){const t=AbortSignal.timeout(q),s=new Uint8Array(await O(r.path)),n=await $(o,t),a=await g(`/drives/${encodeURIComponent(n.driveId)}/items/${encodeURIComponent(n.folderId)}:/${encodeURIComponent(r.name)}:/content`,`the file ${r.name} in a Teams channel`,s,t);if(!a.id||!a.webUrl)throw new m(`Graph returned an incomplete upload for ${r.name}`,0);return{id:a.id,uniqueId:await p(n.driveId,a.id,a.eTag,t),name:a.name??r.name,webUrl:a.webUrl,fileType:a.file?.mimeType??r.mimeType}}}}export{R as graphRetryable,Z as teamsGraph};
@@ -1 +1 @@
1
- import{describedFile as w}from"../files.js";import{declareUnread as y,trimHistory as b,withinFileBudget as v}from"../interface/history.js";import{log as p}from"../log.js";import{describeTeamsFile as I}from"./files.js";import{messageText as h}from"./html.js";import{threadIdOf as k}from"./thread.js";function C(t){if(t.conversationType==="personal")return!1;if(t.conversationType==="groupChat")return!0;const e=k(t.conversationId);return!!e&&e!==t.id}async function H(t){const{read:e}=t;if(!e)return p.warn("could not read the Teams conversation to catch the agent up"),[{kind:"omitted",reason:"unavailable"}];for(const n of e.messages)n.user&&n.at!==void 0&&t.saw?.(n.user.id,n.at);const a=new Map,s=new Map(e.messages.flatMap(n=>n.user?[[n.user.id,n.user]]:[]));await Promise.all([...s.values()].map(async n=>{try{a.set(n.id,await t.sender(n))}catch(d){p.warn({err:d,who:n.id},"could not check a Teams sender for history")}}));const i=B(e.messages,t,a),o=b(i.entries),c=o[1]?.kind==="omitted"&&o[1].reason==="trimmed",r=!e.complete&&!t.openedBy&&c?o.slice(1):o,f=v(r).map(n=>T(n,t));return e.complete?f:y(f,i.opensWithParent)}function B(t,e,a){const s=[];let i=!1,o=0;const c=()=>{o&&(s.push({kind:"omitted",reason:"unauthorized",count:o}),o=0)};for(const r of t){const u=r.id===e.openedBy,f=!!e.selfAppId&&r.app?.id===e.selfAppId,n=r.user?a.get(r.user.id):void 0;if(!f&&!n?.allowed){u&&(i=!0),o+=1;continue}const d=new Set(r.appMentions.filter(l=>l.appId===e.selfAppId).map(l=>l.id));if(!h(r.html,(l,m)=>d.has(l)?"":`@${m}`)&&r.files.length===0)continue;const g=h(r.html,(l,m)=>d.has(l)&&e.selfName?e.selfName:`@${m}`);u&&(i=!0),c(),s.push({kind:"message",principal:f?{id:e.selfAppId,...e.selfName?{displayName:e.selfName.replace(/^@/,"")}:{}}:n.principal,text:g,...f?{self:!0}:{},...r.files.length>0?{shared:r.files}:{}})}return c(),{entries:s,opensWithParent:i}}function T(t,e){if(t.kind!=="message")return t;const{shared:a,...s}=t;if(!a?.length)return s;const i=a.map(o=>{const c=w(I(o)),r=e.remember(o);return r?{...c,ref:r}:{...c,unavailable:"Bishop could not record a reference for it"}});return{...s,files:i}}export{C as landedMidConversation,H as teamsHistory};
1
+ import{describedFile as k}from"../files.js";import{declareUnread as b,trimHistory as v,withinFileBudget as y}from"../interface/history.js";import{log as p}from"../log.js";import{describeTeamsFile as I}from"./files.js";import{messageText as h}from"./html.js";import{threadIdOf as B}from"./thread.js";function H(n){if(n.conversationType==="personal")return!1;if(n.conversationType==="groupChat")return!0;const e=B(n.conversationId);return!!e&&e!==n.id}async function W(n){const{read:e}=n;if(!e)return p.warn("could not read the Teams conversation to catch the agent up"),[{kind:"omitted",reason:"unavailable"}];for(const t of e.messages)t.user&&t.at!==void 0&&n.saw?.(t.user.id,t.at);const a=new Map,i=new Map(e.messages.flatMap(t=>t.user?[[t.user.id,t.user]]:[]));await Promise.all([...i.values()].map(async t=>{try{a.set(t.id,await n.sender(t))}catch(u){p.warn({err:u,who:t.id},"could not check a Teams sender for history")}}));const s=T(e.messages,n,a),o=v(s.entries),c=o[1]?.kind==="omitted"&&o[1].reason==="trimmed",r=!e.complete&&!n.openedBy&&c?o.slice(1):o,f=y(r).map(t=>x(t,n));return e.complete?f:b(f,s.opensWithParent)}function T(n,e,a){const i=[];let s=!1,o=0;const c=()=>{o&&(i.push({kind:"omitted",reason:"unauthorized",count:o}),o=0)};for(const r of n){const d=r.id===e.openedBy,f=!!e.selfAppId&&r.app?.id===e.selfAppId,t=r.user?a.get(r.user.id):void 0;if(!f&&!t?.allowed){d&&(s=!0),o+=1;continue}const u=new Set(r.appMentions.filter(l=>l.appId===e.selfAppId).map(l=>l.id));if(!h(r.html,(l,m)=>u.has(l)?"":`@${m}`)&&r.files.length===0)continue;const w=h(r.html,(l,m)=>u.has(l)&&e.selfName?e.selfName:`@${m}`);d&&(s=!0),c();const g=f?e.self?.():t?.principal();i.push({kind:"message",principal:g??{id:"",externalId:e.selfAppId??"",handle:e.selfName?.replace(/^@/,"")||"unknown",name:e.selfName?.replace(/^@/,"")||"unknown",kind:"bot",affiliation:"unknown",self:!0},text:w,...r.files.length>0?{shared:r.files}:{}})}return c(),{entries:i,opensWithParent:s}}function x(n,e){if(n.kind!=="message")return n;const{shared:a,...i}=n;if(!a?.length)return i;const s=a.map(o=>{const c=k(I(o)),r=e.remember(o);return r?{...c,ref:r}:{...c,unavailable:"Bishop could not record a reference for it"}});return{...i,files:s}}export{H as landedMidConversation,W as teamsHistory};
@@ -1,4 +1,4 @@
1
- import{runCommand as W}from"../commands/run.js";import{BishopError as K}from"../errors.js";import{etiquetteNote as q,threadParticipants as Q}from"../interface/etiquette.js";import{splitMessage as V}from"../interface/format.js";import{showsProgress as X,showsTasks as J,threadVerbosity as Z}from"../interface/verbosity.js";import{log as s}from"../log.js";import{teamsCommandLine as tt}from"./command.js";import{describeTeamsFile as nt,parseTeamsFile as et,teamsAttachment as M,teamsFilePayload as ot}from"./files.js";import{landedMidConversation as at,teamsHistory as it}from"./history.js";import{teamsPresence as rt}from"./presence.js";import{addressesAgent as O,routeActivity as st}from"./route.js";import{baseConversation as ct,conversationOf as I,destinationHandleOf as lt,destinationOfHandle as ht,fileRouteOf as _,sameConversation as dt,scopeOf as ut,threadIdOf as x,threadKeyOf as ft}from"./thread.js";function mt(e){return[...wt(e),...yt].join(`
2
- `)}function wt(e){return X(e)?["This conversation is in Microsoft Teams. Each thing you write is posted as its own message","while you work, so somebody is watching the whole turn. Say what you are doing as you go,","and keep it to a line or two. Your last message is the answer to what was asked.","",J(e)?"Your tool calls show as one message that is rewritten as you work, and replaced by a count of them once you speak again.":"Your tool calls are not shown, so a stretch of them is a silence unless you say what you are doing.","","Nothing more can reach this conversation once the turn ends, so never say you will follow up","separately: finish what you were asked before you stop.",""]:["This conversation is in Microsoft Teams. Only the last thing you write reaches it, and","anything you say on the way there is dropped, so put the whole answer in the message you","finish with. Never say you will follow up separately: nothing more can reach this","conversation until someone writes to you again.","","Nobody is watching while you work. None of your tool calls are visible and nothing you say","arrives until the turn is over, so a turn that takes minutes costs nothing, and there is no","typing indicator to say you are still going.",""]}const yt=["Teams renders a narrower markdown than Slack does. Bold, italics, links, inline code, fenced","code blocks and lists work. Tables do not, so write a list or plain prose where you would","otherwise reach for one.","","A file somebody attaches reaches you in a direct chat and in a channel, and so does an image","pasted into a message anywhere. One Bishop could not download arrives saying why instead of","going missing, so you can tell somebody what happened to it. A group chat is the one place a","file may not reach you at all: if somebody there asks about one you were given nothing for,","say it never arrived and ask them to send it in a direct chat.","","A conversation can hold people talking to each other and not to you. Bishop marks a message",'you may leave alone with a <thread> tag carrying reply="optional": other people are talking',"here and this one did not @mention you, so answer it only if you have something worth adding.","To stay out of it, say <no-output> and nothing else, and Bishop posts nothing at all. Every","message without that tag is yours to answer."],pt="You are not on this agent\u2019s allow list, so it can\u2019t answer you.",C="There was no text in that message for me to act on. Say what you need in words, and @mention me so I see it.",gt=C+" If you attached a file, nothing about it reached me, so send it in a direct chat.",vt=2e4;function xt(e){const y=new Set,p=new Map,g=Q(),v=Z(e.meta,"teams",e.verbosity),F={verbosity:v,model:e.model,configured:e.configuredModel??{},stop:e.stop},T=new Map;let m,N=!1;function P(t){return{name:"teams",guidance:mt(v.of(t.id).level),...m?{self:m}:{}}}async function A(t,n,a){if(!n)return;const r=p.get(n);if(r)return r;const o=ct(t);try{const i=await e.api.member(o,a);return i&&p.set(n,i),i}catch(i){s.warn({err:i,conversation:o},"could not read the Teams roster");return}}async function w(t,n,a=[],r){let o;const i=n?V(n,vt):[""];for(const[c,h]of i.entries()){const f=c===i.length-1&&(a.length>0||r)?{...a.length>0?{files:a}:{},...r?{channel:r}:{}}:void 0,u=await e.api.send(t,h,f);o=u?{id:u.id,text:h}:void 0}return o}function b(t,n){return[t,n?.userPrincipalName,n?.email].filter(a=>!!a).map(a=>a.trim().toLowerCase())}function E(t,n){const a=ot(n);if(!a)return;const r=nt(n);return e.attachments.remember(t,"teams",{id:r.id,name:r.name,payload:a})}async function j(t,n){return n.files.length===0?[]:Promise.all(n.files.map(async a=>{const r=E(t.id,a),o=await e.files.save(t.dirKey,M(a,e.api));return r?{...o,ref:r}:o}))}function D(t,n){return{id:t.from.aadObjectId??t.from.id,...t.from.name?{displayName:t.from.name}:{},...n?.name?{realName:n.name}:{},...n?.email?{email:n.email}:{}}}async function H(t,n){const a=I(n),r=x(a),o=n.recipientId?.replace(/^28:/,"");return it({read:await e.api.history(n),...r?{openedBy:r}:{},...o?{selfAppId:o}:{},...m?{selfName:m.address}:{},sender:async i=>{const c=e.allow.open?void 0:await A(a,i.id,i.id);return{allowed:e.allow.open||b(i.id,c).some(h=>e.allow.allows(h)),principal:{id:i.id,...i.name?{displayName:i.name}:{},...c?.name?{realName:c.name}:{},...c?.email?{email:c.email}:{}}}},remember:i=>E(t.id,i),saw:(i,c)=>g.saw(t.threadKey,i,c)})}function G(t,n,a,r){const o=I(n),i=x(o),c=n.channel?{teamId:n.channel.teamId,channelId:n.channel.channelId,...i?{threadId:i}:{}}:void 0,h=q({participants:g.count(t.threadKey),mentioned:O(n)});return{thread:t,isNewThread:r,get origin(){return P(t)},...h?{note:h}:{},...at(n)?{history:()=>H(t,n)}:{},async messages(){const d=await j(t,n);return[{principal:a,text:n.text,...d.length>0?{files:d}:{}}]},presence:()=>{const d=rt({send:(f,u)=>w(o,f,u,c),post:f=>e.api.send(o,f),update:(f,u)=>e.api.update(o,f,u),threadKey:t.threadKey,verbosity:()=>v.of(t.id).level,..._(ut(n),c)==="none"?{share:!1}:{},...h?{mayDecline:!0}:{}});return T.set(t.threadKey,d),{...d,finish:async()=>{try{await d.finish()}finally{T.delete(t.threadKey)}}}},async notify(d){await w(o,d)},async destination(){return{interface:"teams",handle:lt(n),label:Tt(n)}}}}async function R(t){if(N)return;if(await e.events.seen(`teams:${t.id}`)){s.debug({activity:t.id},"already answered this Teams activity");return}!m&&t.recipientName&&(m={address:`@${t.recipientName}`},s.info({address:m.address},"the agent is known by this name in Teams"));const n=ft(t),a=await e.threads.has(n),r=st(t,{threadKnown:a});if(r.action==="ignore"){s.debug({activity:t.id,thread:n,reason:r.reason},"leaving a Teams message alone");return}const o=t.from.aadObjectId;if(!o){s.warn({activity:t.id},"refusing a Teams message with no Entra object ID");return}g.saw(n,o,Date.now());const i=I(t),c=e.allow.open?void 0:await A(i,o,t.from.id);if(!e.allow.open&&!b(o,c).some(l=>e.allow.allows(l))){s.warn({activity:t.id,who:o,conversation:i},"refusing an unauthorized Teams message"),O(t)&&await w(i,pt).catch(l=>{s.debug({err:l},"could not tell somebody they are not on the allow list")});return}const h=tt(t);if(h){const l=await e.threads.remember(n),k=W(h,{thread:l},F);try{await w(i,k),T.get(n)?.interrupted()}catch(L){s.warn({thread:n,err:L},"couldn't answer a command")}s.info({thread:n,who:o},"ran a command");return}const d=new Set(t.files.map(l=>l.contentUrl)),f=(await e.api.sharedFiles(t)).filter(l=>!d.has(l.contentUrl)),u=f.length>0?{...t,files:[...t.files,...f]}:t;if(!u.text.trim()&&u.files.length===0){if(s.info({activity:t.id},"a Teams message had nothing to act on"),O(t)){const l=t.conversationType==="personal"?C:gt;await w(i,l).catch(k=>{s.debug({err:k},"could not answer a message with nothing in it")})}return}const U=await e.threads.remember(n),Y=D(u,c);s.info({thread:n,who:o,newThread:!a},"running a turn from Teams");const B=e.run(G(U,u,Y,!a)).catch(l=>{s.error({thread:n,err:l},"a turn from Teams failed")}).finally(()=>{y.delete(B)});y.add(B)}async function S(){for(;y.size>0;)await Promise.allSettled([...y])}function $(t){return t==="groupChat"?"Bishop cannot send files to Teams group chats yet.":t==="channel"?"Bishop has no Graph address for this Teams channel, so it cannot send files there.":"Bishop cannot send files to this older Teams destination. Ask for a new report in the conversation first."}async function z(t){const n=ht(t.handle),a=t.files??[],r=a.map(o=>o.name).join(", ");if(a.length>0&&_(n.scope,n.channel)==="none")throw new K($(n.scope));try{await w(n.conversation,t.text,a,n.channel)}catch(o){if(!a.length)throw o;s.warn({handle:t.handle,files:r,err:o},"could not attach files to a Teams report"),await w(n.conversation,`${t.text}
1
+ import{runCommand as Z}from"../commands/run.js";import{BishopError as F}from"../errors.js";import{etiquetteNote as tt,threadParticipants as nt}from"../interface/etiquette.js";import{splitMessage as et}from"../interface/format.js";import{showsProgress as ot,showsTasks as at,threadVerbosity as it}from"../interface/verbosity.js";import{log as c}from"../log.js";import{teamsCommandLine as rt}from"./command.js";import{teamsObserver as st}from"./context.js";import{describeTeamsFile as ct,parseTeamsFile as lt,teamsAttachment as D,teamsFilePayload as ht}from"./files.js";import{landedMidConversation as dt,teamsHistory as ut}from"./history.js";import{teamsPresence as ft}from"./presence.js";import{teamsSighting as R}from"./principal.js";import{addressesAgent as O,routeActivity as mt}from"./route.js";import{baseConversation as wt,conversationOf as S,destinationHandleOf as yt,destinationOfHandle as pt,fileRouteOf as j,sameConversation as vt,scopeOf as H,threadIdOf as G,threadKeyOf as gt}from"./thread.js";function bt(e){return[...Tt(e),...It].join(`
2
+ `)}function Tt(e){return ot(e)?["This conversation is in Microsoft Teams. Each thing you write is posted as its own message","while you work, so somebody is watching the whole turn. Say what you are doing as you go,","and keep it to a line or two. Your last message is the answer to what was asked.","",at(e)?"Your tool calls show as one message that is rewritten as you work, and replaced by a count of them once you speak again.":"Your tool calls are not shown, so a stretch of them is a silence unless you say what you are doing.","","Nothing more can reach this conversation once the turn ends, so never say you will follow up","separately: finish what you were asked before you stop.",""]:["This conversation is in Microsoft Teams. Only the last thing you write reaches it, and","anything you say on the way there is dropped, so put the whole answer in the message you","finish with. Never say you will follow up separately: nothing more can reach this","conversation until someone writes to you again.","","Nobody is watching while you work. None of your tool calls are visible and nothing you say","arrives until the turn is over, so a turn that takes minutes costs nothing, and there is no","typing indicator to say you are still going.",""]}const It=["Teams renders a narrower markdown than Slack does. Bold, italics, links, inline code, fenced","code blocks and lists work. Tables do not, so write a list or plain prose where you would","otherwise reach for one.","","A file somebody attaches reaches you in a direct chat and in a channel, and so does an image","pasted into a message anywhere. One Bishop could not download arrives saying why instead of","going missing, so you can tell somebody what happened to it. A group chat is the one place a","file may not reach you at all: if somebody there asks about one you were given nothing for,","say it never arrived and ask them to send it in a direct chat.","","A conversation can hold people talking to each other and not to you. Bishop marks a message",'you may leave alone with a <thread> tag carrying reply="optional": other people are talking',"here and this one did not @mention you, so answer it only if you have something worth adding.","To stay out of it, say <no-output> and nothing else, and Bishop posts nothing at all. Every","message without that tag is yours to answer."],kt="You are not on this agent\u2019s allow list, so it can\u2019t answer you.",$="There was no text in that message for me to act on. Say what you need in words, and @mention me so I see it.",Ot=$+" If you attached a file, nothing about it reached me, so send it in a direct chat.",St=2e4,Et=3600*1e3;function $t(e){const y=new Set,p=new Map,E=new Map,v=nt(),A=st({api:e.api,...e.tenantId?{tenantId:e.tenantId}:{}}),g=it(e.meta,"teams",e.verbosity),z={verbosity:g,model:e.model,configured:e.configuredModel??{},stop:e.stop},b=new Map;let m,N=!1,x=!1;function T(n){return{name:"teams",guidance:bt(g.of(n.id).level),...m?{self:m}:{}}}async function M(n,t,o){if(!t)return;const i=p.get(t);if(i)return i;if((E.get(t)??0)>Date.now())return;const a=wt(n);let r;try{r=await e.api.member(a,o),r&&p.set(t,r)}catch(s){c.warn({err:s,conversation:a},"could not read the Teams roster")}return!r&&e.allow.open&&E.set(t,Date.now()+Et),r}async function w(n,t,o=[],i){let a;const r=t?et(t,St):[""];for(const[s,l]of r.entries()){const f=s===r.length-1&&(o.length>0||i)?{...o.length>0?{files:o}:{},...i?{channel:i}:{}}:void 0,u=await e.api.send(n,l,f);a=u?{id:u.id,text:l}:void 0}return a}function I(n,t){return[n,t?.userPrincipalName,t?.email].filter(o=>!!o).map(o=>o.trim().toLowerCase())}function _(n,t){const o=ht(t);if(!o)return;const i=ct(t);return e.attachments.remember(n,"teams",{id:i.id,name:i.name,payload:o})}async function L(n,t){return t.files.length===0?[]:Promise.all(t.files.map(async o=>{const i=_(n.id,o),a=await e.files.save(n.dirKey,D(o,e.api));return i?{...a,ref:i}:a}))}function U(n,t,o){return e.principals.see(T(n).name,R({id:t.from.aadObjectId??t.from.id,...t.from.name?{name:t.from.name}:{},bot:t.from.id.startsWith("28:")},o,e.tenantId))}function B(n,t){return e.principals.see("teams",{externalId:n,...t?{handle:t,name:t}:{},kind:"bot",affiliation:"unknown",self:!0})}async function W(n,t){const o=T(n),i=S(t),a=G(i),r=t.recipientId?.replace(/^28:/,"");return ut({read:await e.api.history(t),...a?{openedBy:a}:{},...r?{selfAppId:r}:{},...m?{selfName:m.address}:{},...t.recipientId?{self:()=>B(t.recipientId,t.recipientName)}:{},sender:async s=>{const l=await M(i,s.id,s.id);return{allowed:e.allow.open||I(s.id,l).some(h=>e.allow.allows(h)),principal:()=>e.principals.see(o.name,R({id:s.id,...s.name?{name:s.name}:{},bot:!1},l,e.tenantId))}},remember:s=>_(n.id,s),saw:(s,l)=>v.saw(n.threadKey,s,l)})}function Y(n,t,o,i){const a=S(t),r=G(a),s=t.channel?{teamId:t.channel.teamId,channelId:t.channel.channelId,...r?{threadId:r}:{}}:void 0,l=tt({participants:v.count(n.threadKey),mentioned:O(t)});return{thread:n,isNewThread:i,get origin(){return T(n)},...l?{note:l}:{},...dt(t)?{history:()=>W(n,t)}:{},observe:()=>A.observe({conversationId:t.conversationId,scope:H(t),...t.channel?{channel:t.channel}:{},speaker:o.affiliation}),async messages(){const h=await L(n,t);return[{principal:o,externalId:t.id,text:t.text,...h.length>0?{files:h}:{}}]},presence:()=>{const h=ft({send:(f,u)=>w(a,f,u,s),post:f=>e.api.send(a,f),update:(f,u)=>e.api.update(a,f,u),threadKey:n.threadKey,verbosity:()=>g.of(n.id).level,...j(H(t),s)==="none"?{share:!1}:{},...l?{mayDecline:!0}:{}});return b.set(n.threadKey,h),{...h,finish:async()=>{try{await h.finish()}finally{b.delete(n.threadKey)}}}},async notify(h){await w(a,h)},async destination(){return{interface:"teams",handle:yt(t),label:At(t)}}}}async function q(n){if(x)return;if(await e.events.seen(`teams:${n.id}`)){c.debug({activity:n.id},"already answered this Teams activity");return}!m&&n.recipientName&&(m={address:`@${n.recipientName}`},c.info({address:m.address},"the agent is known by this name in Teams")),!N&&n.recipientId&&(N=!0,B(n.recipientId,n.recipientName));const t=gt(n),o=await e.threads.has(t),i=mt(n,{threadKnown:o});if(i.action==="ignore"){c.debug({activity:n.id,thread:t,reason:i.reason},"leaving a Teams message alone");return}const a=n.from.aadObjectId;if(!a){c.warn({activity:n.id},"refusing a Teams message with no Entra object ID");return}v.saw(t,a,Date.now());const r=S(n),s=await M(r,a,n.from.id);if(!e.allow.open&&!I(a,s).some(d=>e.allow.allows(d))){c.warn({activity:n.id,who:a,conversation:r},"refusing an unauthorized Teams message"),O(n)&&await w(r,kt).catch(d=>{c.debug({err:d},"could not tell somebody they are not on the allow list")});return}const l=rt(n);if(l){const d=await e.threads.remember(t),k=Z(l,{thread:d},z);try{await w(r,k),b.get(t)?.interrupted()}catch(J){c.warn({thread:t,err:J},"couldn't answer a command")}c.info({thread:t,who:a},"ran a command");return}const h=new Set(n.files.map(d=>d.contentUrl)),f=(await e.api.sharedFiles(n)).filter(d=>!h.has(d.contentUrl)),u=f.length>0?{...n,files:[...n.files,...f]}:n;if(!u.text.trim()&&u.files.length===0){if(c.info({activity:n.id},"a Teams message had nothing to act on"),O(n)){const d=n.conversationType==="personal"?$:Ot;await w(r,d).catch(k=>{c.debug({err:k},"could not answer a message with nothing in it")})}return}const K=await e.threads.remember(t),X=U(K,u,s);c.info({thread:t,who:a,newThread:!o},"running a turn from Teams");const P=e.run(Y(K,u,X,!o)).catch(d=>{c.error({thread:t,err:d},"a turn from Teams failed")}).finally(()=>{y.delete(P)});y.add(P)}async function C(){for(;y.size>0;)await Promise.allSettled([...y])}function Q(n){return n==="groupChat"?"Bishop cannot send files to Teams group chats yet.":n==="channel"?"Bishop has no Graph address for this Teams channel, so it cannot send files there.":"Bishop cannot send files to this older Teams destination. Ask for a new report in the conversation first."}async function V(n){const t=pt(n.handle),o=n.files??[],i=o.map(a=>a.name).join(", ");if(o.length>0&&j(t.scope,t.channel)==="none")throw new F(Q(t.scope));try{await w(t.conversation,n.text,o,t.channel)}catch(a){if(!o.length)throw a;c.warn({handle:n.handle,files:i,err:a},"could not attach files to a Teams report"),await w(t.conversation,`${n.text}
3
3
 
4
- _Could not attach ${r}; the report was sent without the file._`,[],n.channel)}return{}}return{name:"teams",handle:R,settle:S,deliver:z,allows(t){return e.allow.open?!0:b(t,p.get(t)).some(n=>e.allow.allows(n))},sameConversation:dt,async resolveDestination(){throw new K("Bishop cannot yet name a Teams channel or chat from what you typed. Ask for a report in the conversation you want it in, which names itself.")},attachment:t=>{const n=et(t);return n?M(n,e.api):void 0},async start(){await e.listener.start(),s.info({allow:e.allow.open?"anyone in the tenant":"a list"},"listening for Teams activities")},async stop(){N=!0,await e.listener.stop(),y.size>0&&(s.info({turns:y.size},"waiting for Teams turns still running"),await S())}}}function Tt(e){return e.conversationType==="personal"?`a chat with ${e.from.name??"someone"}`:e.conversationType==="groupChat"?"a Teams group chat":"a Teams channel thread"}export{xt as createTeamsInterface,mt as teamsGuidance};
4
+ _Could not attach ${i}; the report was sent without the file._`,[],t.channel)}return{}}return{name:"teams",handle:q,changed:n=>A.changed(n),settle:C,deliver:V,allows(n){return e.allow.open?!0:[...I(n.externalId,p.get(n.externalId)),...n.email?[n.email.trim().toLowerCase()]:[]].some(o=>e.allow.allows(o))},sameConversation:vt,async resolveDestination(){throw new F("Bishop cannot yet name a Teams channel or chat from what you typed. Ask for a report in the conversation you want it in, which names itself.")},attachment:n=>{const t=lt(n);return t?D(t,e.api):void 0},async start(){await e.listener.start(),c.info({allow:e.allow.open?"anyone in the tenant":"a list"},"listening for Teams activities")},async stop(){x=!0,await e.listener.stop(),y.size>0&&(c.info({turns:y.size},"waiting for Teams turns still running"),await C())}}}function At(e){return e.conversationType==="personal"?`a chat with ${e.from.name??"someone"}`:e.conversationType==="groupChat"?"a Teams group chat":"a Teams channel thread"}export{$t as createTeamsInterface,bt as teamsGuidance};
@@ -0,0 +1 @@
1
+ function r(n,t){if(!n)return"unknown";const o=n.userRole?.toLowerCase();return o==="guest"||o==="anonymous"?"external":!n.tenantId||!t?"unknown":n.tenantId.toLowerCase()===t.toLowerCase()?"internal":"external"}function s(n,t,o){const e=n.name?.trim()||t?.name?.trim(),i=t?.name?.trim()||e,a=t?.email?.trim();return{externalId:n.id,...e?{handle:e}:{},...i?{name:i}:{},...a?{email:a}:{},kind:n.bot?"bot":"person",affiliation:r(t,o)}}export{r as affiliationOf,s as teamsSighting};
@@ -7,4 +7,4 @@ ${e.error.suggestion}`:"";return`${t}${a}`}function C(n){return n.replace(/("CLI
7
7
  ${p}`:""}`.trim())}return t}async function N(){const n=await c(l,["--version"],{env:f()});if(n.code!==null)return n.output.trim().split(`
8
8
  `)[0]}async function _(){const n=await r(["status"]),e=n.tdp?.sideloading,t=e?.tenant,a=e?.user,s=typeof t=="boolean"||typeof a=="boolean";return{loggedIn:n.loggedIn===!0,...n.username?{username:n.username}:{},...n.tenantId?{tenantId:n.tenantId}:{},...s?{sideloading:{...typeof t=="boolean"?{tenant:t}:{},...typeof a=="boolean"?{user:a}:{}}}:{}}}function A(n=process.env){return!!(n.SSH_CONNECTION||n.SSH_CLIENT)}async function b(){return new Promise(n=>{const e=g(l,["login"],{stdio:"inherit",env:process.env});e.on("error",()=>n(!1)),e.on("exit",t=>n(t===0))})}async function $(n){const e=["app","create","--teams-managed","--name",n.name];n.endpoint&&e.push("--endpoint",n.endpoint);const t=await r(e),a=t.credentials;if(!t.teamsAppId||!a?.CLIENT_ID||!a.TENANT_ID)throw new o("The Teams CLI created an app but reported no credentials for it.");if(!a.CLIENT_SECRET)throw new o(`The Teams CLI created app ${t.teamsAppId} without a client secret.
9
9
  Create one with \`teams app auth secret create ${t.teamsAppId}\`, then run
10
- \`bishop teams setup --manual\`.`);return{teamsAppId:t.teamsAppId,clientId:a.CLIENT_ID,clientSecret:a.CLIENT_SECRET,tenantId:a.TENANT_ID,...t.endpoint?{endpoint:t.endpoint}:{},...t.installLink?{installLink:t.installLink}:{},...t.portalLink?{portalLink:t.portalLink}:{}}}async function v(n){const e=await r(["app","get",n]);return{teamsAppId:e.teamsAppId??n,...e.appId?{clientId:e.appId}:{},...e.name?{name:e.name}:{},...e.endpoint?{endpoint:e.endpoint}:{},...e.installLink?{installLink:e.installLink}:{},...e.portalLink?{portalLink:e.portalLink}:{}}}async function O(n,e){return{needsReinstall:(await r(["app","update",n,"--endpoint",e])).needsReinstall===!0}}const D=["ChannelMessage.Read.Group","ChatMessage.Read.Chat"];async function M(n){const e=await r(["app","rsc","list",n]);if(!Array.isArray(e))throw new o(`\`teams app rsc list ${n}\` did not report a list.`);const t=e.map(a=>a?.name).filter(a=>typeof a=="string");if(e.length>0&&t.length===0)throw new o(`\`teams app rsc list ${n}\` reported no permission names.`);return t}async function P(n,e){try{return{applied:!0,needsReinstall:(await r(["app","rsc","add",n,e,"--type","Application"])).needsReinstall===!0}}catch(t){return i.warn({err:t,teamsAppId:n,permission:e},"could not add an RSC permission"),{applied:!1,needsReinstall:!1,reason:t instanceof Error?t.message:"the update failed"}}}const d="bots[0].supportsFiles";function E(n){const e=n.filter(t=>t.path===d);return e.length===0||e.every(t=>t.oldValue===!0)}async function j(n,e={}){try{const t=await r(["app","manifest","update",n,"--set-json",`${d}=true`,...e.check?["--dry-run"]:["--yes"]]),a=Array.isArray(t.changes)?t.changes:[],s=!E(a);return!e.check&&t.dryRun===!0?(i.warn({teamsAppId:n},"the teams cli previewed the manifest update instead of applying it"),{applied:!1,changed:!1,needsReinstall:!1,reason:"the Teams CLI previewed the change instead of uploading it"}):(t.unpreservedChanges??[]).some(h=>h.path===d)?(i.warn({teamsAppId:n},"the manifest did not keep the file support field"),{applied:!1,changed:!1,needsReinstall:!1,reason:"the manifest did not keep the change"}):{applied:!0,changed:s,needsReinstall:s&&(t.needsReinstall===!0||t.versionBumped===!0)}}catch(t){return i.warn({err:t,teamsAppId:n},"could not turn on Teams file support"),{applied:!1,changed:!1,needsReinstall:!1,reason:t instanceof Error?t.message:"the update failed"}}}const m=80;async function B(n,e){if(e.length>m)return{applied:!1,reason:`a description has to be ${m} characters or fewer, and this one is ${e.length}`};try{return await r(["app","update",n,"--short-description",e]),{applied:!0}}catch(t){return i.warn({err:t,teamsAppId:n},"could not set the app description"),{applied:!1,reason:t instanceof Error?t.message:"the update failed"}}}export{T as INSTALL_TEAMS_CLI,u as INSTALL_TEAMS_CLI_COMMAND,k as LOGIN_DID_NOT_PERSIST,m as MAX_DESCRIPTION,D as RSC_PERMISSIONS,o as TeamsCliError,P as addRscPermission,$ as createTeamsApp,B as describeTeamsApp,M as getRscPermissions,v as getTeamsApp,I as parseCliJson,C as redactSecrets,O as setTeamsEndpoint,j as setTeamsFiles,N as teamsCliVersion,b as teamsLogin,A as teamsLoginWouldUseDeviceCode,_ as teamsStatus};
10
+ \`bishop teams setup --manual\`.`);return{teamsAppId:t.teamsAppId,clientId:a.CLIENT_ID,clientSecret:a.CLIENT_SECRET,tenantId:a.TENANT_ID,...t.endpoint?{endpoint:t.endpoint}:{},...t.installLink?{installLink:t.installLink}:{},...t.portalLink?{portalLink:t.portalLink}:{}}}async function v(n){const e=await r(["app","get",n]);return{teamsAppId:e.teamsAppId??n,...e.appId?{clientId:e.appId}:{},...e.name?{name:e.name}:{},...e.endpoint?{endpoint:e.endpoint}:{},...e.installLink?{installLink:e.installLink}:{},...e.portalLink?{portalLink:e.portalLink}:{}}}async function O(n,e){return{needsReinstall:(await r(["app","update",n,"--endpoint",e])).needsReinstall===!0}}const D=["ChannelMessage.Read.Group","ChatMessage.Read.Chat","ChannelSettings.Read.Group","TeamSettings.Read.Group"];async function M(n){const e=await r(["app","rsc","list",n]);if(!Array.isArray(e))throw new o(`\`teams app rsc list ${n}\` did not report a list.`);const t=e.map(a=>a?.name).filter(a=>typeof a=="string");if(e.length>0&&t.length===0)throw new o(`\`teams app rsc list ${n}\` reported no permission names.`);return t}async function P(n,e){try{return{applied:!0,needsReinstall:(await r(["app","rsc","add",n,e,"--type","Application"])).needsReinstall===!0}}catch(t){return i.warn({err:t,teamsAppId:n,permission:e},"could not add an RSC permission"),{applied:!1,needsReinstall:!1,reason:t instanceof Error?t.message:"the update failed"}}}const d="bots[0].supportsFiles";function E(n){const e=n.filter(t=>t.path===d);return e.length===0||e.every(t=>t.oldValue===!0)}async function j(n,e={}){try{const t=await r(["app","manifest","update",n,"--set-json",`${d}=true`,...e.check?["--dry-run"]:["--yes"]]),a=Array.isArray(t.changes)?t.changes:[],s=!E(a);return!e.check&&t.dryRun===!0?(i.warn({teamsAppId:n},"the teams cli previewed the manifest update instead of applying it"),{applied:!1,changed:!1,needsReinstall:!1,reason:"the Teams CLI previewed the change instead of uploading it"}):(t.unpreservedChanges??[]).some(h=>h.path===d)?(i.warn({teamsAppId:n},"the manifest did not keep the file support field"),{applied:!1,changed:!1,needsReinstall:!1,reason:"the manifest did not keep the change"}):{applied:!0,changed:s,needsReinstall:s&&(t.needsReinstall===!0||t.versionBumped===!0)}}catch(t){return i.warn({err:t,teamsAppId:n},"could not turn on Teams file support"),{applied:!1,changed:!1,needsReinstall:!1,reason:t instanceof Error?t.message:"the update failed"}}}const m=80;async function B(n,e){if(e.length>m)return{applied:!1,reason:`a description has to be ${m} characters or fewer, and this one is ${e.length}`};try{return await r(["app","update",n,"--short-description",e]),{applied:!0}}catch(t){return i.warn({err:t,teamsAppId:n},"could not set the app description"),{applied:!1,reason:t instanceof Error?t.message:"the update failed"}}}export{T as INSTALL_TEAMS_CLI,u as INSTALL_TEAMS_CLI_COMMAND,k as LOGIN_DID_NOT_PERSIST,m as MAX_DESCRIPTION,D as RSC_PERMISSIONS,o as TeamsCliError,P as addRscPermission,$ as createTeamsApp,B as describeTeamsApp,M as getRscPermissions,v as getTeamsApp,I as parseCliJson,C as redactSecrets,O as setTeamsEndpoint,j as setTeamsFiles,N as teamsCliVersion,b as teamsLogin,A as teamsLoginWouldUseDeviceCode,_ as teamsStatus};
@@ -1,29 +1,29 @@
1
- import{loadConfig as y,writeConfig as F}from"../config.js";import{log as I}from"../log.js";import{ensureGitignore as he,resolveAppDetails as W,SetupError as m,withPrompt as G,writeEnv as de}from"../setup.js";import{AZ_LOGIN_COMMAND as j,azAccount as ce,FILES_READ_ALL as le,FILES_READ_WRITE_ALL as z,FILES_READ_WRITE_ALL_ROLE as me,GRAPH_APP_ID as ue,grantGraphFiles as fe,INSTALL_AZ_CLI as we}from"./entra.js";import{addRscPermission as ge,createTeamsApp as Te,describeTeamsApp as Ie,getRscPermissions as Ae,getTeamsApp as _e,INSTALL_TEAMS_CLI as x,INSTALL_TEAMS_CLI_COMMAND as Ee,LOGIN_DID_NOT_PERSIST as Se,RSC_PERMISSIONS as C,setTeamsEndpoint as be,setTeamsFiles as N,teamsCliVersion as U,teamsLogin as ye,teamsLoginWouldUseDeviceCode as $,teamsStatus as v}from"./provision.js";import{readGraphRoles as R,verifyTeamsCredentials as Ce}from"./verify.js";const Y=["The Teams CLI cannot log in from this machine. With no local browser it falls","back to the device code flow, which Microsoft blocks by default in every","tenant."].join(`
1
+ import{loadConfig as y,writeConfig as F}from"../config.js";import{log as I}from"../log.js";import{ensureGitignore as he,resolveAppDetails as W,SetupError as m,withPrompt as G,writeEnv as de}from"../setup.js";import{AZ_LOGIN_COMMAND as j,azAccount as ce,FILES_READ_ALL as le,FILES_READ_WRITE_ALL as z,FILES_READ_WRITE_ALL_ROLE as me,GRAPH_APP_ID as ue,grantGraphFiles as fe,INSTALL_AZ_CLI as we}from"./entra.js";import{addRscPermission as ge,createTeamsApp as Te,describeTeamsApp as Ie,getRscPermissions as Ae,getTeamsApp as _e,INSTALL_TEAMS_CLI as x,INSTALL_TEAMS_CLI_COMMAND as Ee,LOGIN_DID_NOT_PERSIST as Se,RSC_PERMISSIONS as C,setTeamsEndpoint as be,setTeamsFiles as N,teamsCliVersion as U,teamsLogin as ye,teamsLoginWouldUseDeviceCode as v,teamsStatus as $}from"./provision.js";import{readGraphRoles as R,verifyTeamsCredentials as Ce}from"./verify.js";const Y=["The Teams CLI cannot log in from this machine. With no local browser it falls","back to the device code flow, which Microsoft blocks by default in every","tenant."].join(`
2
2
  `),K=[Y,"","So an app cannot be created over SSH. Two ways on from here:",""," Run `bishop teams setup` on a workstation and copy the three BISHOP_TEAMS_*"," lines it writes to .env over to this host.",""," Have somebody else create the app, then run `bishop teams setup --manual`"," here with what they send back. `bishop teams instructions` prints what to"," ask them for."].join(`
3
3
  `),V=[Y,"","So the app has to be re-pointed from a machine that can log in:",""," bishop teams endpoint <url> --app-id <the app id>","","or by editing the bot endpoint in the Teams Developer Portal."].join(`
4
- `);function Z(e){return new m($()?`${x}
4
+ `);function Z(e){return new m(v()?`${x}
5
5
 
6
- ${e}`:x)}function L(e){return{...e.BISHOP_TEAMS_CLIENT_ID?.trim()?{clientId:e.BISHOP_TEAMS_CLIENT_ID.trim()}:{},...e.BISHOP_TEAMS_CLIENT_SECRET?.trim()?{clientSecret:e.BISHOP_TEAMS_CLIENT_SECRET.trim()}:{},...e.BISHOP_TEAMS_TENANT_ID?.trim()?{tenantId:e.BISHOP_TEAMS_TENANT_ID.trim()}:{}}}function O(e){const{clientId:n,clientSecret:t,tenantId:i}=L(e);if(n&&t&&i)return{clientId:n,clientSecret:t,tenantId:i}}function q(e,n){const t=e.teams?.app;if(t&&O(n))return{app:t}}async function J(e,n,t,i){let a;try{a=await de(e,{BISHOP_TEAMS_CLIENT_ID:i.clientId,BISHOP_TEAMS_CLIENT_SECRET:i.clientSecret,BISHOP_TEAMS_TENANT_ID:i.tenantId})}catch(r){throw new m(`The credentials could not be written to .env: ${r instanceof Error?r.message:String(r)}
6
+ ${e}`:x)}function L(e){return{...e.BISHOP_TEAMS_CLIENT_ID?.trim()?{clientId:e.BISHOP_TEAMS_CLIENT_ID.trim()}:{},...e.BISHOP_TEAMS_CLIENT_SECRET?.trim()?{clientSecret:e.BISHOP_TEAMS_CLIENT_SECRET.trim()}:{},...e.BISHOP_TEAMS_TENANT_ID?.trim()?{tenantId:e.BISHOP_TEAMS_TENANT_ID.trim()}:{}}}function O(e){const{clientId:n,clientSecret:t,tenantId:a}=L(e);if(n&&t&&a)return{clientId:n,clientSecret:t,tenantId:a}}function q(e,n){const t=e.teams?.app;if(t&&O(n))return{app:t}}async function J(e,n,t,a){let i;try{i=await de(e,{BISHOP_TEAMS_CLIENT_ID:a.clientId,BISHOP_TEAMS_CLIENT_SECRET:a.clientSecret,BISHOP_TEAMS_TENANT_ID:a.tenantId})}catch(r){throw new m(`The credentials could not be written to .env: ${r instanceof Error?r.message:String(r)}
7
7
 
8
8
  The app was created, so save these somewhere before this scrolls away:
9
9
 
10
- BISHOP_TEAMS_CLIENT_ID=${i.clientId}
11
- BISHOP_TEAMS_CLIENT_SECRET=${i.clientSecret}
12
- BISHOP_TEAMS_TENANT_ID=${i.tenantId}
10
+ BISHOP_TEAMS_CLIENT_ID=${a.clientId}
11
+ BISHOP_TEAMS_CLIENT_SECRET=${a.clientSecret}
12
+ BISHOP_TEAMS_TENANT_ID=${a.tenantId}
13
13
 
14
14
  Then run \`bishop teams setup --manual\` once .env is writable.`)}await F({...n,teams:{...n.teams,app:t}},e);const s=await he(e,[".env","bishop.db*"]);return` .bishop/config.json app configuration
15
- ${a} credentials
15
+ ${i} credentials
16
16
  `+(s.length?` .gitignore added ${s.join(", ")}
17
17
  `:"")}async function k(e){if(e)try{return await _e(e)}catch(n){I.debug({err:n,teamsAppId:e},"could not read the Teams app back");return}}function Q(e,n){return n.map(t=>` teams app rsc add ${e} ${t} --type Application
18
18
  `).join("")}const S=`Without these the agent only hears a message that @mentions it, so a reply to
19
- its own answer never reaches it.
20
- `;async function Ne(e){const n=[],t=[];for(const i of C){const a=await ge(e,i);a.applied||(n.push(i),t.push(` ${i}: ${a.reason}
19
+ its own answer never reaches it, and it can't tell who can read a channel.
20
+ `;async function Ne(e){const n=[],t=[];for(const a of C){const i=await ge(e,a);i.applied||(n.push(a),t.push(` ${a}: ${i.reason}
21
21
  `))}return n.length===0?"":`
22
22
  These permissions could not be added to the app:
23
23
  `+t.join("")+`
24
24
  ${S}Add them with:
25
25
 
26
- `+Q(e,n)}async function X(e){if(e)try{const n=new Set(await Ae(e));return C.filter(t=>!n.has(t))}catch(n){I.debug({err:n,teamsAppId:e},"could not read the app\u2019s RSC permissions");return}}const $e=`Then remove the app from every team and chat it is in, and add it again.
26
+ `+Q(e,n)}async function X(e){if(e)try{const n=new Set(await Ae(e));return C.filter(t=>!n.has(t))}catch(n){I.debug({err:n,teamsAppId:e},"could not read the app\u2019s RSC permissions");return}}const ve=`Then remove the app from every team and chat it is in, and add it again.
27
27
  Teams asks for these permissions only when an app is installed. Updating an
28
28
  app that is already installed does not ask, and it will keep hearing nothing
29
29
  but mentions until it has been removed and installed again.
@@ -31,7 +31,7 @@ but mentions until it has been removed and installed again.
31
31
  can send it a file. An image pasted into a message arrives either way, and a
32
32
  file attached in a channel never does.
33
33
  `;function b(e){return` teams app manifest update ${e} --set-json 'bots[0].supportsFiles=true' --yes
34
- `}function ve(e,n){return`
34
+ `}function $e(e,n){return`
35
35
  File support could not be turned on for this app: ${n??"the update failed"}
36
36
 
37
37
  ${M}Turn it on with:
@@ -49,9 +49,9 @@ This app does not have file support.
49
49
  ${M}Turn it on with:
50
50
 
51
51
  `+b(e)+`
52
- ${Le}`}function D(e,n=" "){const t=e??"<client id>",i=`${n} `;return`${n}az ad app permission add --id ${t} \\
53
- ${i}--api ${ue} \\
54
- ${i}--api-permissions ${me}=Role
52
+ ${Le}`}function D(e,n=" "){const t=e??"<client id>",a=`${n} `;return`${n}az ad app permission add --id ${t} \\
53
+ ${a}--api ${ue} \\
54
+ ${a}--api-permissions ${me}=Role
55
55
  ${n}az ad app permission admin-consent --id ${t}
56
56
  `}const Oe=`Without it the agent cannot send a file to a channel. Files.Read.All still
57
57
  lets it read files people attach there. A file in a direct chat needs none
@@ -85,51 +85,51 @@ Then run:`);if(!t.loggedIn)return A(e.clientId,`${P} Bishop makes it with the Az
85
85
  which is not logged in here. Run \`${j}\`, which is
86
86
  what a tenant with no Azure subscription needs, and then:`);if(t.tenantId&&t.tenantId.toLowerCase()!==e.tenantId.toLowerCase())return A(e.clientId,`${P} The Azure CLI is signed in to tenant
87
87
  ${t.tenantId}, and this app is in ${e.tenantId}, so
88
- Bishop left it alone. Sign in to the app\u2019s tenant and run:`);const i=await fe(e.clientId);if(!i.applied)return A(e.clientId,`Bishop could not grant Files.ReadWrite.All: ${i.reason}
88
+ Bishop left it alone. Sign in to the app\u2019s tenant and run:`);const a=await fe(e.clientId);if(!a.applied)return A(e.clientId,`Bishop could not grant Files.ReadWrite.All: ${a.reason}
89
89
 
90
90
  ${Be}
91
- An administrator makes the grant with:`);const a=await n(e);return a?.includes(z)?ne:a?Me+ke(e.clientId):De}function te(e,n){return n?n.includes(z)?ne:n.includes(le)?Pe:A(e,`The app cannot send a file to a channel: Files.ReadWrite.All is not
91
+ An administrator makes the grant with:`);const i=await n(e);return i?.includes(z)?ne:i?Me+ke(e.clientId):De}function te(e,n){return n?n.includes(z)?ne:n.includes(le)?Pe:A(e,`The app cannot send a file to a channel: Files.ReadWrite.All is not
92
92
  granted on its app registration. An administrator grants it with:`):`
93
93
  Bishop could not check whether the app can read a file attached in a
94
94
  channel. That needs Files.ReadWrite.All on the app registration, which an
95
95
  administrator grants with:
96
96
 
97
- `+D(e)}function ie(e,n){return`
97
+ `+D(e)}function ae(e,n){return`
98
98
  The app is missing ${n.length===1?"a permission":"permissions"}:
99
99
  `+n.map(t=>` ${t}
100
100
  `).join("")+`
101
101
  ${S}Add them with:
102
102
 
103
103
  `+Q(e,n)+`
104
- ${$e}`}const Fe=`
104
+ ${ve}`}const Fe=`
105
105
  ${S}Bishop could not check whether this app has the permissions for that: the
106
106
  Teams CLI is not installed here, or is not logged in. Run this again where it
107
107
  is, or see \`bishop teams instructions\` for what they are.
108
- `;function We(e,n){return n?n.length>0?ie(e,n):"":`
108
+ `;function We(e,n){return n?n.length>0?ae(e,n):"":`
109
109
  ${S}Bishop cannot check this app, so confirm with whoever created it that it has
110
110
  the permissions \`bishop teams instructions\` asks for.
111
111
  `}function Ge(e,n){return!e||!n?.applied?`
112
112
  ${M}Bishop cannot check this app, so confirm with whoever created it that file
113
113
  support is on.
114
- `:n.changed?ee(e):""}async function ae(e,n,t){const i=await k(e.app.id),a=i?.endpoint,s=e.app.id?i?a??"not set":"unknown, the Teams CLI could not read the app":"unknown, no Teams app ID is recorded here";if(n(`This agent is already set up in Teams as "${e.app.displayName??e.app.name}".
114
+ `:n.changed?ee(e):""}async function ie(e,n,t){const a=await k(e.app.id),i=a?.endpoint,s=e.app.id?a?i??"not set":"unknown, the Teams CLI could not read the app":"unknown, no Teams app ID is recorded here";if(n(`This agent is already set up in Teams as "${e.app.displayName??e.app.name}".
115
115
  `+(e.app.id?` App ID ${e.app.id}
116
116
  `:"")+` Endpoint ${s}
117
117
  Its credentials are in .env and its configuration in .bishop/config.json.
118
- `),i&&!a&&n(`
118
+ `),a&&!i&&n(`
119
119
  Teams has nowhere to deliver to until the app is pointed at Bishop.
120
120
  Run \`bishop teams endpoint <url>\` with the URL that reaches this host.
121
- `),e.app.id){const p=await X(e.app.id);p?p.length>0&&n(ie(e.app.id,p)):n(Fe)}if(e.app.id){const p=await N(e.app.id,{check:!0});p.applied?p.changed&&n(ee(e.app.id)):n(Re(e.app.id))}const r=O(process.env);n(te(r?.clientId,r&&await t(r))),n(`
121
+ `),e.app.id){const p=await X(e.app.id);p?p.length>0&&n(ae(e.app.id,p)):n(Fe)}if(e.app.id){const p=await N(e.app.id,{check:!0});p.applied?p.changed&&n(ee(e.app.id)):n(Re(e.app.id))}const r=O(process.env);n(te(r?.clientId,r&&await t(r))),n(`
122
122
  To change where Teams delivers, run \`bishop teams endpoint <url>\`.
123
123
  To set this up again, remove teams.app from .bishop/config.json and the
124
124
  BISHOP_TEAMS_* lines from .env first.
125
- `)}function je(e,n){if(!e)return;const{tenant:t,user:i}=e;t!==!1&&i!==!1||n(`
125
+ `)}function je(e,n){if(!e)return;const{tenant:t,user:a}=e;t!==!1&&a!==!1||n(`
126
126
  Note: custom apps cannot be installed in Teams from here.
127
127
  `+(t===!1?` The tenant-wide setting is off. An administrator turns it on in the Teams
128
128
  Admin Center, under Org-wide app settings, Custom apps.
129
- `:"")+(i===!1?` Your own app setup policy blocks it. An administrator changes it in the
129
+ `:"")+(a===!1?` Your own app setup policy blocks it. An administrator changes it in the
130
130
  Teams Admin Center, under Users, Policies, App setup policy.
131
131
  `:"")+`Setup will still create the app, but somebody will have to install it.
132
- `)}function ze(e,n){const t=e.teams?.app,i=L(n),a=Object.keys(i).length>0;if(t&&!O(n)){const{clientId:s,clientSecret:r,tenantId:p}=i,h=[s?void 0:"BISHOP_TEAMS_CLIENT_ID",r?void 0:"BISHOP_TEAMS_CLIENT_SECRET",p?void 0:"BISHOP_TEAMS_TENANT_ID"].filter(Boolean);throw new m(`An app is already recorded here, but its credentials are not.
132
+ `)}function ze(e,n){const t=e.teams?.app,a=L(n),i=Object.keys(a).length>0;if(t&&!O(n)){const{clientId:s,clientSecret:r,tenantId:p}=a,h=[s?void 0:"BISHOP_TEAMS_CLIENT_ID",r?void 0:"BISHOP_TEAMS_CLIENT_SECRET",p?void 0:"BISHOP_TEAMS_TENANT_ID"].filter(Boolean);throw new m(`An app is already recorded here, but its credentials are not.
133
133
 
134
134
  App ${t.displayName??t.name}${t.id?` (${t.id})`:""}
135
135
  Missing ${h.join(`
@@ -138,23 +138,23 @@ Note: custom apps cannot be installed in Teams from here.
138
138
  Creating a new app would leave that one orphaned in your tenant. Either:
139
139
  finish this one with \`bishop teams setup --manual\`, giving it a client
140
140
  secret from \`teams app auth secret create ${t.id??"<app id>"}\`, or
141
- remove teams.app from .bishop/config.json to start over.`)}if(a&&!t)throw new m(`Teams credentials are already in the environment, but no app is recorded
142
- here: ${Object.keys(i).length} of 3 are set.
141
+ remove teams.app from .bishop/config.json to start over.`)}if(i&&!t)throw new m(`Teams credentials are already in the environment, but no app is recorded
142
+ here: ${Object.keys(a).length} of 3 are set.
143
143
 
144
144
  Creating a new app would overwrite them, and a client secret cannot be
145
145
  read back once it is lost.
146
146
 
147
147
  Run \`bishop teams setup --manual\` to record the app those credentials
148
- belong to, or remove the BISHOP_TEAMS_* lines from .env to start over.`)}async function qe(e,n=R){const t=e.cwd??process.cwd(),i=e.prompt&&process.stdin.isTTY===!0,a=o=>process.stdout.write(o),s=await y(t),r=q(s,process.env);if(r){await ae(r,a,n);return}ze(s,process.env);const p=await U();if(!p)throw Z(K);I.debug({teamsCli:p},"found the teams cli");let h=await v();if(!h.loggedIn){if($())throw new m(K);if(!i)throw new m("The Teams CLI is not logged in to Microsoft 365.\nRun `teams login`, then run this again.");a("The Teams CLI is not logged in to Microsoft 365. Starting `teams login`.\n\n");const o=await ye();if(h=o?await v():h,!h.loggedIn)throw new m(`The Teams CLI is still not logged in, so the app cannot be created.
148
+ belong to, or remove the BISHOP_TEAMS_* lines from .env to start over.`)}async function qe(e,n=R){const t=e.cwd??process.cwd(),a=e.prompt&&process.stdin.isTTY===!0,i=o=>process.stdout.write(o),s=await y(t),r=q(s,process.env);if(r){await ie(r,i,n);return}ze(s,process.env);const p=await U();if(!p)throw Z(K);I.debug({teamsCli:p},"found the teams cli");let h=await $();if(!h.loggedIn){if(v())throw new m(K);if(!a)throw new m("The Teams CLI is not logged in to Microsoft 365.\nRun `teams login`, then run this again.");i("The Teams CLI is not logged in to Microsoft 365. Starting `teams login`.\n\n");const o=await ye();if(h=o?await $():h,!h.loggedIn)throw new m(`The Teams CLI is still not logged in, so the app cannot be created.
149
149
  `+(o?`
150
150
  \`teams login\` reported success, but the login did not survive it.
151
- ${Se}`:"Run `teams login` on its own to see what went wrong."));a(`
151
+ ${Se}`:"Run `teams login` on its own to see what went wrong."));i(`
152
152
  Logged in.
153
- `)}je(h.sideloading,a),await G(i,async o=>{const d=await W(e,t,o),u=e.endpoint?.trim()||await o("Messaging endpoint URL (leave blank to set later)","");a(`
153
+ `)}je(h.sideloading,i),await G(a,async o=>{const d=await W(e,t,o),u=e.endpoint?.trim()||await o("Messaging endpoint URL (leave blank to set later)","");i(`
154
154
  Creating the Teams app for "${d.displayName}"${h.username?` in ${h.username}'s tenant`:""}...
155
- `);const c=await Te({name:d.displayName,...u?{endpoint:u}:{}}),w=d.description?await Ie(c.teamsAppId,d.description):{applied:!1,reason:void 0};d.description&&!w.applied&&a(`
155
+ `);const c=await Te({name:d.displayName,...u?{endpoint:u}:{}}),w=d.description?await Ie(c.teamsAppId,d.description):{applied:!1,reason:void 0};d.description&&!w.applied&&i(`
156
156
  The description was not applied: ${w.reason}
157
- `);const f=await Ne(c.teamsAppId);f&&a(f);const l=await N(c.teamsAppId);l.applied||a(ve(c.teamsAppId,l.reason));const g={id:c.teamsAppId,name:d.name,displayName:d.displayName,...w.applied&&d.description?{description:d.description}:{}},T=await J(t,s,g,c),_=await He(c,n);I.info({teamsAppId:c.teamsAppId,endpoint:u||void 0},"teams app created"),a(`
157
+ `);const f=await Ne(c.teamsAppId);f&&i(f);const l=await N(c.teamsAppId);l.applied||i($e(c.teamsAppId,l.reason));const g={id:c.teamsAppId,name:d.name,displayName:d.displayName,...w.applied&&d.description?{description:d.description}:{}},T=await J(t,s,g,c),_=await He(c,n);I.info({teamsAppId:c.teamsAppId,endpoint:u||void 0},"teams app created"),i(`
158
158
  Done.
159
159
  ${T}`+_+(c.installLink?`
160
160
  Install it in Teams:
@@ -162,8 +162,8 @@ Install it in Teams:
162
162
  `:"")+(u?"":`
163
163
  The app has no messaging endpoint yet, so Teams has nowhere to deliver to.
164
164
  Run \`bishop teams endpoint <url>\` with the URL that reaches this host.
165
- `)+"\nRun `bishop` to start the agent.\n")})}async function Je(e,n=Ce,t=R){const i=e.cwd??process.cwd(),a=e.prompt&&process.stdin.isTTY===!0,s=o=>process.stdout.write(o),r=await y(i),p=r.teams?.app,h=q(r,process.env);if(h){const o=e.appId?.trim();if(o&&!h.app.id){await F({...r,teams:{...r.teams,app:{...h.app,id:o}}},i),s(`Recorded Teams app ID ${o} for "${h.app.displayName??h.app.name}".
166
- `);return}await ae(h,s,t);return}await G(a,async o=>{const d=L(process.env),u=d.clientId||await o("Client ID",""),c=d.clientSecret||await o("Client secret",""),w=d.tenantId||await o("Tenant ID","");if(!u||!c||!w)throw new m(`All three credentials are needed: a client ID, a client secret, and a
165
+ `)+"\nRun `bishop` to start the agent.\n")})}async function Je(e,n=Ce,t=R){const a=e.cwd??process.cwd(),i=e.prompt&&process.stdin.isTTY===!0,s=o=>process.stdout.write(o),r=await y(a),p=r.teams?.app,h=q(r,process.env);if(h){const o=e.appId?.trim();if(o&&!h.app.id){await F({...r,teams:{...r.teams,app:{...h.app,id:o}}},a),s(`Recorded Teams app ID ${o} for "${h.app.displayName??h.app.name}".
166
+ `);return}await ie(h,s,t);return}await G(i,async o=>{const d=L(process.env),u=d.clientId||await o("Client ID",""),c=d.clientSecret||await o("Client secret",""),w=d.tenantId||await o("Tenant ID","");if(!u||!c||!w)throw new m(`All three credentials are needed: a client ID, a client secret, and a
167
167
  tenant ID. Set BISHOP_TEAMS_CLIENT_ID, BISHOP_TEAMS_CLIENT_SECRET and
168
168
  BISHOP_TEAMS_TENANT_ID, or run this in a terminal so they can be typed in.
169
169
 
@@ -175,7 +175,7 @@ Checking the credentials...
175
175
  This is the same request Bishop makes on every turn, so the agent would
176
176
  not have been able to answer anything.`);g.detail?s(`${g.detail} Saving them anyway.
177
177
  `):s(`Accepted.
178
- `);const T=await k(l||void 0),_=e.name??p?.name??T?.name,B=e.displayName??p?.displayName??T?.name,H=e.description??p?.description,E=await W({...e,..._?{name:_}:{},...B?{displayName:B}:{},...H?{description:H}:{}},i,o),se={...l?{id:l}:{},name:E.name,displayName:E.displayName,...E.description?{description:E.description}:{}},oe=await J(i,r,se,f),re=await X(l||void 0),pe=l?await N(l,{check:!0}):void 0;I.info({teamsAppId:l||void 0},"teams app adopted"),s(`
178
+ `);const T=await k(l||void 0),_=e.name??p?.name??T?.name,B=e.displayName??p?.displayName??T?.name,H=e.description??p?.description,E=await W({...e,..._?{name:_}:{},...B?{displayName:B}:{},...H?{description:H}:{}},a,o),se={...l?{id:l}:{},name:E.name,displayName:E.displayName,...E.description?{description:E.description}:{}},oe=await J(a,r,se,f),re=await X(l||void 0),pe=l?await N(l,{check:!0}):void 0;I.info({teamsAppId:l||void 0},"teams app adopted"),s(`
179
179
  Done.
180
180
  ${oe}`+(T?.endpoint?`
181
181
  The app is pointed at ${T.endpoint}
@@ -183,11 +183,11 @@ The app is pointed at ${T.endpoint}
183
183
  Bishop cannot tell where this app is pointed, so confirm with whoever
184
184
  created it that its messaging endpoint reaches this host. Bishop serves it
185
185
  at /api/messages.
186
- `)+We(l,re)+Ge(l,pe)+te(f.clientId,await t(f))+"\nRun `bishop` to start the agent.\n")})}async function Qe(e,n={}){const t=n.cwd??process.cwd(),i=o=>process.stdout.write(o),a=await y(t),s=n.appId?.trim()||a.teams?.app?.id;if(!s)throw new m("No Teams app is recorded here, so there is nothing to point.\nRun `bishop teams setup` first, or pass --app-id.");if(!await U())throw Z(V);if($()&&!(await v()).loggedIn)throw new m(V);const p=(await k(s))?.endpoint;if(p===e){i(`App ${s} already points at ${e}. Nothing to do.
187
- `);return}const{needsReinstall:h}=await be(s,e);I.info({teamsAppId:s,before:p,endpoint:e,needsReinstall:h},"teams messaging endpoint updated"),i(`App ${s} now points at ${e}
188
- `),h&&i(`
186
+ `)+We(l,re)+Ge(l,pe)+te(f.clientId,await t(f))+"\nRun `bishop` to start the agent.\n")})}async function Qe(e,n={}){const t=n.cwd??process.cwd(),a=o=>process.stdout.write(o),i=await y(t),s=n.appId?.trim()||i.teams?.app?.id;if(!s)throw new m("No Teams app is recorded here, so there is nothing to point.\nRun `bishop teams setup` first, or pass --app-id.");if(!await U())throw Z(V);if(v()&&!(await $()).loggedIn)throw new m(V);const p=(await k(s))?.endpoint;if(p===e){a(`App ${s} already points at ${e}. Nothing to do.
187
+ `);return}const{needsReinstall:h}=await be(s,e);I.info({teamsAppId:s,before:p,endpoint:e,needsReinstall:h},"teams messaging endpoint updated"),a(`App ${s} now points at ${e}
188
+ `),h&&a(`
189
189
  The app's manifest changed, which usually means its valid domains did.
190
190
  Reinstall it in Teams before expecting it to receive anything.
191
- `)}function Xe(e){const n=e?.trim()||"https://<the URL that reaches this host>/api/messages";return["Bishop needs a Microsoft Teams app. To create one for it:","","1. Create a Teams app with a bot. A Teams-managed bot is enough and needs no"," Azure subscription:","",` ${Ee}`," teams login"," teams app create --teams-managed --name <app name> --endpoint <url>",""," The install flags are not optional: without them the CLI cannot store a"," login and every command starts logged out.","","2. Point the bot at this messaging endpoint:","",` ${n}`,""," Teams POSTs every activity here, so it has to be reachable over HTTPS.","","3. Give the app these resource-specific consent permissions:","",...C.map(t=>` teams app rsc add <app id> ${t} --type Application`),""," Without them Teams only delivers a message that @mentions the agent, so a"," reply to its own answer never reaches it. Whoever installs the app in a team"," or a chat is who consents, and Teams only asks at install time. An app that"," is already installed has to be removed from every team and chat it is in and"," added again: updating it in place never asks, so it goes on hearing nothing"," but mentions.","","4. Turn on file support, so the agent can be sent a file at all:","",` ${b("<app id>").trim()}`,""," Without it Teams shows no attach button in a chat with the agent, so there"," is nothing for anyone to send. An image pasted into a message arrives either"," way. `--yes` is not optional: the CLI refuses the write without it unless"," somebody is there to answer a prompt.","","5. Let the agent read and send files in a channel. Those are on SharePoint"," rather than on the bot channel, so Bishop reads them through Microsoft"," Graph, which needs one application permission on the app registration:","",` ${j}`,...D("<client id>"," ").trimEnd().split(`
191
+ `)}function Xe(e){const n=e?.trim()||"https://<the URL that reaches this host>/api/messages";return["Bishop needs a Microsoft Teams app. To create one for it:","","1. Create a Teams app with a bot. A Teams-managed bot is enough and needs no"," Azure subscription:","",` ${Ee}`," teams login"," teams app create --teams-managed --name <app name> --endpoint <url>",""," The install flags are not optional: without them the CLI cannot store a"," login and every command starts logged out.","","2. Point the bot at this messaging endpoint:","",` ${n}`,""," Teams POSTs every activity here, so it has to be reachable over HTTPS.","","3. Give the app these resource-specific consent permissions:","",...C.map(t=>` teams app rsc add <app id> ${t} --type Application`),""," Without them Teams only delivers a message that @mentions the agent, so a"," reply to its own answer never reaches it, and the agent cannot tell whether"," a channel is private or shared. Whoever installs the app in a team"," or a chat is who consents, and Teams only asks at install time. An app that"," is already installed has to be removed from every team and chat it is in and"," added again: updating it in place never asks, so it goes on hearing nothing"," but mentions.","","4. Turn on file support, so the agent can be sent a file at all:","",` ${b("<app id>").trim()}`,""," Without it Teams shows no attach button in a chat with the agent, so there"," is nothing for anyone to send. An image pasted into a message arrives either"," way. `--yes` is not optional: the CLI refuses the write without it unless"," somebody is there to answer a prompt.","","5. Let the agent read and send files in a channel. Those are on SharePoint"," rather than on the bot channel, so Bishop reads them through Microsoft"," Graph, which needs one application permission on the app registration:","",` ${j}`,...D("<client id>"," ").trimEnd().split(`
192
192
  `),""," That is Files.ReadWrite.All, and it is tenant-wide, which is why it takes an"," administrator. `--allow-no-subscriptions` is what a tenant with no Azure"," subscription needs, which is the ordinary case for a Teams-managed bot."," Skipping this keeps channel file sending unavailable. Files.Read.All still"," permits the agent to read files people attach there.","","6. Allow the app to be installed. Custom app installation has to be on for the"," tenant (Teams Admin Center, Org-wide app settings, Custom apps) and for"," whoever installs it (Users, Policies, App setup policy).","","7. Send back three values, which are credentials and should travel like any"," other secret:",""," the app registration\u2019s client ID"," a client secret for it"," the tenant ID","","Then run `bishop teams setup --manual` here and give it those three.",""].join(`
193
193
  `)}export{Xe as administratorInstructions,He as applyChannelFileGrant,q as existingTeamsSetup,Qe as repointTeamsEndpoint,qe as setupTeams,Je as setupTeamsManually};
@@ -1,2 +1,2 @@
1
- import{z as i}from"zod";import{FETCH_FILE_TOOL as s,fileTag as f}from"../harness/principal.js";import{SHARE_FILE_DESCRIPTION as l,SHARE_FILE_TOOL as c,ShareRefused as m}from"../outgoing.js";import{defineTool as a,ToolRefusal as r}from"./types.js";function y(t){return[d(t),w()]}function d(t){return a({name:s,description:["Downloads a file somebody shared in this conversation and gives you the path to it.","","Takes the ref from an <attachment> tag. Ask for a file when you need to open it, rather","than for everything in the conversation. Asking for one already downloaded hands back","the same path without fetching it again, so ask rather than reusing a path from earlier","in the conversation. It fails, with the reason, when there is no file to be had."].join(`
2
- `),input:{ref:i.string().trim().min(1).describe("The ref attribute of the file tag naming the file you want")},async run(n,o){const e=await t.fetch(o.thread,n.ref);if(!e)throw new r(`No file with the ref ${n.ref} was shared in this conversation. Refs come from the file tags Bishop writes, and one only works in the conversation it was written in.`);if(!e.path){const h=e.unavailable??"the download produced no file";throw new r(`Bishop could not fetch the file with ref ${n.ref}: ${h}.`+(e.url?` The original is at ${e.url}`:""))}return f({...e,ref:n.ref})}})}function w(){return a({name:c,description:l,input:{path:i.string().trim().min(1).describe("Absolute path to the file to send"),name:i.string().trim().min(1).optional().describe("What the recipient sees it called. Defaults to the filename on disk.")},async run(t,n){const o=n.share;if(!o)throw new r("There is nowhere to put a file in this conversation, so nothing was sent.");try{const e=await o({path:t.path,...t.name?{name:t.name}:{}});return`Registered ${e.name}. Write <attachment ref="${e.ref}" /> in your reply where the file should appear.`}catch(e){throw e instanceof m?new r(e.message):e}}})}export{y as fileTools,w as shareTool};
1
+ import{z as a}from"zod";import{FETCH_FILE_TOOL as h,fileTag as f}from"../harness/principal.js";import{SHARE_FILE_DESCRIPTION as l,SHARE_FILE_TOOL as c,ShareRefused as m}from"../outgoing.js";import{defineTool as s,ToolRefusal as i}from"./types.js";function y(t){return[d(t),w()]}function d(t){return s({name:h,description:["Downloads a file somebody shared in this conversation and gives you the path to it.","","Takes the ref from an <attachment> tag. Ask for a file when you need to open it, rather","than for everything in the conversation. Asking for one already downloaded hands back","the same path without fetching it again, so ask rather than reusing a path from earlier","in the conversation. It fails, with the reason, when there is no file to be had."].join(`
2
+ `),input:{ref:a.string().trim().min(1).describe("The ref attribute of the file tag naming the file you want")},async run(r,n){const e=await t.fetch(n.thread,r.ref);if(!e)throw new i(`No file with the ref ${r.ref} was shared in this conversation. Refs come from the file tags Bishop writes, and one only works in the conversation it was written in.`);if(!e.path){const o=e.unavailable??"the download produced no file";throw new i(`Bishop could not fetch the file with ref ${r.ref}: ${o}.`+(e.url?` The original is at ${e.url}`:""))}return f({...e,ref:r.ref})}})}function w(){return s({name:c,description:l,input:{path:a.string().trim().min(1).describe("Absolute path to the file to send"),name:a.string().trim().min(1).optional().describe("What the recipient sees it called. Defaults to the filename on disk.")},async run(t,r){const n=r.share;if(!n)throw new i("There is nowhere to put a file in this conversation, so nothing was sent.");try{const e=r.stage?await r.stage(t.path):t.path,o=await n({path:e,...t.name?{name:t.name}:{}});return`Registered ${o.name}. Write <attachment ref="${o.ref}" /> in your reply where the file should appear.`}catch(e){throw e instanceof m?new i(e.message):e}}})}export{y as fileTools,w as shareTool};