@agentdeploymentco/bishop 0.3.5 → 0.5.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.
@@ -1,4 +1,4 @@
1
- import{runCommand as Q}from"../commands/run.js";import{BishopError as C}from"../errors.js";import{etiquetteNote as V,threadParticipants as X}from"../interface/etiquette.js";import{splitMessage as J}from"../interface/format.js";import{showsProgress as Z,showsTasks as tt,threadVerbosity as nt}from"../interface/verbosity.js";import{log as c}from"../log.js";import{teamsCommandLine as et}from"./command.js";import{describeTeamsFile as ot,parseTeamsFile as at,teamsAttachment as K,teamsFilePayload as it}from"./files.js";import{landedMidConversation as rt,teamsHistory as st}from"./history.js";import{teamsPresence as ct}from"./presence.js";import{teamsSighting as F}from"./principal.js";import{addressesAgent as O,routeActivity as lt}from"./route.js";import{baseConversation as ht,conversationOf as N,destinationHandleOf as dt,destinationOfHandle as ut,fileRouteOf as P,sameConversation as ft,scopeOf as mt,threadIdOf as D,threadKeyOf as wt}from"./thread.js";function yt(e){return[...pt(e),...gt].join(`
2
- `)}function pt(e){return Z(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.","",tt(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 gt=["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."],vt="You are not on this agent\u2019s allow list, so it can\u2019t answer you.",R="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.",Tt=R+" If you attached a file, nothing about it reached me, so send it in a direct chat.",bt=2e4,It=3600*1e3;function Dt(e){const y=new Set,p=new Map,S=new Map,g=X(),v=nt(e.meta,"teams",e.verbosity),j={verbosity:v,model:e.model,configured:e.configuredModel??{},stop:e.stop},T=new Map;let m,E=!1;function b(n){return{name:"teams",guidance:yt(v.of(n.id).level),...m?{self:m}:{}}}async function A(n,t,o){if(!t)return;const i=p.get(t);if(i)return i;if((S.get(t)??0)>Date.now())return;const a=ht(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&&S.set(t,Date.now()+It),r}async function w(n,t,o=[],i){let a;const r=t?J(t,bt):[""];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 M(n,t){const o=it(t);if(!o)return;const i=ot(t);return e.attachments.remember(n,"teams",{id:i.id,name:i.name,payload:o})}async function H(n,t){return t.files.length===0?[]:Promise.all(t.files.map(async o=>{const i=M(n.id,o),a=await e.files.save(n.dirKey,K(o,e.api));return i?{...a,ref:i}:a}))}function G(n,t,o){return e.principals.see(b(n).name,F({id:t.from.aadObjectId??t.from.id,...t.from.name?{name:t.from.name}:{},bot:t.from.id.startsWith("28:")},o,e.tenantId))}async function $(n,t){const o=b(n),i=N(t),a=D(i),r=t.recipientId?.replace(/^28:/,"");return st({read:await e.api.history(t),...a?{openedBy:a}:{},...r?{selfAppId:r}:{},...m?{selfName:m.address}:{},...t.recipientId?{self:()=>e.principals.see(o.name,{externalId:t.recipientId,...t.recipientName?{handle:t.recipientName}:{},...t.recipientName?{name:t.recipientName}:{},kind:"bot",affiliation:"unknown"})}:{},sender:async s=>{const l=await A(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,F({id:s.id,...s.name?{name:s.name}:{},bot:!1},l,e.tenantId))}},remember:s=>M(n.id,s),saw:(s,l)=>g.saw(n.threadKey,s,l)})}function z(n,t,o,i){const a=N(t),r=D(a),s=t.channel?{teamId:t.channel.teamId,channelId:t.channel.channelId,...r?{threadId:r}:{}}:void 0,l=V({participants:g.count(n.threadKey),mentioned:O(t)});return{thread:n,isNewThread:i,get origin(){return b(n)},...l?{note:l}:{},...rt(t)?{history:()=>$(n,t)}:{},async messages(){const h=await H(n,t);return[{principal:o,text:t.text,...h.length>0?{files:h}:{}}]},presence:()=>{const h=ct({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:()=>v.of(n.id).level,...P(mt(t),s)==="none"?{share:!1}:{},...l?{mayDecline:!0}:{}});return T.set(n.threadKey,h),{...h,finish:async()=>{try{await h.finish()}finally{T.delete(n.threadKey)}}}},async notify(h){await w(a,h)},async destination(){return{interface:"teams",handle:dt(t),label:kt(t)}}}}async function L(n){if(E)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"));const t=wt(n),o=await e.threads.has(t),i=lt(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}g.saw(t,a,Date.now());const r=N(n),s=await A(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,vt).catch(d=>{c.debug({err:d},"could not tell somebody they are not on the allow list")});return}const l=et(n);if(l){const d=await e.threads.remember(t),k=Q(l,{thread:d},j);try{await w(r,k),T.get(t)?.interrupted()}catch(q){c.warn({thread:t,err:q},"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"?R:Tt;await w(r,d).catch(k=>{c.debug({err:k},"could not answer a message with nothing in it")})}return}const _=await e.threads.remember(t),Y=G(_,u,s);c.info({thread:t,who:a,newThread:!o},"running a turn from Teams");const B=e.run(z(_,u,Y,!o)).catch(d=>{c.error({thread:t,err:d},"a turn from Teams failed")}).finally(()=>{y.delete(B)});y.add(B)}async function x(){for(;y.size>0;)await Promise.allSettled([...y])}function U(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 W(n){const t=ut(n.handle),o=n.files??[],i=o.map(a=>a.name).join(", ");if(o.length>0&&P(t.scope,t.channel)==="none")throw new C(U(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}
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 ${i}; the report was sent without the file._`,[],t.channel)}return{}}return{name:"teams",handle:L,settle:x,deliver:W,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:ft,async resolveDestination(){throw new C("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=at(n);return t?K(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(){E=!0,await e.listener.stop(),y.size>0&&(c.info({turns:y.size},"waiting for Teams turns still running"),await x())}}}function kt(e){return e.conversationType==="personal"?`a chat with ${e.from.name??"someone"}`:e.conversationType==="groupChat"?"a Teams group chat":"a Teams channel thread"}export{Dt as createTeamsInterface,yt as teamsGuidance};
4
+ _Could not attach ${i}; the report was sent without the file._`,[],t.channel)}return{}}return{name:"teams",handle:q,changed:n=>A.changed(n),settle:C,deliver:V,allows(n){return e.allow.open?!0:[...I(n.externalId,p.get(n.externalId)),...n.email?[n.email.trim().toLowerCase()]:[]].some(o=>e.allow.allows(o))},sameConversation:vt,async resolveDestination(){throw new F("Bishop cannot yet name a Teams channel or chat from what you typed. Ask for a report in the conversation you want it in, which names itself.")},attachment:n=>{const t=lt(n);return t?D(t,e.api):void 0},async start(){await e.listener.start(),c.info({allow:e.allow.open?"anyone in the tenant":"a list"},"listening for Teams activities")},async stop(){x=!0,await e.listener.stop(),y.size>0&&(c.info({turns:y.size},"waiting for Teams turns still running"),await C())}}}function At(e){return e.conversationType==="personal"?`a chat with ${e.from.name??"someone"}`:e.conversationType==="groupChat"?"a Teams group chat":"a Teams channel thread"}export{$t as createTeamsInterface,bt as teamsGuidance};
@@ -7,4 +7,4 @@ ${e.error.suggestion}`:"";return`${t}${a}`}function C(n){return n.replace(/("CLI
7
7
  ${p}`:""}`.trim())}return t}async function N(){const n=await c(l,["--version"],{env:f()});if(n.code!==null)return n.output.trim().split(`
8
8
  `)[0]}async function _(){const n=await r(["status"]),e=n.tdp?.sideloading,t=e?.tenant,a=e?.user,s=typeof t=="boolean"||typeof a=="boolean";return{loggedIn:n.loggedIn===!0,...n.username?{username:n.username}:{},...n.tenantId?{tenantId:n.tenantId}:{},...s?{sideloading:{...typeof t=="boolean"?{tenant:t}:{},...typeof a=="boolean"?{user:a}:{}}}:{}}}function A(n=process.env){return!!(n.SSH_CONNECTION||n.SSH_CLIENT)}async function b(){return new Promise(n=>{const e=g(l,["login"],{stdio:"inherit",env:process.env});e.on("error",()=>n(!1)),e.on("exit",t=>n(t===0))})}async function $(n){const e=["app","create","--teams-managed","--name",n.name];n.endpoint&&e.push("--endpoint",n.endpoint);const t=await r(e),a=t.credentials;if(!t.teamsAppId||!a?.CLIENT_ID||!a.TENANT_ID)throw new o("The Teams CLI created an app but reported no credentials for it.");if(!a.CLIENT_SECRET)throw new o(`The Teams CLI created app ${t.teamsAppId} without a client secret.
