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