@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.
- package/README.md +84 -3
- package/dist/cli.js +19 -17
- package/dist/config.js +4 -4
- package/dist/db.js +22 -9
- 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 +13 -12
- package/dist/interface/context.js +1 -0
- package/dist/interface/phrases.js +1 -1
- package/dist/nono-setup.js +7 -0
- package/dist/outgoing.js +2 -2
- package/dist/principals.js +6 -4
- package/dist/sandbox.js +20 -0
- package/dist/schedule/store.js +7 -4
- package/dist/sessions/store.js +8 -8
- package/dist/slack/context.js +1 -1
- package/dist/slack/history.js +1 -1
- package/dist/slack/interface.js +2 -2
- 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/provision.js +1 -1
- package/dist/teams/setup.js +39 -39
- package/dist/tools/files.js +2 -2
- package/dist/tools/schedules.js +6 -5
- package/dist/transcript.js +3 -0
- package/dist/turn.js +2 -2
- package/dist/worktrees.js +3 -3
- package/package.json +4 -4
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 ${i}; the report was sent without the file._`,[],t.channel)}return{}}return{name:"teams",handle:
|
|
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};
|
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};
|
package/dist/tools/schedules.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import{z as
|
|
2
|
-
`)}async function
|
|
3
|
-
`),input:{name:
|
|
4
|
-
Next ${
|
|
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 ${
|
|
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
|
|
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
|
-
`),
|
|
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
|
|
2
|
-
and, retrying with the existing branch: ${o}`}function f(t,
|
|
3
|
-
`).filter(o=>o.trim()!=="");return
|
|
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
|
+
"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.
|
|
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.
|
|
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": "^
|
|
62
|
+
"vitest": "^5"
|
|
63
63
|
}
|
|
64
64
|
}
|