@agentdeploymentco/bishop 0.3.3 → 0.3.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +12 -11
- package/dist/cli.js +15 -15
- package/dist/commands/run.js +3 -0
- package/dist/commands/syntax.js +3 -0
- package/dist/db.js +44 -11
- package/dist/gmail/create.js +1 -1
- package/dist/gmail/history.js +1 -1
- package/dist/gmail/interface.js +2 -2
- package/dist/harness/principal.js +12 -9
- package/dist/ids.js +1 -1
- package/dist/interface/authorize.js +1 -1
- package/dist/interface/history.js +1 -1
- package/dist/interface/verbosity.js +1 -1
- package/dist/principals.js +4 -0
- package/dist/schedule/cli.js +7 -7
- package/dist/schedule/prompt.js +2 -2
- package/dist/schedule/runner.js +3 -3
- package/dist/sessions/store.js +13 -7
- package/dist/slack/app.js +2 -2
- package/dist/slack/context.js +1 -0
- package/dist/slack/handlers.js +1 -1
- package/dist/slack/history.js +1 -1
- package/dist/slack/interface.js +2 -2
- package/dist/slack/members.js +1 -1
- package/dist/slack/principal.js +1 -0
- package/dist/slack/users.js +1 -1
- package/dist/teams/command.js +1 -0
- package/dist/teams/create.js +2 -2
- package/dist/teams/fake.js +1 -1
- package/dist/teams/graph.js +1 -1
- package/dist/teams/history.js +1 -0
- package/dist/teams/html.js +6 -0
- package/dist/teams/interface.js +3 -3
- package/dist/teams/presence.js +3 -3
- package/dist/teams/principal.js +1 -0
- package/dist/teams/thread.js +1 -1
- package/dist/tools/schedules.js +5 -5
- package/dist/tools/server.js +1 -1
- package/dist/turn.js +3 -1
- package/package.json +11 -6
- package/dist/slack/commands/run.js +0 -3
- package/dist/slack/commands/syntax.js +0 -3
- package/dist/slack/verbosity.js +0 -1
package/dist/schedule/cli.js
CHANGED
|
@@ -1,17 +1,17 @@
|
|
|
1
|
-
import{userInfo as y}from"node:os";import{WebClient as v}from"@slack/web-api";import{loadConfig as g}from"../config.js";import{dbPath as x,openDb as R}from"../db.js";import{describeMissing as k,loadEnv as A,readCredentials as
|
|
1
|
+
import{userInfo as y}from"node:os";import{WebClient as v}from"@slack/web-api";import{loadConfig as g}from"../config.js";import{dbPath as x,openDb as R}from"../db.js";import{describeMissing as k,loadEnv as A,readCredentials as S}from"../env.js";import{BishopError as a}from"../errors.js";import{gmailAllowList as D}from"../gmail/authorize.js";import{resolveGmailDestination as z}from"../gmail/destination.js";import{interfaceFor as E,UnplaceableDestination as B}from"../interface/destination.js";import{principalLabel as C,principalStore as T}from"../principals.js";import{resolveSlackDestination as F}from"../slack/destination.js";import{describeTime as l,nextRun as h,systemTimeZone as m,validateCron as L,validateTimeZone as j}from"./cron.js";import{scheduleStore as q}from"./store.js";async function p(n){A(n);const t=await g(n),e=R(x(n));return{config:t,db:e,schedules:q(e)}}function u(n,t){const e=n?.trim();if(!e)throw new a(`${t} is required`);return e}function G(n){try{return n()}catch(t){throw t instanceof B?new a(`${t.message} Or say which with --interface.`):t}}async function N(n,t,e,o){if(t==="slack"){if(!e.slack)throw new a("Slack is not configured here, so nothing sent to a Slack destination would arrive. Run `bishop slack setup` first.");return F(new v(e.slack.botToken),n)}if(t==="gmail"){if(!e.gmail)throw new a("Gmail is not configured here, so nothing sent to an address would arrive. Run `bishop gmail setup` first.");return z(D(o),n)}throw new a(`Bishop has no "${t}" interface`)}async function _(n,t){const e=u(t.name,"--name"),o=u(t.cron,"--cron"),i=u(t.prompt,"--prompt"),r=u(t.to,"--to"),s=t.tz?j(t.tz.trim()):m(),{config:w,db:$,schedules:b}=await p(n);try{L(o,s);const f=S();if(!f.ok)throw new a(k(f));const d=await N(r,G(()=>E(r,t.interface)),f.credentials,w.gmail?.allow),c=b.add({name:e,cron:o,tz:s,prompt:i,destination:d,createdBy:`cli:${y().username}`,nextRunAt:h(o,s,Date.now())});process.stdout.write(`Added ${c.id} "${c.name}"
|
|
2
2
|
runs ${c.cron} (${c.tz})
|
|
3
3
|
next ${l(c.nextRunAt,m())}
|
|
4
4
|
to ${d.label} via ${d.interface}
|
|
5
5
|
|
|
6
6
|
A running Bishop picks it up within a minute. Nothing fires while Bishop is stopped.
|
|
7
|
-
`)}finally{$.close()}}function
|
|
8
|
-
`)}async function
|
|
9
|
-
`);return}const i=m();process.stdout.write(`${o.map(
|
|
7
|
+
`)}finally{$.close()}}function Z(n,t,e){const o=[`${n.id} ${n.name}${n.enabled?"":" (disabled)"}`,` runs ${n.cron} (${n.tz})`,` next ${n.enabled?l(n.nextRunAt,t):"never, while it is off"}`,` to ${n.destination.label} via ${n.destination.interface}`,` prompt ${n.prompt}`];return n.lastRunAt&&o.push(` last ${l(n.lastRunAt,t)}`),n.lastError&&o.push(` error ${n.lastError} (${n.consecutiveFailures} in a row)`),o.push(` by ${C(n.createdBy,e)}`),o.join(`
|
|
8
|
+
`)}async function tt(n){const{db:t,schedules:e}=await p(n);try{const o=e.all();if(o.length===0){process.stdout.write(`No schedules here.
|
|
9
|
+
`);return}const i=m(),r=T(t);process.stdout.write(`${o.map(s=>Z(s,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{e.close()}}async function
|
|
14
|
+
`)}finally{t.close()}}async function nt(n,t){const{db:e,schedules:o}=await p(n);try{const i=o.get(t);if(!i)throw new a(`no schedule ${t}. \`bishop schedules list\` has them.`);o.remove(t),process.stdout.write(`Removed ${t} "${i.name}"
|
|
15
|
+
`)}finally{e.close()}}async function et(n,t,e){const{db:o,schedules:i}=await p(n);try{const r=i.get(t);if(!r)throw new a(`no schedule ${t}. \`bishop schedules list\` has them.`);const s=e?h(r.cron,r.tz,Date.now()):r.nextRunAt;i.setEnabled(t,e,s),e&&i.clearFailures(t),process.stdout.write(e?`Enabled ${t} "${r.name}", next ${l(s,m())}
|
|
16
16
|
`:`Disabled ${t} "${r.name}"
|
|
17
|
-
`)}finally{o.close()}}export{
|
|
17
|
+
`)}finally{o.close()}}export{_ as addSchedule,tt as listSchedules,nt as removeSchedule,et as setScheduleEnabled};
|
package/dist/schedule/prompt.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import{NO_OUTPUT as
|
|
2
|
-
`)};function d(e,t){return
|
|
1
|
+
import{NO_OUTPUT as a,renderTag as o,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 ${a} 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."].join(`
|
|
2
|
+
`)};function d(e,t,n){return o("schedule",{id:e.id,name:e.name,cron:e.cron,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,5 @@
|
|
|
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 I}from"../outgoing.js";import{principalLabel as z,SYSTEM_PRINCIPAL as R}from"../principals.js";import{nextRun as $}from"./cron.js";import{schedulePresence as M}from"./presence.js";import{SCHEDULE_ORIGIN as N,scheduleNote as A}from"./prompt.js";const B=3e4,L=5,P=5e3;function C(r){return r.startsWith("cli:")}function F(r,l){return`schedule:${r}:${l}`}function W(r){const l=r.now??Date.now,v=r.tickMs??B,x=r.authorized??C,s=new Map,g=new Map,f=new Set;let u,S=!1;function w(e,t){try{return r.schedules.setNextRun(e.id,$(e.cron,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),p(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 p(e,t){const i=r.deliver(e.destination,{text:t,title:e.name}).then(()=>{}).catch(n=>{a.error({schedule:e.id,to:e.destination.label,err:n},"could not say that a schedule failed")}).finally(()=>{f.delete(i)});f.add(i),await i}async function y(e,t,i){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>=L){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 p(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 p(e,`The schedule "${e.name}" failed: ${i}`)}async function k(e,t,i){const n=M();a.info({schedule:e.id,name:e.name},"a schedule is firing");let c,b;try{b=await r.threads.remember(i)}catch(o){await y(e,l(),o instanceof Error?o.message:String(o));return}const _={thread:b,isNewThread:!0,freshSession:!0,tools:[I],origin:N,note:A(e,t,r.principals?z(e.createdBy,r.principals):e.createdBy),async messages(){return[{principal:R,text:e.prompt}]},presence:()=>n,async notify(o){n.note(o)},async destination(){return e.destination}};try{await r.run(_)}catch(o){c=o instanceof Error?o.message:String(o)}const d=n.outcome(),T=d.failure??c,h=l();if(T){await y(e,h,T);return}if(d.silent){r.schedules.recordSuccess(e.id,h),a.info({schedule:e.id,name:e.name},"a schedule had nothing to report");return}let m;try{m=await r.deliver(e.destination,{text:d.text,title:e.name,...d.files.length>0?{files:d.files}:{}})}catch(o){await y(e,h,o instanceof Error?o.message:String(o));return}if(m.threadKey&&r.attach)try{await r.attach(m.threadKey,i)}catch(o){a.error({schedule:e.id,thread:m.threadKey,err:o},"a report was delivered but nothing will answer a reply to it")}r.schedules.recordSuccess(e.id,h),a.info({schedule:e.id,name:e.name,to:e.destination.label},"a schedule reported")}async function E(){if(S)return;const e=l();for(const t of r.schedules.due(e)){if(!x(t.createdBy)){a.warn({schedule:t.id,createdBy:t.createdBy},"skipping a schedule whose creator can no longer be authorized"),w(t,e);continue}if(!w(t,e))continue;if(s.has(t.id)){a.warn({schedule:t.id,name:t.name},"skipping a firing while the previous one is still running");continue}const i=F(t.id,e);g.set(t.id,i);const n=k(t,e,i).catch(c=>{a.error({schedule:t.id,err:c},"a schedule firing threw")}).finally(()=>{s.delete(t.id),g.delete(t.id)});s.set(t.id,n)}}return{start(){const e=l(),t=r.schedules.due(e);for(const n of t)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"),w(n,e);const i=r.schedules.all().filter(n=>n.enabled);a.info({schedules:i.length,skipped:t.length},"watching the schedules"),u=setInterval(()=>{E().catch(n=>{a.error({err:n},"a scheduler pass failed; trying again on the next tick")})},v),u.unref()},tick:E,async settle(){await Promise.allSettled([...s.values(),...f])},async stop(){if(S=!0,u&&clearInterval(u),s.size>0){a.info({firings:s.size},"stopping scheduled runs still going");for(const n of s.keys()){const c=g.get(n);c&&r.cancel?.(c)}}const e=Promise.allSettled([...s.values(),...f]);let t;const i=new Promise(n=>{t=setTimeout(()=>{a.warn({firings:s.size},"giving up on scheduled runs that would not stop"),n()},P)});await Promise.race([e,i]),t&&clearTimeout(t)}}}export{L as MAX_CONSECUTIVE_FAILURES,C as authorizedToRun,F as firingThread,W as makeScheduler};
|
package/dist/sessions/store.js
CHANGED
|
@@ -1,11 +1,17 @@
|
|
|
1
|
-
import{mint as
|
|
1
|
+
import{mint as D}from"../ids.js";import{log as O}from"../log.js";const f=10080*60*1e3,R="id, thread_key, dir_key";function S(e){return{id:e.id,threadKey:e.thread_key,dirKey:e.dir_key}}function C(e,t){const r=e.prepare("SELECT session_id, harness FROM sessions WHERE thread_id = ?"),a=e.prepare(`INSERT INTO sessions (thread_id, session_id, harness, created_at, updated_at)
|
|
2
2
|
VALUES (?, ?, ?, ?, ?)
|
|
3
3
|
ON CONFLICT (thread_id) DO UPDATE SET session_id = excluded.session_id,
|
|
4
4
|
harness = excluded.harness,
|
|
5
|
-
updated_at = excluded.updated_at`),
|
|
6
|
-
ON CONFLICT (thread_key) DO NOTHING`),
|
|
7
|
-
WHERE agent_dir IS NOT NULL GROUP BY agent_dir`),
|
|
8
|
-
WHERE used_at < ? AND agent_dir IS NOT NULL ORDER BY used_at`),
|
|
5
|
+
updated_at = excluded.updated_at`),o=e.prepare("INSERT OR IGNORE INTO processed_events (event_id, seen_at) VALUES (?, ?)"),i=e.prepare("SELECT 1 FROM processed_events WHERE event_id = ?"),c=e.prepare("SELECT COUNT(*) AS n FROM sessions"),l=e.prepare("SELECT agent_dir FROM threads WHERE id = ?"),E=e.prepare(`SELECT ${R} FROM threads WHERE thread_key = ?`),_=e.prepare(`INSERT INTO threads (id, thread_key, dir_key, created_at, used_at) VALUES (?, ?, ?, ?, ?)
|
|
6
|
+
ON CONFLICT (thread_key) DO NOTHING`),n=e.prepare("SELECT 1 FROM sessions s JOIN threads t ON t.id = s.thread_id WHERE t.thread_key = ?"),u=e.prepare("UPDATE threads SET thread_key = ? WHERE id = ?"),T=e.prepare("UPDATE threads SET agent_dir = ?, used_at = ? WHERE id = ?"),N=e.prepare("UPDATE threads SET used_at = ? WHERE id = ?");return{async get(s){const d=r.get(s);if(d?.session_id){if(d.harness!==t){O.warn({thread:s,recorded:d.harness,running:t},"this thread belongs to a different harness; starting a new session");return}return d.session_id}},async set(s,d){const p=Date.now();a.run(s,d,t,p,p)},async has(s){return n.get(s)!==void 0},async remember(s){const d=Date.now();return _.run(D("t"),s,s,d,d),S(E.get(s))},async seen(s,d){return d?.record===!1?i.get(s)!==void 0:o.run(s,Date.now()).changes===0},count(){return Number(c.get().n)},async getDir(s){return l.get(s)?.agent_dir??void 0},async setDir(s,d){T.run(d,Date.now(),s)},async touchDir(s){N.run(Date.now(),s)},async readdress(s,d){const p=E.get(s);if(!p)return;const g=E.get(d);if(g?.id===p.id)return S(p);if(!g)return u.run(d,p.id),{id:p.id,threadKey:d,dirKey:p.dir_key}}}}function m(e){return async(t,r)=>{const a=await e.readdress(r,t);if(!a){O.warn({thread:t,firing:r},"nothing to attach: a reply here starts a fresh conversation");return}O.info({thread:t,firing:r,id:a.id},"a report thread continues its firing")}}function x(e){const t=e.prepare(`SELECT agent_dir AS dir, MAX(used_at) AS last_used FROM threads
|
|
7
|
+
WHERE agent_dir IS NOT NULL GROUP BY agent_dir`),r=e.prepare("SELECT MAX(used_at) AS last_used FROM threads WHERE agent_dir = ?"),a=e.prepare(`SELECT ${R}, agent_dir FROM threads
|
|
8
|
+
WHERE used_at < ? AND agent_dir IS NOT NULL ORDER BY used_at`),o=e.prepare("SELECT 1 FROM threads WHERE id = ? AND used_at < ?"),i=n=>`SELECT ${R}, agent_dir FROM threads WHERE used_at < ?${n} ORDER BY used_at`,c=e.prepare(i("")),l=e.prepare("DELETE FROM threads WHERE id = ? AND used_at < ?"),E=e.prepare("DELETE FROM processed_events WHERE seen_at < ?"),_=n=>({...S(n),dir:n.agent_dir??""});return{lastActivityByDir(){const n=t.all();return new Map(n.map(u=>[u.dir,Number(u.last_used)]))},lastTurnIn(n){const u=r.get(n);return u?.last_used==null?void 0:Number(u.last_used)},staleThreads(n){return a.all(n).map(_)},quietSince(n,u){return o.get(n,u)!==void 0},collectableThreads(n,u=[]){if(u.length===0)return c.all(n).map(_);const T=` AND id NOT IN (${u.map(()=>"?").join(", ")})`;return e.prepare(i(T)).all(n,...u).map(_)},forgetThreads(n,u){return n.filter(T=>Number(l.run(T.id,u).changes)>0)},pruneEvents(n=Date.now()){return Number(E.run(n-f).changes)}}}function I(e){const t=e.prepare("SELECT value FROM thread_meta WHERE thread_id = ? AND key = ?"),r=e.prepare(`INSERT INTO thread_meta (thread_id, key, value, updated_at) VALUES (?, ?, ?, ?)
|
|
9
9
|
ON CONFLICT (thread_id, key) DO UPDATE SET value = excluded.value,
|
|
10
|
-
updated_at = excluded.updated_at`),
|
|
11
|
-
ON CONFLICT (key) DO UPDATE SET value = excluded.value, updated_at = excluded.updated_at`);return{get(
|
|
10
|
+
updated_at = excluded.updated_at`),a=e.prepare("DELETE FROM thread_meta WHERE thread_id = ? AND key = ?");return{get(o,i){return t.get(o,i)?.value},set(o,i,c){r.run(o,i,c,Date.now())},clear(o,i){a.run(o,i)}}}function y(){const e=new Map,t=(r,a)=>`${r} ${a}`;return{get:(r,a)=>e.get(t(r,a)),set:(r,a,o)=>{e.set(t(r,a),o)},clear:(r,a)=>{e.delete(t(r,a))}}}function M(e){const t=e.prepare("SELECT value FROM interface_state WHERE key = ?"),r=e.prepare(`INSERT INTO interface_state (key, value, updated_at) VALUES (?, ?, ?)
|
|
11
|
+
ON CONFLICT (key) DO UPDATE SET value = excluded.value, updated_at = excluded.updated_at`);return{get(a){return t.get(a)?.value},set(a,o){r.run(a,o,Date.now())}}}const h="context";function v(e,t){const r=e.prepare(`INSERT INTO locations (id, interface, external_id, kind, name, created_at, updated_at)
|
|
12
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
13
|
+
ON CONFLICT (interface, external_id) DO UPDATE SET kind = excluded.kind,
|
|
14
|
+
name = excluded.name,
|
|
15
|
+
updated_at = excluded.updated_at
|
|
16
|
+
RETURNING id`),a=e.prepare("UPDATE threads SET interface = ? WHERE id = ?"),o=e.prepare(`UPDATE threads SET interface = ?, location_id = ?, visibility = ?, audience = ?
|
|
17
|
+
WHERE id = ?`);return{record(i,c,l){if(!l){a.run(c,i);return}const E=l.location,_=Date.now(),n=E?r.get(D("loc"),c,E.externalId,E.kind,E.name??"",_,_):void 0;o.run(c,n?.id??null,l.visibility,l.audience,i)},told:i=>t.get(i,h),tell:(i,c)=>t.set(i,h,c)}}function w(e=y()){return{record(){},told:t=>e.get(t,h),tell:(t,r)=>e.set(t,h,r)}}export{f as EVENT_TTL_MS,M as interfaceState,w as memoryThreadContexts,y as memoryThreadMeta,x as retentionStore,C as sessionStore,v as threadContexts,m as threadHandover,I as threadMeta};
|
package/dist/slack/app.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import{App as u,LogLevel as d}from"@slack/bolt";import{WebAPIPlatformError as m,WebClient as _}from"@slack/web-api";import{BishopError as k}from"../errors.js";import{log as n}from"../log.js";class c extends k{constructor(o){super(o,"SlackStartupError")}}const l={invalid_auth:"BISHOP_SLACK_BOT_TOKEN was rejected by Slack",not_authed:"BISHOP_SLACK_BOT_TOKEN is missing or empty",account_inactive:"the bot token's account is deactivated",token_revoked:"BISHOP_SLACK_BOT_TOKEN has been revoked",token_expired:"BISHOP_SLACK_BOT_TOKEN has expired"};function h(r){if(r instanceof m)return r.data.error;const o=r instanceof Error?r.message:"";return/An API error occurred: (\w+)/.exec(o)?.[1]}async function
|
|
2
|
-
Re-run \`bishop setup slack\`, or check the token in .env.`):new c(`Couldn't reach Slack: ${t instanceof Error?t.message:String(t)}`)}if(!e.user_id||!e.team_id)throw new c("auth.test returned no user_id or team_id");const a={botUserId:e.user_id,botId:e.bot_id??"",teamId:e.team_id,...e.user?.trim()?{username:e.user.trim()}:{},...e.app_name?.trim()?{appName:e.app_name.trim()}:{},scopes:e.response_metadata?.scopes??[]},p=new u({token:r.botToken,appToken:r.appToken,socketMode:!0,logLevel:d.WARN,logger:{debug:(...t)=>n.debug({slack:t}),info:(...t)=>n.info({slack:t}),warn:(...t)=>n.warn({slack:t}),error:(...t)=>n.error({slack:t}),setLevel:()=>{},getLevel:()=>d.WARN,setName:()=>{}}});return n.info({botUserId:a.botUserId,teamId:a.teamId,url:e.url},"connected to Slack"),{app:p,identity:a}}export{c as SlackStartupError,
|
|
1
|
+
import{App as u,LogLevel as d}from"@slack/bolt";import{WebAPIPlatformError as m,WebClient as _}from"@slack/web-api";import{BishopError as k}from"../errors.js";import{log as n}from"../log.js";class c extends k{constructor(o){super(o,"SlackStartupError")}}const l={invalid_auth:"BISHOP_SLACK_BOT_TOKEN was rejected by Slack",not_authed:"BISHOP_SLACK_BOT_TOKEN is missing or empty",account_inactive:"the bot token's account is deactivated",token_revoked:"BISHOP_SLACK_BOT_TOKEN has been revoked",token_expired:"BISHOP_SLACK_BOT_TOKEN has expired"};function h(r){if(r instanceof m)return r.data.error;const o=r instanceof Error?r.message:"";return/An API error occurred: (\w+)/.exec(o)?.[1]}async function I(r){const o=new _(r.botToken);let e;try{e=await o.auth.test()}catch(t){const s=h(t),i=s?l[s]:void 0;throw i?new c(`Slack rejected the bot token: ${i} (${s}).
|
|
2
|
+
Re-run \`bishop setup slack\`, or check the token in .env.`):new c(`Couldn't reach Slack: ${t instanceof Error?t.message:String(t)}`)}if(!e.user_id||!e.team_id)throw new c("auth.test returned no user_id or team_id");const a={botUserId:e.user_id,botId:e.bot_id??"",teamId:e.team_id,...e.enterprise_id?{enterpriseId:e.enterprise_id}:{},...e.user?.trim()?{username:e.user.trim()}:{},...e.app_name?.trim()?{appName:e.app_name.trim()}:{},scopes:e.response_metadata?.scopes??[]},p=new u({token:r.botToken,appToken:r.appToken,socketMode:!0,logLevel:d.WARN,logger:{debug:(...t)=>n.debug({slack:t}),info:(...t)=>n.info({slack:t}),warn:(...t)=>n.warn({slack:t}),error:(...t)=>n.error({slack:t}),setLevel:()=>{},getLevel:()=>d.WARN,setName:()=>{}}});return n.info({botUserId:a.botUserId,teamId:a.teamId,url:e.url},"connected to Slack"),{app:p,identity:a}}export{c as SlackStartupError,I as createSlackApp};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{log as a}from"../log.js";import{affiliationOf as d}from"./principal.js";const S=1440*6e4,N=5*6e4,k=1e3,T=25,b={visibility:"unknown",audience:"unknown"};function h(s){return s.includes("external")?"external":s.length===0||s.includes("unknown")?"unknown":"internal"}function L(s){const{client:p,users:u,home:l,self:y,ttlMs:_=S,now:g=Date.now}=s,m=new Map,c=new Map,f=(n,e,o)=>({externalId:`${l.teamId}:${n}`,kind:e,...o?{name:o}:{}});async function v(n){let e;try{const t=await p.conversations.members({channel:n,limit:k});if(t.response_metadata?.next_cursor)return a.debug({channel:n,max:k},"a channel too big to check everyone in; its audience is unknown"),"unknown";e=t.members??[]}catch(t){return a.debug({channel:n,err:t},"could not list the members of a Slack channel"),"unknown"}const o=e.filter(t=>t!==y),i=new Map((await u.roster())?.map(t=>[t.userId,t])),r=o.filter(t=>!i.has(t));if(r.length>T)return a.debug({channel:n,missing:r.length},"too many channel members to look up one by one; its audience is unknown"),"unknown";const M=await Promise.all(r.map(t=>u.lookup(t))),O=[...o.map(t=>i.get(t)).filter(t=>!!t),...M];return h(O.map(t=>d(t,l)))}async function x(n){let e;try{e=(await p.conversations.info({channel:n})).channel}catch(i){return a.debug({channel:n,err:i},"could not read a Slack conversation"),b}if(!e||e.is_mpim)return b;if(e.is_im)return{location:f(n,"im"),visibility:"one-to-one",audience:"unknown"};const o=e.is_private?"group":"channel";return{location:f(n,o,e.name),visibility:e.is_private?"group":"public",audience:e.is_ext_shared?"external":await v(n)}}function w(n){const e=c.get(n);if(e)return e;const o=x(n).then(i=>{const r=i.visibility!=="unknown"&&i.audience!=="unknown";return m.set(n,{observed:i,expires:g()+(r?_:N)}),i}).finally(()=>c.delete(n));return c.set(n,o),o}return{changed(n){a.debug({channel:n},"a Slack channel changed; reading it again"),m.delete(n);const e=c.get(n),o=()=>{w(n)};e?e.finally(o):o()},async observe({channelId:n,direct:e,speaker:o}){if(e){const r=await u.lookup(o);return{location:f(n,"im"),visibility:"one-to-one",audience:h([d(r,l)])}}const i=m.get(n);return i&&i.expires>g()?i.observed:w(n)}}}export{h as audienceOf,L as slackObserver};
|
package/dist/slack/handlers.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{log as
|
|
1
|
+
import{log as r}from"../log.js";import{markHeld as R,releaseHeld as F}from"./pending.js";import{FILE_SHARE as L,isBotEvent as H,isSelfEvent as O,routeAppMention as W,routeMessage as Y}from"./route.js";import{say as $}from"./say.js";import{mentionsBot as C,messageKeyString as G,spokenAt as J,threadKeyOf as K,threadKeyString as A}from"./thread.js";const Q=new Set(["channel_join","channel_leave","channel_name","group_join","group_leave","group_name"]);function ne(I,j){const{identity:d,botName:m,threads:x,events:q,run:z,stop:D,allows:_,bots:p,participants:T,command:B,changed:N}=j,l=new Map,y=new Map;async function E(e,s,a){const n=l.get(s);if(!n?.length)return[];l.set(s,[]),await y.get(s);const t=await F(a.client,e,n,{botUserId:d.botUserId,...m?{botName:m}:{}}),c=[];for(const h of t)_(h.userId)?c.push(h):r.warn({thread:s,user:h.userId},"dropping a held message");return c}async function P(e,s,a){e.subtype&&Q.has(e.subtype)&&e.channel&&N?.(e.channel);const n=K(e,d.teamId);if(!n){r.debug({source:a},"dropping event with no usable thread key");return}const t=A(n),c=await x.has(t),h={botUserId:d.botUserId,botId:d.botId,...m?{botName:m}:{},threadKnown:c},u=a==="app_mention"?W(e,h):Y(e,h);if((c||u.action==="run")&&e.user&&!O(e,h)&&(!e.subtype||e.subtype===L)&&T.saw(t,e.user,J(e.ts)),u.action==="ignore"){e.user&&e.user!==d.botUserId&&!H(e)&&p.clear(t),r.debug({source:a,thread:t,reason:u.reason},"ignoring event");return}const f=H(e);if(f&&!p.answersBots){r.debug({source:a,thread:t},"ignoring a bot: this agent answers none");return}const v=G(n,e);if(v&&await q.seen(`slack:${v}`)){r.info({source:a,thread:t,message:v,eventId:e.event_id},"duplicate message, skipping");return}const o=e.user??"";if(!_(o)){if(f){r.debug({source:a,thread:t,user:o},"ignoring a bot off the allow list");return}r.warn({source:a,thread:t,user:o},"refusing an unauthorized request");try{await s.client.chat.postEphemeral({channel:n.channelId,user:o,thread_ts:n.threadTs,text:"You're not on this agent's allow list."})}catch(i){r.debug({err:i},"could not post the refusal")}return}const S=await x.remember(t);if(B&&await B({key:n,keyString:t,thread:S,text:u.text},s)){f||p.clear(t),r.info({source:a,thread:t,user:o},"ran a command");return}const b=p.admit(t,f);if(!b.run){const i={source:a,thread:t,user:o},g="holding off: too many bot turns in a row in this thread";if(b.say?r.warn(i,g):r.debug(i,g),b.say)try{await $(s.client,{channel:n.channelId,threadTs:n.threadTs},b.say)}catch(k){r.debug({err:k},"could not say why Bishop is holding off")}return}const w={text:u.text,userId:o,ts:e.ts??n.threadTs,isBot:f,mentionsAgent:C(e.text??"",d.botUserId),files:u.files},U=l.get(t);if(U){U.push(w),r.info({source:a,thread:t,user:o},"holding a message until the turn ends");const i=(y.get(t)??Promise.resolve()).then(()=>R(s.client,n.channelId,w.ts));y.set(t,i),await i;return}l.set(t,[]);try{let i=[w],g=!c;for(;i.length||l.get(t)?.length;){if(!i.length){i=await E(n,t,s);continue}r.info({thread:t,messages:i.length,newThread:g},"running a turn");try{await z({key:n,keyString:t,thread:S,channelId:n.channelId,threadTs:n.threadTs,messages:i,isNewThread:g,participants:T.count(t),isDirectMessage:e.channel_type==="im"||n.channelId.startsWith("D")},s)}catch(k){r.error({thread:t,err:k},"turn failed")}g=!1,i=await E(n,t,s)}}finally{l.delete(t),y.delete(t)}}async function M(e,s,a){try{await P(e,s,a)}catch(n){r.error({source:a,err:n},"failed to handle a Slack event")}}I.event("app_mention",async({event:e,body:s,client:a})=>{await M({...e,event_id:s.event_id},{client:a},"app_mention")}),I.event("message",async({event:e,body:s,client:a})=>{await M({...e,event_id:s.event_id},{client:a},"message")}),I.event("agent_session_stopped",async({event:e})=>{const s=e,a=K(s,d.teamId);if(!a)return;if(!_(s.user??"")){r.warn({user:s.user},"ignoring a stop from an unauthorized user");return}const n=A(a),t=D(n);r.info({thread:n,user:s.user,aborted:t},"stop requested")})}export{ne as registerHandlers};
|
package/dist/slack/history.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{describedFile as
|
|
1
|
+
import{describedFile as b}from"../files.js";import{declareUnread as I,trimHistory as g,withinFileBudget as k}from"../interface/history.js";import{log as d}from"../log.js";import{describeSlackFile as p,slackFilePayload as w}from"./files.js";import{renderMentions as y}from"./mentions.js";import{FILE_SHARE as x}from"./route.js";import{nameBotMentions as l,spokenAt as T}from"./thread.js";const B=200,_=3;function W(t){const e=t.messages[0]?.ts;if(!(!e||e===t.threadTs))return e}async function N(t){const e=await P(t);if(!e)return d.warn({thread:t.key.threadTs},"could not read the thread to catch the agent up"),[{kind:"omitted",reason:"unavailable"}];for(const r of e.messages)r.userId&&!f(r,t.self)&&t.saw?.(r.userId,T(r.ts));const o=F(e.messages,t),s=k(g(o.entries));return M(e.complete?s:I(s,o.opensWithParent),t)}async function P(t){const e=[],o=new Set;let s;try{for(let r=0;r<_;r+=1){const i=await t.client.conversations.replies({channel:t.key.channelId,ts:t.key.threadTs,latest:t.before,inclusive:!1,limit:B,...s?{cursor:s}:{}});for(const n of i.messages??[]){if(!n.ts||n.ts>=t.before||o.has(n.ts))continue;o.add(n.ts);const{subtype:a,files:u}=n;e.push({ts:n.ts,...n.user?{userId:n.user}:{},...n.bot_id?{botId:n.bot_id}:{},...a?{subtype:a}:{},text:n.text??"",files:u??[]})}if(s=i.response_metadata?.next_cursor,!s)return c(e,!0)}return c(e,!1)}catch(r){return d.warn({thread:t.key.threadTs,read:e.length,err:r},"could not read the whole thread"),e.length?c(e,!1):void 0}}function c(t,e){return{messages:[...t].sort((s,r)=>s.ts<r.ts?-1:s.ts>r.ts?1:0),complete:e}}function v(t,e){return t.botId&&!e.answersBots?!1:!!(t.userId&&e.allows(t.userId))}function f(t,e){return t.userId&&t.userId===e.botUserId?!0:!!e.botId&&t.botId===e.botId}function A(t,e){return t.map(o=>{const s=p(o),r=b(s);if(s.unavailable)return r;const i=e.remember({id:s.id,name:s.name,payload:w(o)});return i?{...r,ref:i}:{...r,unavailable:"Bishop could not record a reference for it"}})}function F(t,e){const o=[];let s=!1,r=0;const i=()=>{r&&(o.push({kind:"omitted",reason:"unauthorized",count:r}),r=0)};for(const n of t){const a=f(n,e.self),u=n.ts===e.key.threadTs;if(n.subtype&&n.subtype!==x&&!a)continue;if(!a&&!v(n,e)){u&&(s=!0),r+=1;continue}if(!l(n.text,e.self.botUserId)&&n.files.length===0)continue;const m=l(n.text,e.self.botUserId,e.self.botName);u&&(s=!0),i(),o.push({kind:"message",principal:h,sender:{id:n.userId??n.botId??"",bot:!!n.botId},text:m,...a?{self:!0}:{},...n.files.length>0?{shared:n.files}:{}})}return i(),{entries:o,opensWithParent:s}}async function M(t,e){return Promise.all(t.map(async o=>{if(o.kind!=="message")return o;const{shared:s,sender:r,...i}=o,n=s?A(s,e):[];return{...i,principal:r?.id?await e.principal(r.id,r.bot):h,text:await y(o.text,e.users),...n.length>0?{files:n}:{}}}))}const h={id:"",externalId:"",handle:"unknown",name:"unknown",kind:"unknown",affiliation:"unknown"};export{W as landedMidThread,N as threadHistory};
|
package/dist/slack/interface.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import{etiquetteNote as
|
|
2
|
-
`)},
|
|
1
|
+
import{runCommand as O}from"../commands/run.js";import{commandLine as C}from"../commands/syntax.js";import{etiquetteNote as L,threadParticipants as E}from"../interface/etiquette.js";import{threadVerbosity as H}from"../interface/verbosity.js";import{log as f}from"../log.js";import{createSlackApp as F}from"./app.js";import{createAuthorizer as K}from"./authorizer.js";import{botTurnCap as U}from"./bots.js";import{slackObserver as _}from"./context.js";import{deliverToSlack as z,describeSlackHandle as R,parseSlackHandle as I,resolveSlackDestination as W,slackHandle as j}from"./destination.js";import{describeSlackFile as q,parseSlackFile as G,slackAttachment as S,slackFilePayload as V}from"./files.js";import{registerHandlers as Y}from"./handlers.js";import{landedMidThread as $,threadHistory as J}from"./history.js";import{linkMentions as T,mentionableName as Q,renderMentions as x}from"./mentions.js";import{createPresence as X}from"./presence.js";import{slackSighting as Z}from"./principal.js";import{say as M}from"./say.js";import{threadKeyString as ee}from"./thread.js";import{userDirectory as te}from"./users.js";const ae={name:"slack",guidance:["This conversation is a Slack thread. Each thing you write is posted as its own message while","you work, and the tools you call show as a card between them, so someone is watching the","whole turn. Say what you are doing as you go, and keep it to a line or two. Your last message","is the answer to what was asked. Slack renders standard markdown, tables included, so write","normally and keep it short.","People and other agents are named to you as @name. Write @name to mention one and Bishop","makes it a real Slack mention; a name it cannot match to one account stays plain text, so","use the name you were given. @here, @channel, and @everyone are never mentions, whoever","writes them.","A thread can hold a conversation between other people that has nothing to do with you.",'Bishop marks a message you may leave alone with a <thread> tag carrying reply="optional":',"other people are talking here and this one did not name you, so answer it only if you have","something worth adding. To stay out of it, say <no-output> and nothing else, and Bishop","posts nothing at all. Every message without that tag is yours to answer."].join(`
|
|
2
|
+
`)},ne=3e3;async function oe(t,i,o=ne){const l=await se(t.botUserId,i,o),s=Q(l)??t.username;if(!s)return;const m=[t.appName,l.realName].filter(h=>!!h?.trim()&&h?.toLowerCase()!==s.toLowerCase());return{address:`@${s}`,...m.length>0?{aka:[...new Set(m)]}:{}}}async function se(t,i,o){const l={userId:t};let s;const m=new Promise(h=>{s=setTimeout(()=>h(l),o)});try{return await Promise.race([i.lookup(t).catch(()=>l),m])}finally{clearTimeout(s)}}async function xe(t){const{app:i,identity:o}=await F(t.credentials),l=await K(i.client,t.allow),s=te(i.client,{self:o.botUserId}),m=U(t.maxBotTurns),h=E(),g=_({client:i.client,users:s,home:o,self:o.botUserId}),b=H(t.meta,"slack",t.verbosity),N={verbosity:b,model:t.model,configured:t.configuredModel??{},stop:t.stop},c=await oe(o,s),u={...ae,...c?{self:c}:{}};c?f.info({address:c.address,aka:c.aka},"the agent's name in Slack"):f.warn("could not read the bot's own name; the agent will not know what it is called");const k=new Map,P=async({key:e,keyString:a,thread:r,text:w},d)=>{const n=C(w);if(!n)return!1;const p=O(n,{thread:r},N);try{await M(d.client,{channel:e.channelId,threadTs:e.threadTs},p),k.get(a)?.interrupted()}catch(y){f.warn({thread:a,err:y},"couldn't answer a command")}return!0};async function v(e,a){const r=await s.lookup(e);return t.principals.see(u.name,Z(r,a,o))}function B(e,a){const r=q(a);if(!r.unavailable)return t.attachments.remember(e,u.name,{id:r.id,name:r.name,payload:V(a)})}async function D(e,a){return a.files.length===0?[]:Promise.all(a.files.map(async r=>{const w=B(e.id,r),d=await t.files.save(e.dirKey,S(r,t.credentials.botToken));return w?{...d,ref:w}:d}))}function A(e,a){const r=x(e.messages[0]?.text??"",s),w=$(e),d=L({participants:e.participants,mentioned:e.messages.some(n=>n.mentionsAgent)});return{thread:e.thread,isNewThread:e.isNewThread,origin:u,...d?{note:d}:{},...w?{history:()=>J({client:a.client,key:e.key,before:w,allows:n=>l.allows(n),answersBots:m.answersBots,self:{botUserId:o.botUserId,...o.botId?{botId:o.botId}:{},...c?{botName:c.address}:{}},users:s,principal:v,remember:n=>t.attachments.remember(e.thread.id,u.name,n),saw:(n,p)=>h.saw(e.keyString,n,p)})}:{},observe:()=>g.observe({channelId:e.channelId,direct:e.isDirectMessage,speaker:e.messages.at(-1)?.userId??""}),async messages(){return Promise.all(e.messages.map(async(n,p)=>{const y=await D(e.thread,n);return{principal:await v(n.userId,n.isBot),text:p===0?await r:await x(n.text,s),...y.length>0?{files:y}:{}}}))},presence:()=>{const n=X(e,a,{outgoing:p=>T(p,s),title:()=>r,verbosity:()=>b.of(e.thread.id).level,...d?{mayDecline:!0}:{},...t.taskIntervalMs===void 0?{}:{taskIntervalMs:t.taskIntervalMs}});return k.set(e.keyString,n),{...n,finish:async()=>{try{await n.finish()}finally{k.delete(e.keyString)}}}},async notify(n){await M(a.client,{channel:e.channelId,threadTs:e.threadTs},n)},async destination(){const n=j({channel:e.channelId,threadTs:e.threadTs});return{interface:"slack",handle:n,label:await R(i.client,n)}}}}return Y(i,{identity:o,...c?{botName:c.address}:{},threads:t.threads,events:t.events,run:(e,a)=>t.run(A(e,a)),stop:t.stop,allows:e=>l.allows(e),bots:m,participants:h,command:P,changed:e=>g.changed(e)}),{name:"slack",async start(){await i.start(),o.scopes.includes("files:read")||f.warn("this Slack app has no files:read scope, so it cannot open files people share; reinstall it to grant one"),o.scopes.includes("files:write")||f.warn("this Slack app has no files:write scope, so it cannot send files back; reinstall it to grant one"),s.prime(),f.info("listening on Slack via Socket Mode")},async stop(){l.stop(),await i.stop()},allows:e=>l.allows(e.externalId),resolveDestination:(e,a)=>W(i.client,e,{...a?.optedInOnly?{allows:r=>l.allows(r)}:{}}),sameConversation:(e,a)=>I(e).channel===I(a).channel,deliver:async e=>{const a=await z(i.client,{...e,text:await T(e.text,s)});return a?{threadKey:ee({teamId:o.teamId,channelId:a.channel,threadTs:a.threadTs})}:{}},attachment:e=>{const a=G(e);return a?S(a,t.credentials.botToken):void 0}}}export{ae as SLACK_ORIGIN,xe as createSlackInterface,oe as slackIdentity};
|
package/dist/slack/members.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{log as
|
|
1
|
+
import{log as a}from"../log.js";function d(t){const n=t.profile?.display_name?.trim(),s=t.profile?.real_name?.trim(),e=t.profile?.email?.trim(),r=t.name?.trim(),i=t.enterprise_user?.enterprise_id,o=t.is_restricted||t.is_ultra_restricted||t.is_stranger;return{userId:t.id??"",...n?{displayName:n}:{},...s?{realName:s}:{},...e?{email:e}:{},...r?{handle:r}:{},...t.team_id?{teamId:t.team_id}:{},...i?{enterpriseId:i}:{},...o?{outsider:!0}:{}}}const l="USLACKBOT";function c(t){return!!t.id&&!t.deleted&&t.id!==l}async function p(t,n=()=>!0){const s=[];let e;do{const r=await t.users.list({limit:200,...e?{cursor:e}:{}});for(const i of r.members??[])i.id&&n(i)&&s.push(d(i));e=r.response_metadata?.next_cursor||void 0}while(e);return a.debug({users:s.length},"paged the workspace member list"),s}export{c as isMentionable,p as listMembers,d as toUser};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{mentionableName as r}from"./mentions.js";function l(n,e){return n.outsider?"external":n.teamId?n.teamId===e.teamId||n.enterpriseId&&n.enterpriseId===e.enterpriseId?"internal":"external":"unknown"}function d(n,e,a){const t=r(n),i=n.realName??n.displayName;return{externalId:n.userId,...t?{handle:t}:{},...i?{name:i}:{},...n.email?{email:n.email}:{},kind:e?"bot":"person",affiliation:l(n,a)}}export{l as affiliationOf,d as slackSighting};
|
package/dist/slack/users.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{log as a}from"../log.js";import{isMentionable as D,listMembers as M,toUser as b}from"./members.js";const _=3600*1e3,N=2e3,x=60*1e3;function l(i){return i?.trim().toLowerCase()||void 0}function L(i,
|
|
1
|
+
import{log as a}from"../log.js";import{isMentionable as D,listMembers as M,toUser as b}from"./members.js";const _=3600*1e3,N=2e3,x=60*1e3;function l(i){return i?.trim().toLowerCase()||void 0}function L(i,o){return l(i.handle)===o||l(i.displayName)===o||l(i.realName)===o}function k(i,o={}){const{ttlMs:f=_,deadlineMs:p=N,self:w}=o,c=new Map;let s,u;const m=new Map;async function y(e){let t={userId:e};try{const r=(await i.users.info({user:e})).user;if(r){const g=r.real_name?.trim();t={...b(r),userId:e,...!r.profile?.real_name?.trim()&&g?{realName:g}:{}}}}catch(n){a.debug({userId:e,err:n},"could not resolve the Slack user")}return c.set(e,{user:t,expires:Date.now()+f}),t}function d(){return u??=M(i,e=>D(e)&&e.id!==w).then(e=>{s={users:e,expires:Date.now()+f}}).catch(e=>{s={users:[],expires:Date.now()+x},a.debug({err:e},"could not list the workspace members")}).finally(()=>{u=void 0}),u}async function h(){if(s&&s.expires>Date.now())return s.users;const e=d();let t;const n=new Promise(r=>{t=setTimeout(r,p)});return await Promise.race([e,n]),clearTimeout(t),s?s.users:(a.debug("the member list is still loading; leaving this name unlinked"),[])}return{get size(){return c.size},prime(){d()},async roster(){const e=await h();return e.length>0?e:void 0},async lookup(e){if(!e)return{userId:e};const t=c.get(e);if(t&&t.expires>Date.now())return t.user;const n=m.get(e);if(n)return n;const r=y(e).finally(()=>m.delete(e));return m.set(e,r),r},async findByName(e){const t=l(e.replace(/^@+/,""));if(!t)return;const n=(await h()).filter(r=>L(r,t));if(n.length===1)return n[0];n.length>1&&a.debug({name:t,hits:n.length},"a written name matched more than one user")}}}export{k as userDirectory};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{commandIn as d,commandLine as u}from"../commands/syntax.js";import{htmlText as f}from"./html.js";const r=/<pre\b[^>]*>([\s\S]*?)<\/pre>/gi,l=/<code\b[^>]*>([\s\S]*?)<\/code>/gi;function i(n){return n.replace(/\s+/g," ").trim()}function s(n){const e=[...n.matchAll(r)];if(e.length>1)return;const[t]=e;if(t){const c=n.replace(r,"");return/<code\b/i.test(c)?void 0:t[1]}const o=[...n.matchAll(l)];return o.length===1?o[0]?.[1]:void 0}function a(n){const e=u(n.text);if(e)return e;if(!n.html)return;const t=s(n.html);if(t===void 0)return;const o=f(t);if(i(o)===i(n.text))return d(o)}export{a as teamsCommandLine};
|
package/dist/teams/create.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import{randomUUID as
|
|
2
|
-
`)):a.length>0&&(u=a.map(f=>(
|
|
1
|
+
import{randomUUID as x}from"node:crypto";import{readFile as I}from"node:fs/promises";import{App as $}from"@microsoft/teams.apps";import{BishopError as S}from"../errors.js";import{log as h}from"../log.js";import{retry as g}from"../retry.js";import{teamsRetryable as y}from"./api.js";import{teamsAllowList as M}from"./authorize.js";import{DOWNLOAD_TIMEOUT_MS as U,teamsFilesOf as C}from"./files.js";import{teamsGraph as F}from"./graph.js";import{createTeamsInterface as k}from"./interface.js";import{teamsLogger as v}from"./logger.js";import{createListener as R}from"./server.js";import{graphAddressOf as O}from"./thread.js";const q=3978;async function B(e){const t=async(s,c)=>{try{return await I(s,"utf8")}catch(d){throw new S(`Couldn't read the TLS ${c} at ${s}: ${d.message}`,"ConfigError")}};return{cert:await t(e.cert,"certificate"),key:await t(e.key,"key")}}function p(e,t=[]){return{type:"message",text:e,...t.length>0?{attachments:t}:{}}}const j="application/vnd.microsoft.teams.card.file.consent",A="application/vnd.microsoft.teams.card.file.info";function E(e){if(!e||typeof e!="object")return;const t=e.ref;return typeof t=="string"?t:void 0}function N(e){const t={description:`Bishop wants to send ${e.name}.`,sizeInBytes:e.size,acceptContext:{ref:e.ref},declineContext:{ref:e.ref}};return{contentType:j,name:e.name,content:t}}function _(e){return{id:x(),contentType:A,contentUrl:e.webUrl,name:e.name,content:{uniqueId:e.uniqueId,fileType:e.fileType}}}async function D(e,t){const s=t.uploadInfo?.uploadUrl;if(!s)throw new Error("Teams returned no upload URL for the accepted file");const c=T(s),d=new Uint8Array(await I(e.path)),l=Math.max(0,d.byteLength-1),w=await fetch(c,{method:"PUT",headers:{"content-type":"application/octet-stream","content-length":String(d.byteLength),"content-range":`bytes 0-${l}/${d.byteLength}`},body:d});if(!w.ok)throw new Error(`Teams returned ${w.status} while receiving ${e.name}`)}async function ce(e){const t=M(e.config?.allow),s=R({port:e.config?.port??q,...e.config?.host?{host:e.config.host}:{},...e.config?.tls?{tls:await B(e.config.tls)}:{}}),c=new $({clientId:e.credentials.clientId,clientSecret:e.credentials.clientSecret,tenantId:e.credentials.tenantId,httpServerAdapter:s,logger:v()}),d=F(e.credentials),l=new Map,b=k({api:{async send(r,n,o){const a=o?.files??[];let i=n,u=[];o?.channel&&a.length>0?(u=(await Promise.all(a.map(m=>d.uploadChannelFile(o.channel,m)))).map(_),i=[n,...u.map(m=>`<attachment id="${m.id}"></attachment>`)].filter(Boolean).join(`
|
|
2
|
+
`)):a.length>0&&(u=a.map(f=>(l.set(f.ref,f),N(f))));try{const f=await g(()=>c.send(r,p(i,u)),{what:"teams send",retryable:y});return f?.id?{id:f.id}:void 0}catch(f){for(const m of a)l.delete(m.ref);throw f}},async update(r,n,o){await g(()=>c.api.conversations.updateActivity(r,n,p(o)),{what:"teams update",retryable:y})},async member(r,n){return await g(()=>c.api.conversations.getMemberById(r,n),{what:"teams roster",retryable:y})},async sharedFiles(r){const n=O(r);return n?d.files(n):[]},async history(r){const n=O(r);if(n)return d.conversation(n)},async fetchFile(r){const n=await fetch(T(r),{signal:AbortSignal.timeout(U)});if(!n.ok)throw new Error(`the download returned ${n.status}`);if(!n.body)throw new Error("the download sent no body");return n.body},async fetchAttachment(r){const n=T(r),o=new URL(c.api.serviceUrl);if(n.host!==o.host)throw h.warn({host:n.host,service:o.host},"refusing to send the app credential to a host that is not the Teams service"),new Error("the image is not hosted on the Teams service");const a=await c.api.http.get(n.toString(),{responseType:"arraybuffer",timeout:U});return P(a.data)},async fetchShared(r){return d.fetchShared(r)}},allow:t,...e.config?.verbosity?{verbosity:e.config.verbosity}:{},listener:s,meta:e.meta,model:e.model,...e.configuredModel?{configuredModel:e.configuredModel}:{},threads:e.threads,events:e.events,files:e.files,attachments:e.attachments,principals:e.principals,tenantId:e.credentials.tenantId,run:e.run,stop:e.stop});c.on("message",async({activity:r})=>{await b.handle(G(r))}),c.on("file.consent.accept",async({activity:r,send:n})=>(await L(r.value,n),{status:200})),c.on("file.consent.decline",async({activity:r,send:n})=>{const o=E(r.value.context),a=o?l.get(o):void 0;return o&&l.delete(o),a?(await n(p(`${a.name} was not sent because the file-consent request was declined.`)),{status:200}):(h.warn("received a Teams file-consent decline for an unknown file"),{status:200})});async function L(r,n){const o=E(r.context),a=o?l.get(o):void 0;if(o&&l.delete(o),!a){h.warn("received a Teams file-consent acceptance for an unknown file");return}try{await D(a,r);const i=r.uploadInfo;if(!i?.contentUrl||!i.uniqueId)throw new Error("Teams returned incomplete file information after the upload");await n(p(`${a.name} was sent.`,[{contentType:A,contentUrl:i.contentUrl,name:i.name??a.name,content:{uniqueId:i.uniqueId,fileType:i.fileType??a.mimeType}}]))}catch(i){h.warn({err:i,file:a.name},"could not finish a Teams file upload"),await n(p(`Could not send ${a.name}: ${i.message}`))}}return await c.initialize(),h.info({endpoint:c.server.messagingEndpoint},"the Teams app is mounted"),b}function T(e){const t=new URL(e);if(t.protocol!=="https:")throw new Error("the download URL is not HTTPS");return t}function P(e){if(e instanceof Uint8Array)return e;if(e instanceof ArrayBuffer)return new Uint8Array(e);throw new Error("the download came back as something other than bytes")}function z(e){const t=e.recipient?.id,s=t?e.stripMentionsText?.({accountId:t}):e;return(s?.stripMentionsText?.({tagOnly:!0})??s)?.text??e.text??""}function G(e){const t=W(e.channelData);return{id:e.id,conversationId:e.conversation.id,conversationType:typeof e.conversation.conversationType=="string"?e.conversation.conversationType:"channel",text:z(e),...H(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:C(e.attachments),...t?{channel:t}:{}}}function H(e){const t=e?.find(s=>s.contentType?.toLowerCase()==="text/html"&&typeof s.content=="string")?.content;return typeof t=="string"?{html:t}:{}}function W(e){const t=e?.team?.aadGroupId,s=e?.channel?.id;if(!(typeof t!="string"||typeof s!="string"))return{teamId:t,channelId:s}}export{ce as createTeams,G as inboundOf};
|
package/dist/teams/fake.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{FILE_DOWNLOAD_INFO as
|
|
1
|
+
import{FILE_DOWNLOAD_INFO as F,GRAPH_REFERENCE as E}from"./files.js";function M(){const n=[],p=[],u=[],l=[],h=[],f=[],m=new Map,y=new Map,w=new Map,g=new Map;let a,i,d,b=!1,x=1;const c=(e,t)=>{l.push({url:e,credential:t});const o=g.get(e);if(o===void 0)throw new Error(`nothing hosted at ${e}`);return new TextEncoder().encode(o)};return{sent:n,edits:p,rosterCalls:u,fetched:l,sharedCalls:h,historyCalls:f,addMember(e,t){w.set(`${e}:${t.id}`,t)},addBytes(e,t){g.set(e,t)},addShared(e,t){y.set(e,t)},addHistory(e,t){m.set(e,t)},failNextSend(e=new Error("Teams is down")){a=e},failNextUpdate(e=new Error("Teams is down")){i=e},sendAnonymously(){b=!0},failMember(e=new Error("no roster access")){d=e},async send(e,t,o){if(a){const s=a;throw a=void 0,s}const r=`activity-${x++}`;return n.push({conversationId:e,text:t,id:r,...o?.files?.length?{files:o.files}:{}}),b?void 0:{id:r}},async update(e,t,o){if(i){const s=i;throw i=void 0,s}p.push({conversationId:e,activityId:t,text:o});const r=n.find(s=>s.id===t);if(!r)throw new Error(`no activity ${t} to update`);r.text=o},async member(e,t){if(u.push(e),d)throw d;return w.get(`${e}:${t}`)},async sharedFiles(e){return e.conversationType==="personal"?[]:(h.push(e.id),y.get(e.id)??[])},async history(e){if(e.conversationType==="personal")return;f.push(e.id);const t=m.get(e.conversationId);if(!t)return;const o=r=>Number(r.id)<Number(e.id);return{...t,messages:t.messages.filter(o)}},async fetchFile(e){return c(e,"none")},async fetchAttachment(e){return c(e,"bot")},async fetchShared(e){return c(e,"graph")}}}function _(n={}){return{contentType:F,name:"doc.txt",uniqueId:"1150D938-8870-4044-9F2C-5BBDEBA70C9D",contentUrl:"https://contoso.sharepoint.com/personal/jerry_contoso_com/Documents/doc.txt",etag:"123",downloadUrl:"https://download.contoso.com/doc.txt?tempauth=abc",...n}}function N(n={}){return{contentType:E,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 v(n={}){return{contentType:"image/png",contentUrl:"https://smba.trafficmanager.net/amer/v3/attachments/img-1/views/original",...n}}function C(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 D(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{_ as aFile,D as aGraphMessage,C as aMessage,N as aSharedFile,v as anImage,M as fakeTeams};
|
package/dist/teams/graph.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{readFile as
|
|
1
|
+
import{readFile as P}from"node:fs/promises";import{log as p}from"../log.js";import{retry as b}from"../retry.js";import{DOWNLOAD_TIMEOUT_MS as A,GRAPH_REFERENCE as _}from"./files.js";const g="https://graph.microsoft.com/v1.0",v="https://login.microsoftonline.com",N=1e4,O=2,x=1e3,L=2e4,E=50,C=3,B=10,F=6e4,z=2,q=1e3,H=6e4;class l extends Error{status;retryAfterMs;constructor(i,c,u){super(i),this.status=c,this.retryAfterMs=u,this.name="GraphError"}}function S(e){const{status:i,retryAfterMs:c}=e??{};return typeof i!="number"?!1:i===0?!0:i===429?c===void 0?!0:{retryAfterMs:c}:i>=500}function I(e){const i=e.headers.get("retry-after");if(i===null)return;const c=Number(i);return Number.isFinite(c)&&c>=0?c*1e3:void 0}function U(e){return`u!${Buffer.from(e,"utf8").toString("base64url")}`}function Y(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 M(e){if(!(e.contentType!==_||!e.contentUrl))return{contentType:_,contentUrl:e.contentUrl,...e.id?{uniqueId:e.id}:{},...e.name?{name:e.name}:{}}}function j(e){if(!e.id||e.messageType&&e.messageType!=="message"||e.deletedDateTime)return;const i=e.createdDateTime?Date.parse(e.createdDateTime):Number.NaN,c=e.from?.user,u=e.from?.application,y=e.body?.content??"";return{id:e.id,...Number.isFinite(i)?{at:i}:{},...c?.id?{user:{id:c.id,...c.displayName?{name:c.displayName}:{}}}:{},...u?.id?{app:{id:u.id}}:{},html:e.body?.contentType==="text"?y.replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">"):y,appMentions:(e.mentions??[]).flatMap(m=>{const $=m.mentioned?.application?.id;return $&&m.id!==void 0?[{id:String(m.id),appId:$}]:[]}),files:(e.attachments??[]).flatMap(m=>M(m)??[])}}function G(e,i){return/^\d+$/.test(e)&&/^\d+$/.test(i)?Number(e)<Number(i):e!==i}function D(e){return[...e].sort((i,c)=>(i.at??0)-(c.at??0)||(G(i.id,c.id)?-1:1))}function Q(e){let i;async function c(o){if(i&&i.expiresAt>Date.now())return i.token;const r=await fetch(`${v}/${e.tenantId}/oauth2/v2.0/token`,{method:"POST",headers:{"content-type":"application/x-www-form-urlencoded"},body:new URLSearchParams({client_id:e.clientId,client_secret:e.clientSecret,grant_type:"client_credentials",scope:"https://graph.microsoft.com/.default"}),signal:o}).catch(n=>{throw new l(`could not reach Entra: ${n.message}`,0)});if(!r.ok)throw new l(`Entra refused the app credentials with ${r.status}`,r.status,I(r));const t=await r.json();if(!t.access_token)throw new l("Entra returned no access token",0);const s=(t.expires_in??3600)*1e3;return i={token:t.access_token,expiresAt:Date.now()+s-H},i.token}async function u(o,r,t){return b(async()=>{const s=await fetch(o.startsWith(`${g}/`)?o:`${g}${o}`,{headers:{authorization:`Bearer ${await c(t)}`},signal:t}).catch(n=>{throw new l(`could not reach Graph: ${n.message}`,0)});if(!s.ok)throw new l(`Graph answered ${r} with ${s.status}`,s.status,I(s));return await s.json()},{what:`graph ${r}`,retryable:S,attempts:O,maxDelayMs:x})}async function y(o,r,t,s){return b(async()=>{const n=await fetch(`${g}${o}`,{method:"PUT",headers:{authorization:`Bearer ${await c(s)}`,"content-type":"application/octet-stream","content-length":String(t.byteLength)},body:t,signal:s}).catch(a=>{throw new l(`could not reach Graph: ${a.message}`,0)});if(!n.ok)throw new l(`Graph answered ${r} with ${n.status}`,n.status,I(n));return await n.json()},{what:`graph ${r}`,retryable:S,attempts:z,maxDelayMs:q})}async function m(o,r,t,s){try{const a=(await u(`/drives/${encodeURIComponent(o)}/items/${encodeURIComponent(r)}?$select=sharepointIds`,"the SharePoint identifiers of an uploaded file",s)).sharepointIds?.listItemUniqueId;if(a)return a}catch(n){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 $(o,r){const t=await u(`/teams/${encodeURIComponent(o.teamId)}/channels/${encodeURIComponent(o.channelId)}/filesFolder`,"a channel files folder",r);if(!t.id||!t.parentReference?.driveId)throw new l("Graph returned an incomplete channel files folder",0);return{driveId:t.parentReference.driveId,folderId:t.id}}async function R(o,r){try{return await u(`/shares/${U(o)}/driveItem`,"a shared file",r)}catch(t){const s=t instanceof l?t.status:0;return p.warn({err:t,status:s},"could not read a file shared in a Teams conversation"),s===401||s===403?{refused:"Bishop is not allowed to read files in this Microsoft 365 tenant"}:void 0}}async function T(o,r,t,s){const n=[];let a=o;try{for(let f=0;a&&f<r;f+=1){const d=await u(a,t,s);n.push(...d.value??[]);const h=d["@odata.nextLink"];if(a=h?.startsWith(`${g}/`)?h:void 0,h&&!a)return p.warn({what:t},"Graph offered a next page somewhere other than Graph"),{raw:n,complete:!1,failed:!1}}return{raw:n,complete:!a,failed:!1}}catch(f){return p.warn({err:f,what:t,read:n.length},"could not read the whole conversation"),{raw:n,complete:!1,failed:!0}}}return{async conversation(o){const r=AbortSignal.timeout(L),t=d=>{const h=new Set;return d.flatMap(k=>{const w=j(k);return!w||!G(w.id,o.messageId)||h.has(w.id)?[]:(h.add(w.id),[w])})};if(o.kind==="chat"){const d=await T(`/chats/${o.chatId}/messages?$top=${E}&$orderby=createdDateTime desc`,C,"a chat",r);return d.failed&&d.raw.length===0?void 0:{messages:D(t(d.raw)),complete:d.complete}}const s=o.replyToId;if(!s||s===o.messageId)return{messages:[],complete:!0};const n=`/teams/${o.teamId}/channels/${o.channelId}/messages/${s}`,[a,f]=await Promise.all([u(n,"the message that opened a thread",r).then(d=>d,d=>{p.warn({err:d},"could not read the message that opened a Teams thread")}),T(`${n}/replies?$top=${E}`,B,"a thread",r)]);if(!(!a&&f.failed&&f.raw.length===0))return{messages:D(t([...a?[a]:[],...f.raw])),complete:!!a&&f.complete}},async files(o){const r=AbortSignal.timeout(N);let t;try{t=await u(Y(o),"a message",r)}catch(n){return p.warn({err:n,address:o.kind},"could not read a Teams message from Graph"),[]}const s=(t.attachments??[]).flatMap(n=>M(n)??[]);return Promise.all(s.map(async n=>{const a=await R(n.contentUrl,r);return a?"refused"in a?{...n,unavailable:a.refused}:{...n,name:n.name??a.name,...a.eTag?{etag:a.eTag}:{},...a.size===void 0?{}:{size:a.size},...a.file?.mimeType?{mimeType:a.file.mimeType}:{}}:n}))},async fetchShared(o){const r=AbortSignal.timeout(A),t=await fetch(`${g}/shares/${U(o)}/driveItem/content`,{headers:{authorization:`Bearer ${await c(r)}`},redirect:"manual",signal:r});if(t.status>=300&&t.status<400){const s=t.headers.get("location");if(!s)throw new Error("Graph redirected the download to nowhere");const n=await fetch(s,{signal:AbortSignal.timeout(A)});if(!n.ok)throw new Error(`the download returned ${n.status}`);if(!n.body)throw new Error("the download sent no body");return n.body}if(!t.ok)throw new Error(`Graph answered the download with ${t.status}`);if(!t.body)throw new Error("the download sent no body");return t.body},async uploadChannelFile(o,r){const t=AbortSignal.timeout(F),s=new Uint8Array(await P(r.path)),n=await $(o,t),a=await y(`/drives/${encodeURIComponent(n.driveId)}/items/${encodeURIComponent(n.folderId)}:/${encodeURIComponent(r.name)}:/content`,`the file ${r.name} in a Teams channel`,s,t);if(!a.id||!a.webUrl)throw new l(`Graph returned an incomplete upload for ${r.name}`,0);return{id:a.id,uniqueId:await m(n.driveId,a.id,a.eTag,t),name:a.name??r.name,webUrl:a.webUrl,fileType:a.file?.mimeType??r.mimeType}}}}export{S as graphRetryable,Q as teamsGraph};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{describedFile as k}from"../files.js";import{declareUnread as b,trimHistory as v,withinFileBudget as y}from"../interface/history.js";import{log as p}from"../log.js";import{describeTeamsFile as I}from"./files.js";import{messageText as h}from"./html.js";import{threadIdOf as B}from"./thread.js";function H(n){if(n.conversationType==="personal")return!1;if(n.conversationType==="groupChat")return!0;const e=B(n.conversationId);return!!e&&e!==n.id}async function W(n){const{read:e}=n;if(!e)return p.warn("could not read the Teams conversation to catch the agent up"),[{kind:"omitted",reason:"unavailable"}];for(const t of e.messages)t.user&&t.at!==void 0&&n.saw?.(t.user.id,t.at);const a=new Map,i=new Map(e.messages.flatMap(t=>t.user?[[t.user.id,t.user]]:[]));await Promise.all([...i.values()].map(async t=>{try{a.set(t.id,await n.sender(t))}catch(u){p.warn({err:u,who:t.id},"could not check a Teams sender for history")}}));const s=T(e.messages,n,a),o=v(s.entries),c=o[1]?.kind==="omitted"&&o[1].reason==="trimmed",r=!e.complete&&!n.openedBy&&c?o.slice(1):o,f=y(r).map(t=>x(t,n));return e.complete?f:b(f,s.opensWithParent)}function T(n,e,a){const i=[];let s=!1,o=0;const c=()=>{o&&(i.push({kind:"omitted",reason:"unauthorized",count:o}),o=0)};for(const r of n){const d=r.id===e.openedBy,f=!!e.selfAppId&&r.app?.id===e.selfAppId,t=r.user?a.get(r.user.id):void 0;if(!f&&!t?.allowed){d&&(s=!0),o+=1;continue}const u=new Set(r.appMentions.filter(l=>l.appId===e.selfAppId).map(l=>l.id));if(!h(r.html,(l,m)=>u.has(l)?"":`@${m}`)&&r.files.length===0)continue;const w=h(r.html,(l,m)=>u.has(l)&&e.selfName?e.selfName:`@${m}`);d&&(s=!0),c();const g=f?e.self?.():t?.principal();i.push({kind:"message",principal:g??{id:"",externalId:e.selfAppId??"",handle:e.selfName?.replace(/^@/,"")||"unknown",name:e.selfName?.replace(/^@/,"")||"unknown",kind:"bot",affiliation:"unknown"},text:w,...f?{self:!0}:{},...r.files.length>0?{shared:r.files}:{}})}return c(),{entries:i,opensWithParent:s}}function x(n,e){if(n.kind!=="message")return n;const{shared:a,...i}=n;if(!a?.length)return i;const s=a.map(o=>{const c=k(I(o)),r=e.remember(o);return r?{...c,ref:r}:{...c,unavailable:"Bishop could not record a reference for it"}});return{...i,files:s}}export{H as landedMidConversation,W as teamsHistory};
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
const s={amp:"&",lt:"<",gt:">",quot:'"',apos:"'",nbsp:" "};function a(e){return e.replace(/&(#x[0-9a-f]+|#\d+|[a-z]+);/gi,(r,t)=>t.startsWith("#x")||t.startsWith("#X")?String.fromCodePoint(Number.parseInt(t.slice(2),16)):t.startsWith("#")?String.fromCodePoint(Number.parseInt(t.slice(1),10)):s[t.toLowerCase()]??r)}function c(e){return a(e.replace(/<br\s*\/?>/gi,`
|
|
2
|
+
`).replace(/<[^>]*>/g,""))}function g(e,r){const t=e.replace(/<at\b[^>]*?\bid="([^"]*)"[^>]*>([\s\S]*?)<\/at>/gi,(p,n,o)=>{const i=o.replace(/<[^>]*>/g,"").trim();return r?r(n,i):`@${i}`}).replace(/<img\b[^>]*>/gi,"[image]").replace(/<\/(p|div|li|h[1-6]|blockquote|pre)>/gi,`
|
|
3
|
+
`);return c(t).replace(/[ \t]+\n/g,`
|
|
4
|
+
`).replace(/\n{3,}/g,`
|
|
5
|
+
|
|
6
|
+
`).trim()}export{a as decodeEntities,c as htmlText,g as messageText};
|
package/dist/teams/interface.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import{
|
|
2
|
-
`)}function
|
|
1
|
+
import{runCommand as Q}from"../commands/run.js";import{BishopError as C}from"../errors.js";import{etiquetteNote as V,threadParticipants as X}from"../interface/etiquette.js";import{splitMessage as J}from"../interface/format.js";import{showsProgress as Z,showsTasks as tt,threadVerbosity as nt}from"../interface/verbosity.js";import{log as c}from"../log.js";import{teamsCommandLine as et}from"./command.js";import{describeTeamsFile as ot,parseTeamsFile as at,teamsAttachment as K,teamsFilePayload as it}from"./files.js";import{landedMidConversation as rt,teamsHistory as st}from"./history.js";import{teamsPresence as ct}from"./presence.js";import{teamsSighting as F}from"./principal.js";import{addressesAgent as O,routeActivity as lt}from"./route.js";import{baseConversation as ht,conversationOf as N,destinationHandleOf as dt,destinationOfHandle as ut,fileRouteOf as P,sameConversation as ft,scopeOf as mt,threadIdOf as D,threadKeyOf as wt}from"./thread.js";function yt(e){return[...pt(e),...gt].join(`
|
|
2
|
+
`)}function pt(e){return Z(e)?["This conversation is in Microsoft Teams. Each thing you write is posted as its own message","while you work, so somebody is watching the whole turn. Say what you are doing as you go,","and keep it to a line or two. Your last message is the answer to what was asked.","",tt(e)?"Your tool calls show as one message that is rewritten as you work, and replaced by a count of them once you speak again.":"Your tool calls are not shown, so a stretch of them is a silence unless you say what you are doing.","","Nothing more can reach this conversation once the turn ends, so never say you will follow up","separately: finish what you were asked before you stop.",""]:["This conversation is in Microsoft Teams. Only the last thing you write reaches it, and","anything you say on the way there is dropped, so put the whole answer in the message you","finish with. Never say you will follow up separately: nothing more can reach this","conversation until someone writes to you again.","","Nobody is watching while you work. None of your tool calls are visible and nothing you say","arrives until the turn is over, so a turn that takes minutes costs nothing, and there is no","typing indicator to say you are still going.",""]}const gt=["Teams renders a narrower markdown than Slack does. Bold, italics, links, inline code, fenced","code blocks and lists work. Tables do not, so write a list or plain prose where you would","otherwise reach for one.","","A file somebody attaches reaches you in a direct chat and in a channel, and so does an image","pasted into a message anywhere. One Bishop could not download arrives saying why instead of","going missing, so you can tell somebody what happened to it. A group chat is the one place a","file may not reach you at all: if somebody there asks about one you were given nothing for,","say it never arrived and ask them to send it in a direct chat.","","A conversation can hold people talking to each other and not to you. Bishop marks a message",'you may leave alone with a <thread> tag carrying reply="optional": other people are talking',"here and this one did not @mention you, so answer it only if you have something worth adding.","To stay out of it, say <no-output> and nothing else, and Bishop posts nothing at all. Every","message without that tag is yours to answer."],vt="You are not on this agent\u2019s allow list, so it can\u2019t answer you.",R="There was no text in that message for me to act on. Say what you need in words, and @mention me so I see it.",Tt=R+" If you attached a file, nothing about it reached me, so send it in a direct chat.",bt=2e4,It=3600*1e3;function Dt(e){const y=new Set,p=new Map,S=new Map,g=X(),v=nt(e.meta,"teams",e.verbosity),j={verbosity:v,model:e.model,configured:e.configuredModel??{},stop:e.stop},T=new Map;let m,E=!1;function b(n){return{name:"teams",guidance:yt(v.of(n.id).level),...m?{self:m}:{}}}async function A(n,t,o){if(!t)return;const i=p.get(t);if(i)return i;if((S.get(t)??0)>Date.now())return;const a=ht(n);let r;try{r=await e.api.member(a,o),r&&p.set(t,r)}catch(s){c.warn({err:s,conversation:a},"could not read the Teams roster")}return!r&&e.allow.open&&S.set(t,Date.now()+It),r}async function w(n,t,o=[],i){let a;const r=t?J(t,bt):[""];for(const[s,l]of r.entries()){const f=s===r.length-1&&(o.length>0||i)?{...o.length>0?{files:o}:{},...i?{channel:i}:{}}:void 0,u=await e.api.send(n,l,f);a=u?{id:u.id,text:l}:void 0}return a}function I(n,t){return[n,t?.userPrincipalName,t?.email].filter(o=>!!o).map(o=>o.trim().toLowerCase())}function M(n,t){const o=it(t);if(!o)return;const i=ot(t);return e.attachments.remember(n,"teams",{id:i.id,name:i.name,payload:o})}async function H(n,t){return t.files.length===0?[]:Promise.all(t.files.map(async o=>{const i=M(n.id,o),a=await e.files.save(n.dirKey,K(o,e.api));return i?{...a,ref:i}:a}))}function G(n,t,o){return e.principals.see(b(n).name,F({id:t.from.aadObjectId??t.from.id,...t.from.name?{name:t.from.name}:{},bot:t.from.id.startsWith("28:")},o,e.tenantId))}async function $(n,t){const o=b(n),i=N(t),a=D(i),r=t.recipientId?.replace(/^28:/,"");return st({read:await e.api.history(t),...a?{openedBy:a}:{},...r?{selfAppId:r}:{},...m?{selfName:m.address}:{},...t.recipientId?{self:()=>e.principals.see(o.name,{externalId:t.recipientId,...t.recipientName?{handle:t.recipientName}:{},...t.recipientName?{name:t.recipientName}:{},kind:"bot",affiliation:"unknown"})}:{},sender:async s=>{const l=await A(i,s.id,s.id);return{allowed:e.allow.open||I(s.id,l).some(h=>e.allow.allows(h)),principal:()=>e.principals.see(o.name,F({id:s.id,...s.name?{name:s.name}:{},bot:!1},l,e.tenantId))}},remember:s=>M(n.id,s),saw:(s,l)=>g.saw(n.threadKey,s,l)})}function z(n,t,o,i){const a=N(t),r=D(a),s=t.channel?{teamId:t.channel.teamId,channelId:t.channel.channelId,...r?{threadId:r}:{}}:void 0,l=V({participants:g.count(n.threadKey),mentioned:O(t)});return{thread:n,isNewThread:i,get origin(){return b(n)},...l?{note:l}:{},...rt(t)?{history:()=>$(n,t)}:{},async messages(){const h=await H(n,t);return[{principal:o,text:t.text,...h.length>0?{files:h}:{}}]},presence:()=>{const h=ct({send:(f,u)=>w(a,f,u,s),post:f=>e.api.send(a,f),update:(f,u)=>e.api.update(a,f,u),threadKey:n.threadKey,verbosity:()=>v.of(n.id).level,...P(mt(t),s)==="none"?{share:!1}:{},...l?{mayDecline:!0}:{}});return T.set(n.threadKey,h),{...h,finish:async()=>{try{await h.finish()}finally{T.delete(n.threadKey)}}}},async notify(h){await w(a,h)},async destination(){return{interface:"teams",handle:dt(t),label:kt(t)}}}}async function L(n){if(E)return;if(await e.events.seen(`teams:${n.id}`)){c.debug({activity:n.id},"already answered this Teams activity");return}!m&&n.recipientName&&(m={address:`@${n.recipientName}`},c.info({address:m.address},"the agent is known by this name in Teams"));const t=wt(n),o=await e.threads.has(t),i=lt(n,{threadKnown:o});if(i.action==="ignore"){c.debug({activity:n.id,thread:t,reason:i.reason},"leaving a Teams message alone");return}const a=n.from.aadObjectId;if(!a){c.warn({activity:n.id},"refusing a Teams message with no Entra object ID");return}g.saw(t,a,Date.now());const r=N(n),s=await A(r,a,n.from.id);if(!e.allow.open&&!I(a,s).some(d=>e.allow.allows(d))){c.warn({activity:n.id,who:a,conversation:r},"refusing an unauthorized Teams message"),O(n)&&await w(r,vt).catch(d=>{c.debug({err:d},"could not tell somebody they are not on the allow list")});return}const l=et(n);if(l){const d=await e.threads.remember(t),k=Q(l,{thread:d},j);try{await w(r,k),T.get(t)?.interrupted()}catch(q){c.warn({thread:t,err:q},"couldn't answer a command")}c.info({thread:t,who:a},"ran a command");return}const h=new Set(n.files.map(d=>d.contentUrl)),f=(await e.api.sharedFiles(n)).filter(d=>!h.has(d.contentUrl)),u=f.length>0?{...n,files:[...n.files,...f]}:n;if(!u.text.trim()&&u.files.length===0){if(c.info({activity:n.id},"a Teams message had nothing to act on"),O(n)){const d=n.conversationType==="personal"?R:Tt;await w(r,d).catch(k=>{c.debug({err:k},"could not answer a message with nothing in it")})}return}const _=await e.threads.remember(t),Y=G(_,u,s);c.info({thread:t,who:a,newThread:!o},"running a turn from Teams");const B=e.run(z(_,u,Y,!o)).catch(d=>{c.error({thread:t,err:d},"a turn from Teams failed")}).finally(()=>{y.delete(B)});y.add(B)}async function x(){for(;y.size>0;)await Promise.allSettled([...y])}function U(n){return n==="groupChat"?"Bishop cannot send files to Teams group chats yet.":n==="channel"?"Bishop has no Graph address for this Teams channel, so it cannot send files there.":"Bishop cannot send files to this older Teams destination. Ask for a new report in the conversation first."}async function W(n){const t=ut(n.handle),o=n.files??[],i=o.map(a=>a.name).join(", ");if(o.length>0&&P(t.scope,t.channel)==="none")throw new C(U(t.scope));try{await w(t.conversation,n.text,o,t.channel)}catch(a){if(!o.length)throw a;c.warn({handle:n.handle,files:i,err:a},"could not attach files to a Teams report"),await w(t.conversation,`${n.text}
|
|
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._`,[],t.channel)}return{}}return{name:"teams",handle:L,settle:x,deliver:W,allows(n){return e.allow.open?!0:[...I(n.externalId,p.get(n.externalId)),...n.email?[n.email.trim().toLowerCase()]:[]].some(o=>e.allow.allows(o))},sameConversation:ft,async resolveDestination(){throw new C("Bishop cannot yet name a Teams channel or chat from what you typed. Ask for a report in the conversation you want it in, which names itself.")},attachment:n=>{const t=at(n);return t?K(t,e.api):void 0},async start(){await e.listener.start(),c.info({allow:e.allow.open?"anyone in the tenant":"a list"},"listening for Teams activities")},async stop(){E=!0,await e.listener.stop(),y.size>0&&(c.info({turns:y.size},"waiting for Teams turns still running"),await x())}}}function kt(e){return e.conversationType==="personal"?`a chat with ${e.from.name??"someone"}`:e.conversationType==="groupChat"?"a Teams group chat":"a Teams channel thread"}export{Dt as createTeamsInterface,yt as teamsGuidance};
|
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
|
|
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,s="",i,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,s=t,i=r,l=n.length>0}catch(r){if(n.length>0)try{const a=await e.send(t);c+=1,s=t,i=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")}s="",i=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(){s="",i=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),s="",i=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&&i&&!
|
|
3
|
+
`).trim():""),r=_(t.parts),a=B(n),d=O(k())?`_${$(Date.now()-A,D||!!f)}_`:"";if(!a&&d&&i&&!l)try{await e.update(i.id,`${i.text}
|
|
4
4
|
|
|
5
5
|
${d}`);return}catch(m){o.debug({thread:e.threadKey,err:m},"could not close a Teams turn in place")}const T=[a,d].filter(Boolean).join(`
|
|
6
6
|
|
|
7
|
-
`),p=u.unplaced();if(!(!T&&r.length===0&&p.length===0)){try{await e.send(T,r),
|
|
7
|
+
`),p=u.unplaced();if(!(!T&&r.length===0&&p.length===0)){try{await e.send(T,r),c+=1,s=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,s=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===s.trim()?"":t:c===0?C:""}}export{q as teamsPresence};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
function r(n,t){if(!n)return"unknown";const o=n.userRole?.toLowerCase();return o==="guest"||o==="anonymous"?"external":!n.tenantId||!t?"unknown":n.tenantId.toLowerCase()===t.toLowerCase()?"internal":"external"}function s(n,t,o){const e=n.name?.trim()||t?.name?.trim(),i=t?.name?.trim()||e,a=t?.email?.trim();return{externalId:n.id,...e?{handle:e}:{},...i?{name:i}:{},...a?{email:a}:{},kind:n.bot?"bot":"person",affiliation:r(t,o)}}export{r as affiliationOf,s as teamsSighting};
|
package/dist/teams/thread.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
const s=new Set(["personal","groupChat"]),
|
|
1
|
+
const s=new Set(["personal","groupChat"]),f=/^[1-9]\d*$/;function u(n){const e=n.conversationId;return s.has(n.conversationType)||e.includes(";messageid=")?e:f.test(n.id)?`${e};messageid=${n.id}`:e}function l(n){return n.conversationType==="personal"?"personal":n.conversationType==="groupChat"?"groupChat":"channel"}function v(n,e){return n==="personal"?"consent":n==="channel"&&e?"upload":"none"}function x(n){const e=u(n),i=l(n);return i!=="channel"?`${e};scope=${i}`:n.channel?`${e};scope=channel;teamid=${encodeURIComponent(n.channel.teamId)};channelid=${encodeURIComponent(n.channel.channelId)}`:`${e};scope=channel`}function C(n){const[e="",...i]=n.split(";"),t=new Map(i.map(r=>{const a=r.indexOf("=");return a<0?[r,""]:[r.slice(0,a),r.slice(a+1)]})),c=t.get("messageid"),d=c?`${e};messageid=${c}`:e,o=t.get("scope");if(o==="personal")return{conversation:d,scope:o};if(o==="groupChat")return{conversation:d,scope:o};if(o==="channel"){const r=t.get("teamid"),a=t.get("channelid");return!r||!a?{conversation:d,scope:o}:{conversation:d,scope:o,channel:{teamId:decodeURIComponent(r),channelId:decodeURIComponent(a),...c?{threadId:c}:{}}}}return{conversation:d,scope:"unknown"}}function O(n){return`teams:${u(n)}`}function h(n){return n.split(";")[0]??n}function m(n,e){return p(n)===p(e)}function p(n){const[e="",...i]=n.split(";"),t=i.find(c=>c.startsWith("messageid="));return t?`${e};${t}`:e}function y(n){if(n.conversationType==="personal")return;if(s.has(n.conversationType))return{kind:"chat",chatId:h(n.conversationId),messageId:n.id};if(!n.channel)return;const e=I(n);return{kind:"channel",teamId:n.channel.teamId,channelId:n.channel.channelId,messageId:n.id,...e?{replyToId:e}:{}}}function I(n){const e=$(n.conversationId);return e&&e!==n.id?e:void 0}function $(n){return n.split(";").find(e=>e.startsWith("messageid="))?.slice(10)}export{h as baseConversation,u as conversationOf,x as destinationHandleOf,C as destinationOfHandle,v as fileRouteOf,y as graphAddressOf,m as sameConversation,l as scopeOf,$ as threadIdOf,O as threadKeyOf};
|