@agentdeploymentco/bishop 0.3.1 → 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,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,4 +1,4 @@
1
- import{DEFAULT_VERBOSITY as U}from"../config.js";import{BishopError as Y}from"../errors.js";import{etiquetteNote as $,threadParticipants as z}from"../interface/etiquette.js";import{splitMessage as G}from"../interface/format.js";import{showsProgress as R,showsTasks as H}from"../interface/verbosity.js";import{log as s}from"../log.js";import{describeTeamsFile as L,parseTeamsFile as W,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(e){return[...nt(e),...et].join(`
2
- `)}function nt(e){return R(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.","",H(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 et=["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(e){const h=new Set,w=new Map,p=z(),g=e.verbosity??U,S=tt(g);let u,v=!1;function I(){return{name:"teams",guidance:S,...u?{self:u}:{}}}async function _(t,n){const o=n.from.aadObjectId;if(!o)return;const i=w.get(o);if(i)return i;const a=X(t);try{const r=await e.api.member(a,n.from.id);return r&&w.set(o,r),r}catch(r){s.warn({err:r,conversation:a},"could not read the Teams roster");return}}async function d(t,n){let o;for(const i of G(n,it)){const a=await e.api.send(t,i);o=a?{id:a.id,text:i}:void 0}return o}function T(t,n){return[t,n?.userPrincipalName,n?.email].filter(o=>!!o).map(o=>o.trim().toLowerCase())}function x(t,n){const o=q(n);if(!o)return;const i=L(n);return e.attachments.remember(t,"teams",{id:i.id,name:i.name,payload:o})}async function B(t,n){return n.files.length===0?[]:Promise.all(n.files.map(async o=>{const i=x(t.id,o),a=await e.files.save(t.dirKey,N(o,e.api));return i?{...a,ref:i}:a}))}function F(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 j(t,n,o,i){const a=O(n),r=$({participants:p.count(t.threadKey),mentioned:m(n)});return{thread:t,isNewThread:i,origin:I(),...r?{note:r}:{},async messages(){const c=await B(t,n);return[{principal:o,text:n.text,...c.length>0?{files:c}:{}}]},presence:()=>Q({send:c=>d(a,c),post:c=>e.api.send(a,c),update:(c,y)=>e.api.update(a,c,y),threadKey:t.threadKey,verbosity:()=>g,...r?{mayDecline:!0}:{}}),async notify(c){await d(a,c)},async destination(){return{interface:"teams",handle:a,label:rt(n)}}}}async function K(t){if(v)return;if(await e.events.seen(`teams:${t.id}`)){s.debug({activity:t.id},"already answered this Teams activity");return}!u&&t.recipientName&&(u={address:`@${t.recipientName}`},s.info({address:u.address},"the agent is known by this name in Teams"));const n=Z(t),o=await e.threads.has(n),i=V(t,{threadKnown:o});if(i.action==="ignore"){s.debug({activity:t.id,thread:n,reason:i.reason},"leaving a Teams message alone");return}const a=t.from.aadObjectId;if(!a){s.warn({activity:t.id},"refusing a Teams message with no Entra object ID");return}p.saw(n,a,Date.now());const r=O(t),c=e.allow.open?void 0:await _(r,t);if(!e.allow.open&&!T(a,c).some(l=>e.allow.allows(l))){s.warn({activity:t.id,who:a,conversation:r},"refusing an unauthorized Teams message"),m(t)&&await d(r,ot).catch(l=>{s.debug({err:l},"could not tell somebody they are not on the allow list")});return}const y=new Set(t.files.map(l=>l.contentUrl)),k=(await e.api.sharedFiles(t)).filter(l=>!y.has(l.contentUrl)),f=k.length>0?{...t,files:[...t.files,...k]}:t;if(!f.text.trim()&&f.files.length===0){if(s.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(C=>{s.debug({err:C},"could not answer a message with nothing in it")})}return}const P=await e.threads.remember(n),D=F(f,c);s.info({thread:n,who:a,newThread:!o},"running a turn from Teams");const E=e.run(j(P,f,D,!o)).catch(l=>{s.error({thread:n,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 M(t){const n=(t.files??[]).map(o=>o.name).join(", ");return n&&s.warn({handle:t.handle,files:n},"a report carried files, which Teams cannot send"),await d(t.handle,n?`${t.text}
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
3
 
4
- _Could not attach ${n}: Bishop cannot send files in Teams._`:t.text),{}}return{name:"teams",handle:K,settle:b,deliver:M,allows(t){return e.allow.open?!0:T(t,w.get(t)).some(n=>e.allow.allows(n))},sameConversation:J,async resolveDestination(){throw new Y("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=W(t);return n?N(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(){v=!0,await e.listener.stop(),h.size>0&&(s.info({turns:h.size},"waiting for Teams turns still running"),await b())}}}function rt(e){return e.conversationType==="personal"?`a chat with ${e.from.name??"someone"}`:e.conversationType==="groupChat"?"a Teams group chat":"a Teams channel thread"}export{pt as createTeamsInterface,tt as teamsGuidance};
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};
@@ -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 j,SetupError as m,withPrompt as G,writeEnv as de}from"../setup.js";import{AZ_LOGIN_COMMAND as z,azAccount as ce,FILES_READ_ALL as x,FILES_READ_ALL_ROLE as le,GRAPH_APP_ID as me,grantGraphFiles as ue,INSTALL_AZ_CLI as we}from"./entra.js";import{addRscPermission as fe,createTeamsApp as ge,describeTeamsApp as Te,getRscPermissions as Ie,getTeamsApp as Ae,INSTALL_TEAMS_CLI as U,INSTALL_TEAMS_CLI_COMMAND as _e,LOGIN_DID_NOT_PERSIST as Ee,RSC_PERMISSIONS as C,setTeamsEndpoint as Se,setTeamsFiles as N,teamsCliVersion as W,teamsLogin as be,teamsLoginWouldUseDeviceCode as $,teamsStatus as v}from"./provision.js";import{readGraphRoles as L,verifyTeamsCredentials as ye}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 $,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(`
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($()?`${U}
4
+ `);function Z(e){return new m($()?`${x}
5
5
 
6
- ${e}`:U)}function R(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}=R(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)}
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)}
7
7
 
8
8
  The app was created, so save these somewhere before this scrolls away:
9
9
 
10
- BISHOP_TEAMS_CLIENT_ID=${a.clientId}
11
- BISHOP_TEAMS_CLIENT_SECRET=${a.clientSecret}
12
- BISHOP_TEAMS_TENANT_ID=${a.tenantId}
10
+ BISHOP_TEAMS_CLIENT_ID=${i.clientId}
11
+ BISHOP_TEAMS_CLIENT_SECRET=${i.clientSecret}
12
+ BISHOP_TEAMS_TENANT_ID=${i.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
- ${i} credentials
15
+ ${a} credentials
16
16
  `+(s.length?` .gitignore added ${s.join(", ")}
17
- `:"")}async function k(e){if(e)try{return await Ae(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
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
19
  its own answer never reaches it.
20
- `;async function Ce(e){const n=[],t=[];for(const a of C){const i=await fe(e,a);i.applied||(n.push(a),t.push(` ${a}: ${i.reason}
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}
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 Ie(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 Ne=`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 $e=`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,12 +31,12 @@ 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 $e(e,n){return`
34
+ `}function ve(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:
38
38
 
39
- `+b(e)}function ve(e){return`
39
+ `+b(e)}function Re(e){return`
40
40
  Bishop could not check whether this app has file support: the Teams CLI is
41
41
  not installed here, or is not logged in. Run this again where it is, or turn
42
42
  it on with:
@@ -49,84 +49,87 @@ This app does not have file support.
49
49
  ${M}Turn it on with:
50
50
 
51
51
  `+b(e)+`
52
- ${Le}`}function P(e,n=" "){const t=e??"<client id>",a=`${n} `;return`${n}az ad app permission add --id ${t} \\
53
- ${a}--api ${me} \\
54
- ${a}--api-permissions ${le}=Role
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
55
55
  ${n}az ad app permission admin-consent --id ${t}
56
- `}const Re=`Without it the agent is still told what was attached and says it could not
57
- read it. A file in a direct chat needs none of this.
56
+ `}const Oe=`Without it the agent cannot send a file to a channel. Files.Read.All still
57
+ lets it read files people attach there. A file in a direct chat needs none
58
+ of this.
58
59
  `;function A(e,n){return`
59
60
  ${n}
60
61
 
61
- ${P(e)}
62
- ${Re}`}const B=`A file attached in a channel is read through Microsoft Graph, which needs one
63
- grant in Entra.`;function Oe(e,n=" "){return`${n}az ad app permission admin-consent --id ${e}
64
- `}const ke=`
65
- The Azure CLI accepted the grant for Files.Read.All, and the app cannot
66
- read a file attached in a channel yet: its own token does not carry the
62
+ ${D(e)}
63
+ ${Oe}`}const P=`A file attached in a channel is read through Microsoft Graph, which needs one
64
+ grant in Entra.`;function ke(e,n=" "){return`${n}az ad app permission admin-consent --id ${e}
65
+ `}const Me=`
66
+ The Azure CLI accepted the grant for Files.ReadWrite.All, and the app cannot
67
+ send a file to a channel yet: its own token does not carry the
67
68
  permission. Entra can take minutes to apply a consent on a registration
68
69
  made seconds earlier. Re-run \`bishop teams setup\` to see whether it took,
69
70
  and if it still reports the grant missing, run the consent again:
70
71
 
71
- `,Me=`
72
- The Azure CLI accepted the grant for Files.Read.All, and Bishop could not
72
+ `,De=`
73
+ The Azure CLI accepted the grant for Files.ReadWrite.All, and Bishop could not
73
74
  reach Entra to check that it took. Re-run \`bishop teams setup\` to find out.
74
75
  `,ne=`
75
- The app can read a file attached in a channel: Files.Read.All is granted.
76
- `,Pe=`Consenting an application permission takes an administrator, which is the
76
+ The app can read and send files in a channel: Files.ReadWrite.All is granted.
77
+ `,Pe=`
78
+ The app can read files attached in a channel, but cannot send them: Files.ReadWrite.All is not granted.
79
+ `,Be=`Consenting an application permission takes an administrator, which is the
77
80
  usual reason. An app registration made moments ago can also be too new for
78
81
  Entra to have it everywhere yet, so this is worth running once by hand
79
- before asking anybody. Re-run \`bishop teams setup\` to see whether it took.`;async function Be(e,n=L){const t=await ce();if(!t.installed)return A(e.clientId,`${B} Bishop makes it with the Azure CLI,
82
+ before asking anybody. Re-run \`bishop teams setup\` to see whether it took.`;async function He(e,n=R){const t=await ce();if(!t.installed)return A(e.clientId,`${P} Bishop makes it with the Azure CLI,
80
83
  which is not installed here. ${we}
81
- Then run:`);if(!t.loggedIn)return A(e.clientId,`${B} Bishop makes it with the Azure CLI,
82
- which is not logged in here. Run \`${z}\`, which is
83
- what a tenant with no Azure subscription needs, and then:`);if(t.tenantId&&t.tenantId.toLowerCase()!==e.tenantId.toLowerCase())return A(e.clientId,`${B} The Azure CLI is signed in to tenant
84
+ Then run:`);if(!t.loggedIn)return A(e.clientId,`${P} Bishop makes it with the Azure CLI,
85
+ which is not logged in here. Run \`${j}\`, which is
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
84
87
  ${t.tenantId}, and this app is in ${e.tenantId}, so
85
- Bishop left it alone. Sign in to the app\u2019s tenant and run:`);const a=await ue(e.clientId);if(!a.applied)return A(e.clientId,`Bishop could not grant Files.Read.All: ${a.reason}
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}
86
89
 
87
- ${Pe}
88
- An administrator makes the grant with:`);const i=await n(e);return i?.includes(x)?ne:i?ke+Oe(e.clientId):Me}function te(e,n){return n?n.includes(x)?ne:A(e,`The app cannot read a file attached in a channel: Files.Read.All is not
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
89
92
  granted on its app registration. An administrator grants it with:`):`
90
93
  Bishop could not check whether the app can read a file attached in a
91
- channel. That needs Files.Read.All on the app registration, which an
94
+ channel. That needs Files.ReadWrite.All on the app registration, which an
92
95
  administrator grants with:
93
96
 
94
- `+P(e)}function ae(e,n){return`
97
+ `+D(e)}function ie(e,n){return`
95
98
  The app is missing ${n.length===1?"a permission":"permissions"}:
96
99
  `+n.map(t=>` ${t}
97
100
  `).join("")+`
98
101
  ${S}Add them with:
99
102
 
100
103
  `+Q(e,n)+`
101
- ${Ne}`}const De=`
104
+ ${$e}`}const Fe=`
102
105
  ${S}Bishop could not check whether this app has the permissions for that: the
103
106
  Teams CLI is not installed here, or is not logged in. Run this again where it
104
107
  is, or see \`bishop teams instructions\` for what they are.
105
- `;function He(e,n){return n?n.length>0?ae(e,n):"":`
108
+ `;function We(e,n){return n?n.length>0?ie(e,n):"":`
106
109
  ${S}Bishop cannot check this app, so confirm with whoever created it that it has
107
110
  the permissions \`bishop teams instructions\` asks for.
108
- `}function Fe(e,n){return!e||!n?.applied?`
111
+ `}function Ge(e,n){return!e||!n?.applied?`
109
112
  ${M}Bishop cannot check this app, so confirm with whoever created it that file
110
113
  support is on.
111
- `: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}".
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}".
112
115
  `+(e.app.id?` App ID ${e.app.id}
113
116
  `:"")+` Endpoint ${s}
114
117
  Its credentials are in .env and its configuration in .bishop/config.json.
115
- `),a&&!i&&n(`
118
+ `),i&&!a&&n(`
116
119
  Teams has nowhere to deliver to until the app is pointed at Bishop.
117
120
  Run \`bishop teams endpoint <url>\` with the URL that reaches this host.
118
- `),e.app.id){const p=await X(e.app.id);p?p.length>0&&n(ae(e.app.id,p)):n(De)}if(e.app.id){const p=await N(e.app.id,{check:!0});p.applied?p.changed&&n(ee(e.app.id)):n(ve(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(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(`
119
122
  To change where Teams delivers, run \`bishop teams endpoint <url>\`.
120
123
  To set this up again, remove teams.app from .bishop/config.json and the
121
124
  BISHOP_TEAMS_* lines from .env first.
122
- `)}function je(e,n){if(!e)return;const{tenant:t,user:a}=e;t!==!1&&a!==!1||n(`
125
+ `)}function je(e,n){if(!e)return;const{tenant:t,user:i}=e;t!==!1&&i!==!1||n(`
123
126
  Note: custom apps cannot be installed in Teams from here.
124
127
  `+(t===!1?` The tenant-wide setting is off. An administrator turns it on in the Teams
125
128
  Admin Center, under Org-wide app settings, Custom apps.
126
- `:"")+(a===!1?` Your own app setup policy blocks it. An administrator changes it in the
129
+ `:"")+(i===!1?` Your own app setup policy blocks it. An administrator changes it in the
127
130
  Teams Admin Center, under Users, Policies, App setup policy.
128
131
  `:"")+`Setup will still create the app, but somebody will have to install it.
129
- `)}function Ge(e,n){const t=e.teams?.app,a=R(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.
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.
130
133
 
131
134
  App ${t.displayName??t.name}${t.id?` (${t.id})`:""}
132
135
  Missing ${h.join(`
@@ -135,23 +138,23 @@ Note: custom apps cannot be installed in Teams from here.
135
138
  Creating a new app would leave that one orphaned in your tenant. Either:
136
139
  finish this one with \`bishop teams setup --manual\`, giving it a client
137
140
  secret from \`teams app auth secret create ${t.id??"<app id>"}\`, or
138
- 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
139
- here: ${Object.keys(a).length} of 3 are set.
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.
140
143
 
141
144
  Creating a new app would overwrite them, and a client secret cannot be
142
145
  read back once it is lost.
143
146
 
144
147
  Run \`bishop teams setup --manual\` to record the app those credentials
145
- belong to, or remove the BISHOP_TEAMS_* lines from .env to start over.`)}async function Ve(e,n=L){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}Ge(s,process.env);const p=await W();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(!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 be();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(),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.
146
149
  `+(o?`
147
150
  \`teams login\` reported success, but the login did not survive it.
148
- ${Ee}`:"Run `teams login` on its own to see what went wrong."));i(`
151
+ ${Se}`:"Run `teams login` on its own to see what went wrong."));a(`
149
152
  Logged in.
150
- `)}je(h.sideloading,i),await G(a,async o=>{const d=await j(e,t,o),u=e.endpoint?.trim()||await o("Messaging endpoint URL (leave blank to set later)","");i(`
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(`
151
154
  Creating the Teams app for "${d.displayName}"${h.username?` in ${h.username}'s tenant`:""}...
152
- `);const c=await ge({name:d.displayName,...u?{endpoint:u}:{}}),f=d.description?await Te(c.teamsAppId,d.description):{applied:!1,reason:void 0};d.description&&!f.applied&&i(`
153
- The description was not applied: ${f.reason}
154
- `);const w=await Ce(c.teamsAppId);w&&i(w);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,...f.applied&&d.description?{description:d.description}:{}},T=await J(t,s,g,c),_=await Be(c,n);I.info({teamsAppId:c.teamsAppId,endpoint:u||void 0},"teams app created"),i(`
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(`
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(`
155
158
  Done.
156
159
  ${T}`+_+(c.installLink?`
157
160
  Install it in Teams:
@@ -159,20 +162,20 @@ Install it in Teams:
159
162
  `:"")+(u?"":`
160
163
  The app has no messaging endpoint yet, so Teams has nowhere to deliver to.
161
164
  Run \`bishop teams endpoint <url>\` with the URL that reaches this host.
162
- `)+"\nRun `bishop` to start the agent.\n")})}async function Ze(e,n=ye,t=L){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}".
163
- `);return}await ie(h,s,t);return}await G(i,async o=>{const d=R(process.env),u=d.clientId||await o("Client ID",""),c=d.clientSecret||await o("Client secret",""),f=d.tenantId||await o("Tenant ID","");if(!u||!c||!f)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 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
164
167
  tenant ID. Set BISHOP_TEAMS_CLIENT_ID, BISHOP_TEAMS_CLIENT_SECRET and
165
168
  BISHOP_TEAMS_TENANT_ID, or run this in a terminal so they can be typed in.
166
169
 
167
- Run \`bishop teams instructions\` for what to send whoever creates the app.`);const w={clientId:u,clientSecret:c,tenantId:f},l=e.appId?.trim()||p?.id||await o("Teams app ID (optional)","");s(`
170
+ Run \`bishop teams instructions\` for what to send whoever creates the app.`);const f={clientId:u,clientSecret:c,tenantId:w},l=e.appId?.trim()||p?.id||await o("Teams app ID (optional)","");s(`
168
171
  Checking the credentials...
169
- `);const g=await n(w);if(!g.ok)throw new m(`Microsoft rejected these credentials.
172
+ `);const g=await n(f);if(!g.ok)throw new m(`Microsoft rejected these credentials.
170
173
  ${g.detail}
171
174
 
172
175
  This is the same request Bishop makes on every turn, so the agent would
173
176
  not have been able to answer anything.`);g.detail?s(`${g.detail} Saving them anyway.
174
177
  `):s(`Accepted.
175
- `);const T=await k(l||void 0),_=e.name??p?.name??T?.name,D=e.displayName??p?.displayName??T?.name,H=e.description??p?.description,E=await j({...e,..._?{name:_}:{},...D?{displayName:D}:{},...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,w),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}:{}},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(`
176
179
  Done.
177
180
  ${oe}`+(T?.endpoint?`
178
181
  The app is pointed at ${T.endpoint}
@@ -180,11 +183,11 @@ The app is pointed at ${T.endpoint}
180
183
  Bishop cannot tell where this app is pointed, so confirm with whoever
181
184
  created it that its messaging endpoint reaches this host. Bishop serves it
182
185
  at /api/messages.
183
- `)+He(l,re)+Fe(l,pe)+te(w.clientId,await t(w))+"\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 W())throw Z(V);if($()&&!(await v()).loggedIn)throw new m(V);const p=(await k(s))?.endpoint;if(p===e){a(`App ${s} already points at ${e}. Nothing to do.
184
- `);return}const{needsReinstall:h}=await Se(s,e);I.info({teamsAppId:s,before:p,endpoint:e,needsReinstall:h},"teams messaging endpoint updated"),a(`App ${s} now points at ${e}
185
- `),h&&a(`
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
189
  The app's manifest changed, which usually means its valid domains did.
187
190
  Reinstall it in Teams before expecting it to receive anything.
188
- `)}function Je(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:","",` ${_e}`," 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 a file attached 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:","",` ${z}`,...P("<client id>"," ").trimEnd().split(`
189
- `),""," That is Files.Read.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 costs only channel files: the agent is still told one was"," attached, and says it could not read it.","","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(`
190
- `)}export{Je as administratorInstructions,Be as applyChannelFileGrant,q as existingTeamsSetup,qe as repointTeamsEndpoint,Ve as setupTeams,Ze as setupTeamsManually};
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(`
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
+ `)}export{Xe as administratorInstructions,He as applyChannelFileGrant,q as existingTeamsSetup,Qe as repointTeamsEndpoint,qe as setupTeams,Je as setupTeamsManually};
@@ -1 +1 @@
1
- const i=new Set(["personal","groupChat"]),c=/^[1-9]\d*$/;function a(n){const t=n.conversationId;return i.has(n.conversationType)||t.includes(";messageid=")?t:c.test(n.id)?`${t};messageid=${n.id}`:t}function f(n){return`teams:${a(n)}`}function s(n){return n.split(";")[0]??n}function h(n,t){return o(n)===o(t)}function o(n){const[t="",...e]=n.split(";"),r=e.find(d=>d.startsWith("messageid="));return r?`${t};${r}`:t}function l(n){if(n.conversationType==="personal")return;if(i.has(n.conversationType))return{kind:"chat",chatId:s(n.conversationId),messageId:n.id};if(!n.channel)return;const t=u(n);return{kind:"channel",teamId:n.channel.teamId,channelId:n.channel.channelId,messageId:n.id,...t?{replyToId:t}:{}}}function u(n){const t=n.conversationId.split(";").find(e=>e.startsWith("messageid="))?.slice(10);return t&&t!==n.id?t:void 0}export{s as baseConversation,a as conversationOf,l as graphAddressOf,h as sameConversation,f as threadKeyOf};
1
+ const s=new Set(["personal","groupChat"]),p=/^[1-9]\d*$/;function u(n){const e=n.conversationId;return s.has(n.conversationType)||e.includes(";messageid=")?e:p.test(n.id)?`${e};messageid=${n.id}`:e}function f(n){return n.conversationType==="personal"?"personal":n.conversationType==="groupChat"?"groupChat":"channel"}function $(n,e){return n==="personal"?"consent":n==="channel"&&e?"upload":"none"}function v(n){const e=u(n),t=f(n);return t!=="channel"?`${e};scope=${t}`:n.channel?`${e};scope=channel;teamid=${encodeURIComponent(n.channel.teamId)};channelid=${encodeURIComponent(n.channel.channelId)}`:`${e};scope=channel`}function C(n){const[e="",...t]=n.split(";"),o=new Map(t.map(i=>{const a=i.indexOf("=");return a<0?[i,""]:[i.slice(0,a),i.slice(a+1)]})),c=o.get("messageid"),d=c?`${e};messageid=${c}`:e,r=o.get("scope");if(r==="personal")return{conversation:d,scope:r};if(r==="groupChat")return{conversation:d,scope:r};if(r==="channel"){const i=o.get("teamid"),a=o.get("channelid");return!i||!a?{conversation:d,scope:r}:{conversation:d,scope:r,channel:{teamId:decodeURIComponent(i),channelId:decodeURIComponent(a),...c?{threadId:c}:{}}}}return{conversation:d,scope:"unknown"}}function x(n){return`teams:${u(n)}`}function h(n){return n.split(";")[0]??n}function O(n,e){return l(n)===l(e)}function l(n){const[e="",...t]=n.split(";"),o=t.find(c=>c.startsWith("messageid="));return o?`${e};${o}`:e}function m(n){if(n.conversationType==="personal")return;if(s.has(n.conversationType))return{kind:"chat",chatId:h(n.conversationId),messageId:n.id};if(!n.channel)return;const e=I(n);return{kind:"channel",teamId:n.channel.teamId,channelId:n.channel.channelId,messageId:n.id,...e?{replyToId:e}:{}}}function I(n){const e=n.conversationId.split(";").find(t=>t.startsWith("messageid="))?.slice(10);return e&&e!==n.id?e:void 0}export{h as baseConversation,u as conversationOf,v as destinationHandleOf,C as destinationOfHandle,$ as fileRouteOf,m as graphAddressOf,O as sameConversation,f as scopeOf,x as threadKeyOf};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agentdeploymentco/bishop",
3
- "version": "0.3.1",
3
+ "version": "0.3.2",
4
4
  "description": "An agent gateway. Connects an agent to Slack and email.",
5
5
  "license": "UNLICENSED",
6
6
  "type": "module",
@@ -36,10 +36,10 @@
36
36
  "check": "npm run lint && npm run typecheck && npm test"
37
37
  },
38
38
  "dependencies": {
39
- "@anthropic-ai/claude-agent-sdk": "^0.3",
39
+ "@anthropic-ai/claude-agent-sdk": "0.3.280",
40
40
  "@microsoft/teams.apps": "^2.0.16",
41
41
  "@modelcontextprotocol/sdk": "^1.30.0",
42
- "@openai/codex-sdk": "^0.149.1",
42
+ "@openai/codex-sdk": "0.155.1",
43
43
  "@slack/bolt": "^5.0.0",
44
44
  "@slack/web-api": "^8",
45
45
  "commander": "^13",