@agentdeploymentco/bishop 0.3.0 → 0.3.2

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.
@@ -1 +1 @@
1
- import{BishopError as o}from"../errors.js";import{log as u}from"../log.js";import{retry as $}from"../retry.js";import{splitForSlack as y}from"./format.js";function h(e){return e.threadTs?`${e.channel}:${e.threadTs}`:e.channel}function m(e){const[t,r]=e.split(":");if(!t)throw new o(`not a Slack destination: ${e}`);return{channel:t,...r?{threadTs:r}:{}}}function b(e){return/^[CG][A-Z0-9]{2,}$/.test(e)}function g(e){return/^D[A-Z0-9]{2,}$/.test(e)}function w(e){return/^[UW][A-Z0-9]{2,}$/.test(e)}const S=new Set(["ratelimited","rate_limited"]);function _(e){const t=e;if(!t)return!1;if(t.code==="slack_webapi_rate_limited_error")return{...t.retryAfter?{retryAfterMs:t.retryAfter*1e3}:{}};const r=t.data?.error;return!!(r&&S.has(r))}async function D(e,t){const{channel:r,threadTs:s}=m(t.handle),i=y(t.text);let a;for(const l of i){const c=s??a,f=await $(()=>e.chat.postMessage({channel:r,markdown_text:l,...c?{thread_ts:c}:{}}),{what:"chat.postMessage",retryable:_});a??=f.ts}const n=s??a;return u.info({channel:r,...n?{thread:n}:{},parts:i.length},"delivered to Slack"),s||!a?void 0:{channel:r,threadTs:a}}async function E(e,t){const{channel:r,threadTs:s}=m(t),i=s?" (thread)":"";try{const n=(await e.conversations.info({channel:r})).channel;if(n?.name)return`#${n.name}${i}`;if(n?.is_im&&n.user){const l=await e.users.info({user:n.user}),c=l.user?.profile?.display_name?.trim()||l.user?.name;if(c)return`@${c}`}}catch(a){u.debug({channel:r,err:a},"could not name a Slack destination")}return`${r}${i}`}async function v(e,t){const r=t.toLowerCase();for await(const s of e.paginate("conversations.list",{types:"public_channel,private_channel",exclude_archived:!0,limit:200})){const a=(s.channels??[]).find(n=>n.name?.toLowerCase()===r);if(a)return a}}async function x(e,t){const r=t.toLowerCase();if(r.includes("@"))try{const s=await e.users.lookupByEmail({email:t});if(s.user?.id)return s.user.id}catch(s){u.debug({email:t,err:s},"no Slack user with that address")}for await(const s of e.paginate("users.list",{limit:200})){const i=s.members??[];for(const a of i){if(a.deleted||!a.id)continue;if([a.name,a.profile?.display_name,a.profile?.real_name].some(l=>l?.toLowerCase()===r))return a.id}}}async function M(e,t,r={}){try{return await A(e,t,r)}catch(s){throw s instanceof o?s:T(s,t)}}function T(e,t){const r=e,s=r?.data?.error;return s==="missing_scope"?new o(`Slack won't let this app look up ${t}: it is missing the ${r?.data?.needed??"required"} scope. Bishop's manifest asks for it, so an app installed before this existed has to be reinstalled: run \`bishop slack setup\` again.`):new o(`Slack could not resolve ${t}: ${s??(e instanceof Error?e.message:String(e))}`)}async function A(e,t,r){const s=t.trim();if(!s)throw new o("a Slack destination needs a channel or a person");const[i,a]=s.split(":"),n=(i??"").replace(/^[#@]/,"");if(!n)throw new o(`not a Slack destination: ${t}`);const l=s.startsWith("@")||w(n);if(g(n))return{interface:"slack",handle:h({channel:n,...a?{threadTs:a}:{}}),label:n};if(b(n)){const d=(await e.conversations.info({channel:n})).channel;if(!d?.id)throw new o(`Slack has no conversation ${n}`);return k(d,n),{interface:"slack",handle:h({channel:d.id,...a?{threadTs:a}:{}}),label:d.name?`#${d.name}`:d.id}}if(l){const f=w(n)?n:await x(e,n);if(!f)throw new o(`no Slack user matches @${n}`);if(r.allows&&!r.allows(f))throw new o(`@${n} is not on this agent's allow list, so Bishop won't start sending them messages they never asked for. Add them to slack.allow in .bishop/config.json, or post to a channel they are in instead.`);const p=(await e.conversations.open({users:f})).channel?.id;if(!p)throw new o(`could not open a DM with @${n}`);return{interface:"slack",handle:h({channel:p,...a?{threadTs:a}:{}}),label:`@${n}`}}const c=await v(e,n);if(!c?.id)throw new o(`no Slack channel named #${n} that this app can see. A private channel has to have the app in it.`);return k(c,`#${n}`),{interface:"slack",handle:h({channel:c.id,...a?{threadTs:a}:{}}),label:`#${c.name??n}`}}function k(e,t){if(!(e.is_im||e.is_member!==!1))throw new o(`this app is not in ${t}. Invite it there first, or nothing sent to it will arrive.`)}export{D as deliverToSlack,E as describeSlackHandle,m as parseSlackHandle,M as resolveSlackDestination,h as slackHandle,_ as slackRetryable};
1
+ import{BishopError as l}from"../errors.js";import{log as p}from"../log.js";import{retry as m}from"../retry.js";import{splitForSlack as b}from"./format.js";import{attachToSlack as S}from"./upload.js";function w(e){return e.threadTs?`${e.channel}:${e.threadTs}`:e.channel}function k(e){const[n,a]=e.split(":");if(!n)throw new l(`not a Slack destination: ${e}`);return{channel:n,...a?{threadTs:a}:{}}}function v(e){return/^[CG][A-Z0-9]{2,}$/.test(e)}function x(e){return/^D[A-Z0-9]{2,}$/.test(e)}function $(e){return/^[UW][A-Z0-9]{2,}$/.test(e)}const T=new Set(["ratelimited","rate_limited"]);function _(e){const n=e;if(!n)return!1;if(n.code==="slack_webapi_rate_limited_error")return{...n.retryAfter?{retryAfterMs:n.retryAfter*1e3}:{}};const a=n.data?.error;return!!(a&&T.has(a))}async function L(e,n){const{channel:a,threadTs:r}=k(n.handle),d=b(n.text),s=n.files??[],t=s.length>0?d.pop()??"":void 0;let i;for(const h of d){const c=r??i,f=await m(()=>e.chat.postMessage({channel:a,markdown_text:h,...c?{thread_ts:c}:{}}),{what:"chat.postMessage",retryable:_});i??=f.ts}const o=r??i;if(t!==void 0){const h={channel:a,...o?{threadTs:o}:{}};try{const c=await S(e,h,s,t);if(i??=c?.ts,!c&&t){const f=await m(()=>e.chat.postMessage({channel:a,markdown_text:t,...o?{thread_ts:o}:{}}),{what:"chat.postMessage",retryable:_});i??=f.ts}}catch(c){const f=s.map(u=>u.name).join(", ");p.error({channel:a,files:f,err:c},"could not send a file with a report");for(const u of[t,`_Could not upload ${f}._`]){if(!u)continue;const y=await e.chat.postMessage({channel:a,markdown_text:u,...o?{thread_ts:o}:{}}).catch(()=>{});i??=y?.ts}}}return p.info({channel:a,...o?{thread:o}:{},parts:d.length},"delivered to Slack"),r||!i?void 0:{channel:a,threadTs:i}}async function F(e,n){const{channel:a,threadTs:r}=k(n),d=r?" (thread)":"";try{const t=(await e.conversations.info({channel:a})).channel;if(t?.name)return`#${t.name}${d}`;if(t?.is_im&&t.user){const i=await e.users.info({user:t.user}),o=i.user?.profile?.display_name?.trim()||i.user?.name;if(o)return`@${o}`}}catch(s){p.debug({channel:a,err:s},"could not name a Slack destination")}return`${a}${d}`}async function A(e,n){const a=n.toLowerCase();for await(const r of e.paginate("conversations.list",{types:"public_channel,private_channel",exclude_archived:!0,limit:200})){const s=(r.channels??[]).find(t=>t.name?.toLowerCase()===a);if(s)return s}}async function C(e,n){const a=n.toLowerCase();if(a.includes("@"))try{const r=await e.users.lookupByEmail({email:n});if(r.user?.id)return r.user.id}catch(r){p.debug({email:n,err:r},"no Slack user with that address")}for await(const r of e.paginate("users.list",{limit:200})){const d=r.members??[];for(const s of d){if(s.deleted||!s.id)continue;if([s.name,s.profile?.display_name,s.profile?.real_name].some(i=>i?.toLowerCase()===a))return s.id}}}async function R(e,n,a={}){try{return await I(e,n,a)}catch(r){throw r instanceof l?r:M(r,n)}}function M(e,n){const a=e,r=a?.data?.error;return r==="missing_scope"?new l(`Slack won't let this app look up ${n}: it is missing the ${a?.data?.needed??"required"} scope. Bishop's manifest asks for it, so an app installed before this existed has to be reinstalled: run \`bishop slack setup\` again.`):new l(`Slack could not resolve ${n}: ${r??(e instanceof Error?e.message:String(e))}`)}async function I(e,n,a){const r=n.trim();if(!r)throw new l("a Slack destination needs a channel or a person");const[d,s]=r.split(":"),t=(d??"").replace(/^[#@]/,"");if(!t)throw new l(`not a Slack destination: ${n}`);const i=r.startsWith("@")||$(t);if(x(t))return{interface:"slack",handle:w({channel:t,...s?{threadTs:s}:{}}),label:t};if(v(t)){const c=(await e.conversations.info({channel:t})).channel;if(!c?.id)throw new l(`Slack has no conversation ${t}`);return g(c,t),{interface:"slack",handle:w({channel:c.id,...s?{threadTs:s}:{}}),label:c.name?`#${c.name}`:c.id}}if(i){const h=$(t)?t:await C(e,t);if(!h)throw new l(`no Slack user matches @${t}`);if(a.allows&&!a.allows(h))throw new l(`@${t} is not on this agent's allow list, so Bishop won't start sending them messages they never asked for. Add them to slack.allow in .bishop/config.json, or post to a channel they are in instead.`);const f=(await e.conversations.open({users:h})).channel?.id;if(!f)throw new l(`could not open a DM with @${t}`);return{interface:"slack",handle:w({channel:f,...s?{threadTs:s}:{}}),label:`@${t}`}}const o=await A(e,t);if(!o?.id)throw new l(`no Slack channel named #${t} that this app can see. A private channel has to have the app in it.`);return g(o,`#${t}`),{interface:"slack",handle:w({channel:o.id,...s?{threadTs:s}:{}}),label:`#${o.name??t}`}}function g(e,n){if(!(e.is_im||e.is_member!==!1))throw new l(`this app is not in ${n}. Invite it there first, or nothing sent to it will arrive.`)}export{L as deliverToSlack,F as describeSlackHandle,k as parseSlackHandle,R as resolveSlackDestination,w as slackHandle,_ as slackRetryable};
@@ -1,2 +1,2 @@
1
- import{etiquetteNote as C,threadParticipants as D}from"../interface/etiquette.js";import{log as f}from"../log.js";import{createSlackApp as L}from"./app.js";import{createAuthorizer as O}from"./authorizer.js";import{botTurnCap as E}from"./bots.js";import{runCommand as H}from"./commands/run.js";import{commandLine as F}from"./commands/syntax.js";import{deliverToSlack as K,describeSlackHandle as _,parseSlackHandle as b,resolveSlackDestination as U,slackHandle as z}from"./destination.js";import{describeSlackFile as R,parseSlackFile as W,slackAttachment as v,slackFilePayload as j}from"./files.js";import{registerHandlers as q}from"./handlers.js";import{landedMidThread as G,threadHistory as V}from"./history.js";import{linkMentions as I,mentionableName as S,renderMentions as T}from"./mentions.js";import{createPresence as Y}from"./presence.js";import{say as N}from"./say.js";import{threadKeyString as $}from"./thread.js";import{userDirectory as J}from"./users.js";import{threadVerbosity as Q}from"./verbosity.js";const X={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
- `)},Z=3e3;async function ee(t,s,i=Z){const l=await ae(t.botUserId,s,i),r=S(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 ae(t,s,i){const l={userId:t};let r;const h=new Promise(w=>{r=setTimeout(()=>w(l),i)});try{return await Promise.race([s.lookup(t).catch(()=>l),h])}finally{clearTimeout(r)}}async function ge(t){const{app:s,identity:i}=await L(t.credentials),l=await O(s.client,t.allow),r=J(s.client,{self:i.botUserId}),h=E(t.maxBotTurns),w=D(),g=Q(t.meta,t.verbosity),c=await ee(i,r),y={...X,...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,x=async({key:e,keyString:a,thread:o,text:p},m)=>{const n=F(p);if(!n)return!1;const d=H(n,{thread:o},{verbosity:g});try{await N(m.client,{channel:e.channelId,threadTs:e.threadTs},d),k.get(a)?.interrupted()}catch(u){f.warn({thread:a,err:u},"couldn't answer a command")}return!0};async function B(e){const a=await r.lookup(e.userId),o=S(a);return{id:e.userId,...o?{displayName:o}:{},...a.realName?{realName:a.realName}:{},...a.email?{email:a.email}:{},...e.isBot?{isBot:!0}:{}}}function M(e,a){const o=R(a);if(!o.unavailable)return t.attachments.remember(e,y.name,{id:o.id,name:o.name,payload:j(a)})}async function P(e,a){return a.files.length===0?[]:Promise.all(a.files.map(async o=>{const p=M(e.id,o),m=await t.files.save(e.dirKey,v(o,t.credentials.botToken));return p?{...m,ref:p}:m}))}function A(e,a){const o=T(e.messages[0]?.text??"",r),p=G(e),m=C({participants:e.participants,mentioned:e.messages.some(n=>n.mentionsAgent)});return{thread:e.thread,isNewThread:e.isNewThread,origin:y,...m?{note:m}:{},...p?{history:()=>V({client:a.client,key:e.key,before:p,allows:n=>l.allows(n),answersBots:h.answersBots,self:{botUserId:i.botUserId,...i.botId?{botId:i.botId}:{},...c?{botName:c.address}:{}},users:r,remember:n=>t.attachments.remember(e.thread.id,y.name,n),saw:(n,d)=>w.saw(e.keyString,n,d)})}:{},async messages(){return Promise.all(e.messages.map(async(n,d)=>{const u=await P(e.thread,n);return{principal:await B(n),text:d===0?await o:await T(n.text,r),...u.length>0?{files:u}:{}}}))},presence:()=>{const n=Y(e,a,{outgoing:d=>I(d,r),title:()=>o,verbosity:()=>g.of(e.thread.id).level,...m?{mayDecline:!0}:{},...t.taskIntervalMs===void 0?{}:{taskIntervalMs:t.taskIntervalMs}});return k.set(e.keyString,n),{handle:d=>n.handle(d),finish:async()=>{try{await n.finish()}finally{k.delete(e.keyString)}}}},async notify(n){await N(a.client,{channel:e.channelId,threadTs:e.threadTs},n)},async destination(){const n=z({channel:e.channelId,threadTs:e.threadTs});return{interface:"slack",handle:n,label:await _(s.client,n)}}}}return q(s,{identity:i,...c?{botName:c.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:x}),{name:"slack",async start(){await s.start(),i.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"),r.prime(),f.info("listening on Slack via Socket Mode")},async stop(){l.stop(),await s.stop()},allows:e=>l.allows(e),resolveDestination:(e,a)=>U(s.client,e,{...a?.optedInOnly?{allows:o=>l.allows(o)}:{}}),sameConversation:(e,a)=>b(e).channel===b(a).channel,deliver:async e=>{const a=await K(s.client,{...e,text:await I(e.text,r)});return a?{threadKey:$({teamId:i.teamId,channelId:a.channel,threadTs:a.threadTs})}:{}},attachment:e=>{const a=W(e);return a?v(a,t.credentials.botToken):void 0}}}export{X as SLACK_ORIGIN,ge as createSlackInterface,ee as slackIdentity};
1
+ import{etiquetteNote as C,threadParticipants as L}from"../interface/etiquette.js";import{log as p}from"../log.js";import{createSlackApp as O}from"./app.js";import{createAuthorizer as E}from"./authorizer.js";import{botTurnCap as H}from"./bots.js";import{runCommand as F}from"./commands/run.js";import{commandLine as K}from"./commands/syntax.js";import{deliverToSlack as _,describeSlackHandle as U,parseSlackHandle as b,resolveSlackDestination as z,slackHandle as R}from"./destination.js";import{describeSlackFile as W,parseSlackFile as j,slackAttachment as v,slackFilePayload as q}from"./files.js";import{registerHandlers as G}from"./handlers.js";import{landedMidThread as V,threadHistory as Y}from"./history.js";import{linkMentions as I,mentionableName as S,renderMentions as T}from"./mentions.js";import{createPresence as $}from"./presence.js";import{say as N}from"./say.js";import{threadKeyString as J}from"./thread.js";import{userDirectory as Q}from"./users.js";import{threadVerbosity as X}from"./verbosity.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 ae(t,i,s=ee){const l=await te(t.botUserId,i,s),r=S(l)??t.username;if(!r)return;const m=[t.appName,l.realName].filter(h=>!!h?.trim()&&h?.toLowerCase()!==r.toLowerCase());return{address:`@${r}`,...m.length>0?{aka:[...new Set(m)]}:{}}}async function te(t,i,s){const l={userId:t};let r;const m=new Promise(h=>{r=setTimeout(()=>h(l),s)});try{return await Promise.race([i.lookup(t).catch(()=>l),m])}finally{clearTimeout(r)}}async function be(t){const{app:i,identity:s}=await O(t.credentials),l=await E(i.client,t.allow),r=Q(i.client,{self:s.botUserId}),m=H(t.maxBotTurns),h=L(),g=X(t.meta,t.verbosity),x={verbosity:g,model:t.model,configured:t.configuredModel??{}},c=await ae(s,r),y={...Z,...c?{self:c}:{}};c?p.info({address:c.address,aka:c.aka},"the agent's name in Slack"):p.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:a,thread:o,text:w},d)=>{const n=K(w);if(!n)return!1;const f=F(n,{thread:o},x);try{await N(d.client,{channel:e.channelId,threadTs:e.threadTs},f),k.get(a)?.interrupted()}catch(u){p.warn({thread:a,err:u},"couldn't answer a command")}return!0};async function B(e){const a=await r.lookup(e.userId),o=S(a);return{id:e.userId,...o?{displayName:o}:{},...a.realName?{realName:a.realName}:{},...a.email?{email:a.email}:{},...e.isBot?{isBot:!0}:{}}}function P(e,a){const o=W(a);if(!o.unavailable)return t.attachments.remember(e,y.name,{id:o.id,name:o.name,payload:q(a)})}async function A(e,a){return a.files.length===0?[]:Promise.all(a.files.map(async o=>{const w=P(e.id,o),d=await t.files.save(e.dirKey,v(o,t.credentials.botToken));return w?{...d,ref:w}:d}))}function D(e,a){const o=T(e.messages[0]?.text??"",r),w=V(e),d=C({participants:e.participants,mentioned:e.messages.some(n=>n.mentionsAgent)});return{thread:e.thread,isNewThread:e.isNewThread,origin:y,...d?{note:d}:{},...w?{history:()=>Y({client:a.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=>t.attachments.remember(e.thread.id,y.name,n),saw:(n,f)=>h.saw(e.keyString,n,f)})}:{},async messages(){return Promise.all(e.messages.map(async(n,f)=>{const u=await A(e.thread,n);return{principal:await B(n),text:f===0?await o:await T(n.text,r),...u.length>0?{files:u}:{}}}))},presence:()=>{const n=$(e,a,{outgoing:f=>I(f,r),title:()=>o,verbosity:()=>g.of(e.thread.id).level,...d?{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 N(a.client,{channel:e.channelId,threadTs:e.threadTs},n)},async destination(){const n=R({channel:e.channelId,threadTs:e.threadTs});return{interface:"slack",handle:n,label:await U(i.client,n)}}}}return G(i,{identity:s,...c?{botName:c.address}:{},threads:t.threads,events:t.events,run:(e,a)=>t.run(D(e,a)),stop:t.stop,allows:e=>l.allows(e),bots:m,participants:h,command:M}),{name:"slack",async start(){await i.start(),s.scopes.includes("files:read")||p.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")||p.warn("this Slack app has no files:write scope, so it cannot send files back; reinstall it to grant one"),r.prime(),p.info("listening on Slack via Socket Mode")},async stop(){l.stop(),await i.stop()},allows:e=>l.allows(e),resolveDestination:(e,a)=>z(i.client,e,{...a?.optedInOnly?{allows:o=>l.allows(o)}:{}}),sameConversation:(e,a)=>b(e).channel===b(a).channel,deliver:async e=>{const a=await _(i.client,{...e,text:await I(e.text,r)});return a?{threadKey:J({teamId:s.teamId,channelId:a.channel,threadTs:a.threadTs})}:{}},attachment:e=>{const a=j(e);return a?v(a,t.credentials.botToken):void 0}}}export{Z as SLACK_ORIGIN,be as createSlackInterface,ae as slackIdentity};
@@ -1 +1 @@
1
- import{displayNameOf as t}from"../config.js";const n=["app_mentions:read","chat:write","assistant:write","im:history","channels:history","groups:history","files:read","reactions:write","users:read","users:read.email","usergroups:read","channels:read","groups:read","im:write"],i=["app_mention","agent_session_stopped","app_home_opened","app_context_changed","message.im","message.channels","message.groups"];function r(a){const e=t(a),s=a.description?.trim();return{display_information:{name:e.slice(0,35),...s?{description:s.slice(0,140)}:{}},features:{bot_user:{display_name:e.slice(0,80),always_online:!0},app_home:{messages_tab_enabled:!0,messages_tab_read_only_enabled:!1,home_tab_enabled:!1},agent_view:{agent_description:(s||e).slice(0,300)}},oauth_config:{scopes:{bot:[...n]}},settings:{event_subscriptions:{bot_events:[...i]},interactivity:{is_enabled:!1},socket_mode_enabled:!0,org_deploy_enabled:!1,token_rotation_enabled:!1}}}export{i as BOT_EVENTS,n as BOT_SCOPES,r as buildManifest};
1
+ import{displayNameOf as a}from"../config.js";const i=["app_mentions:read","chat:write","assistant:write","im:history","channels:history","groups:history","files:read","files:write","reactions:write","users:read","users:read.email","usergroups:read","channels:read","groups:read","im:write"],n=["app_mention","agent_session_stopped","app_home_opened","app_context_changed","message.im","message.channels","message.groups"];function r(t){const e=a(t),s=t.description?.trim();return{display_information:{name:e.slice(0,35),...s?{description:s.slice(0,140)}:{}},features:{bot_user:{display_name:e.slice(0,80),always_online:!0},app_home:{messages_tab_enabled:!0,messages_tab_read_only_enabled:!1,home_tab_enabled:!1},agent_view:{agent_description:(s||e).slice(0,300)}},oauth_config:{scopes:{bot:[...i]}},settings:{event_subscriptions:{bot_events:[...n]},interactivity:{is_enabled:!1},socket_mode_enabled:!0,org_deploy_enabled:!1,token_rotation_enabled:!1}}}export{n as BOT_EVENTS,i as BOT_SCOPES,r as buildManifest};
@@ -1,5 +1,5 @@
1
- import{DEFAULT_VERBOSITY as M}from"../config.js";import{saysNoOutput as g}from"../harness/principal.js";import{summaryPhrase as P,toolPhrase as $,turnTally as F}from"../interface/phrases.js";import{showsProgress as L,showsTasks as y}from"../interface/verbosity.js";import{log as p}from"../log.js";import{renameSession as O,setSessionStatus as b}from"./agent-session.js";import{say as R}from"./say.js";import{taskMessage as j}from"./tasks.js";const H=3e4;function K(a,B,r={}){const s=B.client,f={channel:a.channelId,threadTs:a.threadTs},A=Date.now(),D=()=>r.verbosity?.()??M;let n,S,k="",i=!1,c="",x=!1,o,T=!1;async function u(t,e){try{return await e()}catch(l){p.debug({thread:a.keyString,what:t,err:l},"presence call failed");return}}let d=E();function m(t){return d=d.then(()=>b(s,a,t)),d}async function E(){const t=a.messages[0];if(await b(s,a,"processing",t?.userId),!(!r.title||t?.ts!==a.threadTs))try{await O(s,a,await r.title())}catch(e){p.debug({thread:a.keyString,err:e},"could not read the session title")}}async function h(){const t=n;t&&(n=void 0,await u("task.close",()=>t.close()))}async function _(t){await h();const e=await u("say",()=>R(s,f,t));if(!e){i=!1;return}S=e,k=t,i=!0}const I=setInterval(()=>{m("processing")},r.heartbeatMs??H);return I.unref(),{interrupted(){i=!1},async handle(t){const e=D();switch(y(e)||await h(),t.type){case"text":{if(g(t.text)||!t.text.trim()||!L(e))return;await _(r.outgoing?await r.outgoing(t.text):t.text);return}case"tool":if(!y(e))return;n||(n=j(s,f,r.taskIntervalMs),i=!1),n.note($(t));return;case"tool_summary":if(!y(e))return;n?.retitle(P(t.summary));return;case"done":x=!0,c=g(t.text)?"":t.text,T=!!r.mayDecline&&g(t.text);return;case"error":o=t.message;return;default:return}},async finish(){if(clearInterval(I),await h(),T&&!o){p.info({thread:a.keyString},"the agent left this message alone"),await m("active");return}const t=F(Date.now()-A,x||!!o),e=c.trim()&&r.outgoing?await r.outgoing(c):c,l=o?`Something went wrong: ${o}`:e.trim()===k.trim()?"":e.trim(),w=S;!l&&w&&i&&await u("coda",()=>s.chat.update({channel:f.channel,ts:w.ts,markdown_text:`${w.text}
1
+ import{DEFAULT_VERBOSITY as H}from"../config.js";import{saysNoOutput as b}from"../harness/principal.js";import{summaryPhrase as N,toolPhrase as U,turnTally as V}from"../interface/phrases.js";import{showsProgress as Y,showsTasks as B}from"../interface/verbosity.js";import{log as y}from"../log.js";import{fileShelf as q}from"../outgoing.js";import{renameSession as z,setSessionStatus as O}from"./agent-session.js";import{splitForSlack as G}from"./format.js";import{say as j}from"./say.js";import{taskMessage as J}from"./tasks.js";import{attachToSlack as K}from"./upload.js";function Q(r){const l=[];for(const a of r){if(typeof a=="string"){l.push({text:a.trim(),files:[]});continue}const n=l.at(-1);n?n.files.push(a):l.push({text:"",files:[a]})}return l}const W=3e4;function lt(r,l,a={}){const n=l.client,u={channel:r.channelId,threadTs:r.threadTs},L=Date.now(),R=()=>a.verbosity?.()??H;let c,x,$="",i=!1,p="",A=!1,d,D=!1;const w=q();async function g(t,e){try{return await e()}catch(o){y.debug({thread:r.keyString,what:t,err:o},"presence call failed");return}}let S=C();function k(t){return S=S.then(()=>O(n,r,t)),S}async function C(){const t=r.messages[0];if(await O(n,r,"processing",t?.userId),!(!a.title||t?.ts!==r.threadTs))try{await z(n,r,await a.title())}catch(e){y.debug({thread:r.keyString,err:e},"could not read the session title")}}async function T(){const t=c;t&&(c=void 0,await g("task.close",()=>t.close()))}async function _(t){const e=await g("say",()=>j(n,u,t));return e?(x=e,i=!0,!0):(i=!1,!1)}async function E(t,e){const o=t?G(t):[],s=o.pop()??"";let f=!0;for(const h of o)await _(h)||(f=!1);const m=e.map(h=>h.name).join(", ");let I,F=!1;try{I=await K(n,u,e,s)}catch(h){F=!0,y.error({thread:r.keyString,files:m,err:h},"could not upload a file")}return I?(x=I,i=!!s,f):(s&&!await _(s)&&(f=!1),F&&await g("say",()=>j(n,u,`_Could not upload ${m}._`)),i=!1,f)}async function M(t){await T();const e=w.place(t);let o=!0;for(const s of Q(e.parts))(s.files.length?await E(s.text,s.files):await _(s.text))||(o=!1);o&&($=e.text)}const P=setInterval(()=>{k("processing")},a.heartbeatMs??W);return P.unref(),{interrupted(){i=!1},share:t=>w.offer(t),async handle(t){const e=R();switch(B(e)||await T(),t.type){case"text":{if(b(t.text)||!t.text.trim()||!Y(e))return;await M(a.outgoing?await a.outgoing(t.text):t.text);return}case"tool":if(!B(e))return;c||(c=J(n,u,a.taskIntervalMs),i=!1),c.note(U(t));return;case"tool_summary":if(!B(e))return;c?.retitle(N(t.summary));return;case"done":A=!0,p=b(t.text)?"":t.text,D=!!a.mayDecline&&b(t.text);return;case"error":d=t.message;return;default:return}},async finish(){if(clearInterval(P),await T(),D&&!d){y.info({thread:r.keyString},"the agent left this message alone"),await k("active");return}const t=V(Date.now()-L,A||!!d),e=p.trim()&&a.outgoing?await a.outgoing(p):p,o=d?`Something went wrong: ${d}`:w.strip(e)===$.trim()?"":e.trim(),s=x;!o&&s&&i&&await g("coda",()=>n.chat.update({channel:u.channel,ts:s.ts,markdown_text:`${s.text}
2
2
 
3
- _${t}_`}))||await _([l,`_${t}_`].filter(Boolean).join(`
3
+ _${t}_`}))||await M([o,`_${t}_`].filter(Boolean).join(`
4
4
 
5
- `)),await m("active")}}}export{K as createPresence};
5
+ `));const m=w.unplaced();m.length>0&&await E("",m),await k("active")}}}export{lt as createPresence};
@@ -0,0 +1 @@
1
+ import{readFile as d}from"node:fs/promises";import{log as r}from"../log.js";const f=5,u=250;async function h(t,a){for(let e=1;e<=f;e+=1){const s=await t.files.info({file:a}),i={...s.file?.shares?.public,...s.file?.shares?.private},l=Object.values(i).flat()[0]?.ts;if(l)return l;e<f&&await new Promise(o=>setTimeout(o,u))}}async function _(t,a,e,s){const i=await Promise.all(e.map(async n=>({file:await d(n.path),filename:n.name}))),l=await(a.threadTs?t.files.uploadV2({file_uploads:i,channel_id:a.channel,thread_ts:a.threadTs}):t.files.uploadV2({file_uploads:i,channel_id:a.channel}));r.info({channel:a.channel,files:e.map(n=>n.name)},"uploaded files the agent sent");const o=l.files?.[0]?.files?.[0]?.id,c=o?await h(t,o):void 0;if(!c){r.warn({channel:a.channel,files:e.map(n=>n.name)},"slack did not say which message the files landed on");return}if(!s)return{ts:c,text:""};try{await t.chat.update({channel:a.channel,ts:c,markdown_text:s})}catch(n){r.warn({channel:a.channel,err:n},"could not put the words on the message carrying the files");return}return{ts:c,text:s}}export{_ as attachToSlack};
@@ -1 +1,2 @@
1
- import{readFile as w}from"node:fs/promises";import{App as g}from"@microsoft/teams.apps";import{BishopError as u}from"../errors.js";import{log as m}from"../log.js";import{retry as c}from"../retry.js";import{teamsRetryable as d}from"./api.js";import{teamsAllowList as y}from"./authorize.js";import{DOWNLOAD_TIMEOUT_MS as h,teamsFilesOf as b}from"./files.js";import{teamsGraph as T}from"./graph.js";import{createTeamsInterface as I}from"./interface.js";import{teamsLogger as O}from"./logger.js";import{createListener as A}from"./server.js";import{graphAddressOf as E}from"./thread.js";const S=3978;async function L(e){const r=async(o,s)=>{try{return await w(o,"utf8")}catch(i){throw new u(`Couldn't read the TLS ${s} at ${o}: ${i.message}`,"ConfigError")}};return{cert:await r(e.cert,"certificate"),key:await r(e.key,"key")}}function U(e){return{type:"message",text:e}}async function q(e){const r=y(e.config?.allow),o=A({port:e.config?.port??S,...e.config?.host?{host:e.config.host}:{},...e.config?.tls?{tls:await L(e.config.tls)}:{}}),s=new g({clientId:e.credentials.clientId,clientSecret:e.credentials.clientSecret,tenantId:e.credentials.tenantId,httpServerAdapter:o,logger:O()}),i=T(e.credentials),f=I({api:{async send(n,t){const a=await c(()=>s.send(n,t),{what:"teams send",retryable:d});return a?.id?{id:a.id}:void 0},async update(n,t,a){await c(()=>s.api.conversations.updateActivity(n,t,U(a)),{what:"teams update",retryable:d})},async member(n,t){return await c(()=>s.api.conversations.getMemberById(n,t),{what:"teams roster",retryable:d})},async sharedFiles(n){const t=E(n);return t?i.files(t):[]},async fetchFile(n){const t=await fetch(l(n),{signal:AbortSignal.timeout(h)});if(!t.ok)throw new Error(`the download returned ${t.status}`);if(!t.body)throw new Error("the download sent no body");return t.body},async fetchAttachment(n){const t=l(n),a=new URL(s.api.serviceUrl);if(t.host!==a.host)throw m.warn({host:t.host,service:a.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 p=await s.api.http.get(t.toString(),{responseType:"arraybuffer",timeout:h});return x(p.data)},async fetchShared(n){return i.fetchShared(n)}},allow:r,...e.config?.verbosity?{verbosity:e.config.verbosity}:{},listener:o,threads:e.threads,events:e.events,files:e.files,attachments:e.attachments,run:e.run,stop:e.stop});return s.on("message",async({activity:n})=>{await f.handle(R(n))}),await s.initialize(),m.info({endpoint:s.server.messagingEndpoint},"the Teams app is mounted"),f}function l(e){const r=new URL(e);if(r.protocol!=="https:")throw new Error("the download URL is not HTTPS");return r}function x(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 M(e){const r=e.recipient?.id,o=r?e.stripMentionsText?.({accountId:r}):e;return(o?.stripMentionsText?.({tagOnly:!0})??o)?.text??e.text??""}function R(e){const r=k(e.channelData);return{id:e.id,conversationId:e.conversation.id,conversationType:typeof e.conversation.conversationType=="string"?e.conversation.conversationType:"channel",text:M(e),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:b(e.attachments),...r?{channel:r}:{}}}function k(e){const r=e?.team?.aadGroupId,o=e?.channel?.id;if(!(typeof r!="string"||typeof o!="string"))return{teamId:r,channelId:o}}export{q as createTeams,R as inboundOf};
1
+ import{randomUUID as L}from"node:crypto";import{readFile as I}from"node:fs/promises";import{App as $}from"@microsoft/teams.apps";import{BishopError as x}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 S}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 M}from"./server.js";import{graphAddressOf as R}from"./thread.js";const q=3978;async function B(e){const t=async(c,s)=>{try{return await I(c,"utf8")}catch(d){throw new x(`Couldn't read the TLS ${s} at ${c}: ${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:L(),contentType:A,contentUrl:e.webUrl,name:e.name,content:{uniqueId:e.uniqueId,fileType:e.fileType}}}async function D(e,t){const c=t.uploadInfo?.uploadUrl;if(!c)throw new Error("Teams returned no upload URL for the accepted file");const s=T(c),d=new Uint8Array(await I(e.path)),m=Math.max(0,d.byteLength-1),w=await fetch(s,{method:"PUT",headers:{"content-type":"application/octet-stream","content-length":String(d.byteLength),"content-range":`bytes 0-${m}/${d.byteLength}`},body:d});if(!w.ok)throw new Error(`Teams returned ${w.status} while receiving ${e.name}`)}async function se(e){const t=S(e.config?.allow),c=M({port:e.config?.port??q,...e.config?.host?{host:e.config.host}:{},...e.config?.tls?{tls:await B(e.config.tls)}:{}}),s=new $({clientId:e.credentials.clientId,clientSecret:e.credentials.clientSecret,tenantId:e.credentials.tenantId,httpServerAdapter:c,logger:v()}),d=F(e.credentials),m=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(l=>d.uploadChannelFile(o.channel,l)))).map(_),i=[n,...u.map(l=>`<attachment id="${l.id}"></attachment>`)].filter(Boolean).join(`
2
+ `)):a.length>0&&(u=a.map(f=>(m.set(f.ref,f),N(f))));try{const f=await g(()=>s.send(r,p(i,u)),{what:"teams send",retryable:y});return f?.id?{id:f.id}:void 0}catch(f){for(const l of a)m.delete(l.ref);throw f}},async update(r,n,o){await g(()=>s.api.conversations.updateActivity(r,n,p(o)),{what:"teams update",retryable:y})},async member(r,n){return await g(()=>s.api.conversations.getMemberById(r,n),{what:"teams roster",retryable:y})},async sharedFiles(r){const n=R(r);return n?d.files(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(s.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 s.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:c,threads:e.threads,events:e.events,files:e.files,attachments:e.attachments,run:e.run,stop:e.stop});s.on("message",async({activity:r})=>{await b.handle(G(r))}),s.on("file.consent.accept",async({activity:r,send:n})=>(await O(r.value,n),{status:200})),s.on("file.consent.decline",async({activity:r,send:n})=>{const o=E(r.value.context),a=o?m.get(o):void 0;return o&&m.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 O(r,n){const o=E(r.context),a=o?m.get(o):void 0;if(o&&m.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 s.initialize(),h.info({endpoint:s.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,c=t?e.stripMentionsText?.({accountId:t}):e;return(c?.stripMentionsText?.({tagOnly:!0})??c)?.text??e.text??""}function G(e){const t=H(e.channelData);return{id:e.id,conversationId:e.conversation.id,conversationType:typeof e.conversation.conversationType=="string"?e.conversation.conversationType:"channel",text:z(e),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?.team?.aadGroupId,c=e?.channel?.id;if(!(typeof t!="string"||typeof c!="string"))return{teamId:t,channelId:c}}export{se as createTeams,G as inboundOf};
@@ -1,3 +1,3 @@
1
- import{exec as o}from"../exec.js";import{log as i}from"../log.js";const a="az",s="00000003-0000-0000-c000-000000000000",l="01d4889c-1287-42c6-ac1f-5d1e02578ef6",u="Files.Read.All",p="az login --allow-no-subscriptions",f="See https://learn.microsoft.com/cli/azure/install-azure-cli to install it.";async function A(){const e=await o(a,["account","show","--output","json"]);if(e.code===null)return{installed:!1,loggedIn:!1};if(e.code!==0)return{installed:!0,loggedIn:!1};let t;try{const n=JSON.parse(e.stdout);typeof n.tenantId=="string"&&(t=n.tenantId)}catch{}return{installed:!0,loggedIn:!0,...t?{tenantId:t}:{}}}async function m(e){const t=await r(["ad","app","permission","add","--id",e,"--api",s,"--api-permissions",`${l}=Role`]);if(t)return{applied:!1,reason:t};const n=await r(["ad","app","permission","admin-consent","--id",e]);return n?{applied:!1,reason:n}:{applied:!0}}async function r(e){const t=await o(a,[...e,"--only-show-errors"]);return t.code===0?void 0:(i.debug({args:e,code:t.code,output:t.output},"the azure cli failed"),t.code===null?"the Azure CLI could not be run":t.output.trim().split(`
1
+ import{exec as o}from"../exec.js";import{log as i}from"../log.js";const r="az",s="00000003-0000-0000-c000-000000000000",u="01d4889c-1287-42c6-ac1f-5d1e02578ef6",l="75359482-378d-4052-8f01-80520e7db3cd",p="Files.Read.All",f="Files.ReadWrite.All",A="az login --allow-no-subscriptions",L="See https://learn.microsoft.com/cli/azure/install-azure-cli to install it.";async function _(){const e=await o(r,["account","show","--output","json"]);if(e.code===null)return{installed:!1,loggedIn:!1};if(e.code!==0)return{installed:!0,loggedIn:!1};let t;try{const n=JSON.parse(e.stdout);typeof n.tenantId=="string"&&(t=n.tenantId)}catch{}return{installed:!0,loggedIn:!0,...t?{tenantId:t}:{}}}async function I(e){const t=await a(["ad","app","permission","add","--id",e,"--api",s,"--api-permissions",`${l}=Role`]);if(t)return{applied:!1,reason:t};const n=await a(["ad","app","permission","admin-consent","--id",e]);return n?{applied:!1,reason:n}:{applied:!0}}async function a(e){const t=await o(r,[...e,"--only-show-errors"]);return t.code===0?void 0:(i.debug({args:e,code:t.code,output:t.output},"the azure cli failed"),t.code===null?"the Azure CLI could not be run":t.output.trim().split(`
2
2
  `).slice(-3).join(`
3
- `).trim()||`\`az ${e.join(" ")}\` exited ${t.code}`)}export{p as AZ_LOGIN_COMMAND,u as FILES_READ_ALL,l as FILES_READ_ALL_ROLE,s as GRAPH_APP_ID,f as INSTALL_AZ_CLI,A as azAccount,m as grantGraphFiles};
3
+ `).trim()||`\`az ${e.join(" ")}\` exited ${t.code}`)}export{A as AZ_LOGIN_COMMAND,p as FILES_READ_ALL,u as FILES_READ_ALL_ROLE,f as FILES_READ_WRITE_ALL,l as FILES_READ_WRITE_ALL_ROLE,s as GRAPH_APP_ID,L as INSTALL_AZ_CLI,_ as azAccount,I as grantGraphFiles};
@@ -1 +1 @@
1
- import{FILE_DOWNLOAD_INFO as x,GRAPH_REFERENCE as F}from"./files.js";function b(){const n=[],p=[],u=[],l=[],h=[],f=new Map,m=new Map,y=new Map;let r,a,c,w=!1,g=1;const i=(e,t)=>{l.push({url:e,credential:t});const o=y.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,addMember(e,t){m.set(`${e}:${t.id}`,t)},addBytes(e,t){y.set(e,t)},addShared(e,t){f.set(e,t)},failNextSend(e=new Error("Teams is down")){r=e},failNextUpdate(e=new Error("Teams is down")){a=e},sendAnonymously(){w=!0},failMember(e=new Error("no roster access")){c=e},async send(e,t){if(r){const s=r;throw r=void 0,s}const o=`activity-${g++}`;return n.push({conversationId:e,text:t,id:o}),w?void 0:{id:o}},async update(e,t,o){if(a){const d=a;throw a=void 0,d}p.push({conversationId:e,activityId:t,text:o});const s=n.find(d=>d.id===t);if(!s)throw new Error(`no activity ${t} to update`);s.text=o},async member(e,t){if(u.push(e),c)throw c;return m.get(`${e}:${t}`)},async sharedFiles(e){return e.conversationType==="personal"?[]:(h.push(e.id),f.get(e.id)??[])},async fetchFile(e){return i(e,"none")},async fetchAttachment(e){return i(e,"bot")},async fetchShared(e){return i(e,"graph")}}}function T(n={}){return{contentType:x,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 D(n={}){return{contentType:F,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 $(n={}){return{contentType:"image/png",contentUrl:"https://smba.trafficmanager.net/amer/v3/attachments/img-1/views/original",...n}}function A(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}}}export{T as aFile,A as aMessage,D as aSharedFile,$ as anImage,b as fakeTeams};
1
+ import{FILE_DOWNLOAD_INFO as x,GRAPH_REFERENCE as F}from"./files.js";function b(){const n=[],l=[],p=[],u=[],h=[],f=new Map,m=new Map,y=new Map;let s,i,c,w=!1,g=1;const d=(e,t)=>{u.push({url:e,credential:t});const o=y.get(e);if(o===void 0)throw new Error(`nothing hosted at ${e}`);return new TextEncoder().encode(o)};return{sent:n,edits:l,rosterCalls:p,fetched:u,sharedCalls:h,addMember(e,t){m.set(`${e}:${t.id}`,t)},addBytes(e,t){y.set(e,t)},addShared(e,t){f.set(e,t)},failNextSend(e=new Error("Teams is down")){s=e},failNextUpdate(e=new Error("Teams is down")){i=e},sendAnonymously(){w=!0},failMember(e=new Error("no roster access")){c=e},async send(e,t,o){if(s){const a=s;throw s=void 0,a}const r=`activity-${g++}`;return n.push({conversationId:e,text:t,id:r,...o?.files?.length?{files:o.files}:{}}),w?void 0:{id:r}},async update(e,t,o){if(i){const a=i;throw i=void 0,a}l.push({conversationId:e,activityId:t,text:o});const r=n.find(a=>a.id===t);if(!r)throw new Error(`no activity ${t} to update`);r.text=o},async member(e,t){if(p.push(e),c)throw c;return m.get(`${e}:${t}`)},async sharedFiles(e){return e.conversationType==="personal"?[]:(h.push(e.id),f.get(e.id)??[])},async fetchFile(e){return d(e,"none")},async fetchAttachment(e){return d(e,"bot")},async fetchShared(e){return d(e,"graph")}}}function T(n={}){return{contentType:x,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 D(n={}){return{contentType:F,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 $(n={}){return{contentType:"image/png",contentUrl:"https://smba.trafficmanager.net/amer/v3/attachments/img-1/views/original",...n}}function A(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}}}export{T as aFile,A as aMessage,D as aSharedFile,$ as anImage,b as fakeTeams};
@@ -1 +1 @@
1
- import{log as l}from"../log.js";import{retry as E}from"../retry.js";import{DOWNLOAD_TIMEOUT_MS as m,GRAPH_REFERENCE as w}from"./files.js";const p="https://graph.microsoft.com/v1.0",b="https://login.microsoftonline.com",T=1e4,_=2,A=1e3,I=6e4;class u extends Error{status;retryAfterMs;constructor(n,s,f){super(n),this.status=s,this.retryAfterMs=f,this.name="GraphError"}}function k(e){const{status:n,retryAfterMs:s}=e??{};return typeof n!="number"?!1:n===0?!0:n===429?s===void 0?!0:{retryAfterMs:s}:n>=500}function y(e){const n=e.headers.get("retry-after");if(n===null)return;const s=Number(n);return Number.isFinite(s)&&s>=0?s*1e3:void 0}function g(e){return`u!${Buffer.from(e,"utf8").toString("base64url")}`}function G(e){if(e.kind==="chat")return`/chats/${e.chatId}/messages/${e.messageId}`;const n=`/teams/${e.teamId}/channels/${e.channelId}/messages`;return e.replyToId&&e.replyToId!==e.messageId?`${n}/${e.replyToId}/replies/${e.messageId}`:`${n}/${e.messageId}`}function R(e){let n;async function s(i){if(n&&n.expiresAt>Date.now())return n.token;const o=await fetch(`${b}/${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:i}).catch(r=>{throw new u(`could not reach Entra: ${r.message}`,0)});if(!o.ok)throw new u(`Entra refused the app credentials with ${o.status}`,o.status,y(o));const t=await o.json();if(!t.access_token)throw new u("Entra returned no access token",0);const a=(t.expires_in??3600)*1e3;return n={token:t.access_token,expiresAt:Date.now()+a-I},n.token}async function f(i,o,t){return E(async()=>{const a=await fetch(`${p}${i}`,{headers:{authorization:`Bearer ${await s(t)}`},signal:t}).catch(r=>{throw new u(`could not reach Graph: ${r.message}`,0)});if(!a.ok)throw new u(`Graph answered ${o} with ${a.status}`,a.status,y(a));return await a.json()},{what:`graph ${o}`,retryable:k,attempts:_,maxDelayMs:A})}async function $(i,o){try{return await f(`/shares/${g(i)}/driveItem`,"a shared file",o)}catch(t){const a=t instanceof u?t.status:0;return l.warn({err:t,status:a},"could not read a file shared in a Teams conversation"),a===401||a===403?{refused:"Bishop is not allowed to read files in this Microsoft 365 tenant"}:void 0}}return{async files(i){const o=AbortSignal.timeout(T);let t;try{t=await f(G(i),"a message",o)}catch(r){return l.warn({err:r,address:i.kind},"could not read a Teams message from Graph"),[]}const a=(t.attachments??[]).filter(r=>r.contentType===w&&r.contentUrl);return Promise.all(a.map(async r=>{const d=r.contentUrl,c=await $(d,o),h={contentType:w,contentUrl:d,...r.id?{uniqueId:r.id}:{},...r.name?{name:r.name}:{}};return c?"refused"in c?{...h,unavailable:c.refused}:{...h,name:r.name??c.name,...c.eTag?{etag:c.eTag}:{},...c.size===void 0?{}:{size:c.size},...c.file?.mimeType?{mimeType:c.file.mimeType}:{}}:h}))},async fetchShared(i){const o=AbortSignal.timeout(m),t=await fetch(`${p}/shares/${g(i)}/driveItem/content`,{headers:{authorization:`Bearer ${await s(o)}`},redirect:"manual",signal:o});if(t.status>=300&&t.status<400){const a=t.headers.get("location");if(!a)throw new Error("Graph redirected the download to nowhere");const r=await fetch(a,{signal:AbortSignal.timeout(m)});if(!r.ok)throw new Error(`the download returned ${r.status}`);if(!r.body)throw new Error("the download sent no body");return r.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}}}export{k as graphRetryable,R as teamsGraph};
1
+ import{readFile as E}from"node:fs/promises";import{log as f}from"../log.js";import{retry as w}from"../retry.js";import{DOWNLOAD_TIMEOUT_MS as y,GRAPH_REFERENCE as $}from"./files.js";const m="https://graph.microsoft.com/v1.0",U="https://login.microsoftonline.com",S=1e4,R=2,G=1e3,M=6e4,D=2,k=1e3,P=6e4;class u extends Error{status;retryAfterMs;constructor(s,c,h){super(s),this.status=c,this.retryAfterMs=h,this.name="GraphError"}}function I(o){const{status:s,retryAfterMs:c}=o??{};return typeof s!="number"?!1:s===0?!0:s===429?c===void 0?!0:{retryAfterMs:c}:s>=500}function p(o){const s=o.headers.get("retry-after");if(s===null)return;const c=Number(s);return Number.isFinite(c)&&c>=0?c*1e3:void 0}function g(o){return`u!${Buffer.from(o,"utf8").toString("base64url")}`}function v(o){if(o.kind==="chat")return`/chats/${o.chatId}/messages/${o.messageId}`;const s=`/teams/${o.teamId}/channels/${o.channelId}/messages`;return o.replyToId&&o.replyToId!==o.messageId?`${s}/${o.replyToId}/replies/${o.messageId}`:`${s}/${o.messageId}`}function B(o){let s;async function c(a){if(s&&s.expiresAt>Date.now())return s.token;const n=await fetch(`${U}/${o.tenantId}/oauth2/v2.0/token`,{method:"POST",headers:{"content-type":"application/x-www-form-urlencoded"},body:new URLSearchParams({client_id:o.clientId,client_secret:o.clientSecret,grant_type:"client_credentials",scope:"https://graph.microsoft.com/.default"}),signal:a}).catch(t=>{throw new u(`could not reach Entra: ${t.message}`,0)});if(!n.ok)throw new u(`Entra refused the app credentials with ${n.status}`,n.status,p(n));const e=await n.json();if(!e.access_token)throw new u("Entra returned no access token",0);const r=(e.expires_in??3600)*1e3;return s={token:e.access_token,expiresAt:Date.now()+r-P},s.token}async function h(a,n,e){return w(async()=>{const r=await fetch(`${m}${a}`,{headers:{authorization:`Bearer ${await c(e)}`},signal:e}).catch(t=>{throw new u(`could not reach Graph: ${t.message}`,0)});if(!r.ok)throw new u(`Graph answered ${n} with ${r.status}`,r.status,p(r));return await r.json()},{what:`graph ${n}`,retryable:I,attempts:R,maxDelayMs:G})}async function T(a,n,e,r){return w(async()=>{const t=await fetch(`${m}${a}`,{method:"PUT",headers:{authorization:`Bearer ${await c(r)}`,"content-type":"application/octet-stream","content-length":String(e.byteLength)},body:e,signal:r}).catch(i=>{throw new u(`could not reach Graph: ${i.message}`,0)});if(!t.ok)throw new u(`Graph answered ${n} with ${t.status}`,t.status,p(t));return await t.json()},{what:`graph ${n}`,retryable:I,attempts:D,maxDelayMs:k})}async function A(a,n,e,r){try{const i=(await h(`/drives/${encodeURIComponent(a)}/items/${encodeURIComponent(n)}?$select=sharepointIds`,"the SharePoint identifiers of an uploaded file",r)).sharepointIds?.listItemUniqueId;if(i)return i}catch(t){f.debug({err:t,itemId:n},"could not read the SharePoint ID of an uploaded file")}return e?.match(/\{([0-9a-fA-F-]{36})\}/)?.[1]}async function b(a,n){const e=await h(`/teams/${encodeURIComponent(a.teamId)}/channels/${encodeURIComponent(a.channelId)}/filesFolder`,"a channel files folder",n);if(!e.id||!e.parentReference?.driveId)throw new u("Graph returned an incomplete channel files folder",0);return{driveId:e.parentReference.driveId,folderId:e.id}}async function _(a,n){try{return await h(`/shares/${g(a)}/driveItem`,"a shared file",n)}catch(e){const r=e instanceof u?e.status:0;return f.warn({err:e,status:r},"could not read a file shared in a Teams conversation"),r===401||r===403?{refused:"Bishop is not allowed to read files in this Microsoft 365 tenant"}:void 0}}return{async files(a){const n=AbortSignal.timeout(S);let e;try{e=await h(v(a),"a message",n)}catch(t){return f.warn({err:t,address:a.kind},"could not read a Teams message from Graph"),[]}const r=(e.attachments??[]).filter(t=>t.contentType===$&&t.contentUrl);return Promise.all(r.map(async t=>{const i=t.contentUrl,d=await _(i,n),l={contentType:$,contentUrl:i,...t.id?{uniqueId:t.id}:{},...t.name?{name:t.name}:{}};return d?"refused"in d?{...l,unavailable:d.refused}:{...l,name:t.name??d.name,...d.eTag?{etag:d.eTag}:{},...d.size===void 0?{}:{size:d.size},...d.file?.mimeType?{mimeType:d.file.mimeType}:{}}:l}))},async fetchShared(a){const n=AbortSignal.timeout(y),e=await fetch(`${m}/shares/${g(a)}/driveItem/content`,{headers:{authorization:`Bearer ${await c(n)}`},redirect:"manual",signal:n});if(e.status>=300&&e.status<400){const r=e.headers.get("location");if(!r)throw new Error("Graph redirected the download to nowhere");const t=await fetch(r,{signal:AbortSignal.timeout(y)});if(!t.ok)throw new Error(`the download returned ${t.status}`);if(!t.body)throw new Error("the download sent no body");return t.body}if(!e.ok)throw new Error(`Graph answered the download with ${e.status}`);if(!e.body)throw new Error("the download sent no body");return e.body},async uploadChannelFile(a,n){const e=AbortSignal.timeout(M),r=new Uint8Array(await E(n.path)),t=await b(a,e),i=await T(`/drives/${encodeURIComponent(t.driveId)}/items/${encodeURIComponent(t.folderId)}:/${encodeURIComponent(n.name)}:/content`,`the file ${n.name} in a Teams channel`,r,e);if(!i.id||!i.webUrl)throw new u(`Graph returned an incomplete upload for ${n.name}`,0);return{id:i.id,uniqueId:await A(t.driveId,i.id,i.eTag,e),name:i.name??n.name,webUrl:i.webUrl,fileType:i.file?.mimeType??n.mimeType}}}}export{I as graphRetryable,B as teamsGraph};
@@ -1,2 +1,4 @@
1
- import{DEFAULT_VERBOSITY as Y}from"../config.js";import{BishopError as z}from"../errors.js";import{etiquetteNote as C,threadParticipants as G}from"../interface/etiquette.js";import{splitMessage as R}from"../interface/format.js";import{showsProgress as H,showsTasks as L}from"../interface/verbosity.js";import{log as c}from"../log.js";import{describeTeamsFile as W,parseTeamsFile as $,teamsAttachment as N,teamsFilePayload as q}from"./files.js";import{teamsPresence as Q}from"./presence.js";import{addressesAgent as m,routeActivity as V}from"./route.js";import{baseConversation as X,conversationOf as O,sameConversation as J,threadKeyOf as Z}from"./thread.js";function tt(n){return[...et(n),...nt].join(`
2
- `)}function et(n){return H(n)?["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.","",L(n)?"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 nt=["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."],ot="You are not on this agent\u2019s allow list, so it can\u2019t answer you.",A="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.",at=A+" If you attached a file, nothing about it reached me, so send it in a direct chat.",it=2e4;function pt(n){const h=new Set,y=new Map,p=G(),g=n.verbosity??Y,S=tt(g);let u,v=!1;function I(){return{name:"teams",guidance:S,...u?{self:u}:{}}}async function F(t,e){const o=e.from.aadObjectId;if(!o)return;const i=y.get(o);if(i)return i;const a=X(t);try{const r=await n.api.member(a,e.from.id);return r&&y.set(o,r),r}catch(r){c.warn({err:r,conversation:a},"could not read the Teams roster");return}}async function d(t,e){let o;for(const i of R(e,it)){const a=await n.api.send(t,i);o=a?{id:a.id,text:i}:void 0}return o}function T(t,e){return[t,e?.userPrincipalName,e?.email].filter(o=>!!o).map(o=>o.trim().toLowerCase())}function _(t,e){const o=q(e);if(!o)return;const i=W(e);return n.attachments.remember(t,"teams",{id:i.id,name:i.name,payload:o})}async function x(t,e){return e.files.length===0?[]:Promise.all(e.files.map(async o=>{const i=_(t.id,o),a=await n.files.save(t.dirKey,N(o,n.api));return i?{...a,ref:i}:a}))}function B(t,e){return{id:t.from.aadObjectId??t.from.id,...t.from.name?{displayName:t.from.name}:{},...e?.name?{realName:e.name}:{},...e?.email?{email:e.email}:{}}}function K(t,e,o,i){const a=O(e),r=C({participants:p.count(t.threadKey),mentioned:m(e)});return{thread:t,isNewThread:i,origin:I(),...r?{note:r}:{},async messages(){const s=await x(t,e);return[{principal:o,text:e.text,...s.length>0?{files:s}:{}}]},presence:()=>Q({send:s=>d(a,s),post:s=>n.api.send(a,s),update:(s,w)=>n.api.update(a,s,w),threadKey:t.threadKey,verbosity:()=>g,...r?{mayDecline:!0}:{}}),async notify(s){await d(a,s)},async destination(){return{interface:"teams",handle:a,label:rt(e)}}}}async function M(t){if(v)return;if(await n.events.seen(`teams:${t.id}`)){c.debug({activity:t.id},"already answered this Teams activity");return}!u&&t.recipientName&&(u={address:`@${t.recipientName}`},c.info({address:u.address},"the agent is known by this name in Teams"));const e=Z(t),o=await n.threads.has(e),i=V(t,{threadKnown:o});if(i.action==="ignore"){c.debug({activity:t.id,thread:e,reason:i.reason},"leaving a Teams message alone");return}const a=t.from.aadObjectId;if(!a){c.warn({activity:t.id},"refusing a Teams message with no Entra object ID");return}p.saw(e,a,Date.now());const r=O(t),s=n.allow.open?void 0:await F(r,t);if(!n.allow.open&&!T(a,s).some(l=>n.allow.allows(l))){c.warn({activity:t.id,who:a,conversation:r},"refusing an unauthorized Teams message"),m(t)&&await d(r,ot).catch(l=>{c.debug({err:l},"could not tell somebody they are not on the allow list")});return}const w=new Set(t.files.map(l=>l.contentUrl)),k=(await n.api.sharedFiles(t)).filter(l=>!w.has(l.contentUrl)),f=k.length>0?{...t,files:[...t.files,...k]}:t;if(!f.text.trim()&&f.files.length===0){if(c.info({activity:t.id},"a Teams message had nothing to act on"),m(t)){const l=t.conversationType==="personal"?A:at;await d(r,l).catch(U=>{c.debug({err:U},"could not answer a message with nothing in it")})}return}const j=await n.threads.remember(e),D=B(f,s);c.info({thread:e,who:a,newThread:!o},"running a turn from Teams");const E=n.run(K(j,f,D,!o)).catch(l=>{c.error({thread:e,err:l},"a turn from Teams failed")}).finally(()=>{h.delete(E)});h.add(E)}async function b(){for(;h.size>0;)await Promise.allSettled([...h])}async function P(t){return await d(t.handle,t.text),{}}return{name:"teams",handle:M,settle:b,deliver:P,allows(t){return n.allow.open?!0:T(t,y.get(t)).some(e=>n.allow.allows(e))},sameConversation:J,async resolveDestination(){throw new z("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 e=$(t);return e?N(e,n.api):void 0},async start(){await n.listener.start(),c.info({allow:n.allow.open?"anyone in the tenant":"a list"},"listening for Teams activities")},async stop(){v=!0,await n.listener.stop(),h.size>0&&(c.info({turns:h.size},"waiting for Teams turns still running"),await b())}}}function rt(n){return n.conversationType==="personal"?`a chat with ${n.from.name??"someone"}`:n.conversationType==="groupChat"?"a Teams group chat":"a Teams channel thread"}export{pt as createTeamsInterface,tt as teamsGuidance};
1
+ import{DEFAULT_VERBOSITY as U}from"../config.js";import{BishopError as E}from"../errors.js";import{etiquetteNote as Y,threadParticipants as $}from"../interface/etiquette.js";import{splitMessage as z}from"../interface/format.js";import{showsProgress as W,showsTasks as L}from"../interface/verbosity.js";import{log as c}from"../log.js";import{describeTeamsFile as q,parseTeamsFile as Q,teamsAttachment as N,teamsFilePayload as V}from"./files.js";import{teamsPresence as X}from"./presence.js";import{addressesAgent as g,routeActivity as J}from"./route.js";import{baseConversation as Z,conversationOf as A,destinationHandleOf as tt,destinationOfHandle as nt,fileRouteOf as S,sameConversation as et,scopeOf as ot,threadKeyOf as at}from"./thread.js";function it(e){return[...rt(e),...st].join(`
2
+ `)}function rt(e){return W(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.","",L(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 st=["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."],ct="You are not on this agent\u2019s allow list, so it can\u2019t answer you.",B="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.",lt=B+" If you attached a file, nothing about it reached me, so send it in a direct chat.",ht=2e4;function kt(e){const f=new Set,p=new Map,v=$(),T=e.verbosity??U,_=it(T);let y,b=!1;function F(){return{name:"teams",guidance:_,...y?{self:y}:{}}}async function x(t,n){const o=n.from.aadObjectId;if(!o)return;const i=p.get(o);if(i)return i;const a=Z(t);try{const r=await e.api.member(a,n.from.id);return r&&p.set(o,r),r}catch(r){c.warn({err:r,conversation:a},"could not read the Teams roster");return}}async function w(t,n,o=[],i){let a;const r=n?z(n,ht):[""];for(const[u,d]of r.entries()){const h=u===r.length-1&&(o.length>0||i)?{...o.length>0?{files:o}:{},...i?{channel:i}:{}}:void 0,m=await e.api.send(t,d,h);a=m?{id:m.id,text:d}:void 0}return a}function k(t,n){return[t,n?.userPrincipalName,n?.email].filter(o=>!!o).map(o=>o.trim().toLowerCase())}function P(t,n){const o=V(n);if(!o)return;const i=q(n);return e.attachments.remember(t,"teams",{id:i.id,name:i.name,payload:o})}async function j(t,n){return n.files.length===0?[]:Promise.all(n.files.map(async o=>{const i=P(t.id,o),a=await e.files.save(t.dirKey,N(o,e.api));return i?{...a,ref:i}:a}))}function K(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}:{}}}function M(t,n,o,i){const a=A(n),r=a.split(";").find(s=>s.startsWith("messageid="))?.slice(10),u=n.channel?{teamId:n.channel.teamId,channelId:n.channel.channelId,...r?{threadId:r}:{}}:void 0,d=Y({participants:v.count(t.threadKey),mentioned:g(n)});return{thread:t,isNewThread:i,origin:F(),...d?{note:d}:{},async messages(){const s=await j(t,n);return[{principal:o,text:n.text,...s.length>0?{files:s}:{}}]},presence:()=>X({send:(s,h)=>w(a,s,h,u),post:s=>e.api.send(a,s),update:(s,h)=>e.api.update(a,s,h),threadKey:t.threadKey,verbosity:()=>T,...S(ot(n),u)==="none"?{share:!1}:{},...d?{mayDecline:!0}:{}}),async notify(s){await w(a,s)},async destination(){return{interface:"teams",handle:tt(n),label:dt(n)}}}}async function C(t){if(b)return;if(await e.events.seen(`teams:${t.id}`)){c.debug({activity:t.id},"already answered this Teams activity");return}!y&&t.recipientName&&(y={address:`@${t.recipientName}`},c.info({address:y.address},"the agent is known by this name in Teams"));const n=at(t),o=await e.threads.has(n),i=J(t,{threadKnown:o});if(i.action==="ignore"){c.debug({activity:t.id,thread:n,reason:i.reason},"leaving a Teams message alone");return}const a=t.from.aadObjectId;if(!a){c.warn({activity:t.id},"refusing a Teams message with no Entra object ID");return}v.saw(n,a,Date.now());const r=A(t),u=e.allow.open?void 0:await x(r,t);if(!e.allow.open&&!k(a,u).some(l=>e.allow.allows(l))){c.warn({activity:t.id,who:a,conversation:r},"refusing an unauthorized Teams message"),g(t)&&await w(r,ct).catch(l=>{c.debug({err:l},"could not tell somebody they are not on the allow list")});return}const d=new Set(t.files.map(l=>l.contentUrl)),s=(await e.api.sharedFiles(t)).filter(l=>!d.has(l.contentUrl)),h=s.length>0?{...t,files:[...t.files,...s]}:t;if(!h.text.trim()&&h.files.length===0){if(c.info({activity:t.id},"a Teams message had nothing to act on"),g(t)){const l=t.conversationType==="personal"?B:lt;await w(r,l).catch(H=>{c.debug({err:H},"could not answer a message with nothing in it")})}return}const m=await e.threads.remember(n),G=K(h,u);c.info({thread:n,who:a,newThread:!o},"running a turn from Teams");const I=e.run(M(m,h,G,!o)).catch(l=>{c.error({thread:n,err:l},"a turn from Teams failed")}).finally(()=>{f.delete(I)});f.add(I)}async function O(){for(;f.size>0;)await Promise.allSettled([...f])}function D(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 R(t){const n=nt(t.handle),o=t.files??[],i=o.map(a=>a.name).join(", ");if(o.length>0&&S(n.scope,n.channel)==="none")throw new E(D(n.scope));try{await w(n.conversation,t.text,o,n.channel)}catch(a){if(!o.length)throw a;c.warn({handle:t.handle,files:i,err:a},"could not attach files to a Teams report"),await w(n.conversation,`${t.text}
3
+
4
+ _Could not attach ${i}; the report was sent without the file._`,[],n.channel)}return{}}return{name:"teams",handle:C,settle:O,deliver:R,allows(t){return e.allow.open?!0:k(t,p.get(t)).some(n=>e.allow.allows(n))},sameConversation:et,async resolveDestination(){throw new E("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=Q(t);return n?N(n,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(){b=!0,await e.listener.stop(),f.size>0&&(c.info({turns:f.size},"waiting for Teams turns still running"),await O())}}}function dt(e){return e.conversationType==="personal"?`a chat with ${e.from.name??"someone"}`:e.conversationType==="groupChat"?"a Teams group chat":"a Teams channel thread"}export{kt as createTeamsInterface,it as teamsGuidance};
@@ -1,7 +1,7 @@
1
- import{DEFAULT_VERBOSITY as k}from"../config.js";import{saysNoOutput as f}from"../harness/principal.js";import{summaryPhrase as D,toolPhrase as P,turnTally as _}from"../interface/phrases.js";import{showsCost as A,showsProgress as B,showsTasks as d}from"../interface/verbosity.js";import{log as n}from"../log.js";import{teamsTask as I}from"./progress.js";const N="The agent finished without saying anything. Nothing went wrong that it reported, but there is no answer to send.";function F(e){const m=()=>e.verbosity?.()??k,K=Date.now();let a;const y=[];let c=0,i="",o,w="",g=!1,s,T=!1;async function l(){const t=a;t&&(a=void 0,await t.close())}async function p(t){await l();try{const r=await e.send(t);c+=1,i=t,o=r}catch(r){i="",o=void 0,n.debug({thread:e.threadKey,err:r},"could not post to Teams while working")}}return{async handle(t){const r=m();switch(d(r)||await l(),t.type){case"text":{if(f(t.text)||!t.text.trim()||(y.push(t.text),!B(r)))return;await p(t.text);return}case"tool":if(!d(r))return;a||(a=I({send:e.post,update:e.update,threadKey:e.threadKey},e.taskIntervalMs),i="",o=void 0),a.note(P(t));return;case"tool_summary":if(!d(r))return;a?.retitle(D(t.summary));return;case"done":g=!0,w=f(t.text)?"":t.text,T=!!e.mayDecline&&f(t.text);return;case"error":s=t.message;return;default:return}},async finish(){if(await l(),T&&!s){n.info({thread:e.threadKey},"the agent stayed out of a Teams conversation");return}const t=w.trim()||(c===0?y.join(`
1
+ import{DEFAULT_VERBOSITY as E}from"../config.js";import{saysNoOutput as K}from"../harness/principal.js";import{summaryPhrase as N,toolPhrase as S,turnTally as $}from"../interface/phrases.js";import{showsCost as O,showsProgress as j,showsTasks as b}from"../interface/verbosity.js";import{log as o}from"../log.js";import{fileShelf as F}from"../outgoing.js";import{teamsTask as H}from"./progress.js";const C="The agent finished without saying anything. Nothing went wrong that it reported, but there is no answer to send.";function q(e){const k=()=>e.verbosity?.()??E,A=Date.now();let l;const y=[];let h=0,s="",i,w="",D=!1,f,I=!1;const u=F();let c=!1;async function g(){const t=l;t&&(l=void 0,await t.close())}async function P(t,n=[]){await g();try{const r=await e.send(t,n);h+=1,s=t,i=r,c=n.length>0}catch(r){if(n.length>0)try{const a=await e.send(t);h+=1,s=t,i=a,c=!1,o.warn({thread:e.threadKey,files:n.map(d=>d.name),err:r},"could not attach files to a Teams message; sent the text without them");return}catch(a){o.debug({thread:e.threadKey,err:a},"could not post the Teams message without its files")}s="",i=void 0,c=!1,o.debug({thread:e.threadKey,err:r},"could not post to Teams while working")}}function _(t){return t.filter(n=>typeof n!="string")}return{...e.share===!1?{}:{share:t=>u.offer(t)},async handle(t){const n=k();switch(b(n)||await g(),t.type){case"text":{if(K(t.text))return;if(!j(n)){y.push(u.strip(t.text));return}const r=u.place(t.text),a=_(r.parts);if(!r.text.trim()&&a.length===0)return;y.push(r.text),await P(r.text,a);return}case"tool":if(!b(n))return;l||(l=H({send:e.post,update:e.update,threadKey:e.threadKey},e.taskIntervalMs),s="",i=void 0),l.note(S(t));return;case"tool_summary":if(!b(n))return;l?.retitle(N(t.summary));return;case"done":D=!0,w=K(t.text)?"":t.text,I=!!e.mayDecline&&K(t.text);return;case"error":f=t.message;return;default:return}},async finish(){if(await g(),I&&!f){o.info({thread:e.threadKey},"the agent stayed out of a Teams conversation");return}const t=w?u.place(w):{parts:[],text:""},n=t.text.trim()||(h===0?y.join(`
2
2
 
3
- `).trim():""),r=b(t),h=A(m())?`_${_(Date.now()-K,g||!!s)}_`:"";if(!r&&h&&o)try{await e.update(o.id,`${o.text}
3
+ `).trim():""),r=_(t.parts),a=B(n),d=O(k())?`_${$(Date.now()-A,D||!!f)}_`:"";if(!a&&d&&i&&!c)try{await e.update(i.id,`${i.text}
4
4
 
5
- ${h}`);return}catch(u){n.debug({thread:e.threadKey,err:u},"could not close a Teams turn in place")}const x=[r,h].filter(Boolean).join(`
5
+ ${d}`);return}catch(m){o.debug({thread:e.threadKey,err:m},"could not close a Teams turn in place")}const T=[a,d].filter(Boolean).join(`
6
6
 
7
- `);if(x)try{await e.send(x),r&&n.info({thread:e.threadKey,chars:r.length},"answered in Teams")}catch(u){r?n.error({thread:e.threadKey,err:u},"could not post the answer to Teams"):n.debug({thread:e.threadKey,err:u},"could not say what a Teams turn cost")}}};function b(t){return s?`Something went wrong: ${s}`:t?t===i.trim()?"":t:c===0?N:""}}export{F as teamsPresence};
7
+ `),p=u.unplaced();if(!(!T&&r.length===0&&p.length===0)){try{await e.send(T,r),h+=1,s=a,c=r.length>0,a&&o.info({thread:e.threadKey,chars:a.length},"answered in Teams")}catch(m){if(r.length>0)try{await e.send(T),h+=1,s=a,c=!1,o.warn({thread:e.threadKey,files:r.map(x=>x.name),err:m},"could not attach files to the final Teams message; sent the text without them")}catch(x){o.debug({thread:e.threadKey,err:x},"could not post the final Teams message without its files")}a?o.error({thread:e.threadKey,err:m},"could not post the answer to Teams"):o.debug({thread:e.threadKey,err:m},"could not say what a Teams turn cost")}p.length>0&&await P("",p)}}};function B(t){return f?`Something went wrong: ${f}`:t?t===s.trim()?"":t:h===0?C:""}}export{q as teamsPresence};