@agentdeploymentco/bishop 0.5.3 → 0.6.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/dist/cli.js +13 -13
- package/dist/interface/destination.js +1 -1
- package/dist/interface/mentions.js +1 -0
- package/dist/interface/people.js +1 -0
- package/dist/interface/phrases.js +1 -1
- package/dist/schedule/cli.js +10 -10
- package/dist/schedule/cron.js +1 -1
- package/dist/schedule/prompt.js +2 -2
- package/dist/schedule/runner.js +5 -3
- package/dist/schedule/store.js +5 -5
- package/dist/slack/mentions.js +1 -1
- package/dist/teams/create.js +2 -2
- package/dist/teams/destination.js +1 -0
- package/dist/teams/fake.js +1 -1
- package/dist/teams/graph.js +1 -1
- package/dist/teams/interface.js +3 -3
- package/dist/teams/mentions.js +1 -0
- package/dist/teams/presence.js +4 -4
- package/dist/teams/thread.js +1 -1
- package/dist/tools/people.js +3 -0
- package/dist/tools/schedules.js +6 -6
- package/package.json +3 -1
package/dist/cli.js
CHANGED
|
@@ -1,20 +1,20 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import{stat as me}from"node:fs/promises";import{join as le,resolve as he}from"node:path";import{Command as ue}from"commander";import{agentSnapshots as fe,fixedAgentDir as we}from"./agent-dir.js";import{looksRemote as j,parseRepoSpec as Y}from"./agent-source.js";import{everyStore as W,storePath as J}from"./agent-store.js";import{attachmentStore as ge}from"./attachments.js";import{HARNESSES as ye,loadConfig as $,nonEmptyFlag as Q,parseHarnessName as ke,positiveIntFlag as Se,resolveAgent as be,sandboxConfig as ve,writeConfig as Te}from"./config.js";import{dbPath as V,openDb as X}from"./db.js";import{describeMissing as Ie,loadEnv as b,readCredentials as Ee}from"./env.js";import{BishopError as v}from"./errors.js";import{fileStore as xe}from"./files.js";import{DEFAULT_MAX_AGE_DAYS as Ae,makeCollector as _e,sweep as Re}from"./gc.js";import{ghAuthLogin as Ne,ghCliIn as Oe,gitAuthenticatesToGithub as Ge,withTemporaryGhConfig as $e}from"./github/gh.js";import{AGENT_SCOPES as Le,checkGithubSetup as Ce}from"./github/setup.js";import{createGmail as He}from"./gmail/create.js";import{checkGmailSetup as Pe}from"./gmail/setup.js";import{createHarness as
|
|
3
|
-
`),process.exit(1)}async function
|
|
2
|
+
import{stat as me}from"node:fs/promises";import{join as le,resolve as he}from"node:path";import{Command as ue}from"commander";import{agentSnapshots as fe,fixedAgentDir as we}from"./agent-dir.js";import{looksRemote as j,parseRepoSpec as Y}from"./agent-source.js";import{everyStore as W,storePath as J}from"./agent-store.js";import{attachmentStore as ge}from"./attachments.js";import{HARNESSES as ye,loadConfig as $,nonEmptyFlag as Q,parseHarnessName as ke,positiveIntFlag as Se,resolveAgent as be,sandboxConfig as ve,writeConfig as Te}from"./config.js";import{dbPath as V,openDb as X}from"./db.js";import{describeMissing as Ie,loadEnv as b,readCredentials as Ee}from"./env.js";import{BishopError as v}from"./errors.js";import{fileStore as xe}from"./files.js";import{DEFAULT_MAX_AGE_DAYS as Ae,makeCollector as _e,sweep as Re}from"./gc.js";import{ghAuthLogin as Ne,ghCliIn as Oe,gitAuthenticatesToGithub as Ge,withTemporaryGhConfig as $e}from"./github/gh.js";import{AGENT_SCOPES as Le,checkGithubSetup as Ce}from"./github/setup.js";import{createGmail as He}from"./gmail/create.js";import{checkGmailSetup as Pe}from"./gmail/setup.js";import{createHarness as De}from"./harness/create.js";import{sharedFileFetcher as Me}from"./interface/attachments.js";import{principalCheck as Z}from"./interface/authorize.js";import{deliveryRouter as Be}from"./interface/delivery.js";import{conversationMatch as Ue,destinationResolver as Fe,directDestination as Ke}from"./interface/destination.js";import{messageReader as ze}from"./interface/messages.js";import{peopleFinder as qe}from"./interface/people.js";import{log as p,requestedLevel as je}from"./log.js";import{DEFAULT_PROFILE as Ye,setupNono as We}from"./nono-setup.js";import{principalStore as Je}from"./principals.js";import{startReviewer as Qe}from"./review.js";import{checkSandbox as Ve,clearedStaging as Xe,sandboxPolicy as Ze,writeLauncher as et}from"./sandbox.js";import{addSchedule as tt,listSchedules as ot,removeSchedule as nt,setScheduleEnabled as ee}from"./schedule/cli.js";import{authorizedToRun as it,makeScheduler as st}from"./schedule/runner.js";import{scheduleStore as rt}from"./schedule/store.js";import{threadQueue as at}from"./sessions/queue.js";import{interfaceState as ct,retentionStore as te,sessionStore as dt,threadContexts as pt,threadHandover as mt,threadMeta as lt}from"./sessions/store.js";import{ensureGitignore as ht,openBrowser as ut,resolveAppDetails as ft,SetupError as oe,setupSlack as wt,setupSlackManually as gt,withPrompt as ne,writeEnv as ie}from"./setup.js";import{createSlackInterface as yt}from"./slack/interface.js";import{buildManifest as kt}from"./slack/manifest.js";import{createTeams as St}from"./teams/create.js";import{administratorInstructions as bt,repointTeamsEndpoint as vt,setupTeams as Tt,setupTeamsManually as It}from"./teams/setup.js";import{threadModel as Et}from"./thread-model.js";import{fileTools as xt}from"./tools/files.js";import{messageTools as At}from"./tools/messages.js";import{peopleTools as _t}from"./tools/people.js";import{scheduleTools as Rt}from"./tools/schedules.js";import{startToolServer as Nt}from"./tools/server.js";import{transcriptStore as Ot}from"./transcript.js";import{makeTurnRunner as Gt}from"./turn.js";import{version as $t}from"./version.js";import{hasGitIdentity as Lt,isGitRepo as Ct,sharedDirNotice as Ht,WORKTREE_DIR as Pt}from"./worktrees.js";function f(e){process.stderr.write(`${e}
|
|
3
|
+
`),process.exit(1)}async function Dt(e){const t=he(e??".");let n;try{n=(await me(t)).isDirectory()}catch{throw new v(`--agent directory doesn't exist: ${t}`)}if(!n)throw new v(`--agent is not a directory: ${t}`);return t}async function Mt(e){const t=await Ct(e),n={enabled:!0,isRepo:t};return t?(p.info({agentDir:e,worktrees:le(e,Pt)},"worktree mode is on"),await Lt(e)||p.warn({agentDir:e},"git has no user.name or user.email here, so the agent cannot commit what it does in a worktree"),n):(p.warn({agentDir:e},"worktree mode is on but the agent directory is not a git repository; threads share it"),n)}function Bt(e){if(e.kind==="options-replaced"){p.warn({harness:e.harness},`agent.harnesses.${e.harness} replaces agent.options, which is ignored for this run`);return}p.warn({harness:e.harness,configured:e.configured},`--harness overrides the configured harness, but agent.options is still written for that one; move it to agent.harnesses.${e.configured} and give ${e.harness} its own block`)}async function Ut(e=[],t={}){e.length>0&&f(`bishop: unknown command "${e[0]}"
|
|
4
4
|
|
|
5
|
-
Run \`bishop --help\` to see what's available.`);const n=process.cwd(),c=t.agent&&j(t.agent)?Y(t.agent):void 0;t.autoUpdate&&!c&&f("--auto-update needs --agent to be a git URL. Bishop never updates a directory it was pointed at; that state is yours to manage."),b(n);const
|
|
5
|
+
Run \`bishop --help\` to see what's available.`);const n=process.cwd(),c=t.agent&&j(t.agent)?Y(t.agent):void 0;t.autoUpdate&&!c&&f("--auto-update needs --agent to be a git URL. Bishop never updates a directory it was pointed at; that state is yours to manage."),b(n);const i=await $(n),s=Ee();s.ok||f(Ie(s));const r=be(i,t);for(const o of r.warnings)Bt(o);const l=ve(i);if(l){const o=await Ve(l);p.info({nono:o,...l.profile?{profile:l.profile}:{}},"sandbox is on"),l.profile||p.warn("sandbox is enabled but agent.sandbox.profile is missing; run `bishop nono setup`")}const u=i.review?await Qe(i.review,{home:n}):void 0;u&&p.warn("review is configured, but this version only checks the reviewer at startup; no message is reviewed yet (#254)");const m=l?(o,d)=>Ze({home:n,cwd:o,...d?{dirKey:d}:{},...l.profile?{profile:l.profile}:{},...u?.unwritable.length?{unwritable:u.unwritable}:{}}):void 0,a=X(V(n)),h=dt(a,r.harness);let A;const re=async o=>await A?.check(o,await m?.(o))??{ok:!0},_=c?fe({spec:c,store:J(n,c.slug),dirs:h,autoUpdate:!!t.autoUpdate,recheck:re}):void 0,y=_?await _.start():await Dt(t.agent),ae=_?_.forTurn:we(y,h);p.info({bishopDir:n,agentDir:y,...y===n?{}:{separate:!0},...c?{source:c.cloneUrl,autoUpdate:!!t.autoUpdate}:{},app:i.slack?.app?.name},"bishop starting");const R=i.agent?.worktrees?await Mt(y):void 0,T=De(r.harness,{...r.options?{options:r.options}:{},...r.model?{model:r.model}:{},...r.effort?{effort:r.effort}:{},...R?{worktrees:R}:{},...l?{launcher:await et(n)}:{},credentials:s.credentials}),N=await T.check(y,await m?.(y));N.ok||f(T.describeStartupFailure(y,N.error));const L=N.model??r.model;p.info({harness:T.name,credentials:N.credentialSource??"unreported",model:L,...r.effort?{effort:r.effort}:{},...r.options?{agentOptions:Object.keys(r.options)}:{}},"harness ready"),A=T,p.info({sessions:h.count()},"opened session store");const P=_e({retention:te(a),stores:()=>W(n),home:n,...i.gc?.maxAgeDays===void 0?{}:{maxAgeDays:i.gc.maxAgeDays}});P.start();let w;const ce=o=>w?Z(w,S)(o):it(o),de=async(o,d)=>{if(!w)throw new v("no interface is running, so there is nowhere to send");return Fe(w,{optedInOnly:!0})(o,d)},pe=(o,d)=>w?Ue(w)(o,d):!1,D=xe({home:n,...i.files?.maxMb===void 0?{}:{maxMb:i.files.maxMb}}),M=ge(a),S=Je(a),B=rt(a),U=await Nt({tools:[...Rt({schedules:B,authorized:ce,resolve:de,sameConversation:pe,principals:S}),...xt({fetch:Me({interfaces:()=>w??[],store:M,files:D})}),...s.credentials.slack?At({read:ze({interfaces:()=>w??[]})}):[],...s.credentials.teams?_t({find:qe({interfaces:()=>w??[]})}):[]]}),O=lt(a),C=Et(O),F={...L?{model:L}:{},...r.effort?{effort:r.effort}:{}},K=Gt({harness:T,cwd:ae,sessions:h,model:C,tools:U,contexts:pt(a,O),transcript:Ot(a),principals:S,...R&&!R.isRepo?{sharedDirNotice:Ht(y)}:{},...m?{sandbox:{policy:(o,d)=>m(o,d.dirKey),staging:await Xe(n)}}:{}}),z=at(),I={threads:h,events:h,files:D,attachments:M,principals:S,run:o=>z.run(o.thread.threadKey,()=>K.run(o)),stop:o=>K.stop(o)},g=[];if(s.credentials.slack&&g.push(await yt({...I,credentials:s.credentials.slack,meta:O,model:C,configuredModel:F,...i.slack?.verbosity?{verbosity:i.slack.verbosity}:{},...i.slack?.allow?{allow:i.slack.allow}:{},...i.slack?.maxBotTurns===void 0?{}:{maxBotTurns:i.slack.maxBotTurns}})),s.credentials.gmail)try{g.push(await He({...I,credentials:s.credentials.gmail,...i.gmail?{config:i.gmail}:{},state:ct(a)}))}catch(o){o instanceof v&&f(o.message);const d=s.credentials.gmail,G=d.kind==="oauth"?"the authorized Gmail account":`${d.user}`;f(`Couldn't reach the mailbox for ${G}:
|
|
6
6
|
${o instanceof Error?o.message:String(o)}
|
|
7
7
|
|
|
8
|
-
Run \`bishop gmail setup\` to check the credentials.`)}if(
|
|
8
|
+
Run \`bishop gmail setup\` to check the credentials.`)}if(s.credentials.teams)try{g.push(await St({...I,credentials:s.credentials.teams,meta:O,model:C,configuredModel:F,...i.teams?{config:i.teams}:{}}))}catch(o){o instanceof v&&f(o.message),f(`Couldn't start the Teams app for client ${s.credentials.teams.clientId}:
|
|
9
9
|
${o instanceof Error?o.message:String(o)}
|
|
10
10
|
|
|
11
|
-
Check the credentials in .env and the \`teams\` block in .bishop/config.json.`)}w=g;for(const o of g)await o.start();
|
|
11
|
+
Check the credentials in .env and the \`teams\` block in .bishop/config.json.`)}w=g;for(const o of g)await o.start();p.info({interfaces:g.map(o=>o.name)},"bishop is listening");const q=st({schedules:B,run:I.run,threads:h,cancel:I.stop,deliver:Be(g),authorized:Z(g,S),reach:Ke(g,S),principals:S,attach:mt(h)});q.start();for(const o of["SIGINT","SIGTERM"])process.once(o,()=>{p.info({signal:o,inFlight:z.size},"shutting down"),q.stop().catch(d=>p.error({err:d},"the scheduler failed to stop cleanly")).then(()=>Promise.allSettled(g.map(d=>d.stop()))).then(async d=>{const G=d.filter(E=>E.status==="rejected");for(const E of G)p.error({err:E.reason},"an interface failed to stop cleanly");await U.stop().catch(E=>{p.error({err:E},"the tool endpoint failed to stop cleanly")}),await P.stop(),a.close(),process.exit(G.length>0?1:0)})})}function Ft(e){for(const t of e.snapshots)process.stdout.write(`removed snapshot ${t.commit} (unused for ${t.ageDays} days)
|
|
12
12
|
`);for(const t of e.worktrees){const n=t.branch?`, branch ${t.branch}`:"";process.stdout.write(`released ${t.path}${n}
|
|
13
13
|
`)}process.stdout.write(`snapshots: removed ${e.snapshots.length}, kept ${e.keptSnapshots}
|
|
14
14
|
worktrees: released ${e.worktrees.length}
|
|
15
15
|
files: removed ${e.files} download directories
|
|
16
16
|
rows: forgot ${e.threads} threads, ${e.events} events
|
|
17
|
-
`)}async function
|
|
17
|
+
`)}async function Kt(e){const t=process.cwd();b(t),e.agent&&!j(e.agent)&&f("gc --agent takes the git URL Bishop cloned, not a path. It narrows the pass to the files Bishop owns for one agent, and it owns none for a directory agent. Run `bishop gc` with no --agent to collect what its threads left behind.");const n=e.agent?[J(t,Y(e.agent).slug)]:await W(t),c=await $(t),i=e.maxAgeDays??c.gc?.maxAgeDays;je()||(p.level="error");const s=X(V(t));try{Ft(await Re({retention:te(s),stores:n,home:t,...e.agent?{only:n}:{},...i===void 0?{}:{maxAgeDays:i}}))}finally{s.close()}}function zt(){return!(process.env.SSH_CONNECTION||process.env.SSH_TTY||process.env.SSH_CLIENT||process.platform==="linux"&&!process.env.DISPLAY&&!process.env.WAYLAND_DISPLAY)}async function qt(e={}){const t=process.cwd();b(t);const n=process.env.BISHOP_GMAIL_CLIENT_ID?.trim(),c=process.env.BISHOP_GMAIL_CLIENT_SECRET?.trim(),i=process.env.BISHOP_GMAIL_REFRESH_TOKEN?.trim(),s=process.env.BISHOP_GMAIL_USER?.trim(),r=process.env.BISHOP_GMAIL_SERVICE_ACCOUNT?.trim();n&&!c&&f("BISHOP_GMAIL_CLIENT_ID is set but BISHOP_GMAIL_CLIENT_SECRET is not.");const l=e.browser===!1||!zt()?"paste":"browser",u=await ne(!0,async m=>Pe({...n&&c?{client:{clientId:n,clientSecret:c}}:{},...i?{refreshToken:i}:{},...s?{user:s}:{},...r?{serviceAccount:r}:{},mode:l,openBrowser:ut,readPaste:async a=>(process.stdout.write(`Open this in a browser and approve it:
|
|
18
18
|
|
|
19
19
|
${a}
|
|
20
20
|
|
|
@@ -22,14 +22,14 @@ It will finish on a http://127.0.0.1 address that fails to load. That is expecte
|
|
|
22
22
|
Copy the whole address out of the browser and paste it here.
|
|
23
23
|
|
|
24
24
|
`),m("Redirect URL",""))}));if(process.stdout.write(`${u.report}
|
|
25
|
-
`),u.refreshToken){const m=await
|
|
25
|
+
`),u.refreshToken){const m=await ie(t,{BISHOP_GMAIL_REFRESH_TOKEN:u.refreshToken});process.stdout.write(`
|
|
26
26
|
Saved BISHOP_GMAIL_REFRESH_TOKEN to ${m}
|
|
27
|
-
`)}u.ok||process.exit(1)}async function
|
|
27
|
+
`)}u.ok||process.exit(1)}async function jt(e){const t=process.cwd(),n=process.env.GH_TOKEN?.trim();process.env.GH_TOKEN!==void 0&&!n&&delete process.env.GH_TOKEN,b(t);const c=process.env.GH_TOKEN?.trim(),i=!!c&&!n,s=e.prompt&&process.stdin.isTTY===!0,r=e.print?process.stderr:process.stdout,l=a=>r.write(a),u=a=>Ce({...c?{token:c,tokenIsSaved:i}:{},...s?{login:()=>Ne(Le.map(h=>h.scope),a),confirm:async h=>/^y/i.test(await ne(!0,A=>A(h,"y"),r))}:{},gitConfigured:Ge,gh:Oe(a),...e.temporary?{temporary:!0}:{}}),m=e.temporary&&!c?await $e(u):await u();if(l(`${m.report}
|
|
28
28
|
`),e.print)m.token&&process.stdout.write(`${JSON.stringify({GH_TOKEN:m.token})}
|
|
29
|
-
`);else if(m.token){if(m.source==="gh"){const h=await
|
|
29
|
+
`);else if(m.token){if(m.source==="gh"){const h=await ie(t,{GH_TOKEN:m.token});l(`
|
|
30
30
|
Saved GH_TOKEN to ${h}
|
|
31
|
-
`)}const a=await
|
|
32
|
-
`)}m.ok||process.exit(1)}async function
|
|
31
|
+
`)}const a=await ht(t,[".env"]);a.length>0&&l(`Added ${a.join(", ")} to .gitignore
|
|
32
|
+
`)}m.ok||process.exit(1)}async function Yt(e){const t=process.cwd(),n=await We({config:await $(t),name:e.name});process.stdout.write(`${n.report}
|
|
33
33
|
`),n.config&&process.stdout.write(`Saved ${await Te(n.config,t)}
|
|
34
|
-
`),n.ok||process.exit(1)}async function
|
|
35
|
-
`)}const k=new ue().name("bishop").description("An agent gateway. Runs an agent and connects it to Slack, email, and Teams.").version(
|
|
34
|
+
`),n.ok||process.exit(1)}async function Wt(e){b(process.cwd());const t={...e.name===void 0?{}:{name:e.name},...e.displayName===void 0?{}:{displayName:e.displayName},...e.description===void 0?{}:{description:e.description},prompt:e.prompt};e.manual?await gt(t):await wt(t)}async function Jt(e){if(e.appId!==void 0&&!e.manual)throw new oe("--app-id adopts an app somebody else created, so it needs --manual too.");if(e.endpoint!==void 0&&e.manual)throw new oe("--endpoint sets the URL on an app Bishop is creating, so it cannot be used\nwith --manual. Point an adopted app with `bishop teams endpoint <url>`,\nor ask whoever created it to.");b(process.cwd());const t={...e.name===void 0?{}:{name:e.name},...e.displayName===void 0?{}:{displayName:e.displayName},...e.description===void 0?{}:{description:e.description},...e.endpoint===void 0?{}:{endpoint:e.endpoint},...e.appId===void 0?{}:{appId:e.appId},prompt:e.prompt};e.manual?await It(t):await Tt(t)}async function Qt(e){const t=process.cwd(),n=await $(t),c=await ft({name:e.name??n.slack?.app?.name,displayName:e.displayName??n.slack?.app?.displayName,description:e.description??n.slack?.app?.description,prompt:!1},t,async(i,s)=>s);process.stdout.write(`${JSON.stringify(kt(c),null,2)}
|
|
35
|
+
`)}const k=new ue().name("bishop").description("An agent gateway. Runs an agent and connects it to Slack, email, and Teams.").version($t,"-v, --version").enablePositionalOptions().option("--agent <path-or-url>","the agent's directory, or a git URL to clone; defaults to the current directory").option(`--harness <${ye.join("|")}>`,"which SDK runs the agent",ke).option("--model <model>","the model to run, in the harness's own naming",Q("--model")).option("--effort <effort>","how hard the model should think, in the harness's own naming",Q("--effort")).option("--auto-update","for a git-URL --agent, check for a new commit at the start of every new thread").allowExcessArguments(!0).action((e,t)=>Ut(t.args,e));k.command("gc").description("Collect the snapshots, worktrees, and rows old conversations left behind").option("--max-age-days <days>",`how long a quiet thread's remains are kept; defaults to gc.maxAgeDays, then ${Ae}`,Se("--max-age-days")).option("--agent <url>","only this agent's snapshots; defaults to every agent Bishop has cloned here").action(Kt);const x=k.command("schedules").description("Prompts the agent runs on a clock, and where their answers go");x.command("list").description("Everything scheduled here, in your own time zone").action(()=>ot(process.cwd())),x.command("add").description("Add a schedule, or with --at a reminder that runs once and is then deleted").requiredOption("--name <name>","what a person sees in a list, and the subject of an email").option("--cron <expression>","five-field cron, or @daily and friends").option("--at <time>","run once instead, at an ISO 8601 time like 2026-10-09T15:00").requiredOption("--prompt <text>","what the agent is asked, including when to stay quiet").requiredOption("--to <destination>","#channel or @person for Slack, an address for Gmail").option("--tz <zone>","IANA zone --cron or --at is read in; defaults to this machine").option("--interface <name>","which interface delivers, when --to is ambiguous").action(e=>tt(process.cwd(),e)),x.command("rm <id>").description("Remove a schedule").action(e=>nt(process.cwd(),e)),x.command("disable <id>").description("Stop a schedule firing, keeping it").action(e=>ee(process.cwd(),e,!1)),x.command("enable <id>").description("Start a disabled schedule firing again, from now").action(e=>ee(process.cwd(),e,!0));const Vt=k.command("gmail").description("Gmail interface commands");Vt.command("setup").description("Authorize a Gmail mailbox, or check the one already configured").option("--no-browser","print a URL to open elsewhere instead of opening one here").action(qt);const Xt=k.command("github").description("GitHub credentials for the agent");Xt.command("setup").description("Give the agent a GitHub token, or check the one it's using").option("--no-prompt","don't offer to run `gh auth login`").option("--print","write the credential to stdout as JSON instead of to .env").option("--temporary","log in without touching this machine's own gh login").action(jt);const Zt=k.command("nono").description("The nono sandbox the agent runs under");Zt.command("setup").description("Write a nono profile that lets a sandboxed agent push").option("--name <name>","the profile name",Ye).action(Yt);const se=k.command("slack").description("Slack interface commands");se.command("setup").description("Create and install a Slack app, and save its credentials here").option("--name <name>","app name; defaults to the current directory name").option("--display-name <name>","bot display name in Slack; defaults to --name").option("--description <text>","short description shown in Slack").option("--no-prompt","don't prompt for anything left off").option("--manual","adopt an app you created in Slack yourself, using its tokens").action(Wt),se.command("manifest").description("Print the app manifest, for creating an app in Slack by hand").option("--name <name>","app name; defaults to config or the directory name").option("--display-name <name>","bot display name; defaults to --name").option("--description <text>","short description shown in Slack").action(Qt);const H=k.command("teams").description("Microsoft Teams interface commands");H.command("setup").description("Create a Teams app and save its credentials here, or check the one configured").option("--name <name>","app name; defaults to the current directory name").option("--display-name <name>","app name shown in Teams; defaults to --name").option("--description <text>","short description shown in Teams").option("--endpoint <url>","the URL Teams POSTs to; can be set later").option("--app-id <id>","with --manual, the Teams app ID you were given").option("--no-prompt","don't prompt for anything left off").option("--manual","adopt an app somebody else created, using its credentials").action(Jt),H.command("endpoint <url>").description("Point the Teams app at a different URL, changing nothing else").option("--app-id <id>","the Teams app to point; defaults to the one in config").action((e,t)=>vt(e,t.appId===void 0?{}:{appId:t.appId})),H.command("instructions").description("Print what to send whoever creates the Teams app, when that is an administrator").option("--endpoint <url>","the URL Teams should POST to, if you already know it").action(e=>{process.stdout.write(bt(e.endpoint))});try{await k.parseAsync()}catch(e){throw e instanceof v&&f(e.message),e}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{BishopError as
|
|
1
|
+
import{BishopError as o}from"../errors.js";import{looksLikeAddress as f}from"../gmail/authorize.js";function l(r,t,e){if(t)return t;if(r.startsWith("#")||r.startsWith("@"))return e==="teams"?"teams":"slack";if(/^[CDGUW][A-Z0-9]{2,}(:|$)/.test(r))return"slack";if(f(r))return"gmail";throw new u(e==="teams"?`can't tell where "${r}" is. Write #channel for a channel in this team, @person for somebody in this conversation, or an email address.`:`can't tell where "${r}" is. Write #channel or @person for Slack, or an email address for Gmail.`)}class u extends o{constructor(t){super(t,"UnplaceableDestination")}}function p(r){const t=new Map(r.map(e=>[e.name,e]));return(e,n)=>e.interface!==n.interface?!1:t.get(e.interface)?.sameConversation(e.handle,n.handle)??!1}function h(r,t={}){const e=new Map(r.map(n=>[n.name,n]));return async(n,a)=>{const i=l(n,void 0,a?.interface),s=e.get(i);if(!s){const c=[...e.keys()].join(", ")||"none";throw new o(`nothing can reach ${n}: the ${i} interface is not running (running: ${c})`)}return s.resolveDestination(n,{...t,...a?{from:a}:{}})}}function w(r,t){const e=new Map(r.map(n=>[n.name,n]));return async n=>{const a=t.find(n);if(!a)return;const i=e.get(a.interface);if(i)return i.resolveDestination(a.principal.externalId)}}export{u as UnplaceableDestination,p as conversationMatch,h as destinationResolver,w as directDestination,l as interfaceFor};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
const n=/(```[\s\S]*?```|`[^`\n]*`)/;export{n as CODE};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{log as i}from"../log.js";const d="teams_find_person";function p(t){return async(e,n,s)=>{const r=t.interfaces().find(o=>o.name===e);if(!r?.findPeople)return{kind:"refused",reason:`Looking people up is not something ${e} can do.`};try{return await r.findPeople(n,s)}catch(o){return i.warn({thread:n.threadKey,origin:e,err:o},"could not look somebody up"),{kind:"refused",reason:"Bishop could not read who is in this conversation just now."}}}}export{d as FIND_PERSON_TOOL,p as peopleFinder};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{FETCH_FILE_TOOL as i}from"../harness/principal.js";import{SHARE_FILE_TOOL as a}from"../outgoing.js";import{READ_MESSAGE_TOOL as s}from"./messages.js";const l={Read:e=>e?`Reading ${e}`:"Reading a file",Write:e=>e?`Writing ${e}`:"Writing a file",Edit:e=>e?`Editing ${e}`:"Editing a file",NotebookEdit:e=>e?`Editing ${e}`:"Editing a notebook",Bash:e=>e??"Running a command",BashOutput:()=>"Checking on a command",Glob:e=>e?`Looking for ${e}`:"Looking through files",Grep:e=>e?`Searching for ${e}`:"Searching the code",WebFetch:e=>e?`Reading ${e}`:"Fetching a page",WebSearch:e=>e?`Searching for ${e}`:"Searching the web",Task:e=>e?`Working on ${e}`:"Delegating a subtask",TodoWrite:()=>"Planning",ExitPlanMode:()=>"Finishing up a plan",AskUserQuestion:()=>"Considering options"},
|
|
1
|
+
import{FETCH_FILE_TOOL as i}from"../harness/principal.js";import{SHARE_FILE_TOOL as a}from"../outgoing.js";import{READ_MESSAGE_TOOL as s}from"./messages.js";import{FIND_PERSON_TOOL as c}from"./people.js";const l={Read:e=>e?`Reading ${e}`:"Reading a file",Write:e=>e?`Writing ${e}`:"Writing a file",Edit:e=>e?`Editing ${e}`:"Editing a file",NotebookEdit:e=>e?`Editing ${e}`:"Editing a notebook",Bash:e=>e??"Running a command",BashOutput:()=>"Checking on a command",Glob:e=>e?`Looking for ${e}`:"Looking through files",Grep:e=>e?`Searching for ${e}`:"Searching the code",WebFetch:e=>e?`Reading ${e}`:"Fetching a page",WebSearch:e=>e?`Searching for ${e}`:"Searching the web",Task:e=>e?`Working on ${e}`:"Delegating a subtask",TodoWrite:()=>"Planning",ExitPlanMode:()=>"Finishing up a plan",AskUserQuestion:()=>"Considering options"},u={schedule_create:"Setting up a schedule",schedule_list:"Checking what is scheduled",schedule_update:"Changing a schedule",schedule_delete:"Removing a schedule",[i]:"Fetching a file from this conversation",[s]:"Reading a message from this conversation",[c]:"Looking somebody up in this conversation",[a]:"Getting a file ready to send"};function g(e){if(!e.startsWith("mcp__"))return;const[,n,...o]=e.split("__");return n==="bishop"?u[o.join("__")]??"Sorting something out":n?`Calling ${n}`:"Calling a tool"}function h(e){const n=e.replaceAll(/[_-]+/g," ").replaceAll(/([a-z\d])([A-Z])/g,"$1 $2").trim();if(!n)return"Working";const o=n.toLowerCase();return o.charAt(0).toUpperCase()+o.slice(1)}const r=100;function m(e){const n=e.trim();return n.length>r?`${n.slice(0,r-3)}...`:n}function S(e){const n=e.summary?.trim();if(n)return m(n);const o=l[e.name];if(o)return o(e.detail);const t=g(e.name);return t||h(e.name)}function d(e){const n=Math.max(0,Math.round(e/1e3)),o=Math.floor(n/60),t=n%60;return o?`${o}m ${t}s`:`${t}s`}function E(e){return e===1?"Used 1 tool":`Used ${e} tools`}function k(e,n){return`${n?"Worked for":"Stopped after"} ${d(e)}`}export{d as elapsed,h as humanizeToolName,m as summaryPhrase,S as toolPhrase,E as toolTally,k as turnTally};
|
package/dist/schedule/cli.js
CHANGED
|
@@ -1,17 +1,17 @@
|
|
|
1
|
-
import{userInfo as
|
|
2
|
-
runs ${
|
|
3
|
-
next ${l
|
|
4
|
-
to ${
|
|
1
|
+
import{userInfo as x}from"node:os";import{WebClient as R}from"@slack/web-api";import{loadConfig as A}from"../config.js";import{dbPath as k,openDb as D}from"../db.js";import{describeMissing as S,loadEnv as E,readCredentials as B}from"../env.js";import{BishopError as s}from"../errors.js";import{gmailAllowList as T}from"../gmail/authorize.js";import{resolveGmailDestination as z}from"../gmail/destination.js";import{interfaceFor as C,UnplaceableDestination as F}from"../interface/destination.js";import{principalLabel as L,principalStore as j}from"../principals.js";import{resolveSlackDestination as q}from"../slack/destination.js";import{describeTime as m,describeTiming as $,nextRun as b,parseAt as G,systemTimeZone as u,validateCron as N,validateTimeZone as Z}from"./cron.js";import{scheduleStore as I}from"./store.js";async function f(e){E(e);const t=await A(e),n=D(k(e));return{config:t,db:n,schedules:I(n)}}function w(e,t){const n=e?.trim();if(!n)throw new s(`${t} is required`);return n}function M(e){try{return e()}catch(t){throw t instanceof F?new s(`${t.message} Or say which with --interface.`):t}}async function O(e,t,n,o){if(t==="slack"){if(!n.slack)throw new s("Slack is not configured here, so nothing sent to a Slack destination would arrive. Run `bishop slack setup` first.");return q(new R(n.slack.botToken),e)}if(t==="gmail"){if(!n.gmail)throw new s("Gmail is not configured here, so nothing sent to an address would arrive. Run `bishop gmail setup` first.");return z(T(o),e)}throw new s(`Bishop has no "${t}" interface`)}async function ot(e,t){const n=w(t.name,"--name"),o=t.cron?.trim()||void 0,i=t.at?.trim()||void 0;if(o===void 0==(i===void 0))throw new s("give exactly one of --cron, for a schedule that repeats, or --at");const r=w(t.prompt,"--prompt"),a=w(t.to,"--to"),c=t.tz?Z(t.tz.trim()):u(),{config:y,db:v,schedules:g}=await f(e);try{let p;o?(N(o,c),p=b(o,c,Date.now())):p=G(i??"",c);const d=B();if(!d.ok)throw new s(S(d));const h=await O(a,M(()=>C(a,t.interface)),d.credentials,y.gmail?.allow),l=g.add({name:n,...o?{cron:o}:{},tz:c,prompt:r,destination:h,createdBy:`cli:${x().username}`,nextRunAt:p});process.stdout.write(`Added ${l.id} "${l.name}"
|
|
2
|
+
runs ${$(l)}
|
|
3
|
+
next ${m(l.nextRunAt,u())}
|
|
4
|
+
to ${h.label} via ${h.interface}
|
|
5
5
|
|
|
6
6
|
A running Bishop picks it up within a minute. Nothing fires while Bishop is stopped.
|
|
7
|
-
`)}finally{
|
|
8
|
-
`)}async function
|
|
9
|
-
`);return}const i=
|
|
7
|
+
`)}finally{v.close()}}function P(e,t,n){const o=[`${e.id} ${e.name}${e.enabled?"":" (disabled)"}`,` runs ${$(e)}`,` next ${e.enabled?m(e.nextRunAt,t):"never, while it is off"}`,` to ${e.destination.label} via ${e.destination.interface}`,` prompt ${e.prompt}`];return e.lastRunAt&&o.push(` last ${m(e.lastRunAt,t)}`),e.lastError&&o.push(` error ${e.lastError} (${e.consecutiveFailures} in a row)`),o.push(` by ${L(e.createdBy,n)}`),o.join(`
|
|
8
|
+
`)}async function it(e){const{db:t,schedules:n}=await f(e);try{const o=n.all();if(o.length===0){process.stdout.write(`No schedules here.
|
|
9
|
+
`);return}const i=u(),r=j(t);process.stdout.write(`${o.map(a=>P(a,i,r)).join(`
|
|
10
10
|
|
|
11
11
|
`)}
|
|
12
12
|
`),process.stdout.write(`
|
|
13
13
|
Times shown in ${i}.
|
|
14
|
-
`)}finally{t.close()}}async function
|
|
15
|
-
`)}finally{
|
|
14
|
+
`)}finally{t.close()}}async function rt(e,t){const{db:n,schedules:o}=await f(e);try{const i=o.get(t);if(!i)throw new s(`no schedule ${t}. \`bishop schedules list\` has them.`);o.remove(t),process.stdout.write(`Removed ${t} "${i.name}"
|
|
15
|
+
`)}finally{n.close()}}async function st(e,t,n){const{db:o,schedules:i}=await f(e);try{const r=i.get(t);if(!r)throw new s(`no schedule ${t}. \`bishop schedules list\` has them.`);const{cron:a}=r;if(n&&!a&&r.nextRunAt<=Date.now())throw new s(`${t} is a reminder whose time went by while it was off. Remove it, or add it again with a time in the future.`);const c=n&&a?b(a,r.tz,Date.now()):r.nextRunAt;i.setEnabled(t,n,c),n&&i.clearFailures(t),process.stdout.write(n?`Enabled ${t} "${r.name}", next ${m(c,u())}
|
|
16
16
|
`:`Disabled ${t} "${r.name}"
|
|
17
|
-
`)}finally{o.close()}}export{
|
|
17
|
+
`)}finally{o.close()}}export{ot as addSchedule,it as listSchedules,rt as removeSchedule,st as setScheduleEnabled};
|
package/dist/schedule/cron.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{CronExpressionParser as
|
|
1
|
+
import{CronExpressionParser as p}from"cron-parser";import{DateTime as w}from"luxon";import{BishopError as x}from"../errors.js";const h=300*1e3,y=20;class o extends x{constructor(t){super(t,"CronError")}}function A(){return Intl.DateTimeFormat().resolvedOptions().timeZone||"UTC"}function u(e){try{return new Intl.DateTimeFormat("en-US",{timeZone:e}),e}catch{throw new o(`"${e}" is not an IANA time zone. Write one like America/New_York.`)}}function c(e,t){try{return t()}catch(r){throw r instanceof o?r:new o(`"${e}" is not a cron expression Bishop can read: ${r instanceof Error?r.message:String(r)}`)}}function d(e,t,r){return c(e,()=>p.parse(e.trim(),{tz:t,currentDate:new Date(r)}))}function D(e,t,r){const n=d(e,t,r);return c(e,()=>n.next().getTime())}function v(e,t,r=Date.now()){const n=e.trim();if(!n)throw new o("a schedule needs a cron expression");u(t);const s=n.split(/\s+/);if(!n.startsWith("@")&&s.length!==5)throw new o(`"${n}" has ${s.length} ${s.length===1?"field":"fields"}. Bishop takes the five-field form: minute hour day-of-month month day-of-week. A sixth field would be seconds, which is finer than anything here is meant to be.`);const i=d(n,t,r);c(n,()=>{let a=i.next().getTime();for(let f=1;f<y;f+=1){if(!i.hasNext())return;const m=i.next().getTime();if(m-a<h)throw new o(`"${n}" fires every ${Math.round((m-a)/1e3)}s, and Bishop's floor is one firing every ${h/6e4} minutes. Every turn costs the agent real work, so a schedule this frequent is nearly always a mistyped field.`);a=m}})}function l(e,t){return new Intl.DateTimeFormat("en-US",{timeZone:t,month:"short",day:"numeric",year:"numeric",hour:"numeric",minute:"2-digit",timeZoneName:"short"}).format(new Date(e))}function E(e,t,r=Date.now()){u(t);const n=e.trim(),s=w.fromISO(n,{zone:t});if(!s.isValid)throw new o(`"${n}" is not a time Bishop can read. Write an ISO 8601 time like 2026-10-09T15:00.`);const i=s.toMillis();if(i<=r)throw new o(`${l(i,t)} has already passed. A reminder needs a time in the future.`);return i}function I(e){return e.cron?`${e.cron} (${e.tz})`:`once, ${l(e.nextRunAt,e.tz)}`}export{o as CronError,h as MIN_INTERVAL_MS,l as describeTime,I as describeTiming,D as nextRun,E as parseAt,A as systemTimeZone,v as validateCron,u as validateTimeZone};
|
package/dist/schedule/prompt.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import{NO_OUTPUT as
|
|
2
|
-
`)};function d(e,t,n){return
|
|
1
|
+
import{NO_OUTPUT as o,renderTag as a,saysNoOutput as i}from"../harness/principal.js";const r="NOTHING TO REPORT",h={name:"schedule",guidance:["This turn was started by a schedule rather than a person. Nobody is waiting on it and","nobody sees it run: everything you write is collected and, if it is worth sending, arrives","as one message at the destination named in the <schedule> tag. Nothing reaches anyone until","the turn is over, so a turn that takes minutes costs nothing.","",`Say nothing when there is nothing worth saying. Answer with ${o} and nothing else,`,"and Bishop sends no message at all. That is the normal outcome of a watching schedule, and it is","better than a message saying everything is fine. When you do have something, lead with it","and keep it short: the reader has no thread of context to fall back on.","","Each firing is a new conversation with no memory of the last one, and nothing you leave in","the working directory is guaranteed to be there next time. If a schedule needs to know what","it already reported, put it somewhere that outlasts a turn and that you know how to find","again.","",'A <schedule> tag marked once="true" is a reminder someone set. It runs this one time and is',"then gone, so there is no later firing to leave anything for."].join(`
|
|
2
|
+
`)};function d(e,t,n){return a("schedule",{id:e.id,name:e.name,cron:e.cron,once:e.cron?void 0:"true",tz:e.tz,"fired-at":new Date(t).toISOString(),to:e.destination.label,"created-by":n})}function u(e){const t=e.trim();return!t||i(t)?!0:t.replaceAll(/[^a-z0-9]/gi,"").toLowerCase()===r.replaceAll(" ","").toLowerCase()}export{h as SCHEDULE_ORIGIN,r as SILENCE,u as isSilent,d as scheduleNote};
|
package/dist/schedule/runner.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
|
-
import{log as a}from"../log.js";import{SHARE_FILE_TOOL as
|
|
1
|
+
import{log as a}from"../log.js";import{SHARE_FILE_TOOL as z}from"../outgoing.js";import{principalLabel as I,SYSTEM_PRINCIPAL as _}from"../principals.js";import{describeTime as R,nextRun as N}from"./cron.js";import{schedulePresence as A}from"./presence.js";import{SCHEDULE_ORIGIN as M,scheduleNote as C}from"./prompt.js";import{ONCE as L}from"./store.js";const O=3e4,P=5,F=5e3;function D(r){return r.startsWith("cli:")}function U(r,d){return`schedule:${r}:${d}`}function j(r){const d=r.now??Date.now,T=r.tickMs??O,b=r.authorized??D,c=new Map,y=new Map,l=new Set;let h,v=!1;function p(e,t){try{return r.schedules.setNextRun(e.id,N(e.cron??L,e.tz,t)),!0}catch(i){const n=i instanceof Error?i.message:String(i);return a.error({schedule:e.id,cron:e.cron,tz:e.tz,err:i},"disabling a schedule whose cron expression cannot be read"),r.schedules.setEnabled(e.id,!1),r.schedules.recordFailure(e.id,t,n),u(e,`The schedule "${e.name}" is off: Bishop can no longer read when it should run. ${n}
|
|
2
2
|
|
|
3
|
-
Remove it, or add it again with a cron expression that works.`),!1}}async function
|
|
3
|
+
Remove it, or add it again with a cron expression that works.`),!1}}async function u(e,t,i=e.destination){const n=r.deliver(i,{text:t,title:e.name}).then(()=>{}).catch(o=>{a.error({schedule:e.id,to:i.label,err:o},"could not say that a schedule failed")}).finally(()=>{l.delete(n)});l.add(n),await n}async function S(e,t,i){if(!e.cron){a.error({schedule:e.id,name:e.name,err:i},"a reminder failed"),await u(e,`The reminder "${e.name}" failed: ${i}`);return}const n=r.schedules.recordFailure(e.id,t,i);if(a.error({schedule:e.id,name:e.name,failures:n,err:i},"a scheduled run failed"),n>=P){r.schedules.setEnabled(e.id,!1),a.error({schedule:e.id,name:e.name,failures:n},"disabling a schedule after too many failures in a row"),await u(e,`The schedule "${e.name}" has failed ${n} times in a row and is now off. The last failure was: ${i}
|
|
4
4
|
|
|
5
|
-
Nothing more will run until \`bishop schedules enable ${e.id}\`.`);return}n===1&&await
|
|
5
|
+
Nothing more will run until \`bishop schedules enable ${e.id}\`.`);return}n===1&&await u(e,`The schedule "${e.name}" failed: ${i}`)}async function x(e,t,i){const n=A();a.info({schedule:e.id,name:e.name},"a schedule is firing");let o,f;try{f=await r.threads.remember(i)}catch(s){await S(e,d(),s instanceof Error?s.message:String(s));return}const k={thread:f,isNewThread:!0,freshSession:!0,tools:[z],origin:M,note:C(e,t,r.principals?I(e.createdBy,r.principals):e.createdBy),async messages(){return[{principal:_,text:e.prompt}]},presence:()=>n,async notify(s){n.note(s)},async destination(){return e.destination}};try{await r.run(k)}catch(s){o=s instanceof Error?s.message:String(s)}const m=n.outcome(),E=m.failure??o,w=d();if(E){await S(e,w,E);return}if(m.silent){r.schedules.recordSuccess(e.id,w),a.info({schedule:e.id,name:e.name},"a schedule had nothing to report");return}let g;try{g=await r.deliver(e.destination,{text:m.text,title:e.name,...m.files.length>0?{files:m.files}:{}})}catch(s){await S(e,w,s instanceof Error?s.message:String(s));return}if(g.threadKey&&r.attach)try{await r.attach(g.threadKey,i)}catch(s){a.error({schedule:e.id,thread:g.threadKey,err:s},"a report was delivered but nothing will answer a reply to it")}r.schedules.recordSuccess(e.id,w),a.info({schedule:e.id,name:e.name,to:e.destination.label},"a schedule reported")}function $(e){r.schedules.remove(e.id);const t=R(e.nextRunAt,e.tz);if(!b(e.createdBy)){a.warn({schedule:e.id,createdBy:e.createdBy,due:t},"deleted a missed reminder whose creator can no longer be authorized");return}const i=`Bishop wasn't running at ${t}, when your reminder "${e.name}" was due, so it didn't run. It would have asked:
|
|
6
|
+
|
|
7
|
+
${e.prompt}`,n=(async()=>{let o;try{o=await r.reach?.(e.createdBy)}catch(f){a.warn({schedule:e.id,createdBy:e.createdBy,err:f},"can't message a reminder's creator directly, so telling its destination instead"),o=e.destination}if(!o){a.warn({schedule:e.id,name:e.name,createdBy:e.createdBy,due:t},"deleted a reminder that came due while Bishop was not running");return}a.info({schedule:e.id,name:e.name,due:t,to:o.label},"telling the creator about a reminder that came due while Bishop was not running"),await u(e,i,o)})().catch(o=>{a.error({schedule:e.id,err:o},"could not report a missed reminder")}).finally(()=>{l.delete(n)});l.add(n)}async function B(){if(v)return;const e=d();for(const t of r.schedules.due(e)){const i=!t.cron;if(!b(t.createdBy)){a.warn({schedule:t.id,createdBy:t.createdBy},i?"deleting a reminder whose creator can no longer be authorized":"skipping a schedule whose creator can no longer be authorized"),i?r.schedules.remove(t.id):p(t,e);continue}if(i){if(c.has(t.id))continue;r.schedules.remove(t.id)}else if(!p(t,e))continue;if(c.has(t.id)){a.warn({schedule:t.id,name:t.name},"skipping a firing while the previous one is still running");continue}const n=U(t.id,e);y.set(t.id,n);const o=x(t,e,n).catch(f=>{a.error({schedule:t.id,err:f},"a schedule firing threw")}).finally(()=>{c.delete(t.id),y.delete(t.id)});c.set(t.id,o)}}return{start(){const e=d(),t=r.schedules.due(e);for(const n of t){if(!n.cron){$(n);continue}a.info({schedule:n.id,name:n.name,due:new Date(n.nextRunAt).toISOString()},"skipping a firing that came due while Bishop was not running"),p(n,e)}const i=r.schedules.all().filter(n=>n.enabled);a.info({schedules:i.length,skipped:t.length},"watching the schedules"),h=setInterval(()=>{B().catch(n=>{a.error({err:n},"a scheduler pass failed; trying again on the next tick")})},T),h.unref()},tick:B,async settle(){await Promise.allSettled([...c.values(),...l])},async stop(){if(v=!0,h&&clearInterval(h),c.size>0){a.info({firings:c.size},"stopping scheduled runs still going");for(const n of c.keys()){const o=y.get(n);o&&r.cancel?.(o)}}const e=Promise.allSettled([...c.values(),...l]);let t;const i=new Promise(n=>{t=setTimeout(()=>{a.warn({firings:c.size},"giving up on scheduled runs that would not stop"),n()},F)});await Promise.race([e,i]),t&&clearTimeout(t)}}}export{P as MAX_CONSECUTIVE_FAILURES,D as authorizedToRun,U as firingThread,j as makeScheduler};
|
package/dist/schedule/store.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import{mint as
|
|
2
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 1, ?, NULL, NULL, 0)`),
|
|
1
|
+
import{mint as A}from"../ids.js";const l="@once";function u(t){return{id:t.id,name:t.name,...t.cron===l?{}:{cron:t.cron},tz:t.tz,prompt:t.prompt,destination:{interface:t.interface,handle:t.handle,label:t.label},createdBy:t.created_by,createdAt:t.created_at,enabled:t.enabled===1,nextRunAt:t.next_run_at,...t.last_run_at===null?{}:{lastRunAt:t.last_run_at},...t.last_error===null?{}:{lastError:t.last_error},consecutiveFailures:t.consecutive_failures}}function h(t){const a="id, name, cron, tz, prompt, interface, handle, label, created_by, created_at, enabled, next_run_at, last_run_at, last_error, consecutive_failures",d=t.prepare(`INSERT INTO schedules (${a})
|
|
2
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 1, ?, NULL, NULL, 0)`),o=t.prepare(`SELECT ${a} FROM schedules ORDER BY created_at`),s=t.prepare(`SELECT ${a} FROM schedules WHERE id = ?`),i=t.prepare(`SELECT ${a} FROM schedules WHERE enabled = 1 AND next_run_at <= ? ORDER BY next_run_at`),E=t.prepare("DELETE FROM schedules WHERE id = ?"),_=t.prepare(`UPDATE schedules
|
|
3
3
|
SET name = ?, cron = ?, tz = ?, prompt = ?, interface = ?, handle = ?, label = ?,
|
|
4
4
|
created_by = ?, next_run_at = ?
|
|
5
|
-
WHERE id = ?`),
|
|
5
|
+
WHERE id = ?`),p=t.prepare("UPDATE schedules SET enabled = ?, next_run_at = ? WHERE id = ?"),R=t.prepare("UPDATE schedules SET next_run_at = ? WHERE id = ?"),f=t.prepare(`UPDATE schedules
|
|
6
6
|
SET last_run_at = ?, last_error = NULL, consecutive_failures = 0
|
|
7
|
-
WHERE id = ?`),
|
|
7
|
+
WHERE id = ?`),m=t.prepare(`UPDATE schedules
|
|
8
8
|
SET last_run_at = ?, last_error = ?, consecutive_failures = consecutive_failures + 1
|
|
9
|
-
WHERE id = ?`),
|
|
9
|
+
WHERE id = ?`),T=t.prepare("UPDATE schedules SET last_error = NULL, consecutive_failures = 0 WHERE id = ?");return{all(){return o.all().map(u)},get(e){const r=s.get(e);return r?u(r):void 0},add(e){const r=A("sch"),n=e.createdAt??Date.now();return d.run(r,e.name,e.cron??l,e.tz,e.prompt,e.destination.interface,e.destination.handle,e.destination.label,e.createdBy,n,e.nextRunAt),{id:r,...e,createdAt:n,enabled:!0,consecutiveFailures:0}},remove(e){return E.run(e).changes>0},update(e,r){return _.run(r.name,r.cron??l,r.tz,r.prompt,r.destination.interface,r.destination.handle,r.destination.label,r.createdBy,r.nextRunAt,e).changes>0},setEnabled(e,r,n){const c=s.get(e);return c?(p.run(r?1:0,n??c.next_run_at,e),!0):!1},due(e){return i.all(e).map(u)},setNextRun(e,r){R.run(r,e)},recordSuccess(e,r){f.run(r,e)},clearFailures(e){T.run(e)},recordFailure(e,r,n){return m.run(r,n,e),s.get(e)?.consecutive_failures??0}}}export{l as ONCE,h as scheduleStore};
|
package/dist/slack/mentions.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{log as
|
|
1
|
+
import{CODE as N}from"../interface/mentions.js";import{log as f}from"../log.js";function E(e){const r=e.displayName?.trim();if(r&&!/\s/.test(r))return r;const n=e.handle?.trim();return n&&!/\s/.test(n)?n:void 0}const w=2e3,A=/<@([UW][A-Z0-9]+)(?:\|([^>]*))?>|<!(here|channel|everyone)(?:\|[^>]*)?>|<!subteam\^([A-Z0-9]+)(?:\|([^>]*))?>|<#(C[A-Z0-9]+)(?:\|([^>]*))?>/g;async function M(e,r,n=w){return h(e,async()=>{const i=g(e,r).catch(()=>{});let a;const o=new Promise(t=>{a=setTimeout(()=>t(void 0),n)}),l=await Promise.race([i,o]);return clearTimeout(a),l!==void 0?l:(f.debug("naming the people mentioned took too long; using the labels Slack sent"),g(e,k))})}const I=/<((?:https?|mailto):[^|>\s]+)(?:\|([^>]*))?>/g;function P(e){return e.replace(I,(r,n,i)=>i?.trim()||L(n)).replaceAll("<","<").replaceAll(">",">").replaceAll("&","&")}function L(e){return e.startsWith("mailto:")?e.slice(7):e}const k={lookup:async e=>({userId:e}),findByName:async()=>{}};async function h(e,r){try{return await r()}catch(n){return f.debug({err:n},"could not translate mentions; passing the text through"),e}}async function g(e,r){const n=[...e.matchAll(A)];if(n.length===0)return e;const i=await Promise.all(n.map(async l=>{const[,t,c,s,u,p,d,y]=l;if(t){const m=await r.lookup(t),b=c?.trim().replace(/^@+/,"");return`@${E(m)||b||t}`}return s?`@${s}`:u?`@${p?.trim().replace(/^@+/,"")||u}`:d?`#${y?.trim().replace(/^#+/,"")||d}`:l[0]}));let a="",o=0;return n.forEach((l,t)=>{const c=l.index??0;a+=e.slice(o,c)+i[t],o=c+l[0].length}),a+e.slice(o)}const T=/(^|[^\p{L}\p{N}@/<])@([\p{L}\p{N}][\p{L}\p{N}._-]{0,79})/gu,_=new Set(["here","channel","everyone","group"]);async function D(e,r){return h(e,async()=>{const n=e.split(N);return(await Promise.all(n.map((a,o)=>o%2===1?a:$(a,r)))).join("")})}async function $(e,r){const n=[...e.matchAll(T)];if(n.length===0)return e;const i=await Promise.all(n.map(async t=>{const c=t[1]??"",s=t[2]??"",u=s.replace(/[._-]+$/,""),p=s.slice(u.length);if(!u||_.has(u.toLowerCase()))return t[0];const d=await r.findByName(u);return d?`${c}<@${d.userId}>${p}`:t[0]}));let a="",o=0;n.forEach((t,c)=>{const s=t.index??0;a+=e.slice(o,s)+i[c],o=s+t[0].length});const l=a+e.slice(o);return l!==e&&f.debug("linked a mention in agent text"),l}export{D as linkMentions,E as mentionableName,P as plainText,M as renderMentions};
|
package/dist/teams/create.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import{randomUUID as
|
|
2
|
-
`)):
|
|
1
|
+
import{randomUUID as L}from"node:crypto";import{readFile as T}from"node:fs/promises";import{App as M}from"@microsoft/teams.apps";import{BishopError as $}from"../errors.js";import{log as h}from"../log.js";import{retry as p}from"../retry.js";import{teamsRetryable as u}from"./api.js";import{teamsAllowList as S}from"./authorize.js";import{DOWNLOAD_TIMEOUT_MS as U,teamsFilesOf as v}from"./files.js";import{teamsGraph as C}from"./graph.js";import{createTeamsInterface as k}from"./interface.js";import{teamsLogger as F}from"./logger.js";import{createListener as R}from"./server.js";import{graphAddressOf as E}from"./thread.js";const B=3978,D=500;async function q(e){const r=async(s,o)=>{try{return await T(s,"utf8")}catch(i){throw new $(`Couldn't read the TLS ${o} at ${s}: ${i.message}`,"ConfigError")}};return{cert:await r(e.cert,"certificate"),key:await r(e.key,"key")}}function w(e,r=[],s=[]){return{type:"message",text:e,...r.length>0?{attachments:r}:{},...s.length>0?{entities:s.map(o=>({type:"mention",mentioned:{id:o.id,name:o.name,role:"user"},text:o.text}))}:{}}}const P="application/vnd.microsoft.teams.card.file.consent",A="application/vnd.microsoft.teams.card.file.info";function O(e){if(!e||typeof e!="object")return;const r=e.ref;return typeof r=="string"?r:void 0}function _(e){const r={description:`Bishop wants to send ${e.name}.`,sizeInBytes:e.size,acceptContext:{ref:e.ref},declineContext:{ref:e.ref}};return{contentType:P,name:e.name,content:r}}function j(e){return{id:L(),contentType:A,contentUrl:e.webUrl,name:e.name,content:{uniqueId:e.uniqueId,fileType:e.fileType}}}async function N(e,r){const s=r.uploadInfo?.uploadUrl;if(!s)throw new Error("Teams returned no upload URL for the accepted file");const o=I(s),i=new Uint8Array(await T(e.path)),f=Math.max(0,i.byteLength-1),y=await fetch(o,{method:"PUT",headers:{"content-type":"application/octet-stream","content-length":String(i.byteLength),"content-range":`bytes 0-${f}/${i.byteLength}`},body:i});if(!y.ok)throw new Error(`Teams returned ${y.status} while receiving ${e.name}`)}async function ie(e){const r=S(e.config?.allow),s=R({port:e.config?.port??B,...e.config?.host?{host:e.config.host}:{},...e.config?.tls?{tls:await q(e.config.tls)}:{}}),o=new M({clientId:e.credentials.clientId,clientSecret:e.credentials.clientSecret,tenantId:e.credentials.tenantId,httpServerAdapter:s,logger:F()}),i=C(e.credentials),f=new Map,b=k({api:{async send(t,n,a){const c=a?.files??[];let d=n,g=[];a?.channel&&c.length>0?(g=(await Promise.all(c.map(m=>i.uploadChannelFile(a.channel,m)))).map(j),d=[n,...g.map(m=>`<attachment id="${m.id}"></attachment>`)].filter(Boolean).join(`
|
|
2
|
+
`)):c.length>0&&(g=c.map(l=>(f.set(l.ref,l),_(l))));try{const l=w(d,g,a?.mentions),m=await p(()=>o.send(t,l),{what:"teams send",retryable:u});return m?.id?{id:m.id}:void 0}catch(l){for(const m of c)f.delete(m.ref);throw l}},async update(t,n,a,c){await p(()=>o.api.conversations.updateActivity(t,n,w(a,[],c)),{what:"teams update",retryable:u})},async members(t){const n=await p(()=>o.api.conversations.getPagedMembers(t,D),{what:"teams roster page",retryable:u});return{members:n.members,complete:!n.continuationToken}},async channelInfo(t){return i.channel(t)},async channels(t){return i.channels(t)},async openChat(t){try{return(await p(()=>o.api.conversations.create({members:[{id:t,role:"user"}],tenantId:e.credentials.tenantId,channelData:{tenant:{id:e.credentials.tenantId}}}),{what:"teams open chat",retryable:u})).id}catch(n){if(n.response?.status===403)return;throw n}},async member(t,n){return await p(()=>o.api.conversations.getMemberById(t,n),{what:"teams roster",retryable:u})},async sharedFiles(t){const n=E(t);return n?i.files(n):[]},async history(t){const n=E(t);if(n)return i.conversation(n)},async fetchFile(t){const n=await fetch(I(t),{signal:AbortSignal.timeout(U)});if(!n.ok)throw new Error(`the download returned ${n.status}`);if(!n.body)throw new Error("the download sent no body");return n.body},async fetchAttachment(t){const n=I(t),a=new URL(o.api.serviceUrl);if(n.host!==a.host)throw h.warn({host:n.host,service:a.host},"refusing to send the app credential to a host that is not the Teams service"),new Error("the image is not hosted on the Teams service");const c=await o.api.http.get(n.toString(),{responseType:"arraybuffer",timeout:U});return G(c.data)},async fetchShared(t){return i.fetchShared(t)}},allow:r,...e.config?.verbosity?{verbosity:e.config.verbosity}:{},listener:s,meta:e.meta,model:e.model,...e.configuredModel?{configuredModel:e.configuredModel}:{},threads:e.threads,events:e.events,files:e.files,attachments:e.attachments,principals:e.principals,tenantId:e.credentials.tenantId,run:e.run,stop:e.stop});o.on("message",async({activity:t})=>{await b.handle(H(t))}),o.on("conversationUpdate",async({activity:t})=>{const n=t.channelData?.channel?.id,a=t.channelData?.team?.aadGroupId;b.changed({conversationId:t.conversation.id,...typeof n=="string"?{channelId:n}:{},...typeof a=="string"?{teamId:a}:{}})}),o.on("file.consent.accept",async({activity:t,send:n})=>(await x(t.value,n),{status:200})),o.on("file.consent.decline",async({activity:t,send:n})=>{const a=O(t.value.context),c=a?f.get(a):void 0;return a&&f.delete(a),c?(await n(w(`${c.name} was not sent because the file-consent request was declined.`)),{status:200}):(h.warn("received a Teams file-consent decline for an unknown file"),{status:200})});async function x(t,n){const a=O(t.context),c=a?f.get(a):void 0;if(a&&f.delete(a),!c){h.warn("received a Teams file-consent acceptance for an unknown file");return}try{await N(c,t);const d=t.uploadInfo;if(!d?.contentUrl||!d.uniqueId)throw new Error("Teams returned incomplete file information after the upload");await n(w(`${c.name} was sent.`,[{contentType:A,contentUrl:d.contentUrl,name:d.name??c.name,content:{uniqueId:d.uniqueId,fileType:d.fileType??c.mimeType}}]))}catch(d){h.warn({err:d,file:c.name},"could not finish a Teams file upload"),await n(w(`Could not send ${c.name}: ${d.message}`))}}return await o.initialize(),h.info({endpoint:o.server.messagingEndpoint},"the Teams app is mounted"),b}function I(e){const r=new URL(e);if(r.protocol!=="https:")throw new Error("the download URL is not HTTPS");return r}function G(e){if(e instanceof Uint8Array)return e;if(e instanceof ArrayBuffer)return new Uint8Array(e);throw new Error("the download came back as something other than bytes")}function z(e){const r=e.recipient?.id,s=r?e.stripMentionsText?.({accountId:r}):e;return(s?.stripMentionsText?.({tagOnly:!0})??s)?.text??e.text??""}function H(e){const r=J(e.channelData);return{id:e.id,conversationId:e.conversation.id,conversationType:typeof e.conversation.conversationType=="string"?e.conversation.conversationType:"channel",text:z(e),...W(e.attachments),mentioned:e.isRecipientMentioned?.()===!0,from:{id:e.from.id,...e.from.aadObjectId?{aadObjectId:e.from.aadObjectId}:{},...e.from.name?{name:e.from.name}:{}},...e.recipient?.id?{recipientId:e.recipient.id}:{},...e.recipient?.name?{recipientName:e.recipient.name}:{},files:v(e.attachments),...r?{channel:r}:{}}}function W(e){const r=e?.find(s=>s.contentType?.toLowerCase()==="text/html"&&typeof s.content=="string")?.content;return typeof r=="string"?{html:r}:{}}function J(e){const r=e?.team?.aadGroupId,s=e?.channel?.id;if(!(typeof r!="string"||typeof s!="string"))return{teamId:r,channelId:s}}export{ie as createTeams,H as inboundOf};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{BishopError as t}from"../errors.js";import{log as m}from"../log.js";import{findPeople as w}from"./mentions.js";import{baseConversation as p,channelHandleOf as u,destinationOfHandle as f}from"./thread.js";const y=/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;async function k(a,e,r={}){const n=e.trim();if(y.test(n)){const h=a.nameOf(n);return h?d(a,n,`@${h}`,h):d(a,n,"a personal Teams chat","this person")}const o=r.from?.interface==="teams"?r.from.handle:void 0;if(n.startsWith("#"))return $(a,n.slice(1).trim(),o);if(n.startsWith("@"))return T(a,n.slice(1).trim(),o,r);throw new t(`can't tell where "${e}" is in Teams. Write #channel for a channel in this team, or @person for somebody in this conversation.`)}async function $(a,e,r){if(!e)throw new t("a Teams channel needs a name after the #");const n=r?f(r):void 0;if(n?.scope!=="channel")throw new t(`Bishop finds #${e} in the team it is asked from, and this conversation is not in a team. Ask in a channel of the team you mean.`);const o=n.channel?.teamId;if(!o)throw new t(`Teams did not say which team this channel belongs to, so Bishop cannot look for #${e}. Ask for the report in the channel itself instead.`);let h;try{h=await a.api.channels(o)}catch(s){throw m.warn({err:s,team:o},"could not list the channels in a Teams team"),s.status===403?new t("This team has not given the app permission to list its channels. Reinstalling the app in the team grants it."):new t("Bishop could not list the channels in this team just now.")}const i=h.find(s=>s.name===e);if(!i){const s=h.find(c=>c.name.toLowerCase()===e.toLowerCase());throw new t(s?`No channel in this team is named exactly #${e}. Did you mean #${s.name}?`:`No channel in this team is named #${e}.`)}if(i.membershipType&&i.membershipType!=="standard"&&i.id!==n.channel?.channelId)throw new t(`#${i.name} is a ${i.membershipType} channel, and Bishop can't tell from here whether the app is in it. Ask for the report from inside that channel.`);return{interface:"teams",handle:u(i.id,{teamId:o,channelId:i.id}),label:`#${i.name}`}}async function T(a,e,r,n){if(!e)throw new t("a Teams person needs a name after the @");if(!r)throw new t(`Bishop finds @${e} among the people in the conversation it is asked from, and there is none here.`);const o=p(f(r).conversation),h=await a.roster.of(o);if(!h)throw new t("Bishop could not read who is in this conversation just now.");const i=w(h.people,e);if(i.length===0)throw new t(`Nobody in this conversation is called ${e}`+(h.complete?".":" among the people Bishop can see.")+" Ask from a conversation they are in.");if(i.length>1){const c=i.map(l=>l.email?`${l.name} (${l.email})`:l.name).join(", ");throw new t(`More than one person here could be ${e}: ${c}. Say which.`)}const[s]=i;if(n.optedInOnly){const c=await a.api.member(o,s.id).catch(l=>{throw m.warn({err:l,conversation:o},"could not read the Teams roster"),new t(`Bishop could not check whether ${s.name} is on the allow list just now.`)});if(!c||!a.allows(c))throw new t(`${s.name} is not on this agent's allow list, so Bishop won't start sending them messages they never asked for. Add them to teams.allow in .bishop/config.json, or post to a channel they are in instead.`)}return d(a,s.id,`@${s.name}`,s.name)}async function d(a,e,r,n=r){const o=await a.api.openChat(e).catch(h=>{throw m.warn({err:h},"could not open a Teams personal chat"),new t(`Teams could not open a chat with ${n} just now.`)});if(!o)throw new t(`Teams won't let the agent message ${n}: they have not added the app, and are not in a team that has it. They can add it in Teams, or the report can go to a channel they are in.`);return{interface:"teams",handle:`${o};scope=personal`,label:r}}export{k as resolveTeamsDestination};
|
package/dist/teams/fake.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{FILE_DOWNLOAD_INFO as
|
|
1
|
+
import{FILE_DOWNLOAD_INFO as N,GRAPH_REFERENCE as $}from"./files.js";function v(){const n=[],p=[],u=[],f=[],m=[],y=[],w=[],g=new Map,b=new Map,x=new Map,E=[],F=new Map,M=new Map,d=new Map,T=new Map;let i,c,a,C=!1,_=1;const l=(e,t)=>{f.push({url:e,credential:t});const r=T.get(e);if(r===void 0)throw new Error(`nothing hosted at ${e}`);return new TextEncoder().encode(r)};return{sent:n,edits:p,rosterCalls:u,fetched:f,sharedCalls:m,historyCalls:y,listCalls:w,openCalls:E,addTeam(e,t){b.set(e,t)},addReachable(e,t){x.set(e,t)},async channels(e){const t=b.get(e);if(t instanceof Error)throw t;if(!t)throw Object.assign(new Error("Graph answered with 404"),{status:404});return t},async openChat(e){return E.push(e),x.get(e)},addMember(e,t){d.set(`${e}:${t.id}`,t)},addChannel(e,t){g.set(e,t)},addBytes(e,t){T.set(e,t)},addShared(e,t){M.set(e,t)},addHistory(e,t){F.set(e,t)},failNextSend(e=new Error("Teams is down")){i=e},failNextUpdate(e=new Error("Teams is down")){c=e},sendAnonymously(){C=!0},failMember(e=new Error("no roster access")){a=e},async send(e,t,r){if(i){const o=i;throw i=void 0,o}const s=`activity-${_++}`;return n.push({conversationId:e,text:t,id:s,...r?.files?.length?{files:r.files}:{},...r?.mentions?.length?{mentions:r.mentions}:{}}),C?void 0:{id:s}},async update(e,t,r,s){if(c){const h=c;throw c=void 0,h}p.push({conversationId:e,activityId:t,text:r,...s?.length?{mentions:s}:{}});const o=n.find(h=>h.id===t);if(!o)throw new Error(`no activity ${t} to update`);o.text=r,s?.length?o.mentions=s:delete o.mentions},async member(e,t){if(u.push(e),a)throw a;return d.get(`${e}:${t}`)},async members(e){if(w.push(e),a)throw a;const t=`${e}:`;return{members:[...d].filter(([s])=>s.startsWith(t)).map(([,s])=>s),complete:!0}},async channelInfo({channelId:e}){return g.get(e)},async sharedFiles(e){return e.conversationType==="personal"?[]:(m.push(e.id),M.get(e.id)??[])},async history(e){if(e.conversationType==="personal")return;y.push(e.id);const t=F.get(e.conversationId);if(!t)return;const r=s=>Number(s.id)<Number(e.id);return{...t,messages:t.messages.filter(r)}},async fetchFile(e){return l(e,"none")},async fetchAttachment(e){return l(e,"bot")},async fetchShared(e){return l(e,"graph")}}}function A(n={}){return{contentType:N,name:"doc.txt",uniqueId:"1150D938-8870-4044-9F2C-5BBDEBA70C9D",contentUrl:"https://contoso.sharepoint.com/personal/jerry_contoso_com/Documents/doc.txt",etag:"123",downloadUrl:"https://download.contoso.com/doc.txt?tempauth=abc",...n}}function I(n={}){return{contentType:$,name:"deploy.log",uniqueId:"fe1a6fcf-0000-4000-8000-00000000000b",etag:'"{FE1A6FCF-0000-4000-8000-00000000000B},2"',contentUrl:"https://contoso.sharepoint.com/sites/ops/Shared Documents/General/deploy.log",size:24,mimeType:"text/plain",...n}}function B(n={}){return{contentType:"image/png",contentUrl:"https://smba.trafficmanager.net/amer/v3/attachments/img-1/views/original",...n}}function O(n={}){return{id:"1700000000001",conversationId:"19:chat@thread.v2",conversationType:"personal",text:"what is the deploy status",mentioned:!1,recipientId:"28:the-agent",recipientName:"bishop",files:[],...n,from:{id:"29:bot-scoped-id",aadObjectId:"00000000-0000-0000-0000-00000000000a",name:"Hayes",...n.from}}}function S(n={}){return{id:"1700000000000",at:17e11,user:{id:"00000000-0000-0000-0000-00000000000b",name:"Kim"},html:"<p>the deploy is stuck on step three</p>",appMentions:[],files:[],...n}}export{A as aFile,S as aGraphMessage,O as aMessage,I as aSharedFile,B as anImage,v as fakeTeams};
|
package/dist/teams/graph.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{readFile as
|
|
1
|
+
import{readFile as C}from"node:fs/promises";import{log as p}from"../log.js";import{retry as _}from"../retry.js";import{DOWNLOAD_TIMEOUT_MS as E,GRAPH_REFERENCE as U}from"./files.js";const w="https://graph.microsoft.com/v1.0",O="https://login.microsoftonline.com",T=1e4,x=2,L=1e3,B=2e4,R=50,F=3,z=10,q=6e4,H=2,Y=1e3,j=6e4;class m extends Error{status;retryAfterMs;constructor(i,c,d){super(i),this.status=c,this.retryAfterMs=d,this.name="GraphError"}}function G(e){const{status:i,retryAfterMs:c}=e??{};return typeof i!="number"?!1:i===0?!0:i===429?c===void 0?!0:{retryAfterMs:c}:i>=500}function b(e){const i=e.headers.get("retry-after");if(i===null)return;const c=Number(i);return Number.isFinite(c)&&c>=0?c*1e3:void 0}function M(e){return`u!${Buffer.from(e,"utf8").toString("base64url")}`}function W(e){if(e.kind==="chat")return`/chats/${e.chatId}/messages/${e.messageId}`;const i=`/teams/${e.teamId}/channels/${e.channelId}/messages`;return e.replyToId&&e.replyToId!==e.messageId?`${i}/${e.replyToId}/replies/${e.messageId}`:`${i}/${e.messageId}`}function D(e){if(!(e.contentType!==U||!e.contentUrl))return{contentType:U,contentUrl:e.contentUrl,...e.id?{uniqueId:e.id}:{},...e.name?{name:e.name}:{}}}function X(e){if(!e.id||e.messageType&&e.messageType!=="message"||e.deletedDateTime)return;const i=e.createdDateTime?Date.parse(e.createdDateTime):Number.NaN,c=e.from?.user,d=e.from?.application,g=e.body?.content??"";return{id:e.id,...Number.isFinite(i)?{at:i}:{},...c?.id?{user:{id:c.id,...c.displayName?{name:c.displayName}:{}}}:{},...d?.id?{app:{id:d.id}}:{},html:e.body?.contentType==="text"?g.replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">"):g,appMentions:(e.mentions??[]).flatMap(h=>{const $=h.mentioned?.application?.id;return $&&h.id!==void 0?[{id:String(h.id),appId:$}]:[]}),files:(e.attachments??[]).flatMap(h=>D(h)??[])}}function k(e,i){return/^\d+$/.test(e)&&/^\d+$/.test(i)?Number(e)<Number(i):e!==i}function v(e){return[...e].sort((i,c)=>(i.at??0)-(c.at??0)||(k(i.id,c.id)?-1:1))}function Z(e){let i;async function c(s){if(i&&i.expiresAt>Date.now())return i.token;const r=await fetch(`${O}/${e.tenantId}/oauth2/v2.0/token`,{method:"POST",headers:{"content-type":"application/x-www-form-urlencoded"},body:new URLSearchParams({client_id:e.clientId,client_secret:e.clientSecret,grant_type:"client_credentials",scope:"https://graph.microsoft.com/.default"}),signal:s}).catch(n=>{throw new m(`could not reach Entra: ${n.message}`,0)});if(!r.ok)throw new m(`Entra refused the app credentials with ${r.status}`,r.status,b(r));const t=await r.json();if(!t.access_token)throw new m("Entra returned no access token",0);const a=(t.expires_in??3600)*1e3;return i={token:t.access_token,expiresAt:Date.now()+a-j},i.token}async function d(s,r,t){return _(async()=>{const a=await fetch(s.startsWith(`${w}/`)?s:`${w}${s}`,{headers:{authorization:`Bearer ${await c(t)}`},signal:t}).catch(n=>{throw new m(`could not reach Graph: ${n.message}`,0)});if(!a.ok)throw new m(`Graph answered ${r} with ${a.status}`,a.status,b(a));return await a.json()},{what:`graph ${r}`,retryable:G,attempts:x,maxDelayMs:L})}async function g(s,r,t,a){return _(async()=>{const n=await fetch(`${w}${s}`,{method:"PUT",headers:{authorization:`Bearer ${await c(a)}`,"content-type":"application/octet-stream","content-length":String(t.byteLength)},body:t,signal:a}).catch(o=>{throw new m(`could not reach Graph: ${o.message}`,0)});if(!n.ok)throw new m(`Graph answered ${r} with ${n.status}`,n.status,b(n));return await n.json()},{what:`graph ${r}`,retryable:G,attempts:H,maxDelayMs:Y})}async function h(s,r,t,a){try{const o=(await d(`/drives/${encodeURIComponent(s)}/items/${encodeURIComponent(r)}?$select=sharepointIds`,"the SharePoint identifiers of an uploaded file",a)).sharepointIds?.listItemUniqueId;if(o)return o}catch(n){p.debug({err:n,itemId:r},"could not read the SharePoint ID of an uploaded file")}return t?.match(/\{([0-9a-fA-F-]{36})\}/)?.[1]}async function $(s,r){const t=await d(`/teams/${encodeURIComponent(s.teamId)}/channels/${encodeURIComponent(s.channelId)}/filesFolder`,"a channel files folder",r);if(!t.id||!t.parentReference?.driveId)throw new m("Graph returned an incomplete channel files folder",0);return{driveId:t.parentReference.driveId,folderId:t.id}}async function N(s,r){try{return await d(`/shares/${M(s)}/driveItem`,"a shared file",r)}catch(t){const a=t instanceof m?t.status:0;return p.warn({err:t,status:a},"could not read a file shared in a Teams conversation"),a===401||a===403?{refused:"Bishop is not allowed to read files in this Microsoft 365 tenant"}:void 0}}async function I(s,r,t,a){const n=[];let o=s;try{for(let u=0;o&&u<r;u+=1){const l=await d(o,t,a);n.push(...l.value??[]);const f=l["@odata.nextLink"];if(o=f?.startsWith(`${w}/`)?f:void 0,f&&!o)return p.warn({what:t},"Graph offered a next page somewhere other than Graph"),{raw:n,complete:!1,failed:!1}}return{raw:n,complete:!o,failed:!1}}catch(u){return p.warn({err:u,what:t,read:n.length},"could not read the whole conversation"),{raw:n,complete:!1,failed:!0}}}const A=new Set;async function S(s,r,t,a,n){try{return await d(s,r,n)}catch(o){const u=`${t} ${a}`;o.status===403&&!A.has(u)?(A.add(u),p.warn({team:t,permission:a},"this team never consented to a permission the agent is told about its channels with; they are reported as unknown until the app is reinstalled there")):p.debug({err:o,team:t},`could not read ${r}`);return}}return{async channel({teamId:s,channelId:r}){const t=AbortSignal.timeout(T),a=encodeURIComponent(s),[n,o]=await Promise.all([S(`/teams/${a}/channels/${encodeURIComponent(r)}?$select=displayName,membershipType`,"a channel",s,"ChannelSettings.Read.Group",t),S(`/teams/${a}?$select=visibility`,"a team",s,"TeamSettings.Read.Group",t)]);if(n)return{...n.displayName?{name:n.displayName}:{},...n.membershipType?{membershipType:n.membershipType}:{},...o?.visibility?{teamVisibility:o.visibility}:{}}},async channels(s){const r=AbortSignal.timeout(T);return((await d(`/teams/${encodeURIComponent(s)}/channels?$select=id,displayName,membershipType`,"a team\u2019s channels",r)).value??[]).flatMap(a=>a.id&&a.displayName?[{id:a.id,name:a.displayName,...a.membershipType?{membershipType:a.membershipType}:{}}]:[])},async conversation(s){const r=AbortSignal.timeout(B),t=l=>{const f=new Set;return l.flatMap(P=>{const y=X(P);return!y||!k(y.id,s.messageId)||f.has(y.id)?[]:(f.add(y.id),[y])})};if(s.kind==="chat"){const l=await I(`/chats/${s.chatId}/messages?$top=${R}&$orderby=createdDateTime desc`,F,"a chat",r);return l.failed&&l.raw.length===0?void 0:{messages:v(t(l.raw)),complete:l.complete}}const a=s.replyToId;if(!a||a===s.messageId)return{messages:[],complete:!0};const n=`/teams/${s.teamId}/channels/${s.channelId}/messages/${a}`,[o,u]=await Promise.all([d(n,"the message that opened a thread",r).then(l=>l,l=>{p.warn({err:l},"could not read the message that opened a Teams thread")}),I(`${n}/replies?$top=${R}`,z,"a thread",r)]);if(!(!o&&u.failed&&u.raw.length===0))return{messages:v(t([...o?[o]:[],...u.raw])),complete:!!o&&u.complete}},async files(s){const r=AbortSignal.timeout(T);let t;try{t=await d(W(s),"a message",r)}catch(n){return p.warn({err:n,address:s.kind},"could not read a Teams message from Graph"),[]}const a=(t.attachments??[]).flatMap(n=>D(n)??[]);return Promise.all(a.map(async n=>{const o=await N(n.contentUrl,r);return o?"refused"in o?{...n,unavailable:o.refused}:{...n,name:n.name??o.name,...o.eTag?{etag:o.eTag}:{},...o.size===void 0?{}:{size:o.size},...o.file?.mimeType?{mimeType:o.file.mimeType}:{}}:n}))},async fetchShared(s){const r=AbortSignal.timeout(E),t=await fetch(`${w}/shares/${M(s)}/driveItem/content`,{headers:{authorization:`Bearer ${await c(r)}`},redirect:"manual",signal:r});if(t.status>=300&&t.status<400){const a=t.headers.get("location");if(!a)throw new Error("Graph redirected the download to nowhere");const n=await fetch(a,{signal:AbortSignal.timeout(E)});if(!n.ok)throw new Error(`the download returned ${n.status}`);if(!n.body)throw new Error("the download sent no body");return n.body}if(!t.ok)throw new Error(`Graph answered the download with ${t.status}`);if(!t.body)throw new Error("the download sent no body");return t.body},async uploadChannelFile(s,r){const t=AbortSignal.timeout(q),a=new Uint8Array(await C(r.path)),n=await $(s,t),o=await g(`/drives/${encodeURIComponent(n.driveId)}/items/${encodeURIComponent(n.folderId)}:/${encodeURIComponent(r.name)}:/content`,`the file ${r.name} in a Teams channel`,a,t);if(!o.id||!o.webUrl)throw new m(`Graph returned an incomplete upload for ${r.name}`,0);return{id:o.id,uniqueId:await h(n.driveId,o.id,o.eTag,t),name:o.name??r.name,webUrl:o.webUrl,fileType:o.file?.mimeType??r.mimeType}}}}export{G as graphRetryable,Z as teamsGraph};
|
package/dist/teams/interface.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import{runCommand as
|
|
2
|
-
`)}function
|
|
1
|
+
import{runCommand as tn}from"../commands/run.js";import{BishopError as on}from"../errors.js";import{etiquetteNote as an,threadParticipants as rn}from"../interface/etiquette.js";import{splitMessage as sn}from"../interface/format.js";import{showsProgress as cn,showsTasks as ln,threadVerbosity as hn}from"../interface/verbosity.js";import{log as h}from"../log.js";import{teamsCommandLine as dn}from"./command.js";import{teamsObserver as mn}from"./context.js";import{resolveTeamsDestination as fn}from"./destination.js";import{describeTeamsFile as un,parseTeamsFile as wn,teamsAttachment as G,teamsFilePayload as yn}from"./files.js";import{landedMidConversation as pn,teamsHistory as vn}from"./history.js";import{findPeople as gn,linkMentions as bn,teamsRoster as Tn}from"./mentions.js";import{teamsPresence as In}from"./presence.js";import{teamsSighting as L}from"./principal.js";import{addressesAgent as E,routeActivity as kn}from"./route.js";import{baseConversation as On,conversationOf as N,destinationHandleOf as xn,destinationOfHandle as Sn,fileRouteOf as W,sameConversation as An,scopeOf as Y,threadIdOf as K,threadIn as M,threadKeyOf as En}from"./thread.js";function Nn(o){return[...Kn(o),...Mn].join(`
|
|
2
|
+
`)}function Kn(o){return cn(o)?["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.","",ln(o)?"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 Mn=["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.","","People are named to you by their Teams display name. Write @ and the name exactly as you were",'given it, spaces included, and Bishop makes it a real mention, as in "@Kim Lee, can you take','a look?". A name that does not match exactly one person stays plain text. You can always',"mention the person whose message you are answering. @here, @channel, @everyone and team","mentions are never created, whoever writes them.","",'Asked to mention somebody by part of their name, like "mention Kim", look them up with',"teams_find_person first. If one person matches, write @ and the full name it gives. If more","than one does, ask which one was meant rather than guessing.","","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."],_n="You are not on this agent\u2019s allow list, so it can\u2019t answer you.",z="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.",Bn=z+" If you attached a file, nothing about it reached me, so send it in a direct chat.",Pn=2e4,Cn=3600*1e3;function Zn(o){const p=new Set,b=new Map,_=new Map,I=rn(),B=mn({api:o.api,...o.tenantId?{tenantId:o.tenantId}:{}}),g=Tn(o.api),k=hn(o.meta,"teams",o.verbosity),U={verbosity:k,model:o.model,configured:o.configuredModel??{},stop:o.stop},O=new Map;let w,P=!1,C=!1;function x(e){return{name:"teams",guidance:Nn(k.of(e.id).level),...w?{self:w}:{}}}async function D(e,n,t){if(!n)return;const i=b.get(n);if(i)return i;if((_.get(n)??0)>Date.now())return;const a=On(e);let r;try{r=await o.api.member(a,t),r&&b.set(n,r)}catch(s){h.warn({err:s,conversation:a},"could not read the Teams roster")}return!r&&o.allow.open&&_.set(n,Date.now()+Cn),r}async function y(e,n,t={}){const{files:i=[],opened:a}=t;let{channel:r}=t,s=e,c;const v=n?sn(n,Pn):[""];for(const[d,m]of v.entries()){const f=d===v.length-1,u=t.mentions?await bn(m,t.mentions):{text:m,mentions:[]},T=f&&(i.length>0||r)||u.mentions.length>0?{...f&&i.length>0?{files:i}:{},...f&&r?{channel:r}:{},...u.mentions.length>0?{mentions:u.mentions}:{}}:void 0,l=await o.api.send(s,u.text,T);a&&d===0&&l&&(a.root=l.id,s=M(e,l.id),r&&(r={...r,threadId:l.id})),c=l?{id:l.id,text:u.text,...u.mentions.length>0?{mentions:u.mentions}:{}}:void 0}return c}function F(e,n,t){const i=[n?.handle,n?.name].filter(a=>!!a);return{...i.length>0&&t?{sender:{id:t,names:i}}:{},roster:async()=>(await g.of(e))?.people}}function j(e,n){return[e,n?.userPrincipalName,n?.email].filter(t=>!!t).map(t=>t.trim().toLowerCase())}function S(e,n){return o.allow.open||j(e,n).some(t=>o.allow.allows(t))}function R(e,n){const t=yn(n);if(!t)return;const i=un(n);return o.attachments.remember(e,"teams",{id:i.id,name:i.name,payload:t})}async function q(e,n){return n.files.length===0?[]:Promise.all(n.files.map(async t=>{const i=R(e.id,t),a=await o.files.save(e.dirKey,G(t,o.api));return i?{...a,ref:i}:a}))}function Q(e,n,t){return o.principals.see(x(e).name,L({id:n.from.aadObjectId??n.from.id,...n.from.name?{name:n.from.name}:{},bot:n.from.id.startsWith("28:")},t,o.tenantId))}function H(e,n){return o.principals.see("teams",{externalId:e,...n?{handle:n,name:n}:{},kind:"bot",affiliation:"unknown",self:!0})}async function V(e,n){const t=x(e),i=N(n),a=K(i),r=n.recipientId?.replace(/^28:/,"");return vn({read:await o.api.history(n),...a?{openedBy:a}:{},...r?{selfAppId:r}:{},...w?{selfName:w.address}:{},...n.recipientId?{self:()=>H(n.recipientId,n.recipientName)}:{},sender:async s=>{const c=await D(i,s.id,s.id);return{allowed:S(s.id,c),principal:()=>o.principals.see(t.name,L({id:s.id,...s.name?{name:s.name}:{},bot:!1},c,o.tenantId))}},remember:s=>R(e.id,s),saw:(s,c)=>I.saw(e.threadKey,s,c)})}function X(e,n,t,i){const a=N(n),r=K(a),s=n.channel?{teamId:n.channel.teamId,channelId:n.channel.channelId,...r?{threadId:r}:{}}:void 0,c=an({participants:I.count(e.threadKey),mentioned:E(n)}),v=F(a,t,n.from.id);return{thread:e,isNewThread:i,get origin(){return x(e)},...c?{note:c}:{},...pn(n)?{history:()=>V(e,n)}:{},observe:()=>B.observe({conversationId:n.conversationId,scope:Y(n),...n.channel?{channel:n.channel}:{},speaker:t.affiliation}),async messages(){const d=await q(e,n);return[{principal:t,externalId:n.id,text:n.text,...d.length>0?{files:d}:{}}]},presence:()=>{const d=In({send:(m,f)=>y(a,m,{...f?{files:f}:{},...s?{channel:s}:{},mentions:v}),post:m=>o.api.send(a,m),update:(m,f,u)=>o.api.update(a,m,f,u),threadKey:e.threadKey,verbosity:()=>k.of(e.id).level,...W(Y(n),s)==="none"?{share:!1}:{},...c?{mayDecline:!0}:{}});return O.set(e.threadKey,d),{...d,finish:async()=>{try{await d.finish()}finally{O.delete(e.threadKey)}}}},async notify(d){await y(a,d)},async destination(){return{interface:"teams",handle:xn(n),label:Dn(n)}}}}async function J(e){if(C)return;if(await o.events.seen(`teams:${e.id}`)){h.debug({activity:e.id},"already answered this Teams activity");return}!w&&e.recipientName&&(w={address:`@${e.recipientName}`},h.info({address:w.address},"the agent is known by this name in Teams")),!P&&e.recipientId&&(P=!0,H(e.recipientId,e.recipientName));const n=En(e),t=await o.threads.has(n),i=kn(e,{threadKnown:t});if(i.action==="ignore"){h.debug({activity:e.id,thread:n,reason:i.reason},"leaving a Teams message alone");return}const a=e.from.aadObjectId;if(!a){h.warn({activity:e.id},"refusing a Teams message with no Entra object ID");return}I.saw(n,a,Date.now());const r=N(e),s=await D(r,a,e.from.id);if(!S(a,s)){h.warn({activity:e.id,who:a,conversation:r},"refusing an unauthorized Teams message"),E(e)&&await y(r,_n).catch(l=>{h.debug({err:l},"could not tell somebody they are not on the allow list")});return}const c=dn(e);if(c){const l=await o.threads.remember(n),A=tn(c,{thread:l},U);try{await y(r,A),O.get(n)?.interrupted()}catch(en){h.warn({thread:n,err:en},"couldn't answer a command")}h.info({thread:n,who:a},"ran a command");return}const v=new Set(e.files.map(l=>l.contentUrl)),d=(await o.api.sharedFiles(e)).filter(l=>!v.has(l.contentUrl)),m=d.length>0?{...e,files:[...e.files,...d]}:e;if(!m.text.trim()&&m.files.length===0){if(h.info({activity:e.id},"a Teams message had nothing to act on"),E(e)){const l=e.conversationType==="personal"?z:Bn;await y(r,l).catch(A=>{h.debug({err:A},"could not answer a message with nothing in it")})}return}const f=await o.threads.remember(n),u=Q(f,m,s);h.info({thread:n,who:a,newThread:!t},"running a turn from Teams");const T=o.run(X(f,m,u,!t)).catch(l=>{h.error({thread:n,err:l},"a turn from Teams failed")}).finally(()=>{p.delete(T)});p.add(T)}async function $(){for(;p.size>0;)await Promise.allSettled([...p])}function Z(e){return e==="groupChat"?"Bishop cannot send files to Teams group chats yet.":e==="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 nn(e){const n=Sn(e.handle),t=e.files??[],i=t.map(s=>s.name).join(", ");if(t.length>0&&W(n.scope,n.channel)==="none")throw new on(Z(n.scope));const a=n.scope==="channel"&&!K(n.conversation)?{}:void 0,r=F(n.conversation);try{await y(n.conversation,e.text,{...n.channel?{channel:n.channel}:{},mentions:r,files:t,...a?{opened:a}:{}})}catch(s){if(!t.length)throw s;h.warn({handle:e.handle,files:i,err:s},"could not attach files to a Teams report");const c=a?.root;await y(c?M(n.conversation,c):n.conversation,`${e.text}
|
|
3
3
|
|
|
4
|
-
_Could not attach ${i}; the report was sent without the file._`,
|
|
4
|
+
_Could not attach ${i}; the report was sent without the file._`,{...n.channel?{channel:c?{...n.channel,threadId:c}:n.channel}:{},mentions:r,...a&&!c?{opened:a}:{}})}return a?.root?{threadKey:`teams:${M(n.conversation,a.root)}`}:{}}return{name:"teams",handle:J,changed:e=>{B.changed(e),g.changed(e.conversationId),e.channelId&&g.changed(e.channelId)},settle:$,deliver:nn,allows(e){return o.allow.open?!0:[...j(e.externalId,b.get(e.externalId)),...e.email?[e.email.trim().toLowerCase()]:[]].some(t=>o.allow.allows(t))},sameConversation:An,async findPeople(e,n){const t=await g.of(e.threadKey.replace(/^teams:/,""));return t?{kind:"people",people:gn(t.people,n).map(a=>({name:a.name,...a.email?{email:a.email}:{}})),complete:t.complete}:{kind:"refused",reason:"Bishop could not read who is in this conversation just now."}},resolveDestination:(e,n)=>fn({api:o.api,roster:g,allows:t=>S(t.aadObjectId,t),nameOf:t=>b.get(t)?.name},e,n),attachment:e=>{const n=wn(e);return n?G(n,o.api):void 0},async start(){await o.listener.start(),h.info({allow:o.allow.open?"anyone in the tenant":"a list"},"listening for Teams activities")},async stop(){C=!0,await o.listener.stop(),p.size>0&&(h.info({turns:p.size},"waiting for Teams turns still running"),await $())}}}function Dn(o){return o.conversationType==="personal"?`a chat with ${o.from.name??"someone"}`:o.conversationType==="groupChat"?"a Teams group chat":"a Teams channel thread"}export{Zn as createTeamsInterface,Nn as teamsGuidance};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{CODE as g}from"../interface/mentions.js";import{log as d}from"../log.js";import{baseConversation as m}from"./thread.js";const w=2e3,h=/(^|[^\p{L}\p{N}@/<])@(?=[\p{L}\p{N}])/gu,L=new Set(["here","channel","everyone","team"]),y=new RegExp("^\\s+\\p{Lu}","u");async function R(t,r,o=w){const s=t.split(g);if(!s.some((e,n)=>n%2===0&&new RegExp(h).test(e)))return{text:t,mentions:[]};try{const e=await b(r.roster?.(),o)??[],n=r.sender,a=n?n.names.map(l=>({id:n.id,name:l})):[],i=x([...e,...a]),c=[],u=s.map((l,f)=>f%2===1?l:S(l,i,n?.id,c)).join("");return c.length>0&&d.debug({mentions:c.length},"linked mentions in agent text"),{text:u,mentions:c}}catch(e){return d.debug({err:e},"could not link mentions; sending the text as written"),{text:t,mentions:[]}}}function x(t){return t.filter(r=>r.name.trim()).sort((r,o)=>o.name.trim().length-r.name.trim().length)}function S(t,r,o,s){let e="",n=0;for(const a of t.matchAll(h)){const i=(a.index??0)+(a[1]??"").length;if(i<n)continue;const c=t.slice(i+1),u=T(c,r,o);if(!u)continue;const l=c.slice(0,u.name.trim().length),f=`<at>${l}</at>`;s.push({id:u.id,name:u.name,text:f}),e+=t.slice(n,i)+f,n=i+1+l.length}return e+t.slice(n)}function T(t,r,o){let s=0;const e=new Map;for(const i of r){const c=i.name.trim();if(s>0&&c.length<s)break;k(t,c)&&(s=c.length,e.has(i.id)||e.set(i.id,i))}if(e.size===0)return;const n=t.slice(0,s);if(L.has(n.toLowerCase()))return;const a=o!==void 0&&e.has(o);if(!(!a&&!/\s/.test(n)&&y.test(t.slice(s)))){if(e.size===1)return[...e.values()][0];if(a)return e.get(o);d.debug({hits:e.size},"a written name matched more than one person")}}function k(t,r){return t.slice(0,r.length).toLowerCase()!==r.toLowerCase()?!1:!/^[\p{L}\p{N}]/u.test(t.slice(r.length))}async function b(t,r){if(!t)return;let o;const s=new Promise(e=>{o=setTimeout(()=>{d.debug("the Teams roster took too long; only the sender can be mentioned"),e(void 0)},r)});try{return await Promise.race([t.catch(()=>{}),s])}finally{clearTimeout(o)}}const C=10*6e4,_=6e4;function A(t,r=Date.now){const o=new Map;async function s(e,n){try{const{members:a,complete:i}=await t.members(e);return i||d.debug({conversation:e},"a roster bigger than one page; using the first page"),{people:a.flatMap(u=>{const l=u.name?.trim(),f=(u.email??u.userPrincipalName)?.trim();return l?[{id:u.id,name:l,...f?{email:f}:{}}]:[]}),complete:i}}catch(a){d.debug({conversation:e,err:a},"could not read a Teams roster for mentions"),n.expires=r()+_;return}}return{of(e){const n=m(e),a=o.get(n);if(a&&a.expires>r())return a.roster;for(const[c,u]of o)u.expires<=r()&&o.delete(c);const i={roster:Promise.resolve(void 0),expires:r()+C};return i.roster=s(n,i),o.set(n,i),i.roster},changed(e){o.delete(m(e))}}}function v(t,r){const o=r.trim().replace(/^@+/,"").toLowerCase();if(!o)return[];const s=t.filter(n=>n.name.toLowerCase()===o);if(s.length>0)return s;const e=p(o);return t.filter(n=>{const a=new Set(p(n.name.toLowerCase()));return e.length>0&&e.every(i=>a.has(i))?!0:n.email?.toLowerCase().split("@")[0]===o})}function p(t){return t.split(/[^\p{L}\p{N}]+/u).filter(Boolean)}export{v as findPeople,R as linkMentions,A as teamsRoster};
|
package/dist/teams/presence.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import{DEFAULT_VERBOSITY as E}from"../config.js";import{saysNoOutput as K}from"../harness/principal.js";import{summaryPhrase as N,toolPhrase as S,turnTally as $}from"../interface/phrases.js";import{showsCost as O,showsProgress as j,showsTasks as b}from"../interface/verbosity.js";import{log as o}from"../log.js";import{fileShelf as F}from"../outgoing.js";import{teamsTask as H}from"./progress.js";const C="The agent finished without saying anything. Nothing went wrong that it reported, but there is no answer to send.";function q(e){const k=()=>e.verbosity?.()??E,A=Date.now();let h;const y=[];let c=0,
|
|
1
|
+
import{DEFAULT_VERBOSITY as E}from"../config.js";import{saysNoOutput as K}from"../harness/principal.js";import{summaryPhrase as N,toolPhrase as S,turnTally as $}from"../interface/phrases.js";import{showsCost as O,showsProgress as j,showsTasks as b}from"../interface/verbosity.js";import{log as o}from"../log.js";import{fileShelf as F}from"../outgoing.js";import{teamsTask as H}from"./progress.js";const C="The agent finished without saying anything. Nothing went wrong that it reported, but there is no answer to send.";function q(e){const k=()=>e.verbosity?.()??E,A=Date.now();let h;const y=[];let c=0,i="",s,w="",D=!1,f,I=!1;const u=F();let l=!1;async function g(){const t=h;t&&(h=void 0,await t.close())}async function P(t,n=[]){await g();try{const r=await e.send(t,n);c+=1,i=t,s=r,l=n.length>0}catch(r){if(n.length>0)try{const a=await e.send(t);c+=1,i=t,s=a,l=!1,o.warn({thread:e.threadKey,files:n.map(d=>d.name),err:r},"could not attach files to a Teams message; sent the text without them");return}catch(a){o.debug({thread:e.threadKey,err:a},"could not post the Teams message without its files")}i="",s=void 0,l=!1,o.debug({thread:e.threadKey,err:r},"could not post to Teams while working")}}function _(t){return t.filter(n=>typeof n!="string")}return{...e.share===!1?{}:{share:t=>u.offer(t)},interrupted(){i="",s=void 0,l=!1},async handle(t){const n=k();switch(b(n)||await g(),t.type){case"text":{if(K(t.text))return;if(!j(n)){y.push(u.strip(t.text));return}const r=u.place(t.text),a=_(r.parts);if(!r.text.trim()&&a.length===0)return;y.push(r.text),await P(r.text,a);return}case"tool":if(!b(n))return;h||(h=H({send:e.post,update:e.update,threadKey:e.threadKey},e.taskIntervalMs),i="",s=void 0),h.note(S(t));return;case"tool_summary":if(!b(n))return;h?.retitle(N(t.summary));return;case"done":D=!0,w=K(t.text)?"":t.text,I=!!e.mayDecline&&K(t.text);return;case"error":f=t.message;return;default:return}},async finish(){if(await g(),I&&!f){o.info({thread:e.threadKey},"the agent stayed out of a Teams conversation");return}const t=w?u.place(w):{parts:[],text:""},n=t.text.trim()||(c===0?y.join(`
|
|
2
2
|
|
|
3
|
-
`).trim():""),r=_(t.parts),a=B(n),d=O(k())?`_${$(Date.now()-A,D||!!f)}_`:"";if(!a&&d&&
|
|
3
|
+
`).trim():""),r=_(t.parts),a=B(n),d=O(k())?`_${$(Date.now()-A,D||!!f)}_`:"";if(!a&&d&&s&&!l)try{await e.update(s.id,`${s.text}
|
|
4
4
|
|
|
5
|
-
${d}
|
|
5
|
+
${d}`,s.mentions);return}catch(m){o.debug({thread:e.threadKey,err:m},"could not close a Teams turn in place")}const T=[a,d].filter(Boolean).join(`
|
|
6
6
|
|
|
7
|
-
`),p=u.unplaced();if(!(!T&&r.length===0&&p.length===0)){try{await e.send(T,r),c+=1,
|
|
7
|
+
`),p=u.unplaced();if(!(!T&&r.length===0&&p.length===0)){try{await e.send(T,r),c+=1,i=a,l=r.length>0,a&&o.info({thread:e.threadKey,chars:a.length},"answered in Teams")}catch(m){if(r.length>0)try{await e.send(T),c+=1,i=a,l=!1,o.warn({thread:e.threadKey,files:r.map(x=>x.name),err:m},"could not attach files to the final Teams message; sent the text without them")}catch(x){o.debug({thread:e.threadKey,err:x},"could not post the final Teams message without its files")}a?o.error({thread:e.threadKey,err:m},"could not post the answer to Teams"):o.debug({thread:e.threadKey,err:m},"could not say what a Teams turn cost")}p.length>0&&await P("",p)}}};function B(t){return f?`Something went wrong: ${f}`:t?t===i.trim()?"":t:c===0?C:""}}export{q as teamsPresence};
|
package/dist/teams/thread.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
const s=new Set(["personal","groupChat"]),
|
|
1
|
+
const s=new Set(["personal","groupChat"]),l=/^[1-9]\d*$/;function u(n){const e=n.conversationId;return s.has(n.conversationType)||e.includes(";messageid=")?e:l.test(n.id)?`${e};messageid=${n.id}`:e}function h(n){return n.conversationType==="personal"?"personal":n.conversationType==="groupChat"?"groupChat":"channel"}function x(n,e){return n==="personal"?"consent":n==="channel"&&e?"upload":"none"}function C(n){const e=u(n),o=h(n);return o!=="channel"?`${e};scope=${o}`:n.channel?I(e,n.channel):`${e};scope=channel`}function I(n,e){return`${n};scope=channel;teamid=${encodeURIComponent(e.teamId)};channelid=${encodeURIComponent(e.channelId)}`}function O(n){const[e="",...o]=n.split(";"),t=new Map(o.map(i=>{const a=i.indexOf("=");return a<0?[i,""]:[i.slice(0,a),i.slice(a+1)]})),c=t.get("messageid"),d=c?`${e};messageid=${c}`:e,r=t.get("scope");if(r==="personal")return{conversation:d,scope:r};if(r==="groupChat")return{conversation:d,scope:r};if(r==="channel"){const i=t.get("teamid"),a=t.get("channelid");return!i||!a?{conversation:d,scope:r}:{conversation:d,scope:r,channel:{teamId:decodeURIComponent(i),channelId:decodeURIComponent(a),...c?{threadId:c}:{}}}}return{conversation:d,scope:"unknown"}}function m(n){return`teams:${u(n)}`}function f(n){return n.split(";")[0]??n}function y(n,e){const[o,t]=[p(n),p(e)];return o.base!==t.base?!1:o.thread===void 0||t.thread===void 0||o.thread===t.thread}function p(n){const[e="",...o]=n.split(";"),t=o.find(c=>c.startsWith("messageid="));return t?{base:e,thread:t}:{base:e}}function T(n){if(n.conversationType==="personal")return;if(s.has(n.conversationType))return{kind:"chat",chatId:f(n.conversationId),messageId:n.id};if(!n.channel)return;const e=$(n);return{kind:"channel",teamId:n.channel.teamId,channelId:n.channel.channelId,messageId:n.id,...e?{replyToId:e}:{}}}function $(n){const e=v(n.conversationId);return e&&e!==n.id?e:void 0}function R(n,e){return`${f(n)};messageid=${e}`}function v(n){return n.split(";").find(e=>e.startsWith("messageid="))?.slice(10)}export{f as baseConversation,I as channelHandleOf,u as conversationOf,C as destinationHandleOf,O as destinationOfHandle,x as fileRouteOf,T as graphAddressOf,y as sameConversation,h as scopeOf,v as threadIdOf,R as threadIn,m as threadKeyOf};
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import{z as h}from"zod";import{FIND_PERSON_TOOL as c}from"../interface/people.js";import{defineTool as l,ToolRefusal as d}from"./types.js";const r=20;function g(i){return[l({name:c,description:["Finds the people in this Teams conversation a name could mean, with their full display","names.","",'Use it when you are asked to mention somebody by part of their name, like "mention Kim".',"If one person matches, write @ and the full name it gives and Bishop makes it a mention.","If more than one does, ask which one was meant rather than picking. It searches this","conversation and no other, and in a very large one only the first page of its members."].join(`
|
|
2
|
+
`),input:{name:h.string().trim().min(1).describe('The name as you were given it, like "Kim"')},async run(e,a){const n=await i.find(a.origin.name,a.thread,e.name);if(n.kind==="refused")throw new d(n.reason);return f(e.name,n.people,n.complete)}})]}function f(i,e,a){const n=a?"":" This conversation is too large to search whole, so somebody further down was not checked.";if(e.length===0)return`Nobody in this conversation matches "${i}".${n}`;const s=e.slice(0,r).map(o=>o.email?`- ${o.name} (${o.email})`:`- ${o.name}`),m=e.length>r?[`- and ${e.length-r} more`]:[],[t]=e;if(t&&e.length===1){const o=/\s/.test(t.name)?"":` Follow the name with punctuation or a lowercase word, as in "@${t.name}, can you".`;return`One person matches: ${t.name}. Write @${t.name} to mention them.${o}${n}`}return[`${e.length} people match "${i}". Ask which one was meant before mentioning anybody.${n}`,...s,...m].join(`
|
|
3
|
+
`)}export{g as peopleTools};
|
package/dist/tools/schedules.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import{z as
|
|
2
|
-
`)}async function
|
|
3
|
-
`),input:{name:
|
|
4
|
-
Next ${
|
|
1
|
+
import{z as s}from"zod";import{BishopError as B}from"../errors.js";import{NO_OUTPUT as G}from"../harness/principal.js";import{log as R}from"../log.js";import{principalLabel as W}from"../principals.js";import{describeTime as $,describeTiming as I,nextRun as E,parseAt as O,systemTimeZone as C,validateCron as L,validateTimeZone as j}from"../schedule/cron.js";import{defineTool as A,ToolRefusal as d}from"./types.js";const Z=20;function _(e){return e.principal.id}function y(e){try{return e()}catch(n){throw n instanceof B?new d(n.message):n}}async function D(e){try{return await e()}catch(n){throw n instanceof B?new d(n.message):n}}function P(e,n,i){const o=[`${e.id} ${e.name}${e.enabled?"":" (disabled)"}`,` runs ${I(e)}`,` next ${e.enabled?$(e.nextRunAt,n):"never, while it is off"}`,` to ${e.destination.label}`,` prompt ${e.prompt}`,` by ${W(e.createdBy,i)}`];return e.lastError&&o.push(` error ${e.lastError} (${e.consecutiveFailures} in a row)`),o.join(`
|
|
2
|
+
`)}async function k(e,n){const i=await n.destination(),o=_(n);return e.schedules.all().filter(t=>t.createdBy===o||e.sameConversation(t.destination,i))}function S(e,n,i){const o=_(n);if(!e.authorized(o))throw new d(`Bishop can't run a schedule created by ${n.principal.name}, so ${i?"changing this one":"making one"} would write something that never fires. Ask an operator to add it with \`bishop schedules add\`.`);if(i?.createdBy===o)return o;const t=e.schedules.all().filter(l=>l.createdBy===o).length;if(t>=Z)throw new d(`You already have ${t} schedules, which is the limit. Remove one with schedule_delete before ${i?"taking on":"adding"} another.`);return o}function U(e){const n=e.now??Date.now;return A({name:"schedule_create",description:["Runs a prompt on a cron schedule, or once at a set time, and posts the answer where you","say. Give cron for a schedule that repeats, or at for a reminder, which runs once and is","then deleted. 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 ${G} 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).optional().describe("Five-field cron, or @daily and friends. No more than once every five minutes. Leave it out when giving at"),at:s.string().trim().min(1).optional().describe("For a reminder: when it runs, as ISO 8601 like 2026-10-09T15:00, read in tz unless it carries an offset. Leave it out when giving cron"),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, or an email address. In Teams a channel is one in this team, by its exact name, and a person is somebody in this conversation. Leave it out to post back into this conversation."),tz:s.string().trim().min(1).optional().describe("IANA zone cron or at is read in, like Europe/London. Defaults to Bishop's")},async run(i,o){const{cron:t,at:l}=i;if(t===void 0==(l===void 0))throw new d("Give exactly one of cron, for a schedule that repeats, or at.");const b=S(e,o),m=i.tz,c=m?y(()=>j(m)):C(),v=y(t?()=>(L(t,c),E(t,c,n())):()=>O(l??"",c,n())),u=i.to,p=await o.destination(),f=u?await D(()=>e.resolve(u,p)):p,r=e.schedules.add({name:i.name,...t?{cron:t}:{},tz:c,prompt:i.prompt,destination:f,createdBy:b,nextRunAt:v});return R.info({schedule:r.id,name:r.name,cron:r.cron,tz:r.tz,at:new Date(r.nextRunAt).toISOString(),by:r.createdBy,to:f.label},"the agent created a schedule"),r.cron?`Created ${r.id} "${r.name}", running ${r.cron} (${c}).
|
|
4
|
+
Next ${$(r.nextRunAt,c)}, posting to ${f.label}.`:`Created reminder ${r.id} "${r.name}", running once at ${$(r.nextRunAt,c)}, posting to ${f.label}. It is deleted once it runs.`}})}function q(e){return A({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,i){const o=await k(e,i);if(o.length===0)return"Nothing is scheduled here, and you have not made any.";const t=C();return`${o.map(l=>P(l,t,e.principals)).join(`
|
|
5
5
|
|
|
6
6
|
`)}
|
|
7
7
|
|
|
8
|
-
Times in ${
|
|
9
|
-
${
|
|
8
|
+
Times in ${t}.`}})}function M(e){const n=e.now??Date.now;return A({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:s.string().trim().min(1).describe("The id schedule_list reports"),name:s.string().trim().min(1).optional().describe("A new name"),cron:s.string().trim().min(1).optional().describe("A new five-field cron, or @daily and friends. No more than once every five minutes. Turns a reminder into a schedule that repeats"),at:s.string().trim().min(1).optional().describe("A new time to run once, as ISO 8601 like 2026-10-09T15:00. Turns a repeating schedule into a reminder"),prompt:s.string().trim().min(1).optional().describe("A new prompt, whole. It replaces the old one rather than adding to it"),to:s.string().trim().min(1).optional().describe("A new destination: #channel or @person, or an email address"),tz:s.string().trim().min(1).optional().describe("A new IANA zone, like Europe/London")},async run(i,o){const t=`No schedule ${i.id} that you can change. schedule_list has the ones you can.`;if(!(await k(e,o)).some(z=>z.id===i.id))throw new d(t);const{name:b,cron:m,at:c,prompt:v,to:u,tz:p}=i;if([b,m,c,v,u,p].every(z=>z===void 0))throw new d("Nothing to change. Name at least one of name, cron, at, prompt, to, or tz.");if(m!==void 0&&c!==void 0)throw new d("Give cron or at, not both.");const f=p?y(()=>j(p)):void 0,r=u?await D(async()=>e.resolve(u,await o.destination())):void 0,a=e.schedules.get(i.id);if(!a)throw new d(t);const x=S(e,o,a),w=f??a.tz,g=c===void 0?m??a.cron:void 0;let T=a.nextRunAt;c!==void 0?T=y(()=>O(c,w,n())):g&&(g!==a.cron||w!==a.tz)&&(y(()=>L(g,w)),T=E(g,w,n()));const h={...a,name:b??a.name,cron:g,tz:w,prompt:v??a.prompt,destination:r??a.destination,createdBy:x,nextRunAt:T};e.schedules.update(a.id,h);const{destination:N}=h;R.info({schedule:a.id,name:h.name,cron:h.cron,tz:h.tz,by:x,to:N.label},"the agent changed a schedule");const F=h.enabled?`Next ${$(h.nextRunAt,w)}`:`It is off, so nothing fires until an operator runs \`bishop schedules enable ${a.id}\``;return`Changed ${a.id} "${h.name}", running ${I(h)}.
|
|
9
|
+
${F}, posting to ${N.label}.`}})}function X(e){return A({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,i){const t=(await k(e,i)).find(l=>l.id===n.id);if(!t)throw new d(`No schedule ${n.id} that you can remove. schedule_list has the ones you can.`);return e.schedules.remove(t.id),R.info({schedule:t.id,name:t.name,by:_(i)},"the agent removed a schedule"),`Removed ${t.id} "${t.name}".`}})}function ne(e){return[U(e),q(e),M(e),X(e)]}export{Z as MAX_PER_CREATOR,_ as createdBy,ne as scheduleTools};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agentdeploymentco/bishop",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"description": "An agent gateway. Connects an agent to Slack, email, and Microsoft Teams.",
|
|
5
5
|
"license": "UNLICENSED",
|
|
6
6
|
"type": "module",
|
|
@@ -54,11 +54,13 @@
|
|
|
54
54
|
"commander": "^13",
|
|
55
55
|
"cron-parser": "^5.10.0",
|
|
56
56
|
"dotenv": "^17",
|
|
57
|
+
"luxon": "^3.7.2",
|
|
57
58
|
"pino": "^9",
|
|
58
59
|
"zod": "^4"
|
|
59
60
|
},
|
|
60
61
|
"devDependencies": {
|
|
61
62
|
"@biomejs/biome": "^2.5.10",
|
|
63
|
+
"@types/luxon": "^3.7.6",
|
|
62
64
|
"@types/node": "^24",
|
|
63
65
|
"esbuild": "^0.28.2",
|
|
64
66
|
"tsx": "^4",
|