9
9
  Create one with \`teams app auth secret create ${t.teamsAppId}\`, then run
10
- \`bishop teams setup --manual\`.`);return{teamsAppId:t.teamsAppId,clientId:a.CLIENT_ID,clientSecret:a.CLIENT_SECRET,tenantId:a.TENANT_ID,...t.endpoint?{endpoint:t.endpoint}:{},...t.installLink?{installLink:t.installLink}:{},...t.portalLink?{portalLink:t.portalLink}:{}}}async function v(n){const e=await r(["app","get",n]);return{teamsAppId:e.teamsAppId??n,...e.appId?{clientId:e.appId}:{},...e.name?{name:e.name}:{},...e.endpoint?{endpoint:e.endpoint}:{},...e.installLink?{installLink:e.installLink}:{},...e.portalLink?{portalLink:e.portalLink}:{}}}async function O(n,e){return{needsReinstall:(await r(["app","update",n,"--endpoint",e])).needsReinstall===!0}}const D=["ChannelMessage.Read.Group","ChatMessage.Read.Chat"];async function M(n){const e=await r(["app","rsc","list",n]);if(!Array.isArray(e))throw new o(`\`teams app rsc list ${n}\` did not report a list.`);const t=e.map(a=>a?.name).filter(a=>typeof a=="string");if(e.length>0&&t.length===0)throw new o(`\`teams app rsc list ${n}\` reported no permission names.`);return t}async function P(n,e){try{return{applied:!0,needsReinstall:(await r(["app","rsc","add",n,e,"--type","Application"])).needsReinstall===!0}}catch(t){return i.warn({err:t,teamsAppId:n,permission:e},"could not add an RSC permission"),{applied:!1,needsReinstall:!1,reason:t instanceof Error?t.message:"the update failed"}}}const d="bots[0].supportsFiles";function E(n){const e=n.filter(t=>t.path===d);return e.length===0||e.every(t=>t.oldValue===!0)}async function j(n,e={}){try{const t=await r(["app","manifest","update",n,"--set-json",`${d}=true`,...e.check?["--dry-run"]:["--yes"]]),a=Array.isArray(t.changes)?t.changes:[],s=!E(a);return!e.check&&t.dryRun===!0?(i.warn({teamsAppId:n},"the teams cli previewed the manifest update instead of applying it"),{applied:!1,changed:!1,needsReinstall:!1,reason:"the Teams CLI previewed the change instead of uploading it"}):(t.unpreservedChanges??[]).some(h=>h.path===d)?(i.warn({teamsAppId:n},"the manifest did not keep the file support field"),{applied:!1,changed:!1,needsReinstall:!1,reason:"the manifest did not keep the change"}):{applied:!0,changed:s,needsReinstall:s&&(t.needsReinstall===!0||t.versionBumped===!0)}}catch(t){return i.warn({err:t,teamsAppId:n},"could not turn on Teams file support"),{applied:!1,changed:!1,needsReinstall:!1,reason:t instanceof Error?t.message:"the update failed"}}}const m=80;async function B(n,e){if(e.length>m)return{applied:!1,reason:`a description has to be ${m} characters or fewer, and this one is ${e.length}`};try{return await r(["app","update",n,"--short-description",e]),{applied:!0}}catch(t){return i.warn({err:t,teamsAppId:n},"could not set the app description"),{applied:!1,reason:t instanceof Error?t.message:"the update failed"}}}export{T as INSTALL_TEAMS_CLI,u as INSTALL_TEAMS_CLI_COMMAND,k as LOGIN_DID_NOT_PERSIST,m as MAX_DESCRIPTION,D as RSC_PERMISSIONS,o as TeamsCliError,P as addRscPermission,$ as createTeamsApp,B as describeTeamsApp,M as getRscPermissions,v as getTeamsApp,I as parseCliJson,C as redactSecrets,O as setTeamsEndpoint,j as setTeamsFiles,N as teamsCliVersion,b as teamsLogin,A as teamsLoginWouldUseDeviceCode,_ as teamsStatus};
10
+ \`bishop teams setup --manual\`.`);return{teamsAppId:t.teamsAppId,clientId:a.CLIENT_ID,clientSecret:a.CLIENT_SECRET,tenantId:a.TENANT_ID,...t.endpoint?{endpoint:t.endpoint}:{},...t.installLink?{installLink:t.installLink}:{},...t.portalLink?{portalLink:t.portalLink}:{}}}async function v(n){const e=await r(["app","get",n]);return{teamsAppId:e.teamsAppId??n,...e.appId?{clientId:e.appId}:{},...e.name?{name:e.name}:{},...e.endpoint?{endpoint:e.endpoint}:{},...e.installLink?{installLink:e.installLink}:{},...e.portalLink?{portalLink:e.portalLink}:{}}}async function O(n,e){return{needsReinstall:(await r(["app","update",n,"--endpoint",e])).needsReinstall===!0}}const D=["ChannelMessage.Read.Group","ChatMessage.Read.Chat","ChannelSettings.Read.Group","TeamSettings.Read.Group"];async function M(n){const e=await r(["app","rsc","list",n]);if(!Array.isArray(e))throw new o(`\`teams app rsc list ${n}\` did not report a list.`);const t=e.map(a=>a?.name).filter(a=>typeof a=="string");if(e.length>0&&t.length===0)throw new o(`\`teams app rsc list ${n}\` reported no permission names.`);return t}async function P(n,e){try{return{applied:!0,needsReinstall:(await r(["app","rsc","add",n,e,"--type","Application"])).needsReinstall===!0}}catch(t){return i.warn({err:t,teamsAppId:n,permission:e},"could not add an RSC permission"),{applied:!1,needsReinstall:!1,reason:t instanceof Error?t.message:"the update failed"}}}const d="bots[0].supportsFiles";function E(n){const e=n.filter(t=>t.path===d);return e.length===0||e.every(t=>t.oldValue===!0)}async function j(n,e={}){try{const t=await r(["app","manifest","update",n,"--set-json",`${d}=true`,...e.check?["--dry-run"]:["--yes"]]),a=Array.isArray(t.changes)?t.changes:[],s=!E(a);return!e.check&&t.dryRun===!0?(i.warn({teamsAppId:n},"the teams cli previewed the manifest update instead of applying it"),{applied:!1,changed:!1,needsReinstall:!1,reason:"the Teams CLI previewed the change instead of uploading it"}):(t.unpreservedChanges??[]).some(h=>h.path===d)?(i.warn({teamsAppId:n},"the manifest did not keep the file support field"),{applied:!1,changed:!1,needsReinstall:!1,reason:"the manifest did not keep the change"}):{applied:!0,changed:s,needsReinstall:s&&(t.needsReinstall===!0||t.versionBumped===!0)}}catch(t){return i.warn({err:t,teamsAppId:n},"could not turn on Teams file support"),{applied:!1,changed:!1,needsReinstall:!1,reason:t instanceof Error?t.message:"the update failed"}}}const m=80;async function B(n,e){if(e.length>m)return{applied:!1,reason:`a description has to be ${m} characters or fewer, and this one is ${e.length}`};try{return await r(["app","update",n,"--short-description",e]),{applied:!0}}catch(t){return i.warn({err:t,teamsAppId:n},"could not set the app description"),{applied:!1,reason:t instanceof Error?t.message:"the update failed"}}}export{T as INSTALL_TEAMS_CLI,u as INSTALL_TEAMS_CLI_COMMAND,k as LOGIN_DID_NOT_PERSIST,m as MAX_DESCRIPTION,D as RSC_PERMISSIONS,o as TeamsCliError,P as addRscPermission,$ as createTeamsApp,B as describeTeamsApp,M as getRscPermissions,v as getTeamsApp,I as parseCliJson,C as redactSecrets,O as setTeamsEndpoint,j as setTeamsFiles,N as teamsCliVersion,b as teamsLogin,A as teamsLoginWouldUseDeviceCode,_ as teamsStatus};
@@ -1,29 +1,29 @@
1
- import{loadConfig as y,writeConfig as F}from"../config.js";import{log as I}from"../log.js";import{ensureGitignore as he,resolveAppDetails as W,SetupError as m,withPrompt as G,writeEnv as de}from"../setup.js";import{AZ_LOGIN_COMMAND as j,azAccount as ce,FILES_READ_ALL as le,FILES_READ_WRITE_ALL as z,FILES_READ_WRITE_ALL_ROLE as me,GRAPH_APP_ID as ue,grantGraphFiles as fe,INSTALL_AZ_CLI as we}from"./entra.js";import{addRscPermission as ge,createTeamsApp as Te,describeTeamsApp as Ie,getRscPermissions as Ae,getTeamsApp as _e,INSTALL_TEAMS_CLI as x,INSTALL_TEAMS_CLI_COMMAND as Ee,LOGIN_DID_NOT_PERSIST as Se,RSC_PERMISSIONS as C,setTeamsEndpoint as be,setTeamsFiles as N,teamsCliVersion as U,teamsLogin as ye,teamsLoginWouldUseDeviceCode as $,teamsStatus as v}from"./provision.js";import{readGraphRoles as R,verifyTeamsCredentials as Ce}from"./verify.js";const Y=["The Teams CLI cannot log in from this machine. With no local browser it falls","back to the device code flow, which Microsoft blocks by default in every","tenant."].join(`
1
+ import{loadConfig as y,writeConfig as F}from"../config.js";import{log as I}from"../log.js";import{ensureGitignore as he,resolveAppDetails as W,SetupError as m,withPrompt as G,writeEnv as de}from"../setup.js";import{AZ_LOGIN_COMMAND as j,azAccount as ce,FILES_READ_ALL as le,FILES_READ_WRITE_ALL as z,FILES_READ_WRITE_ALL_ROLE as me,GRAPH_APP_ID as ue,grantGraphFiles as fe,INSTALL_AZ_CLI as we}from"./entra.js";import{addRscPermission as ge,createTeamsApp as Te,describeTeamsApp as Ie,getRscPermissions as Ae,getTeamsApp as _e,INSTALL_TEAMS_CLI as x,INSTALL_TEAMS_CLI_COMMAND as Ee,LOGIN_DID_NOT_PERSIST as Se,RSC_PERMISSIONS as C,setTeamsEndpoint as be,setTeamsFiles as N,teamsCliVersion as U,teamsLogin as ye,teamsLoginWouldUseDeviceCode as v,teamsStatus as $}from"./provision.js";import{readGraphRoles as R,verifyTeamsCredentials as Ce}from"./verify.js";const Y=["The Teams CLI cannot log in from this machine. With no local browser it falls","back to the device code flow, which Microsoft blocks by default in every","tenant."].join(`
2
2
  `),K=[Y,"","So an app cannot be created over SSH. Two ways on from here:",""," Run `bishop teams setup` on a workstation and copy the three BISHOP_TEAMS_*"," lines it writes to .env over to this host.",""," Have somebody else create the app, then run `bishop teams setup --manual`"," here with what they send back. `bishop teams instructions` prints what to"," ask them for."].join(`
3
3
  `),V=[Y,"","So the app has to be re-pointed from a machine that can log in:",""," bishop teams endpoint <url> --app-id <the app id>","","or by editing the bot endpoint in the Teams Developer Portal."].join(`
4
- `);function Z(e){return new m($()?`${x}
4
+ `);function Z(e){return new m(v()?`${x}
5
5
 
6
- ${e}`:x)}function L(e){return{...e.BISHOP_TEAMS_CLIENT_ID?.trim()?{clientId:e.BISHOP_TEAMS_CLIENT_ID.trim()}:{},...e.BISHOP_TEAMS_CLIENT_SECRET?.trim()?{clientSecret:e.BISHOP_TEAMS_CLIENT_SECRET.trim()}:{},...e.BISHOP_TEAMS_TENANT_ID?.trim()?{tenantId:e.BISHOP_TEAMS_TENANT_ID.trim()}:{}}}function O(e){const{clientId:n,clientSecret:t,tenantId:i}=L(e);if(n&&t&&i)return{clientId:n,clientSecret:t,tenantId:i}}function q(e,n){const t=e.teams?.app;if(t&&O(n))return{app:t}}async function J(e,n,t,i){let a;try{a=await de(e,{BISHOP_TEAMS_CLIENT_ID:i.clientId,BISHOP_TEAMS_CLIENT_SECRET:i.clientSecret,BISHOP_TEAMS_TENANT_ID:i.tenantId})}catch(r){throw new m(`The credentials could not be written to .env: ${r instanceof Error?r.message:String(r)}
6
+ ${e}`:x)}function L(e){return{...e.BISHOP_TEAMS_CLIENT_ID?.trim()?{clientId:e.BISHOP_TEAMS_CLIENT_ID.trim()}:{},...e.BISHOP_TEAMS_CLIENT_SECRET?.trim()?{clientSecret:e.BISHOP_TEAMS_CLIENT_SECRET.trim()}:{},...e.BISHOP_TEAMS_TENANT_ID?.trim()?{tenantId:e.BISHOP_TEAMS_TENANT_ID.trim()}:{}}}function O(e){const{clientId:n,clientSecret:t,tenantId:a}=L(e);if(n&&t&&a)return{clientId:n,clientSecret:t,tenantId:a}}function q(e,n){const t=e.teams?.app;if(t&&O(n))return{app:t}}async function J(e,n,t,a){let i;try{i=await de(e,{BISHOP_TEAMS_CLIENT_ID:a.clientId,BISHOP_TEAMS_CLIENT_SECRET:a.clientSecret,BISHOP_TEAMS_TENANT_ID:a.tenantId})}catch(r){throw new m(`The credentials could not be written to .env: ${r instanceof Error?r.message:String(r)}
7
7
 
8
8
  The app was created, so save these somewhere before this scrolls away:
9
9
 
10
- BISHOP_TEAMS_CLIENT_ID=${i.clientId}
11
- BISHOP_TEAMS_CLIENT_SECRET=${i.clientSecret}
12
- BISHOP_TEAMS_TENANT_ID=${i.tenantId}
10
+ BISHOP_TEAMS_CLIENT_ID=${a.clientId}
11
+ BISHOP_TEAMS_CLIENT_SECRET=${a.clientSecret}
12
+ BISHOP_TEAMS_TENANT_ID=${a.tenantId}
13
13
 
14
14
  Then run \`bishop teams setup --manual\` once .env is writable.`)}await F({...n,teams:{...n.teams,app:t}},e);const s=await he(e,[".env","bishop.db*"]);return` .bishop/config.json app configuration
15
- ${a} credentials
15
+ ${i} credentials
16
16
  `+(s.length?` .gitignore added ${s.join(", ")}
17
17
  `:"")}async function k(e){if(e)try{return await _e(e)}catch(n){I.debug({err:n,teamsAppId:e},"could not read the Teams app back");return}}function Q(e,n){return n.map(t=>` teams app rsc add ${e} ${t} --type Application
18
18
  `).join("")}const S=`Without these the agent only hears a message that @mentions it, so a reply to
19
- its own answer never reaches it.
20
- `;async function Ne(e){const n=[],t=[];for(const i of C){const a=await ge(e,i);a.applied||(n.push(i),t.push(` ${i}: ${a.reason}
19
+ its own answer never reaches it, and it can't tell who can read a channel.
20
+ `;async function Ne(e){const n=[],t=[];for(const a of C){const i=await ge(e,a);i.applied||(n.push(a),t.push(` ${a}: ${i.reason}
21
21
  `))}return n.length===0?"":`
22
22
  These permissions could not be added to the app:
23
23
  `+t.join("")+`
24
24
  ${S}Add them with:
25
25
 
26
- `+Q(e,n)}async function X(e){if(e)try{const n=new Set(await Ae(e));return C.filter(t=>!n.has(t))}catch(n){I.debug({err:n,teamsAppId:e},"could not read the app\u2019s RSC permissions");return}}const $e=`Then remove the app from every team and chat it is in, and add it again.
26
+ `+Q(e,n)}async function X(e){if(e)try{const n=new Set(await Ae(e));return C.filter(t=>!n.has(t))}catch(n){I.debug({err:n,teamsAppId:e},"could not read the app\u2019s RSC permissions");return}}const ve=`Then remove the app from every team and chat it is in, and add it again.
27
27
  Teams asks for these permissions only when an app is installed. Updating an
28
28
  app that is already installed does not ask, and it will keep hearing nothing
29
29
  but mentions until it has been removed and installed again.
@@ -31,7 +31,7 @@ but mentions until it has been removed and installed again.
31
31
  can send it a file. An image pasted into a message arrives either way, and a
32
32
  file attached in a channel never does.
33
33
  `;function b(e){return` teams app manifest update ${e} --set-json 'bots[0].supportsFiles=true' --yes
34
- `}function ve(e,n){return`
34
+ `}function $e(e,n){return`
35
35
  File support could not be turned on for this app: ${n??"the update failed"}
36
36
 
37
37
  ${M}Turn it on with:
@@ -49,9 +49,9 @@ This app does not have file support.
49
49
  ${M}Turn it on with:
50
50
 
51
51
  `+b(e)+`
52
- ${Le}`}function D(e,n=" "){const t=e??"<client id>",i=`${n} `;return`${n}az ad app permission add --id ${t} \\
53
- ${i}--api ${ue} \\
54
- ${i}--api-permissions ${me}=Role
52
+ ${Le}`}function D(e,n=" "){const t=e??"<client id>",a=`${n} `;return`${n}az ad app permission add --id ${t} \\
53
+ ${a}--api ${ue} \\
54
+ ${a}--api-permissions ${me}=Role
55
55
  ${n}az ad app permission admin-consent --id ${t}
56
56
  `}const Oe=`Without it the agent cannot send a file to a channel. Files.Read.All still
57
57
  lets it read files people attach there. A file in a direct chat needs none
@@ -85,51 +85,51 @@ Then run:`);if(!t.loggedIn)return A(e.clientId,`${P} Bishop makes it with the Az
85
85
  which is not logged in here. Run \`${j}\`, which is
86
86
  what a tenant with no Azure subscription needs, and then:`);if(t.tenantId&&t.tenantId.toLowerCase()!==e.tenantId.toLowerCase())return A(e.clientId,`${P} The Azure CLI is signed in to tenant
87
87
  ${t.tenantId}, and this app is in ${e.tenantId}, so
88
- Bishop left it alone. Sign in to the app\u2019s tenant and run:`);const i=await fe(e.clientId);if(!i.applied)return A(e.clientId,`Bishop could not grant Files.ReadWrite.All: ${i.reason}
88
+ Bishop left it alone. Sign in to the app\u2019s tenant and run:`);const a=await fe(e.clientId);if(!a.applied)return A(e.clientId,`Bishop could not grant Files.ReadWrite.All: ${a.reason}
89
89
 
90
90
  ${Be}
91
- An administrator makes the grant with:`);const a=await n(e);return a?.includes(z)?ne:a?Me+ke(e.clientId):De}function te(e,n){return n?n.includes(z)?ne:n.includes(le)?Pe:A(e,`The app cannot send a file to a channel: Files.ReadWrite.All is not
91
+ An administrator makes the grant with:`);const i=await n(e);return i?.includes(z)?ne:i?Me+ke(e.clientId):De}function te(e,n){return n?n.includes(z)?ne:n.includes(le)?Pe:A(e,`The app cannot send a file to a channel: Files.ReadWrite.All is not
92
92
  granted on its app registration. An administrator grants it with:`):`
93
93
  Bishop could not check whether the app can read a file attached in a
94
94
  channel. That needs Files.ReadWrite.All on the app registration, which an
95
95
  administrator grants with:
96
96
 
97
- `+D(e)}function ie(e,n){return`
97
+ `+D(e)}function ae(e,n){return`
98
98
  The app is missing ${n.length===1?"a permission":"permissions"}:
99
99
  `+n.map(t=>` ${t}
100
100
  `).join("")+`
101
101
  ${S}Add them with:
102
102
 
103
103
  `+Q(e,n)+`
104
- ${$e}`}const Fe=`
104
+ ${ve}`}const Fe=`
105
105
  ${S}Bishop could not check whether this app has the permissions for that: the
106
106
  Teams CLI is not installed here, or is not logged in. Run this again where it
107
107
  is, or see \`bishop teams instructions\` for what they are.
108
- `;function We(e,n){return n?n.length>0?ie(e,n):"":`
108
+ `;function We(e,n){return n?n.length>0?ae(e,n):"":`
109
109
  ${S}Bishop cannot check this app, so confirm with whoever created it that it has
110
110
  the permissions \`bishop teams instructions\` asks for.
111
111
  `}function Ge(e,n){return!e||!n?.applied?`
112
112
  ${M}Bishop cannot check this app, so confirm with whoever created it that file
113
113
  support is on.
114
- `:n.changed?ee(e):""}async function ae(e,n,t){const i=await k(e.app.id),a=i?.endpoint,s=e.app.id?i?a??"not set":"unknown, the Teams CLI could not read the app":"unknown, no Teams app ID is recorded here";if(n(`This agent is already set up in Teams as "${e.app.displayName??e.app.name}".
114
+ `:n.changed?ee(e):""}async function ie(e,n,t){const a=await k(e.app.id),i=a?.endpoint,s=e.app.id?a?i??"not set":"unknown, the Teams CLI could not read the app":"unknown, no Teams app ID is recorded here";if(n(`This agent is already set up in Teams as "${e.app.displayName??e.app.name}".
115
115
  `+(e.app.id?` App ID ${e.app.id}
116
116
  `:"")+` Endpoint ${s}
117
117
  Its credentials are in .env and its configuration in .bishop/config.json.
118
- `),i&&!a&&n(`
118
+ `),a&&!i&&n(`
119
119
  Teams has nowhere to deliver to until the app is pointed at Bishop.
120
120
  Run \`bishop teams endpoint <url>\` with the URL that reaches this host.
121
- `),e.app.id){const p=await X(e.app.id);p?p.length>0&&n(ie(e.app.id,p)):n(Fe)}if(e.app.id){const p=await N(e.app.id,{check:!0});p.applied?p.changed&&n(ee(e.app.id)):n(Re(e.app.id))}const r=O(process.env);n(te(r?.clientId,r&&await t(r))),n(`
121
+ `),e.app.id){const p=await X(e.app.id);p?p.length>0&&n(ae(e.app.id,p)):n(Fe)}if(e.app.id){const p=await N(e.app.id,{check:!0});p.applied?p.changed&&n(ee(e.app.id)):n(Re(e.app.id))}const r=O(process.env);n(te(r?.clientId,r&&await t(r))),n(`
122
122
  To change where Teams delivers, run \`bishop teams endpoint <url>\`.
123
123
  To set this up again, remove teams.app from .bishop/config.json and the
124
124
  BISHOP_TEAMS_* lines from .env first.
125
- `)}function je(e,n){if(!e)return;const{tenant:t,user:i}=e;t!==!1&&i!==!1||n(`
125
+ `)}function je(e,n){if(!e)return;const{tenant:t,user:a}=e;t!==!1&&a!==!1||n(`
126
126
  Note: custom apps cannot be installed in Teams from here.
127
127
  `+(t===!1?` The tenant-wide setting is off. An administrator turns it on in the Teams
128
128
  Admin Center, under Org-wide app settings, Custom apps.
129
- `:"")+(i===!1?` Your own app setup policy blocks it. An administrator changes it in the
129
+ `:"")+(a===!1?` Your own app setup policy blocks it. An administrator changes it in the
130
130
  Teams Admin Center, under Users, Policies, App setup policy.
131
131
  `:"")+`Setup will still create the app, but somebody will have to install it.
132
- `)}function ze(e,n){const t=e.teams?.app,i=L(n),a=Object.keys(i).length>0;if(t&&!O(n)){const{clientId:s,clientSecret:r,tenantId:p}=i,h=[s?void 0:"BISHOP_TEAMS_CLIENT_ID",r?void 0:"BISHOP_TEAMS_CLIENT_SECRET",p?void 0:"BISHOP_TEAMS_TENANT_ID"].filter(Boolean);throw new m(`An app is already recorded here, but its credentials are not.
132
+ `)}function ze(e,n){const t=e.teams?.app,a=L(n),i=Object.keys(a).length>0;if(t&&!O(n)){const{clientId:s,clientSecret:r,tenantId:p}=a,h=[s?void 0:"BISHOP_TEAMS_CLIENT_ID",r?void 0:"BISHOP_TEAMS_CLIENT_SECRET",p?void 0:"BISHOP_TEAMS_TENANT_ID"].filter(Boolean);throw new m(`An app is already recorded here, but its credentials are not.
133
133
 
134
134
  App ${t.displayName??t.name}${t.id?` (${t.id})`:""}
135
135
  Missing ${h.join(`
@@ -138,23 +138,23 @@ Note: custom apps cannot be installed in Teams from here.
138
138
  Creating a new app would leave that one orphaned in your tenant. Either:
139
139
  finish this one with \`bishop teams setup --manual\`, giving it a client
140
140
  secret from \`teams app auth secret create ${t.id??"<app id>"}\`, or
141
- remove teams.app from .bishop/config.json to start over.`)}if(a&&!t)throw new m(`Teams credentials are already in the environment, but no app is recorded
142
- here: ${Object.keys(i).length} of 3 are set.
141
+ remove teams.app from .bishop/config.json to start over.`)}if(i&&!t)throw new m(`Teams credentials are already in the environment, but no app is recorded
142
+ here: ${Object.keys(a).length} of 3 are set.
143
143
 
144
144
  Creating a new app would overwrite them, and a client secret cannot be
145
145
  read back once it is lost.
146
146
 
147
147
  Run \`bishop teams setup --manual\` to record the app those credentials
148
- belong to, or remove the BISHOP_TEAMS_* lines from .env to start over.`)}async function qe(e,n=R){const t=e.cwd??process.cwd(),i=e.prompt&&process.stdin.isTTY===!0,a=o=>process.stdout.write(o),s=await y(t),r=q(s,process.env);if(r){await ae(r,a,n);return}ze(s,process.env);const p=await U();if(!p)throw Z(K);I.debug({teamsCli:p},"found the teams cli");let h=await v();if(!h.loggedIn){if($())throw new m(K);if(!i)throw new m("The Teams CLI is not logged in to Microsoft 365.\nRun `teams login`, then run this again.");a("The Teams CLI is not logged in to Microsoft 365. Starting `teams login`.\n\n");const o=await ye();if(h=o?await v():h,!h.loggedIn)throw new m(`The Teams CLI is still not logged in, so the app cannot be created.
148
+ belong to, or remove the BISHOP_TEAMS_* lines from .env to start over.`)}async function qe(e,n=R){const t=e.cwd??process.cwd(),a=e.prompt&&process.stdin.isTTY===!0,i=o=>process.stdout.write(o),s=await y(t),r=q(s,process.env);if(r){await ie(r,i,n);return}ze(s,process.env);const p=await U();if(!p)throw Z(K);I.debug({teamsCli:p},"found the teams cli");let h=await $();if(!h.loggedIn){if(v())throw new m(K);if(!a)throw new m("The Teams CLI is not logged in to Microsoft 365.\nRun `teams login`, then run this again.");i("The Teams CLI is not logged in to Microsoft 365. Starting `teams login`.\n\n");const o=await ye();if(h=o?await $():h,!h.loggedIn)throw new m(`The Teams CLI is still not logged in, so the app cannot be created.
149
149
  `+(o?`
150
150
  \`teams login\` reported success, but the login did not survive it.
151
- ${Se}`:"Run `teams login` on its own to see what went wrong."));a(`
151
+ ${Se}`:"Run `teams login` on its own to see what went wrong."));i(`
152
152
  Logged in.
153
- `)}je(h.sideloading,a),await G(i,async o=>{const d=await W(e,t,o),u=e.endpoint?.trim()||await o("Messaging endpoint URL (leave blank to set later)","");a(`
153
+ `)}je(h.sideloading,i),await G(a,async o=>{const d=await W(e,t,o),u=e.endpoint?.trim()||await o("Messaging endpoint URL (leave blank to set later)","");i(`
154
154
  Creating the Teams app for "${d.displayName}"${h.username?` in ${h.username}'s tenant`:""}...
155
- `);const c=await Te({name:d.displayName,...u?{endpoint:u}:{}}),w=d.description?await Ie(c.teamsAppId,d.description):{applied:!1,reason:void 0};d.description&&!w.applied&&a(`
155
+ `);const c=await Te({name:d.displayName,...u?{endpoint:u}:{}}),w=d.description?await Ie(c.teamsAppId,d.description):{applied:!1,reason:void 0};d.description&&!w.applied&&i(`
156
156
  The description was not applied: ${w.reason}
157
- `);const f=await Ne(c.teamsAppId);f&&a(f);const l=await N(c.teamsAppId);l.applied||a(ve(c.teamsAppId,l.reason));const g={id:c.teamsAppId,name:d.name,displayName:d.displayName,...w.applied&&d.description?{description:d.description}:{}},T=await J(t,s,g,c),_=await He(c,n);I.info({teamsAppId:c.teamsAppId,endpoint:u||void 0},"teams app created"),a(`
157
+ `);const f=await Ne(c.teamsAppId);f&&i(f);const l=await N(c.teamsAppId);l.applied||i($e(c.teamsAppId,l.reason));const g={id:c.teamsAppId,name:d.name,displayName:d.displayName,...w.applied&&d.description?{description:d.description}:{}},T=await J(t,s,g,c),_=await He(c,n);I.info({teamsAppId:c.teamsAppId,endpoint:u||void 0},"teams app created"),i(`
158
158
  Done.
159
159
  ${T}`+_+(c.installLink?`
160
160
  Install it in Teams:
@@ -162,8 +162,8 @@ Install it in Teams:
162
162
  `:"")+(u?"":`
163
163
  The app has no messaging endpoint yet, so Teams has nowhere to deliver to.
164
164
  Run \`bishop teams endpoint <url>\` with the URL that reaches this host.
165
- `)+"\nRun `bishop` to start the agent.\n")})}async function Je(e,n=Ce,t=R){const i=e.cwd??process.cwd(),a=e.prompt&&process.stdin.isTTY===!0,s=o=>process.stdout.write(o),r=await y(i),p=r.teams?.app,h=q(r,process.env);if(h){const o=e.appId?.trim();if(o&&!h.app.id){await F({...r,teams:{...r.teams,app:{...h.app,id:o}}},i),s(`Recorded Teams app ID ${o} for "${h.app.displayName??h.app.name}".
166
- `);return}await ae(h,s,t);return}await G(a,async o=>{const d=L(process.env),u=d.clientId||await o("Client ID",""),c=d.clientSecret||await o("Client secret",""),w=d.tenantId||await o("Tenant ID","");if(!u||!c||!w)throw new m(`All three credentials are needed: a client ID, a client secret, and a
165
+ `)+"\nRun `bishop` to start the agent.\n")})}async function Je(e,n=Ce,t=R){const a=e.cwd??process.cwd(),i=e.prompt&&process.stdin.isTTY===!0,s=o=>process.stdout.write(o),r=await y(a),p=r.teams?.app,h=q(r,process.env);if(h){const o=e.appId?.trim();if(o&&!h.app.id){await F({...r,teams:{...r.teams,app:{...h.app,id:o}}},a),s(`Recorded Teams app ID ${o} for "${h.app.displayName??h.app.name}".
166
+ `);return}await ie(h,s,t);return}await G(i,async o=>{const d=L(process.env),u=d.clientId||await o("Client ID",""),c=d.clientSecret||await o("Client secret",""),w=d.tenantId||await o("Tenant ID","");if(!u||!c||!w)throw new m(`All three credentials are needed: a client ID, a client secret, and a
167
167
  tenant ID. Set BISHOP_TEAMS_CLIENT_ID, BISHOP_TEAMS_CLIENT_SECRET and
168
168
  BISHOP_TEAMS_TENANT_ID, or run this in a terminal so they can be typed in.
169
169
 
@@ -175,7 +175,7 @@ Checking the credentials...
175
175
  This is the same request Bishop makes on every turn, so the agent would
176
176
  not have been able to answer anything.`);g.detail?s(`${g.detail} Saving them anyway.
177
177
  `):s(`Accepted.
178
- `);const T=await k(l||void 0),_=e.name??p?.name??T?.name,B=e.displayName??p?.displayName??T?.name,H=e.description??p?.description,E=await W({...e,..._?{name:_}:{},...B?{displayName:B}:{},...H?{description:H}:{}},i,o),se={...l?{id:l}:{},name:E.name,displayName:E.displayName,...E.description?{description:E.description}:{}},oe=await J(i,r,se,f),re=await X(l||void 0),pe=l?await N(l,{check:!0}):void 0;I.info({teamsAppId:l||void 0},"teams app adopted"),s(`
178
+ `);const T=await k(l||void 0),_=e.name??p?.name??T?.name,B=e.displayName??p?.displayName??T?.name,H=e.description??p?.description,E=await W({...e,..._?{name:_}:{},...B?{displayName:B}:{},...H?{description:H}:{}},a,o),se={...l?{id:l}:{},name:E.name,displayName:E.displayName,...E.description?{description:E.description}:{}},oe=await J(a,r,se,f),re=await X(l||void 0),pe=l?await N(l,{check:!0}):void 0;I.info({teamsAppId:l||void 0},"teams app adopted"),s(`
179
179
  Done.
180
180
  ${oe}`+(T?.endpoint?`
181
181
  The app is pointed at ${T.endpoint}
@@ -183,11 +183,11 @@ The app is pointed at ${T.endpoint}
183
183
  Bishop cannot tell where this app is pointed, so confirm with whoever
184
184
  created it that its messaging endpoint reaches this host. Bishop serves it
185
185
  at /api/messages.
186
- `)+We(l,re)+Ge(l,pe)+te(f.clientId,await t(f))+"\nRun `bishop` to start the agent.\n")})}async function Qe(e,n={}){const t=n.cwd??process.cwd(),i=o=>process.stdout.write(o),a=await y(t),s=n.appId?.trim()||a.teams?.app?.id;if(!s)throw new m("No Teams app is recorded here, so there is nothing to point.\nRun `bishop teams setup` first, or pass --app-id.");if(!await U())throw Z(V);if($()&&!(await v()).loggedIn)throw new m(V);const p=(await k(s))?.endpoint;if(p===e){i(`App ${s} already points at ${e}. Nothing to do.
187
- `);return}const{needsReinstall:h}=await be(s,e);I.info({teamsAppId:s,before:p,endpoint:e,needsReinstall:h},"teams messaging endpoint updated"),i(`App ${s} now points at ${e}
188
- `),h&&i(`
186
+ `)+We(l,re)+Ge(l,pe)+te(f.clientId,await t(f))+"\nRun `bishop` to start the agent.\n")})}async function Qe(e,n={}){const t=n.cwd??process.cwd(),a=o=>process.stdout.write(o),i=await y(t),s=n.appId?.trim()||i.teams?.app?.id;if(!s)throw new m("No Teams app is recorded here, so there is nothing to point.\nRun `bishop teams setup` first, or pass --app-id.");if(!await U())throw Z(V);if(v()&&!(await $()).loggedIn)throw new m(V);const p=(await k(s))?.endpoint;if(p===e){a(`App ${s} already points at ${e}. Nothing to do.
187
+ `);return}const{needsReinstall:h}=await be(s,e);I.info({teamsAppId:s,before:p,endpoint:e,needsReinstall:h},"teams messaging endpoint updated"),a(`App ${s} now points at ${e}
188
+ `),h&&a(`
189
189
  The app's manifest changed, which usually means its valid domains did.
190
190
  Reinstall it in Teams before expecting it to receive anything.
191
- `)}function Xe(e){const n=e?.trim()||"https://<the URL that reaches this host>/api/messages";return["Bishop needs a Microsoft Teams app. To create one for it:","","1. Create a Teams app with a bot. A Teams-managed bot is enough and needs no"," Azure subscription:","",` ${Ee}`," teams login"," teams app create --teams-managed --name <app name> --endpoint <url>",""," The install flags are not optional: without them the CLI cannot store a"," login and every command starts logged out.","","2. Point the bot at this messaging endpoint:","",` ${n}`,""," Teams POSTs every activity here, so it has to be reachable over HTTPS.","","3. Give the app these resource-specific consent permissions:","",...C.map(t=>` teams app rsc add <app id> ${t} --type Application`),""," Without them Teams only delivers a message that @mentions the agent, so a"," reply to its own answer never reaches it. Whoever installs the app in a team"," or a chat is who consents, and Teams only asks at install time. An app that"," is already installed has to be removed from every team and chat it is in and"," added again: updating it in place never asks, so it goes on hearing nothing"," but mentions.","","4. Turn on file support, so the agent can be sent a file at all:","",` ${b("<app id>").trim()}`,""," Without it Teams shows no attach button in a chat with the agent, so there"," is nothing for anyone to send. An image pasted into a message arrives either"," way. `--yes` is not optional: the CLI refuses the write without it unless"," somebody is there to answer a prompt.","","5. Let the agent read and send files in a channel. Those are on SharePoint"," rather than on the bot channel, so Bishop reads them through Microsoft"," Graph, which needs one application permission on the app registration:","",` ${j}`,...D("<client id>"," ").trimEnd().split(`
191
+ `)}function Xe(e){const n=e?.trim()||"https://<the URL that reaches this host>/api/messages";return["Bishop needs a Microsoft Teams app. To create one for it:","","1. Create a Teams app with a bot. A Teams-managed bot is enough and needs no"," Azure subscription:","",` ${Ee}`," teams login"," teams app create --teams-managed --name <app name> --endpoint <url>",""," The install flags are not optional: without them the CLI cannot store a"," login and every command starts logged out.","","2. Point the bot at this messaging endpoint:","",` ${n}`,""," Teams POSTs every activity here, so it has to be reachable over HTTPS.","","3. Give the app these resource-specific consent permissions:","",...C.map(t=>` teams app rsc add <app id> ${t} --type Application`),""," Without them Teams only delivers a message that @mentions the agent, so a"," reply to its own answer never reaches it, and the agent cannot tell whether"," a channel is private or shared. Whoever installs the app in a team"," or a chat is who consents, and Teams only asks at install time. An app that"," is already installed has to be removed from every team and chat it is in and"," added again: updating it in place never asks, so it goes on hearing nothing"," but mentions.","","4. Turn on file support, so the agent can be sent a file at all:","",` ${b("<app id>").trim()}`,""," Without it Teams shows no attach button in a chat with the agent, so there"," is nothing for anyone to send. An image pasted into a message arrives either"," way. `--yes` is not optional: the CLI refuses the write without it unless"," somebody is there to answer a prompt.","","5. Let the agent read and send files in a channel. Those are on SharePoint"," rather than on the bot channel, so Bishop reads them through Microsoft"," Graph, which needs one application permission on the app registration:","",` ${j}`,...D("<client id>"," ").trimEnd().split(`
192
192
  `),""," That is Files.ReadWrite.All, and it is tenant-wide, which is why it takes an"," administrator. `--allow-no-subscriptions` is what a tenant with no Azure"," subscription needs, which is the ordinary case for a Teams-managed bot."," Skipping this keeps channel file sending unavailable. Files.Read.All still"," permits the agent to read files people attach there.","","6. Allow the app to be installed. Custom app installation has to be on for the"," tenant (Teams Admin Center, Org-wide app settings, Custom apps) and for"," whoever installs it (Users, Policies, App setup policy).","","7. Send back three values, which are credentials and should travel like any"," other secret:",""," the app registration\u2019s client ID"," a client secret for it"," the tenant ID","","Then run `bishop teams setup --manual` here and give it those three.",""].join(`
193
193
  `)}export{Xe as administratorInstructions,He as applyChannelFileGrant,q as existingTeamsSetup,Qe as repointTeamsEndpoint,qe as setupTeams,Je as setupTeamsManually};
@@ -1,2 +1,2 @@
1
- import{z as i}from"zod";import{FETCH_FILE_TOOL as s,fileTag as f}from"../harness/principal.js";import{SHARE_FILE_DESCRIPTION as l,SHARE_FILE_TOOL as c,ShareRefused as m}from"../outgoing.js";import{defineTool as a,ToolRefusal as r}from"./types.js";function y(t){return[d(t),w()]}function d(t){return a({name:s,description:["Downloads a file somebody shared in this conversation and gives you the path to it.","","Takes the ref from an <attachment> tag. Ask for a file when you need to open it, rather","than for everything in the conversation. Asking for one already downloaded hands back","the same path without fetching it again, so ask rather than reusing a path from earlier","in the conversation. It fails, with the reason, when there is no file to be had."].join(`
2
- `),input:{ref:i.string().trim().min(1).describe("The ref attribute of the file tag naming the file you want")},async run(n,o){const e=await t.fetch(o.thread,n.ref);if(!e)throw new r(`No file with the ref ${n.ref} was shared in this conversation. Refs come from the file tags Bishop writes, and one only works in the conversation it was written in.`);if(!e.path){const h=e.unavailable??"the download produced no file";throw new r(`Bishop could not fetch the file with ref ${n.ref}: ${h}.`+(e.url?` The original is at ${e.url}`:""))}return f({...e,ref:n.ref})}})}function w(){return a({name:c,description:l,input:{path:i.string().trim().min(1).describe("Absolute path to the file to send"),name:i.string().trim().min(1).optional().describe("What the recipient sees it called. Defaults to the filename on disk.")},async run(t,n){const o=n.share;if(!o)throw new r("There is nowhere to put a file in this conversation, so nothing was sent.");try{const e=await o({path:t.path,...t.name?{name:t.name}:{}});return`Registered ${e.name}. Write <attachment ref="${e.ref}" /> in your reply where the file should appear.`}catch(e){throw e instanceof m?new r(e.message):e}}})}export{y as fileTools,w as shareTool};
1
+ import{z as a}from"zod";import{FETCH_FILE_TOOL as h,fileTag as f}from"../harness/principal.js";import{SHARE_FILE_DESCRIPTION as l,SHARE_FILE_TOOL as c,ShareRefused as m}from"../outgoing.js";import{defineTool as s,ToolRefusal as i}from"./types.js";function y(t){return[d(t),w()]}function d(t){return s({name:h,description:["Downloads a file somebody shared in this conversation and gives you the path to it.","","Takes the ref from an <attachment> tag. Ask for a file when you need to open it, rather","than for everything in the conversation. Asking for one already downloaded hands back","the same path without fetching it again, so ask rather than reusing a path from earlier","in the conversation. It fails, with the reason, when there is no file to be had."].join(`
2
+ `),input:{ref:a.string().trim().min(1).describe("The ref attribute of the file tag naming the file you want")},async run(r,n){const e=await t.fetch(n.thread,r.ref);if(!e)throw new i(`No file with the ref ${r.ref} was shared in this conversation. Refs come from the file tags Bishop writes, and one only works in the conversation it was written in.`);if(!e.path){const o=e.unavailable??"the download produced no file";throw new i(`Bishop could not fetch the file with ref ${r.ref}: ${o}.`+(e.url?` The original is at ${e.url}`:""))}return f({...e,ref:r.ref})}})}function w(){return s({name:c,description:l,input:{path:a.string().trim().min(1).describe("Absolute path to the file to send"),name:a.string().trim().min(1).optional().describe("What the recipient sees it called. Defaults to the filename on disk.")},async run(t,r){const n=r.share;if(!n)throw new i("There is nowhere to put a file in this conversation, so nothing was sent.");try{const e=r.stage?await r.stage(t.path):t.path,o=await n({path:e,...t.name?{name:t.name}:{}});return`Registered ${o.name}. Write <attachment ref="${o.ref}" /> in your reply where the file should appear.`}catch(e){throw e instanceof m?new i(e.message):e}}})}export{y as fileTools,w as shareTool};
@@ -1,8 +1,9 @@
1
- import{z as s}from"zod";import{BishopError as f}from"../errors.js";import{NO_OUTPUT as T}from"../harness/principal.js";import{log as y}from"../log.js";import{principalLabel as R}from"../principals.js";import{describeTime as w,nextRun as z,systemTimeZone as g,validateCron as _,validateTimeZone as A}from"../schedule/cron.js";import{defineTool as l,ToolRefusal as d}from"./types.js";const k=20;function m(e){return e.principal.id}function b(e){try{return e()}catch(n){throw n instanceof f?new d(n.message):n}}async function B(e){try{return await e()}catch(n){throw n instanceof f?new d(n.message):n}}function E(e,n,t){const i=[`${e.id} ${e.name}${e.enabled?"":" (disabled)"}`,` runs ${e.cron} (${e.tz})`,` next ${e.enabled?w(e.nextRunAt,n):"never, while it is off"}`,` to ${e.destination.label}`,` prompt ${e.prompt}`,` by ${R(e.createdBy,t)}`];return e.lastError&&i.push(` error ${e.lastError} (${e.consecutiveFailures} in a row)`),i.join(`
2
- `)}async function $(e,n){const t=await n.destination(),i=m(n);return e.schedules.all().filter(o=>o.createdBy===i||e.sameConversation(o.destination,t))}function N(e){const n=e.now??Date.now;return l({name:"schedule_create",description:["Runs a prompt on a cron schedule and posts the answer where you say.","Each firing is a fresh session with no memory of the last one, so the prompt has to","carry everything it needs.","",`The prompt must say when to stay quiet, and say that answering ${T} is how.`,"A schedule that reports every time it runs is one people learn to ignore."].join(`
3
- `),input:{name:s.string().trim().min(1).describe("What a person sees in a list, and an email subject"),cron:s.string().trim().min(1).describe("Five-field cron, or @daily and friends. No more than once every five minutes"),prompt:s.string().trim().min(1).describe("What the agent is asked at each firing, including when to say nothing"),to:s.string().trim().min(1).optional().describe("Where the answer goes: #channel or @person for Slack, or an email address. Leave it out to post back into this conversation."),tz:s.string().trim().min(1).optional().describe("IANA zone the expression is read in, like Europe/London. Defaults to Bishop's")},async run(t,i){const o=m(i);if(!e.authorized(o))throw new d(`Bishop can't run a schedule created by ${i.principal.name}, so making one would write something that never fires. Ask an operator to add it with \`bishop schedules add\`.`);const a=e.schedules.all().filter(v=>v.createdBy===o).length;if(a>=k)throw new d(`You already have ${a} schedules, which is the limit. Remove one with schedule_delete before adding another.`);const u=t.tz,c=u?b(()=>A(u)):g();b(()=>_(t.cron,c));const p=t.to,h=p?await B(()=>e.resolve(p)):await i.destination(),r=e.schedules.add({name:t.name,cron:t.cron,tz:c,prompt:t.prompt,destination:h,createdBy:o,nextRunAt:z(t.cron,c,n())});return y.info({schedule:r.id,name:r.name,cron:r.cron,tz:r.tz,by:r.createdBy,to:h.label},"the agent created a schedule"),`Created ${r.id} "${r.name}", running ${r.cron} (${c}).
4
- Next ${w(r.nextRunAt,c)}, posting to ${h.label}.`}})}function x(e){return l({name:"schedule_list",description:"Lists the schedules you created, and any that post into this conversation. Not schedules other people aimed elsewhere.",input:{},async run(n,t){const i=await $(e,t);if(i.length===0)return"Nothing is scheduled here, and you have not made any.";const o=g();return`${i.map(a=>E(a,o,e.principals)).join(`
1
+ import{z as a}from"zod";import{BishopError as _}from"../errors.js";import{NO_OUTPUT as D}from"../harness/principal.js";import{log as $}from"../log.js";import{principalLabel as W}from"../principals.js";import{describeTime as v,nextRun as k,systemTimeZone as N,validateCron as x,validateTimeZone as B}from"../schedule/cron.js";import{defineTool as y,ToolRefusal as h}from"./types.js";const Z=20;function z(e){return e.principal.id}function w(e){try{return e()}catch(t){throw t instanceof _?new h(t.message):t}}async function E(e){try{return await e()}catch(t){throw t instanceof _?new h(t.message):t}}function F(e,t,n){const o=[`${e.id} ${e.name}${e.enabled?"":" (disabled)"}`,` runs ${e.cron} (${e.tz})`,` next ${e.enabled?v(e.nextRunAt,t):"never, while it is off"}`,` to ${e.destination.label}`,` prompt ${e.prompt}`,` by ${W(e.createdBy,n)}`];return e.lastError&&o.push(` error ${e.lastError} (${e.consecutiveFailures} in a row)`),o.join(`
2
+ `)}async function A(e,t){const n=await t.destination(),o=z(t);return e.schedules.all().filter(i=>i.createdBy===o||e.sameConversation(i.destination,n))}function C(e,t,n){const o=z(t);if(!e.authorized(o))throw new h(`Bishop can't run a schedule created by ${t.principal.name}, so ${n?"changing this one":"making one"} would write something that never fires. Ask an operator to add it with \`bishop schedules add\`.`);if(n?.createdBy===o)return o;const i=e.schedules.all().filter(c=>c.createdBy===o).length;if(i>=Z)throw new h(`You already have ${i} schedules, which is the limit. Remove one with schedule_delete before ${n?"taking on":"adding"} another.`);return o}function P(e){const t=e.now??Date.now;return y({name:"schedule_create",description:["Runs a prompt on a cron schedule and posts the answer where you say.","Each firing is a fresh session with no memory of the last one, so the prompt has to","carry everything it needs.","",`The prompt must say when to stay quiet, and say that answering ${D} is how.`,"A schedule that reports every time it runs is one people learn to ignore."].join(`
3
+ `),input:{name:a.string().trim().min(1).describe("What a person sees in a list, and an email subject"),cron:a.string().trim().min(1).describe("Five-field cron, or @daily and friends. No more than once every five minutes"),prompt:a.string().trim().min(1).describe("What the agent is asked at each firing, including when to say nothing"),to:a.string().trim().min(1).optional().describe("Where the answer goes: #channel or @person for Slack, or an email address. Leave it out to post back into this conversation."),tz:a.string().trim().min(1).optional().describe("IANA zone the expression is read in, like Europe/London. Defaults to Bishop's")},async run(n,o){const i=C(e,o),c=n.tz,l=c?w(()=>B(c)):N();w(()=>x(n.cron,l));const p=n.to,m=p?await E(()=>e.resolve(p)):await o.destination(),s=e.schedules.add({name:n.name,cron:n.cron,tz:l,prompt:n.prompt,destination:m,createdBy:i,nextRunAt:k(n.cron,l,t())});return $.info({schedule:s.id,name:s.name,cron:s.cron,tz:s.tz,by:s.createdBy,to:m.label},"the agent created a schedule"),`Created ${s.id} "${s.name}", running ${s.cron} (${l}).
4
+ Next ${v(s.nextRunAt,l)}, posting to ${m.label}.`}})}function S(e){return y({name:"schedule_list",description:"Lists the schedules you created, and any that post into this conversation. Not schedules other people aimed elsewhere.",input:{},async run(t,n){const o=await A(e,n);if(o.length===0)return"Nothing is scheduled here, and you have not made any.";const i=N();return`${o.map(c=>F(c,i,e.principals)).join(`
5
5
 
6
6
  `)}
7
7
 
8
- Times in ${o}.`}})}function j(e){return l({name:"schedule_delete",description:"Removes a schedule by id. Only ones schedule_list shows you.",input:{id:s.string().trim().min(1).describe("The id schedule_list reports")},async run(n,t){const o=(await $(e,t)).find(a=>a.id===n.id);if(!o)throw new d(`No schedule ${n.id} that you can remove. schedule_list has the ones you can.`);return e.schedules.remove(o.id),y.info({schedule:o.id,name:o.name,by:m(t)},"the agent removed a schedule"),`Removed ${o.id} "${o.name}".`}})}function U(e){return[N(e),x(e),j(e)]}export{k as MAX_PER_CREATOR,m as createdBy,U as scheduleTools};
8
+ Times in ${i}.`}})}function U(e){const t=e.now??Date.now;return y({name:"schedule_update",description:"Changes a schedule by id and keeps the id. Only ones schedule_list shows you. Anything you leave out stays as it is.",input:{id:a.string().trim().min(1).describe("The id schedule_list reports"),name:a.string().trim().min(1).optional().describe("A new name"),cron:a.string().trim().min(1).optional().describe("A new five-field cron, or @daily and friends. No more than once every five minutes"),prompt:a.string().trim().min(1).optional().describe("A new prompt, whole. It replaces the old one rather than adding to it"),to:a.string().trim().min(1).optional().describe("A new destination: #channel or @person for Slack, or an email address"),tz:a.string().trim().min(1).optional().describe("A new IANA zone, like Europe/London")},async run(n,o){const i=`No schedule ${n.id} that you can change. schedule_list has the ones you can.`;if(!(await A(e,o)).some(b=>b.id===n.id))throw new h(i);const{name:l,cron:p,prompt:m,to:s,tz:g}=n;if([l,p,m,s,g].every(b=>b===void 0))throw new h("Nothing to change. Name at least one of name, cron, prompt, to, or tz.");const L=g?w(()=>B(g)):void 0,O=s?await E(()=>e.resolve(s)):void 0,r=e.schedules.get(n.id);if(!r)throw new h(i);const T=C(e,o,r),u=L??r.tz,f=p??r.cron;w(()=>x(f,u));const j=f!==r.cron||u!==r.tz,d={...r,name:l??r.name,cron:f,tz:u,prompt:m??r.prompt,destination:O??r.destination,createdBy:T,nextRunAt:j?k(f,u,t()):r.nextRunAt};e.schedules.update(r.id,d);const{destination:R}=d;$.info({schedule:r.id,name:d.name,cron:d.cron,tz:d.tz,by:T,to:R.label},"the agent changed a schedule");const I=d.enabled?`Next ${v(d.nextRunAt,u)}`:`It is off, so nothing fires until an operator runs \`bishop schedules enable ${r.id}\``;return`Changed ${r.id} "${d.name}", running ${d.cron} (${u}).
9
+ ${I}, posting to ${R.label}.`}})}function q(e){return y({name:"schedule_delete",description:"Removes a schedule by id. Only ones schedule_list shows you.",input:{id:a.string().trim().min(1).describe("The id schedule_list reports")},async run(t,n){const i=(await A(e,n)).find(c=>c.id===t.id);if(!i)throw new h(`No schedule ${t.id} that you can remove. schedule_list has the ones you can.`);return e.schedules.remove(i.id),$.info({schedule:i.id,name:i.name,by:z(n)},"the agent removed a schedule"),`Removed ${i.id} "${i.name}".`}})}function Q(e){return[P(e),S(e),U(e),q(e)]}export{Z as MAX_PER_CREATOR,z as createdBy,Q as scheduleTools};
@@ -0,0 +1,3 @@
1
+ import{promptParts as o}from"./harness/principal.js";import{log as a}from"./log.js";import{placedRefs as l}from"./outgoing.js";import{SYSTEM_PRINCIPAL as c}from"./principals.js";function h(t){const n=t.prepare(`INSERT INTO transcript
2
+ (thread_id, turn_id, direction, kind, principal_id, external_id, text, files, at)
3
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`);return{record(i){if(i.length!==0)try{t.exec("BEGIN");try{for(const r of i)n.run(r.threadId,r.turnId,r.direction,r.kind,r.principalId??null,r.externalId??null,r.text,r.files?.length?JSON.stringify(r.files):null,r.at);t.exec("COMMIT")}catch(r){throw t.exec("ROLLBACK"),r}}catch(r){a.warn({err:r,thread:i[0]?.threadId,turn:i[0]?.turnId},"couldn't record the transcript")}}}}function u(t){const n=o(t),i={threadId:t.threadId,turnId:t.turnId,direction:"in"},r=[];if(n.history){const e=(t.history??[]).flatMap(d=>d.kind==="message"?d.files??[]:[]);r.push({...i,kind:"history",principalId:c.id,text:n.history,...e.length?{files:e}:{},at:t.at})}return t.messages.forEach((e,d)=>{r.push({...i,kind:"message",principalId:e.principal.id,...e.externalId?{externalId:e.externalId}:{},text:n.messages[d]??"",...e.files?.length?{files:e.files}:{},at:t.at})}),r}function x(t){const n=l(t.text).flatMap(i=>{const r=t.shared.get(i);return r?[r]:[]});return{threadId:t.threadId,turnId:t.turnId,direction:"out",kind:t.kind,...t.agent?{principalId:t.agent.id}:{},text:t.text,...n.length?{files:n}:{},at:t.at}}export{u as inbound,x as outbound,h as transcriptStore};
package/dist/turn.js CHANGED
@@ -1,3 +1,3 @@
1
- import{contextChangeNote as I}from"./harness/principal.js";import{log as n}from"./log.js";import{worktreeName as P}from"./worktrees.js";function B(r){const{location:i,visibility:g,audience:m}=r;return{...i?{location:{kind:i.kind,...i.name?{name:i.name}:{}}}:{},visibility:g,audience:m}}const v=Symbol("not yet");function J(r){if(r)try{return JSON.parse(r)}catch{return}}function F(r){const i=new Map,g=new Set;async function m(e){const a=r.sharedDirNotice;if(!(!a||g.has(e.thread.threadKey))){g.add(e.thread.threadKey);try{await e.notify(a)}catch(d){n.debug({thread:e.thread.threadKey,err:d},"could not deliver the shared-directory notice")}}}function S(e,a,d){if(!r.tools||e.tools===!1)return;const s=Array.isArray(e.tools)?e.tools:void 0;if(s&&s.length===0)return;const o=d.share?.bind(d);return r.tools.grant({thread:e.thread,principal:a,origin:e.origin,destination:()=>e.destination(),...o?{share:o}:{}},...s?[{only:s}]:[])}async function O(e){await m(e);const a=new AbortController,d=e.presence();let s;try{i.set(e.thread.threadKey,a);const o=e.observe?.().catch(t=>(n.warn({thread:e.thread.threadKey,err:t},"could not read where this conversation takes place"),{visibility:"unknown",audience:"unknown"})),c=e.freshSession?void 0:await r.sessions.get(e.thread.id),T=await r.cwd(e.thread),[A,k]=await Promise.all([e.messages(),c?void 0:e.history?.().catch(t=>{n.warn({thread:e.thread.threadKey,err:t},"could not read the history")})]),w=o&&await Promise.race([o,Promise.resolve(v)]),K=w===v?void 0:w;w===v&&o&&(n.debug({thread:e.thread.threadKey},"where this conversation takes place isn't read yet; not waiting for it"),o.then(t=>r.contexts?.record(e.thread.id,e.origin.name,t)).catch(t=>{n.warn({thread:e.thread.threadKey,err:t},"could not record where this conversation takes place")}));const[b,...p]=A;if(!b)throw new Error("a turn with no messages");const C=p.at(-1)?.principal??b.principal;if(a.signal.aborted){n.info({thread:e.thread.threadKey},"turn stopped before the agent started");return}s=S(e,C,d),r.contexts?.record(e.thread.id,e.origin.name,K);const u=r.contexts?.told(e.thread.id),h=K?B(K):o&&J(u),l=h&&JSON.stringify(h),x=!!(c&&l&&u!==l);x&&h&&n.info({thread:e.thread.threadKey,...h},"this conversation changed since the agent was told about it; telling it again");const N=[e.note,x&&h?I(h):void 0].filter(Boolean).join(`
1
+ import{createHash as q,randomUUID as z}from"node:crypto";import{rm as G}from"node:fs/promises";import{join as L}from"node:path";import{contextChangeNote as Q,guidanceChangeNote as V}from"./harness/principal.js";import{mint as W}from"./ids.js";import{log as o}from"./log.js";import{stageFile as X}from"./sandbox.js";import{inbound as Z,outbound as $}from"./transcript.js";import{worktreeName as ee}from"./worktrees.js";function te(r){const{location:s,visibility:b,audience:I}=r;return{...s?{location:{kind:s.kind,...s.name?{name:s.name}:{}}}:{},visibility:b,audience:I}}const D=Symbol("not yet");function re(r){if(r)try{return JSON.parse(r)}catch{return}}function le(r){const s=new Map,b=new Set;async function I(e){const a=r.sharedDirNotice;if(!(!a||b.has(e.thread.threadKey))){b.add(e.thread.threadKey);try{await e.notify(a)}catch(i){o.debug({thread:e.thread.threadKey,err:i},"could not deliver the shared-directory notice")}}}function J(e,a,i,g,f){if(!r.tools||e.tools===!1)return;const c=Array.isArray(e.tools)?e.tools:void 0;if(c&&c.length===0)return;const l=i.share?.bind(i),n=l&&(async d=>{const m=await l(d);return g.set(m.ref,m),m});return r.tools.grant({thread:e.thread,principal:a,origin:e.origin,destination:()=>e.destination(),...n?{share:n}:{},...f?{stage:f}:{}},...c?[{only:c}]:[])}async function M(e){const a=r.principals;if(!a)return;const i=a.agent(e.origin.name);if(i)return i;try{return a.agent((await e.destination()).interface)}catch(g){o.debug({thread:e.thread.threadKey,err:g},"couldn't name the agent's account");return}}async function U(e){await I(e);const a=new AbortController,i=W("trn"),g=new Map,f=e.presence();let c,l;try{s.set(e.thread.threadKey,a);const n=e.observe?.().catch(t=>(o.warn({thread:e.thread.threadKey,err:t},"could not read where this conversation takes place"),{visibility:"unknown",audience:"unknown"})),d=e.freshSession?void 0:await r.sessions.get(e.thread.id),m=await r.cwd(e.thread),[_,p]=await Promise.all([e.messages(),d?void 0:e.history?.().catch(t=>{o.warn({thread:e.thread.threadKey,err:t},"could not read the history")})]),N=n&&await Promise.race([n,Promise.resolve(D)]),S=N===D?void 0:N;N===D&&n&&(o.debug({thread:e.thread.threadKey},"where this conversation takes place isn't read yet; not waiting for it"),n.then(t=>r.contexts?.record(e.thread.id,e.origin.name,t)).catch(t=>{o.warn({thread:e.thread.threadKey,err:t},"could not record where this conversation takes place")}));const[x,...O]=_;if(!x)throw new Error("a turn with no messages");const H=O.at(-1)?.principal??x.principal,R=r.transcript?await M(e):void 0;if(a.signal.aborted){o.info({thread:e.thread.threadKey},"turn stopped before the agent started");return}const K=await r.sandbox?.policy(m,e.thread);let A;if(K&&r.sandbox){const t=L(r.sandbox.staging,z());l=t,A=C=>X(K,C,t)}c=J(e,H,f,g,A),r.contexts?.record(e.thread.id,e.origin.name,S);const B=r.contexts?.told(e.thread.id),h=S?te(S):n&&re(B),y=h&&JSON.stringify(h),E=!!(d&&y&&B!==y);E&&h&&o.info({thread:e.thread.threadKey,...h},"this conversation changed since the agent was told about it; telling it again");const w=e.origin,T=q("sha256").update(w.guidance).digest("hex"),P=r.contexts?.guided(e.thread.id),j=!!(d&&P&&P!==T);j&&o.info({thread:e.thread.threadKey,interface:w.name},"the interface's guidance changed since the agent was given it; giving it again");const v=[e.note,E&&h?Q(h):void 0,j?V(w.guidance):void 0].filter(Boolean).join(`
2
2
 
3
- `),E=h?{...e.origin,context:h}:e.origin,y=r.model?.of(e.thread.id)??{};let f;for await(const t of r.harness.runTurn({messages:[b,...p],cwd:T,threadKey:e.thread.threadKey,worktree:P(e.thread.dirKey),...c?{sessionId:c}:{},...y.model?{model:y.model}:{},...y.effort?{effort:y.effort}:{},...N?{note:N}:{},...k?.length?{history:k}:{},origin:E,...s?{tools:s.access}:{},signal:a.signal}))t.type==="session"&&(t.model&&(f=t.model),t.sessionId!==c&&await r.sessions.set(e.thread.id,t.sessionId),l&&r.contexts?.told(e.thread.id)!==l&&r.contexts?.tell(e.thread.id,l)),t.type==="text"&&n.debug({thread:e.thread.threadKey,chars:t.text.length},"agent said something"),t.type==="tool"&&n.debug({thread:e.thread.threadKey,tool:t.name,detail:t.summary??t.detail},"tool activity"),t.type==="done"&&n.info({thread:e.thread.threadKey,...f?{model:f}:{},...t.usage},"turn complete"),t.type==="error"&&n.error({thread:e.thread.threadKey,...f?{model:f}:{},kind:t.kind,err:t.message},"turn errored"),await d.handle(t)}catch(o){const c=o instanceof Error?o.message:String(o);throw n.error({thread:e.thread.threadKey,err:o},"turn failed before the agent answered"),await d.handle({type:"error",kind:"turn_failed",message:c}),o}finally{s?.revoke(),i.get(e.thread.threadKey)===a&&i.delete(e.thread.threadKey),await d.finish()}}return{run:O,stop(e){const a=i.get(e);return a?(a.abort(),!0):!1}}}export{F as makeTurnRunner};
3
+ `),Y=h?{...w,context:h}:w,k=r.model?.of(e.thread.id)??{};let u;const F=(t,C)=>r.transcript?.record([$({threadId:e.thread.id,turnId:i,kind:t,agent:R,text:C,shared:g,at:Date.now()})]);r.transcript?.record(Z({threadId:e.thread.id,turnId:i,messages:[x,...O],...v?{note:v}:{},...p?.length?{history:p}:{},at:Date.now()}));for await(const t of r.harness.runTurn({messages:[x,...O],cwd:m,threadKey:e.thread.threadKey,worktree:ee(e.thread.dirKey),...d?{sessionId:d}:{},...k.model?{model:k.model}:{},...k.effort?{effort:k.effort}:{},...v?{note:v}:{},...p?.length?{history:p}:{},origin:Y,...c?{tools:c.access}:{},...K?{sandbox:K}:{},signal:a.signal}))t.type==="session"&&(t.model&&(u=t.model),t.sessionId!==d&&await r.sessions.set(e.thread.id,t.sessionId),y&&r.contexts?.told(e.thread.id)!==y&&r.contexts?.tell(e.thread.id,y),r.contexts?.guided(e.thread.id)!==T&&r.contexts?.guide(e.thread.id,T)),t.type==="text"&&(F("progress",t.text),o.debug({thread:e.thread.threadKey,chars:t.text.length},"agent said something")),t.type==="tool"&&o.debug({thread:e.thread.threadKey,tool:t.name,detail:t.summary??t.detail},"tool activity"),t.type==="done"&&(F("result",t.text),o.info({thread:e.thread.threadKey,turn:i,...u?{model:u}:{},...t.usage},"turn complete")),t.type==="error"&&o.error({thread:e.thread.threadKey,turn:i,...u?{model:u}:{},kind:t.kind,err:t.message},"turn errored"),await f.handle(t)}catch(n){const d=n instanceof Error?n.message:String(n);throw o.error({thread:e.thread.threadKey,turn:i,err:n},"turn failed before the agent answered"),await f.handle({type:"error",kind:"turn_failed",message:d}),n}finally{c?.revoke(),s.get(e.thread.threadKey)===a&&s.delete(e.thread.threadKey),await f.finish(),l&&await G(l,{recursive:!0,force:!0}).catch(n=>o.warn({dir:l,err:n},"couldn't remove a turn's shared-file copies"))}}return{run:U,stop(e){const a=s.get(e);return a?(a.abort(),!0):!1}}}export{le as makeTurnRunner};
package/dist/worktrees.js CHANGED
@@ -1,3 +1,3 @@
1
- import{createHash as w}from"node:crypto";import{existsSync as a,mkdirSync as h}from"node:fs";import{join as p,resolve as u}from"node:path";import{BishopError as l}from"./errors.js";import{exec as i}from"./exec.js";const d=64,s="bishop-",g=".claude/worktrees";function R(t){const r=t.replaceAll(/[^a-zA-Z0-9]+/g,"-").replaceAll(/-+/g,"-").replaceAll(/^-+|-+$/g,""),e=`${s}${r}`;if(e.length<=d)return e;const o=w("sha256").update(t).digest("hex").slice(0,8),n=d-s.length-o.length-1;return`${s}${r.slice(0,n)}-${o}`}function m(t,r){return u(t,g,r)}function I(t){return`\`${t}\` isn't a git repository, so I'm working in it directly. Another thread changing the same files at the same time can conflict.`}function N(t){return!!(t?.enabled&&t.isRepo)}function x(t){return a(p(t,".git"))}async function y(t){const r=await i("git",["rev-parse","--show-prefix"],{cwd:t});return r.code===0?r.stdout.trim():""}async function M(t,r){const e=m(t,r),o=await y(t);if(x(e))return f(e,o);if(a(e))throw new l(`${e} exists but is not a git worktree, which usually means an earlier attempt failed partway. Remove the directory and this thread will get a fresh one.`);const n=await i("git",["worktree","add","-b",r,e,"HEAD"],{cwd:t});if(n.code!==0){await i("git",["worktree","prune"],{cwd:t});const c=await i("git",["worktree","add",e,r],{cwd:t});if(c.code!==0)throw new l(`Couldn't create a git worktree for this thread at ${e}: ${k(n,c)}`)}return f(e,o)}function k(t,r){const e=t.stderr.trim(),o=r.stderr.trim();return!o||o===e?e:`${e}
2
- and, retrying with the existing branch: ${o}`}function f(t,r){if(!r)return t;const e=u(t,r);return h(e,{recursive:!0}),e}async function P(t){const r=await i("git",["rev-parse","--path-format=absolute","--git-dir","--git-common-dir"],{cwd:t}),e=r.stdout.split(`
3
- `).filter(o=>o.trim()!=="");return r.code!==0||e.length!==2?[]:[...new Set(e)]}async function S(t){return(await i("git",["rev-parse","--show-toplevel"],{cwd:t})).code===0}async function W(t){const r=await i("git",["config","--get","user.email"],{cwd:t}),e=await i("git",["config","--get","user.name"],{cwd:t});return r.code===0&&r.stdout.trim()!==""&&e.code===0&&e.stdout.trim()!==""}export{g as WORKTREE_DIR,M as ensureWorktree,P as gitDirs,W as hasGitIdentity,S as isGitRepo,N as isolates,I as sharedDirNotice,R as worktreeName,m as worktreePath};
1
+ import{createHash as w}from"node:crypto";import{existsSync as a,mkdirSync as p}from"node:fs";import{join as h,resolve as u}from"node:path";import{BishopError as d}from"./errors.js";import{exec as i}from"./exec.js";const l=64,s="bishop-",m=".claude/worktrees";function I(t){const e=t.replaceAll(/[^a-zA-Z0-9]+/g,"-").replaceAll(/-+/g,"-").replaceAll(/^-+|-+$/g,""),r=`${s}${e}`;if(r.length<=l)return r;const o=w("sha256").update(t).digest("hex").slice(0,8),n=l-s.length-o.length-1;return`${s}${e.slice(0,n)}-${o}`}function g(t,e){return u(t,m,e)}function N(t){return`\`${t}\` isn't a git repository, so I'm working in it directly. Another thread changing the same files at the same time can conflict.`}function M(t){return!!(t?.enabled&&t.isRepo)}function x(t){return a(h(t,".git"))}async function y(t){const e=await i("git",["rev-parse","--show-prefix"],{cwd:t});return e.code===0?e.stdout.trim():""}async function P(t,e){const r=g(t,e),o=await y(t);if(x(r))return f(r,o);if(a(r))throw new d(`${r} exists but is not a git worktree, which usually means an earlier attempt failed partway. Remove the directory and this thread will get a fresh one.`);const n=await i("git",["worktree","add","-b",e,r,"HEAD"],{cwd:t});if(n.code!==0){await i("git",["worktree","prune"],{cwd:t});const c=await i("git",["worktree","add",r,e],{cwd:t});if(c.code!==0)throw new d(`Couldn't create a git worktree for this thread at ${r}: ${k(n,c)}`)}return f(r,o)}function k(t,e){const r=t.stderr.trim(),o=e.stderr.trim();return!o||o===r?r:`${r}
2
+ and, retrying with the existing branch: ${o}`}function f(t,e){if(!e)return t;const r=u(t,e);return p(r,{recursive:!0}),r}async function S(t){const e=await i("git",["rev-parse","--path-format=absolute","--git-dir","--git-common-dir"],{cwd:t}),r=e.stdout.split(`
3
+ `).filter(o=>o.trim()!=="");return e.code!==0||r.length!==2?[]:[...new Set(r)]}async function $(t){const e=await i("git",["rev-parse","--show-toplevel"],{cwd:t});return e.code===0?e.stdout.trim():void 0}async function W(t){return await $(t)!==void 0}async function _(t){const e=await i("git",["config","--get","user.email"],{cwd:t}),r=await i("git",["config","--get","user.name"],{cwd:t});return e.code===0&&e.stdout.trim()!==""&&r.code===0&&r.stdout.trim()!==""}export{m as WORKTREE_DIR,P as ensureWorktree,S as gitDirs,_ as hasGitIdentity,W as isGitRepo,M as isolates,$ as repoRoot,N as sharedDirNotice,I as worktreeName,g as worktreePath};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agentdeploymentco/bishop",
3
- "version": "0.3.5",
3
+ "version": "0.5.0",
4
4
  "description": "An agent gateway. Connects an agent to Slack and email.",
5
5
  "license": "UNLICENSED",
6
6
  "type": "module",
@@ -41,10 +41,10 @@
41
41
  "check": "npm run lint && npm run typecheck && npm test"
42
42
  },
43
43
  "dependencies": {
44
- "@anthropic-ai/claude-agent-sdk": "0.3.280",
44
+ "@anthropic-ai/claude-agent-sdk": "0.3.287",
45
45
  "@microsoft/teams.apps": "^2.0.16",
46
46
  "@modelcontextprotocol/sdk": "^1.30.0",
47
- "@openai/codex-sdk": "0.155.1",
47
+ "@openai/codex-sdk": "0.160.0",
48
48
  "@slack/bolt": "^5.0.0",
49
49
  "@slack/web-api": "^8",
50
50
  "commander": "^13",
@@ -59,6 +59,6 @@
59
59
  "esbuild": "^0.28.2",
60
60
  "tsx": "^4",
61
61
  "typescript": "^5",
62
- "vitest": "^3"
62
+ "vitest": "^5"
63
63
  }
64
64
  }