@agentdeploymentco/bishop 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +800 -0
- package/dist/agent-dir.js +1 -0
- package/dist/agent-source.js +1 -0
- package/dist/agent-store.js +5 -0
- package/dist/attachments.js +2 -0
- package/dist/cli.js +30 -0
- package/dist/config.js +4 -0
- package/dist/db.js +52 -0
- package/dist/env.js +12 -0
- package/dist/errors.js +1 -0
- package/dist/exec.js +1 -0
- package/dist/files.js +2 -0
- package/dist/gc.js +2 -0
- package/dist/github/gh.js +3 -0
- package/dist/github/setup.js +15 -0
- package/dist/gmail/api.js +1 -0
- package/dist/gmail/auth.js +2 -0
- package/dist/gmail/authorize.js +1 -0
- package/dist/gmail/create.js +1 -0
- package/dist/gmail/destination.js +1 -0
- package/dist/gmail/fake.js +5 -0
- package/dist/gmail/files.js +1 -0
- package/dist/gmail/history.js +1 -0
- package/dist/gmail/interface.js +2 -0
- package/dist/gmail/labels.js +1 -0
- package/dist/gmail/loopback.js +1 -0
- package/dist/gmail/message.js +17 -0
- package/dist/gmail/oauth.js +3 -0
- package/dist/gmail/presence.js +5 -0
- package/dist/gmail/route.js +1 -0
- package/dist/gmail/setup.js +9 -0
- package/dist/harness/claude-options.js +1 -0
- package/dist/harness/claude.js +11 -0
- package/dist/harness/codex.js +10 -0
- package/dist/harness/create.js +1 -0
- package/dist/harness/fake.js +1 -0
- package/dist/harness/layers.js +1 -0
- package/dist/harness/principal.js +14 -0
- package/dist/harness/types.js +0 -0
- package/dist/interface/attachments.js +1 -0
- package/dist/interface/authorize.js +1 -0
- package/dist/interface/delivery.js +1 -0
- package/dist/interface/destination.js +1 -0
- package/dist/interface/history.js +1 -0
- package/dist/interface/state.js +1 -0
- package/dist/interface/types.js +0 -0
- package/dist/log.js +1 -0
- package/dist/retry.js +1 -0
- package/dist/schedule/cli.js +17 -0
- package/dist/schedule/cron.js +1 -0
- package/dist/schedule/presence.js +5 -0
- package/dist/schedule/prompt.js +2 -0
- package/dist/schedule/runner.js +5 -0
- package/dist/schedule/store.js +6 -0
- package/dist/sessions/queue.js +1 -0
- package/dist/sessions/store.js +27 -0
- package/dist/setup.js +48 -0
- package/dist/slack/agent-session.js +1 -0
- package/dist/slack/app.js +2 -0
- package/dist/slack/authorize.js +2 -0
- package/dist/slack/authorizer.js +3 -0
- package/dist/slack/bots.js +1 -0
- package/dist/slack/commands/run.js +1 -0
- package/dist/slack/commands/syntax.js +3 -0
- package/dist/slack/destination.js +1 -0
- package/dist/slack/directory.js +1 -0
- package/dist/slack/etiquette.js +1 -0
- package/dist/slack/files.js +1 -0
- package/dist/slack/format.js +4 -0
- package/dist/slack/handlers.js +1 -0
- package/dist/slack/history.js +1 -0
- package/dist/slack/inspect.js +2 -0
- package/dist/slack/interface.js +2 -0
- package/dist/slack/manifest.js +1 -0
- package/dist/slack/members.js +1 -0
- package/dist/slack/mentions.js +1 -0
- package/dist/slack/pending.js +1 -0
- package/dist/slack/phrases.js +1 -0
- package/dist/slack/presence.js +5 -0
- package/dist/slack/provision.js +19 -0
- package/dist/slack/route.js +1 -0
- package/dist/slack/say.js +1 -0
- package/dist/slack/status.js +1 -0
- package/dist/slack/tasks.js +1 -0
- package/dist/slack/thread.js +1 -0
- package/dist/slack/users.js +1 -0
- package/dist/slack/verbosity.js +1 -0
- package/dist/tools/files.js +2 -0
- package/dist/tools/schedules.js +8 -0
- package/dist/tools/server.js +1 -0
- package/dist/tools/types.js +1 -0
- package/dist/turn.js +1 -0
- package/dist/version.js +1 -0
- package/dist/worktrees.js +2 -0
- package/package.json +52 -0
|
@@ -0,0 +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 D}from"../env.js";import{BishopError as s}from"../errors.js";import{gmailAllowList as S}from"../gmail/authorize.js";import{resolveGmailDestination as z}from"../gmail/destination.js";import{interfaceFor as E,UnplaceableDestination as B}from"../interface/destination.js";import{resolveSlackDestination as C}from"../slack/destination.js";import{describeTime as l,nextRun as h,systemTimeZone as m,validateCron as T,validateTimeZone as F}from"./cron.js";import{scheduleStore as j}from"./store.js";async function u(n){A(n);const t=await g(n),e=R(x(n));return{config:t,db:e,schedules:j(e)}}function p(n,t){const e=n?.trim();if(!e)throw new s(`${t} is required`);return e}function q(n){try{return n()}catch(t){throw t instanceof B?new s(`${t.message} Or say which with --interface.`):t}}async function G(n,t,e,o){if(t==="slack"){if(!e.slack)throw new s("Slack is not configured here, so nothing sent to a Slack destination would arrive. Run `bishop slack setup` first.");return C(new v(e.slack.botToken),n)}if(t==="gmail"){if(!e.gmail)throw new s("Gmail is not configured here, so nothing sent to an address would arrive. Run `bishop gmail setup` first.");return z(S(o),n)}throw new s(`Bishop has no "${t}" interface`)}async function V(n,t){const e=p(t.name,"--name"),o=p(t.cron,"--cron"),i=p(t.prompt,"--prompt"),r=p(t.to,"--to"),a=t.tz?F(t.tz.trim()):m(),{config:w,db:$,schedules:b}=await u(n);try{T(o,a);const f=D();if(!f.ok)throw new s(k(f));const d=await G(r,q(()=>E(r,t.interface)),f.credentials,w.gmail?.allow),c=b.add({name:e,cron:o,tz:a,prompt:i,destination:d,createdBy:`cli:${y().username}`,nextRunAt:h(o,a,Date.now())});process.stdout.write(`Added ${c.id} "${c.name}"
|
|
2
|
+
runs ${c.cron} (${c.tz})
|
|
3
|
+
next ${l(c.nextRunAt,m())}
|
|
4
|
+
to ${d.label} via ${d.interface}
|
|
5
|
+
|
|
6
|
+
A running Bishop picks it up within a minute. Nothing fires while Bishop is stopped.
|
|
7
|
+
`)}finally{$.close()}}function L(n,t){const e=[`${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&&e.push(` last ${l(n.lastRunAt,t)}`),n.lastError&&e.push(` error ${n.lastError} (${n.consecutiveFailures} in a row)`),e.push(` by ${n.createdBy}`),e.join(`
|
|
8
|
+
`)}async function X(n){const{db:t,schedules:e}=await u(n);try{const o=e.all();if(o.length===0){process.stdout.write(`No schedules here.
|
|
9
|
+
`);return}const i=m();process.stdout.write(`${o.map(r=>L(r,i)).join(`
|
|
10
|
+
|
|
11
|
+
`)}
|
|
12
|
+
`),process.stdout.write(`
|
|
13
|
+
Times shown in ${i}.
|
|
14
|
+
`)}finally{t.close()}}async function Y(n,t){const{db:e,schedules:o}=await u(n);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{e.close()}}async function _(n,t,e){const{db:o,schedules:i}=await u(n);try{const r=i.get(t);if(!r)throw new s(`no schedule ${t}. \`bishop schedules list\` has them.`);const a=e?h(r.cron,r.tz,Date.now()):r.nextRunAt;i.setEnabled(t,e,a),e&&i.clearFailures(t),process.stdout.write(e?`Enabled ${t} "${r.name}", next ${l(a,m())}
|
|
16
|
+
`:`Disabled ${t} "${r.name}"
|
|
17
|
+
`)}finally{o.close()}}export{V as addSchedule,X as listSchedules,Y as removeSchedule,_ as setScheduleEnabled};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{CronExpressionParser as l}from"cron-parser";import{BishopError as d}from"../errors.js";const f=300*1e3,p=20;class o extends d{constructor(t){super(t,"CronError")}}function g(){return Intl.DateTimeFormat().resolvedOptions().timeZone||"UTC"}function w(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 u(e,t,r){return c(e,()=>l.parse(e.trim(),{tz:t,currentDate:new Date(r)}))}function T(e,t,r){const n=u(e,t,r);return c(e,()=>n.next().getTime())}function $(e,t,r=Date.now()){const n=e.trim();if(!n)throw new o("a schedule needs a cron expression");w(t);const i=n.split(/\s+/);if(!n.startsWith("@")&&i.length!==5)throw new o(`"${n}" has ${i.length} ${i.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 s=u(n,t,r);c(n,()=>{let a=s.next().getTime();for(let h=1;h<p;h+=1){if(!s.hasNext())return;const m=s.next().getTime();if(m-a<f)throw new o(`"${n}" fires every ${Math.round((m-a)/1e3)}s, and Bishop's floor is one firing every ${f/6e4} minutes. Every turn costs the agent real work, so a schedule this frequent is nearly always a mistyped field.`);a=m}})}function v(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))}export{o as CronError,f as MIN_INTERVAL_MS,v as describeTime,T as nextRun,g as systemTimeZone,$ as validateCron,w as validateTimeZone};
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import{isSilent as i}from"./prompt.js";function c(){const n=[],r=[];let s="",t;return{async handle(e){switch(e.type){case"text":n.push(e.text);return;case"done":s=e.text;return;case"error":t=e.message;return;default:return}},async finish(){},note(e){r.push(e)},outcome(){const e=s.trim()||n.join(`
|
|
2
|
+
|
|
3
|
+
`).trim();return{text:[...r,e].filter(Boolean).join(`
|
|
4
|
+
|
|
5
|
+
`),...t?{failure:t}:{},silent:i(e)}}}}export{c as schedulePresence};
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import{NO_OUTPUT as n,renderTag as a,saysNoOutput as o}from"../harness/principal.js";const i="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 ${n} 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){return a("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":e.createdBy})}function u(e){const t=e.trim();return!t||o(t)?!0:t.replaceAll(/[^a-z0-9]/gi,"").toLowerCase()===i.replaceAll(" ","").toLowerCase()}export{h as SCHEDULE_ORIGIN,i as SILENCE,u as isSilent,d as scheduleNote};
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import{log as a}from"../log.js";import{nextRun as $}from"./cron.js";import{schedulePresence as z}from"./presence.js";import{SCHEDULE_ORIGIN as _,scheduleNote as I}from"./prompt.js";const N=3e4,R=5,M=5e3;function A(i){return i.startsWith("cli:")}function B(i,l){return`schedule:${i}:${l}`}function U(i){const l=i.now??Date.now,T=i.tickMs??N,k=i.authorized??A,s=new Map,m=new Map,d=new Set;let u,p=!1;function g(e,n){try{return i.schedules.setNextRun(e.id,$(e.cron,e.tz,n)),!0}catch(r){const t=r instanceof Error?r.message:String(r);return a.error({schedule:e.id,cron:e.cron,tz:e.tz,err:r},"disabling a schedule whose cron expression cannot be read"),i.schedules.setEnabled(e.id,!1),i.schedules.recordFailure(e.id,n,t),w(e,`The schedule "${e.name}" is off: Bishop can no longer read when it should run. ${t}
|
|
2
|
+
|
|
3
|
+
Remove it, or add it again with a cron expression that works.`),!1}}async function w(e,n){const r=i.deliver(e.destination,{text:n,title:e.name}).then(()=>{}).catch(t=>{a.error({schedule:e.id,to:e.destination.label,err:t},"could not say that a schedule failed")}).finally(()=>{d.delete(r)});d.add(r),await r}async function S(e,n,r){const t=i.schedules.recordFailure(e.id,n,r);if(a.error({schedule:e.id,name:e.name,failures:t,err:r},"a scheduled run failed"),t>=R){i.schedules.setEnabled(e.id,!1),a.error({schedule:e.id,name:e.name,failures:t},"disabling a schedule after too many failures in a row"),await w(e,`The schedule "${e.name}" has failed ${t} times in a row and is now off. The last failure was: ${r}
|
|
4
|
+
|
|
5
|
+
Nothing more will run until \`bishop schedules enable ${e.id}\`.`);return}t===1&&await w(e,`The schedule "${e.name}" failed: ${r}`)}async function E(e,n,r){const t=z(),c={threadKey:r,isNewThread:!0,freshSession:!0,tools:!1,origin:_,note:I(e,n),async messages(){return[{principal:{id:`schedule:${e.id}`,displayName:e.name},text:e.prompt}]},presence:()=>t,async notify(o){t.note(o)},async destination(){return e.destination}};a.info({schedule:e.id,name:e.name},"a schedule is firing");let b;try{await i.run(c)}catch(o){b=o instanceof Error?o.message:String(o)}const y=t.outcome(),x=y.failure??b,f=l();if(x){await S(e,f,x);return}if(y.silent){i.schedules.recordSuccess(e.id,f),a.info({schedule:e.id,name:e.name},"a schedule had nothing to report");return}let h;try{h=await i.deliver(e.destination,{text:y.text,title:e.name})}catch(o){await S(e,f,o instanceof Error?o.message:String(o));return}if(h.threadKey&&i.attach)try{await i.attach(h.threadKey,r)}catch(o){a.error({schedule:e.id,thread:h.threadKey,err:o},"a report was delivered but nothing will answer a reply to it")}i.schedules.recordSuccess(e.id,f),a.info({schedule:e.id,name:e.name,to:e.destination.label},"a schedule reported")}async function v(){if(p)return;const e=l();for(const n of i.schedules.due(e)){if(!k(n.createdBy)){a.warn({schedule:n.id,createdBy:n.createdBy},"skipping a schedule whose creator can no longer be authorized"),g(n,e);continue}if(!g(n,e))continue;if(s.has(n.id)){a.warn({schedule:n.id,name:n.name},"skipping a firing while the previous one is still running");continue}const r=B(n.id,e);m.set(n.id,r);const t=E(n,e,r).catch(c=>{a.error({schedule:n.id,err:c},"a schedule firing threw")}).finally(()=>{s.delete(n.id),m.delete(n.id)});s.set(n.id,t)}}return{start(){const e=l(),n=i.schedules.due(e);for(const t of n)a.info({schedule:t.id,name:t.name,due:new Date(t.nextRunAt).toISOString()},"skipping a firing that came due while Bishop was not running"),g(t,e);const r=i.schedules.all().filter(t=>t.enabled);a.info({schedules:r.length,skipped:n.length},"watching the schedules"),u=setInterval(()=>{v().catch(t=>{a.error({err:t},"a scheduler pass failed; trying again on the next tick")})},T),u.unref()},tick:v,async settle(){await Promise.allSettled([...s.values(),...d])},async stop(){if(p=!0,u&&clearInterval(u),s.size>0){a.info({firings:s.size},"stopping scheduled runs still going");for(const t of s.keys()){const c=m.get(t);c&&i.cancel?.(c)}}const e=Promise.allSettled([...s.values(),...d]);let n;const r=new Promise(t=>{n=setTimeout(()=>{a.warn({firings:s.size},"giving up on scheduled runs that would not stop"),t()},M)});await Promise.race([e,r]),n&&clearTimeout(n)}}}export{R as MAX_CONSECUTIVE_FAILURES,A as authorizedToRun,B as firingThread,U as makeScheduler};
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import{randomUUID as h}from"node:crypto";function l(t){return{id:t.id,name:t.name,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 A(){return h().replaceAll("-","").slice(0,8)}function m(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",u=t.prepare(`INSERT INTO schedules (${a})
|
|
2
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 1, ?, NULL, NULL, 0)`),d=t.prepare(`SELECT ${a} FROM schedules ORDER BY created_at`),s=t.prepare(`SELECT ${a} FROM schedules WHERE id = ?`),o=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 SET enabled = ?, next_run_at = ? WHERE id = ?"),i=t.prepare("UPDATE schedules SET next_run_at = ? WHERE id = ?"),p=t.prepare(`UPDATE schedules
|
|
3
|
+
SET last_run_at = ?, last_error = NULL, consecutive_failures = 0
|
|
4
|
+
WHERE id = ?`),R=t.prepare(`UPDATE schedules
|
|
5
|
+
SET last_run_at = ?, last_error = ?, consecutive_failures = consecutive_failures + 1
|
|
6
|
+
WHERE id = ?`),f=t.prepare("UPDATE schedules SET last_error = NULL, consecutive_failures = 0 WHERE id = ?");return{all(){return d.all().map(l)},get(e){const r=s.get(e);return r?l(r):void 0},add(e){const r=A(),n=e.createdAt??Date.now();return u.run(r,e.name,e.cron,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},setEnabled(e,r,n){const c=s.get(e);return c?(_.run(r?1:0,n??c.next_run_at,e),!0):!1},due(e){return o.all(e).map(l)},setNextRun(e,r){i.run(r,e)},recordSuccess(e,r){p.run(r,e)},clearFailures(e){f.run(e)},recordFailure(e,r,n){return R.run(r,n,e),s.get(e)?.consecutive_failures??0}}}export{m as scheduleStore};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
function u(){const e=new Map;return{get size(){return e.size},run(t,r){const s=(e.get(t)??Promise.resolve()).then(r,r),n=s.then(()=>{},()=>{});return e.set(t,n),n.then(()=>{e.get(t)===n&&e.delete(t)}),s}}}export{u as threadQueue};
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import{log as k}from"../log.js";const R=10080*60*1e3;function D(e,d){const s=e.prepare("SELECT session_id, harness FROM sessions WHERE thread_key = ?"),a=e.prepare(`INSERT INTO sessions (thread_key, session_id, harness, created_at, updated_at)
|
|
2
|
+
VALUES (?, ?, ?, ?, ?)
|
|
3
|
+
ON CONFLICT (thread_key) DO UPDATE SET session_id = excluded.session_id,
|
|
4
|
+
harness = excluded.harness,
|
|
5
|
+
updated_at = excluded.updated_at`),o=e.prepare("INSERT OR IGNORE INTO processed_events (event_id, seen_at) VALUES (?, ?)"),c=e.prepare("SELECT 1 FROM processed_events WHERE event_id = ?"),i=e.prepare("SELECT COUNT(*) AS n FROM sessions"),y=e.prepare("SELECT agent_dir FROM thread_dirs WHERE thread_key = ?"),h=e.prepare("SELECT workspace_key FROM sessions WHERE thread_key = ?"),O=e.prepare(`INSERT INTO sessions (thread_key, session_id, harness, created_at, updated_at, workspace_key)
|
|
6
|
+
VALUES (?, ?, ?, ?, ?, ?)
|
|
7
|
+
ON CONFLICT (thread_key) DO UPDATE SET session_id = excluded.session_id,
|
|
8
|
+
harness = excluded.harness,
|
|
9
|
+
updated_at = excluded.updated_at,
|
|
10
|
+
workspace_key = excluded.workspace_key`),l=e.prepare(`INSERT INTO thread_dirs (thread_key, agent_dir, used_at) VALUES (?, ?, ?)
|
|
11
|
+
ON CONFLICT (thread_key) DO UPDATE SET agent_dir = excluded.agent_dir,
|
|
12
|
+
used_at = excluded.used_at`),T=e.prepare("UPDATE thread_dirs SET used_at = ? WHERE thread_key = ?");return{async get(n){const u=s.get(n);if(u?.session_id){if(u.harness!==d){k.warn({thread:n,recorded:u.harness,running:d},"this thread belongs to a different harness; starting a new session");return}return u.session_id}},async set(n,u){const _=Date.now();a.run(n,u,d,_,_)},async has(n){return s.get(n)!==void 0},async remember(){},async seen(n,u){return u?.record===!1?c.get(n)!==void 0:o.run(n,Date.now()).changes===0},count(){return Number(i.get().n)},async getDir(n){return y.get(n)?.agent_dir},async setDir(n,u){l.run(n,u,Date.now())},async touchDir(n){T.run(Date.now(),n)},async getWorkspace(n){return h.get(n)?.workspace_key??void 0},async continueFrom(n,u,_){const t=Date.now();O.run(n,u,d,t,t,_)}}}function L(e){return async(d,s)=>{const a=await e.get(s);if(!a){k.warn({thread:d,continues:s},"nothing to continue: that thread has no session, so a reply here starts fresh");return}const o=await e.getWorkspace(s)??s;await e.continueFrom(d,a,o),k.info({thread:d,continues:s,session:a,workspace:o},"a new thread continues another")}}function f(e){const d=e.prepare("SELECT agent_dir AS dir, MAX(used_at) AS last_used FROM thread_dirs GROUP BY agent_dir"),s=e.prepare("SELECT MAX(used_at) AS last_used FROM thread_dirs WHERE agent_dir = ?"),a=e.prepare("SELECT thread_key, agent_dir FROM thread_dirs WHERE used_at < ? ORDER BY used_at"),o=e.prepare("SELECT 1 FROM thread_dirs WHERE thread_key = ? AND used_at < ?"),c=e.prepare("DELETE FROM thread_dirs WHERE thread_key = ? AND used_at < ?"),i=t=>`DELETE FROM sessions WHERE thread_key IN (
|
|
13
|
+
SELECT s.thread_key FROM sessions s
|
|
14
|
+
JOIN thread_dirs d ON d.thread_key = COALESCE(s.workspace_key, s.thread_key)
|
|
15
|
+
WHERE d.used_at < ?${t}
|
|
16
|
+
)`,y=e.prepare(i("")),h=t=>`DELETE FROM thread_meta WHERE thread_key IN (
|
|
17
|
+
SELECT s.thread_key FROM sessions s
|
|
18
|
+
JOIN thread_dirs d ON d.thread_key = COALESCE(s.workspace_key, s.thread_key)
|
|
19
|
+
WHERE d.used_at < ?${t}
|
|
20
|
+
)`,O=e.prepare(h("")),l=t=>`SELECT thread_key FROM thread_dirs WHERE used_at < ?${t.replaceAll("d.","")}
|
|
21
|
+
UNION
|
|
22
|
+
SELECT s.thread_key FROM sessions s
|
|
23
|
+
JOIN thread_dirs d ON d.thread_key = COALESCE(s.workspace_key, s.thread_key)
|
|
24
|
+
WHERE d.used_at < ?${t}`,T=e.prepare(l("")),n=e.prepare("DELETE FROM thread_meta WHERE updated_at < ? AND thread_key NOT IN (SELECT thread_key FROM sessions)"),u=e.prepare("DELETE FROM processed_events WHERE seen_at < ?"),_=e.prepare("DELETE FROM thread_files WHERE thread_key = ?");return{lastActivityByDir(){const t=d.all();return new Map(t.map(r=>[r.dir,Number(r.last_used)]))},lastTurnIn(t){const r=s.get(t);return r?.last_used==null?void 0:Number(r.last_used)},staleThreads(t){return a.all(t).map(E=>({threadKey:E.thread_key,dir:E.agent_dir}))},quietSince(t,r){return o.get(t,r)!==void 0},forgetThreads(t,r){let E=0;for(const p of t)E+=Number(c.run(p,r).changes);return E},pruneSessions(t,r=[]){if(r.length===0)return Number(y.run(t).changes);const E=` AND d.thread_key NOT IN (${r.map(()=>"?").join(", ")})`;return Number(e.prepare(i(E)).run(t,...r).changes)},pruneThreadMeta(t,r=[]){let E=Number(n.run(t).changes);if(r.length===0)return E+Number(O.run(t).changes);const p=` AND d.thread_key NOT IN (${r.map(()=>"?").join(", ")})`;return E+=Number(e.prepare(h(p)).run(t,...r).changes),E},collectableThreads(t,r=[]){const E=r.length===0?"":` AND d.thread_key NOT IN (${r.map(()=>"?").join(", ")})`,p=r.length===0?T:e.prepare(l(E)),N=r.length===0?[t,t]:[t,...r,t,...r];return p.all(...N).map(g=>g.thread_key)},forgetFiles(t){let r=0;for(const E of t)r+=Number(_.run(E).changes);return r},pruneEvents(t=Date.now()){return Number(u.run(t-R).changes)}}}function w(e){const d=e.prepare("SELECT value FROM thread_meta WHERE thread_key = ? AND key = ?"),s=e.prepare(`INSERT INTO thread_meta (thread_key, key, value, updated_at) VALUES (?, ?, ?, ?)
|
|
25
|
+
ON CONFLICT (thread_key, key) DO UPDATE SET value = excluded.value,
|
|
26
|
+
updated_at = excluded.updated_at`),a=e.prepare("DELETE FROM thread_meta WHERE thread_key = ? AND key = ?");return{get(o,c){return d.get(o,c)?.value},set(o,c,i){s.run(o,c,i,Date.now())},clear(o,c){a.run(o,c)}}}function C(){const e=new Map,d=(s,a)=>`${s}\0${a}`;return{get:(s,a)=>e.get(d(s,a)),set:(s,a,o)=>{e.set(d(s,a),o)},clear:(s,a)=>{e.delete(d(s,a))}}}function m(e){const d=e.prepare("SELECT value FROM interface_state WHERE key = ?"),s=e.prepare(`INSERT INTO interface_state (key, value, updated_at) VALUES (?, ?, ?)
|
|
27
|
+
ON CONFLICT (key) DO UPDATE SET value = excluded.value, updated_at = excluded.updated_at`);return{get(a){return d.get(a)?.value},set(a,o){s.run(a,o,Date.now())}}}export{R as EVENT_TTL_MS,m as interfaceState,C as memoryThreadMeta,f as retentionStore,D as sessionStore,L as threadContinuation,w as threadMeta};
|
package/dist/setup.js
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import{appendFile as y,readFile as f,writeFile as A}from"node:fs/promises";import{basename as _,join as h}from"node:path";import{createInterface as x}from"node:readline/promises";import{loadConfig as g,writeConfig as B}from"./config.js";import{BishopError as C}from"./errors.js";import{exec as O}from"./exec.js";import{log as u}from"./log.js";import{inspectBotToken as $,missingScopes as b,verifyAppToken as v}from"./slack/inspect.js";import{BOT_EVENTS as L}from"./slack/manifest.js";import{provisionSlackApp as N,slackAuthLogin as P,slackAuthTeams as w,slackCliVersion as K}from"./slack/provision.js";class d extends C{constructor(t){super(t,"SetupError")}}const E=["Bishop needs the Slack CLI to create the app, and it is not installed.",""," brew install --cask slack-cli","","Or see https://docs.slack.dev/tools/slack-cli/guides/installing-the-slack-cli-for-mac-and-linux/"].join(`
|
|
2
|
+
`);function j(n,t){const e=n.slack?.app?.id?.trim(),a=t.BISHOP_SLACK_BOT_TOKEN?.trim(),o=t.BISHOP_SLACK_APP_TOKEN?.trim();if(e&&a&&o)return{appId:e}}function H(n){return _(n).toLowerCase().replaceAll(/[^a-z0-9-]+/g,"-").replaceAll(/^-+|-+$/g,"")||"agent"}function D(n){return n.split(/[-_\s]+/).filter(Boolean).map(t=>t.charAt(0).toUpperCase()+t.slice(1)).join(" ")}async function k(n,t,e){const a=n.name?.trim()||H(t),o=n.name?.trim()||await e("App name",a)||a,p=n.displayName?.trim()||D(o),s=n.displayName?.trim()||await e("Display name",p)||p,r=n.description?.trim()??await e("Description","");return{name:o,displayName:s,...r?{description:r}:{}}}async function R(n,t){const e=h(n,".gitignore");let a="";try{a=await f(e,"utf8")}catch{}const o=new Set(a.split(`
|
|
3
|
+
`).map(i=>i.trim())),p=t.filter(i=>!o.has(i));if(p.length===0)return[];const r=`${a&&!a.endsWith(`
|
|
4
|
+
`)?`
|
|
5
|
+
`:""}${a?`
|
|
6
|
+
`:""}# Bishop credentials and local state
|
|
7
|
+
${p.join(`
|
|
8
|
+
`)}
|
|
9
|
+
`;return await y(e,r,"utf8"),p}async function F(n,t){const e=h(n,".env");let a="";try{a=await f(e,"utf8")}catch{}const o=a?a.split(`
|
|
10
|
+
`):[];for(const[s,r]of Object.entries(t)){const i=o.findIndex(c=>c.startsWith(`${s}=`));i>=0?o[i]=`${s}=${r}`:o.push(`${s}=${r}`)}const p=`${o.filter((s,r)=>s!==""||r<o.length-1).join(`
|
|
11
|
+
`)}
|
|
12
|
+
`;return await A(e,p,{encoding:"utf8",mode:384}),e}async function S(n,t,e=process.stdout){const a=n?x({input:process.stdin,output:e}):void 0,o=async(p,s)=>{if(!a)return s;const r=s?` [${s}]`:"";return(await a.question(`${p}${r}: `)).trim()||s};try{return await t(o)}finally{a?.close()}}function I(n,t){const e=j(n,process.env);return e?(t(`This agent is already set up in Slack as app ${e.appId}.
|
|
13
|
+
Its credentials are in .env and its configuration in .bishop/config.json.
|
|
14
|
+
|
|
15
|
+
To set it up again, remove slack.app.id from .bishop/config.json and the
|
|
16
|
+
BISHOP_SLACK_* lines from .env first.
|
|
17
|
+
`),!0):!1}async function T(n,t,e){await B({...t,slack:{...t.slack,...e.workspace?{workspace:e.workspace}:{},app:e.app}},n);const a=await F(n,{BISHOP_SLACK_BOT_TOKEN:e.botToken,BISHOP_SLACK_APP_TOKEN:e.appToken}),o=await R(n,[".env","bishop.db"]);return` .bishop/config.json app configuration
|
|
18
|
+
${a} credentials
|
|
19
|
+
`+(o.length?` .gitignore added ${o.join(", ")}
|
|
20
|
+
`:"")}async function X(n){const t=n.cwd??process.cwd(),e=n.prompt&&process.stdin.isTTY===!0,a=r=>process.stdout.write(r),o=await g(t);if(I(o,a))return;const p=await K();if(!p)throw new d(E);u.debug({slackCli:p},"found the slack cli");let s=await w();if(s.length===0){if(!e)throw new d("The Slack CLI is not logged in to a workspace.\nRun `slack auth login`, then run this again.");if(a("The Slack CLI is not logged in to a workspace. Starting `slack auth login`.\n\n"),s=await P()?await w():[],s.length===0)throw new d("The Slack CLI is still not logged in, so the app cannot be created.\nRun `slack auth login` on its own to see what went wrong.");a(`
|
|
21
|
+
Logged in.
|
|
22
|
+
`)}await S(e,async r=>{const i=await k(n,t,r),c=s[0];a(`
|
|
23
|
+
Creating the Slack app for "${i.name}" in ${c?.domain??"your workspace"}...
|
|
24
|
+
`);const l=await N({app:i,...c?{teamId:c.id}:{}}),m=await T(t,o,{app:{...i,...l.appId?{id:l.appId}:{}},...c?.domain?{workspace:c.domain}:{},botToken:l.botToken,appToken:l.appToken});u.info({appId:l.appId,manifestCorrected:l.manifestCorrected},"slack app provisioned"),a(`
|
|
25
|
+
Done.
|
|
26
|
+
${m}`+(l.manifestCorrected?"":`
|
|
27
|
+
Note: the app is named "${i.displayName} (local)" in Slack. The Slack CLI
|
|
28
|
+
adds that suffix and Bishop could not remove it. Rename it in App Settings,
|
|
29
|
+
or re-run setup once \`slack auth login\` is refreshed.
|
|
30
|
+
`)+"\nRun `bishop` to start the agent.\n")})}async function Z(n){const t=n.cwd??process.cwd(),e=n.prompt&&process.stdin.isTTY===!0,a=p=>process.stdout.write(p),o=await g(t);I(o,a)||await S(e,async p=>{const s=process.env.BISHOP_SLACK_BOT_TOKEN?.trim()||await p("Bot token (xoxb-...)",""),r=process.env.BISHOP_SLACK_APP_TOKEN?.trim()||await p("App-level token (xapp-...)","");if(!s||!r)throw new d(`Both tokens are needed.
|
|
31
|
+
Bot token: App Settings, OAuth & Permissions, after installing
|
|
32
|
+
App-level token: App Settings, Basic Information, with connections:write
|
|
33
|
+
|
|
34
|
+
Set BISHOP_SLACK_BOT_TOKEN and BISHOP_SLACK_APP_TOKEN, or run in a terminal
|
|
35
|
+
so they can be typed in.`);a(`
|
|
36
|
+
Checking the tokens...
|
|
37
|
+
`);const i=await $(s);await v(r);const c=b(i.grantedScopes);c.length>0&&a(`
|
|
38
|
+
This app is missing scopes Bishop uses: ${c.join(", ")}
|
|
39
|
+
Add them in App Settings under OAuth & Permissions and reinstall the app.
|
|
40
|
+
Bishop will run without them, but the features that need them will fail.
|
|
41
|
+
`);const l=await k({...n,name:n.name??i.appName,displayName:n.displayName??i.appName},t,p),m=await T(t,o,{app:{...l,...i.appId?{id:i.appId}:{}},...i.workspace?{workspace:i.workspace}:{},botToken:s,appToken:r});u.info({appId:i.appId,teamId:i.teamId},"slack app adopted"),a(`
|
|
42
|
+
Done. Using app ${i.appId??"(unknown id)"} in ${i.teamName??i.teamId}.
|
|
43
|
+
${m}
|
|
44
|
+
Bishop cannot check which events the app subscribes to. If the agent never
|
|
45
|
+
answers, confirm the app subscribes to: ${L.join(", ")}
|
|
46
|
+
|
|
47
|
+
Run \`bishop\` to start the agent.
|
|
48
|
+
`)})}async function nn(n){const[t,e]=process.platform==="darwin"?["open",[n]]:process.platform==="win32"?["cmd",["/c","start","",n]]:["xdg-open",[n]];await O(t,e)}export{d as SetupError,H as defaultAppName,R as ensureGitignore,j as existingSetup,nn as openBrowser,k as resolveAppDetails,X as setupSlack,Z as setupSlackManually,D as titleize,S as withPrompt,F as writeEnv};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{log as i}from"../log.js";import{plainText as h}from"./mentions.js";async function u(a,n,t,e){try{await a.apiCall("agents.sessions.setStatus",{channel_id:n.channelId,thread_ts:n.threadTs,status:t,...e?{initiator_user_id:e}:{}})}catch(r){i.debug({...n,status:t,err:r},"could not set the agent session status")}}async function f(a,n,t){const e=m(t);if(e)try{await a.apiCall("agents.sessions.rename",{channel_id:n.channelId,thread_ts:n.threadTs,title:e})}catch(r){const o=[...e].length;i.warn({...n,length:o,err:r},"could not name the agent session")}}const l=80,d=/[^\p{L}\p{M}\p{Nd} !"&'(),\-.=?_|]/gu;function m(a){const t=h(a).normalize("NFC").replace(d," ").replace(/\s+/g," ").trim();if(!t)return"";if([...t].length<=l)return t;let e="",r=0;for(const{segment:s}of new Intl.Segmenter().segment(t)){const c=[...s].length;if(r+c>l-3)break;e+=s,r+=c}const o=e.trimEnd();return o?`${o}...`:""}export{f as renameSession,m as sessionTitle,u as setSessionStatus};
|
|
@@ -0,0 +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 O(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.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,O as createSlackApp};
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
function p(t){const n=t.trim().replace(/^@+/,"");return{kind:/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(n)?"email":"name",value:n}}function $(t){return t.some(n=>p(n).kind==="name")}function s(t){return t?.trim().toLowerCase()||void 0}function g(t,n){const o=new Set,d=[],c=[];for(const r of t){const{kind:l,value:u}=p(r);if(!u)continue;const i=u.toLowerCase(),a=n.users.filter(e=>l==="email"?s(e.email)===i:s(e.name)===i||s(e.displayName)===i||s(e.realName)===i),m=l==="email"?[]:n.groups.filter(e=>s(e.handle)===i||s(e.name)===i),f=[...a.map(e=>`user ${e.id}${e.name?` (@${e.name})`:""}`),...m.map(e=>`group ${e.id}${e.handle?` (@${e.handle})`:""}`)];if(f.length===0){d.push(r);continue}if(a.length+m.length>1){c.push({entry:r,candidates:f});continue}for(const e of a)o.add(e.id);for(const e of m)for(const h of e.memberIds)o.add(h)}return{allowed:o,unresolved:d,ambiguous:c}}function w(t){return t.map(n=>` "${n.entry}" matches ${n.candidates.length}: ${n.candidates.join(", ")}`).join(`
|
|
2
|
+
`)}export{p as classifyEntry,w as describeAmbiguities,$ as entriesNeedingUserList,g as resolveAllowList};
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import{BishopError as w}from"../errors.js";import{log as r}from"../log.js";import{describeAmbiguities as g,resolveAllowList as m}from"./authorize.js";import{loadDirectory as f}from"./directory.js";const p=300*1e3;class c extends w{constructor(t){super(t,"AuthorizationConfigError")}}async function A(l,t){if(!t||t.length===0)return r.warn("no allow list configured; anyone who can reach the bot can use this agent"),{allows:()=>!0,open:!0,stop:()=>{}};const s=t;let n=new Set;async function i(o){const h=await f(l,s),e=m(s,h);if(e.ambiguous.length>0){const d=`These allow-list entries match more than one thing:
|
|
2
|
+
${g(e.ambiguous)}
|
|
3
|
+
Use an email address or a distinct group handle to say which you mean.`;if(o)throw new c(d);r.error({ambiguous:e.ambiguous},"allow list became ambiguous; keeping previous");return}if(e.unresolved.length>0&&r.warn({unresolved:e.unresolved},"some allow-list entries matched nobody; they are being ignored"),e.allowed.size===0){const u=`None of the ${s.length} allow-list entries resolved to a Slack user. Nobody would be able to use the agent.`;if(o)throw new c(u);r.error("allow list resolved to nobody; keeping previous");return}n=e.allowed,r.info({allowed:n.size,entries:s.length},"resolved the allow list")}await i(!0);const a=setInterval(()=>{i(!1).catch(o=>{r.error({err:o},"allow-list refresh failed; keeping the previous set")})},p);return a.unref(),{allows:o=>n.has(o),open:!1,stop:()=>clearInterval(a)}}export{c as AuthorizationConfigError,A as createAuthorizer};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
const u=3;function o(e){return`I've answered ${e} bot ${e===1?"message":"messages"} in a row here, so I'm holding off until a person says something.`}function i(e=3){const s=new Map;return{answersBots:e>0,get size(){return s.size},clear(n){s.delete(n)},admit(n,t){if(!t)return s.delete(n),{run:!0};const r=s.get(n)??0;return s.set(n,Math.min(r+1,e+1)),r<e?{run:!0}:r===e&&e>0?{run:!1,say:o(e)}:{run:!1}}}}export{u as DEFAULT_MAX_BOT_TURNS,i as botTurnCap};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{Command as l,CommanderError as p,InvalidArgumentError as v}from"commander";import{VERBOSITY as h}from"../../config.js";import{log as y}from"../../log.js";import{parseVerbosity as f}from"../verbosity.js";import{commandOutput as u,tokenize as g}from"./syntax.js";const n="default";function w(t){if(t.trim().toLowerCase()===n)return n;const e=f(t);if(!e)throw new v(`Use one of ${h.join(", ")}, or ${n}.`);return e}function C(t,e,i){const m=[];let a;const s=new l;s.name("!").description("Bishop commands. These are handled here and never reach the agent.").exitOverride().configureOutput({writeOut:r=>m.push(r),writeErr:r=>m.push(r),outputError:(r,o)=>o(r)}),s.command("verbosity").description("How much of a turn this thread shows while it is running.").argument("[level]",`${h.join(", ")}, or ${n} to follow the configured level. Omit it to see the current one.`,w).action(r=>{if(!r){const c=i.verbosity.of(e.threadKey),d=c.fromThread?"":" (the configured default)";a=`Thread verbosity is ${c.level}${d}`;return}if(r===n){i.verbosity.clear(e.threadKey),a=`Thread verbosity follows the configured default, which is ${i.verbosity.of(e.threadKey).level}`;return}const o=f(r);o&&(i.verbosity.set(e.threadKey,o),a=`Thread verbosity set to ${o}`)});try{s.parse(g(t),{from:"user"})}catch(r){if(!(r instanceof p)){const o=r instanceof Error?r.message:String(r);return y.debug({thread:e.threadKey,line:t,err:r},"a command failed"),u(o)}}return u(a??(m.join("").trim()||s.helpInformation()))}export{C as runCommand};
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
const f="!>";function o(i){const n=i.trim();if(n.startsWith("```")&&n.endsWith("```")&&n.length>=6){const t=n.slice(3,-3);return t.includes("```")?void 0:t}if(n.startsWith("`")&&n.endsWith("`")&&n.length>=2){const t=n.slice(1,-1);return t.includes("`")?void 0:t}}const c=/^-{0,2}[a-z][a-z0-9-]*$/i;function d(i){const n=o(i)?.trim();if(!n)return;const t=/^!\s+(\S+)(?:\s+([\s\S]*))?$/.exec(n),e=t?.[1];if(!e||!c.test(e))return;const r=t?.[2]?.trim();return r?`${e} ${r}`:e}function l(i){const n=[];let t="",e=!1,r;for(let u=0;u<i.length;u+=1){const s=i[u];if(s==="\\"&&r!=="'"&&u+1<i.length){t+=i[++u],e=!0;continue}if(r){s===r?r=void 0:t+=s;continue}if(s==='"'||s==="'"){r=s,e=!0;continue}if(/\s/.test(s)){e&&n.push(t),t="",e=!1;continue}t+=s,e=!0}if(r)throw new Error(`unterminated ${r==='"'?"double":"single"} quote`);return e&&n.push(t),n}function a(i){const n=i.trim().split(`
|
|
2
|
+
`),[t="",...e]=n;return["```",`!> ${t}`,...e,"```"].join(`
|
|
3
|
+
`)}export{d as commandLine,a as commandOutput,l as tokenize};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{BishopError as o}from"../errors.js";import{log as u}from"../log.js";import{retry as $}from"../retry.js";import{splitForSlack as y}from"./format.js";function h(e){return e.threadTs?`${e.channel}:${e.threadTs}`:e.channel}function m(e){const[t,r]=e.split(":");if(!t)throw new o(`not a Slack destination: ${e}`);return{channel:t,...r?{threadTs:r}:{}}}function b(e){return/^[CG][A-Z0-9]{2,}$/.test(e)}function g(e){return/^D[A-Z0-9]{2,}$/.test(e)}function w(e){return/^[UW][A-Z0-9]{2,}$/.test(e)}const S=new Set(["ratelimited","rate_limited"]);function _(e){const t=e;if(!t)return!1;if(t.code==="slack_webapi_rate_limited_error")return{...t.retryAfter?{retryAfterMs:t.retryAfter*1e3}:{}};const r=t.data?.error;return!!(r&&S.has(r))}async function D(e,t){const{channel:r,threadTs:s}=m(t.handle),i=y(t.text);let a;for(const l of i){const c=s??a,f=await $(()=>e.chat.postMessage({channel:r,markdown_text:l,...c?{thread_ts:c}:{}}),{what:"chat.postMessage",retryable:_});a??=f.ts}const n=s??a;return u.info({channel:r,...n?{thread:n}:{},parts:i.length},"delivered to Slack"),s||!a?void 0:{channel:r,threadTs:a}}async function E(e,t){const{channel:r,threadTs:s}=m(t),i=s?" (thread)":"";try{const n=(await e.conversations.info({channel:r})).channel;if(n?.name)return`#${n.name}${i}`;if(n?.is_im&&n.user){const l=await e.users.info({user:n.user}),c=l.user?.profile?.display_name?.trim()||l.user?.name;if(c)return`@${c}`}}catch(a){u.debug({channel:r,err:a},"could not name a Slack destination")}return`${r}${i}`}async function v(e,t){const r=t.toLowerCase();for await(const s of e.paginate("conversations.list",{types:"public_channel,private_channel",exclude_archived:!0,limit:200})){const a=(s.channels??[]).find(n=>n.name?.toLowerCase()===r);if(a)return a}}async function x(e,t){const r=t.toLowerCase();if(r.includes("@"))try{const s=await e.users.lookupByEmail({email:t});if(s.user?.id)return s.user.id}catch(s){u.debug({email:t,err:s},"no Slack user with that address")}for await(const s of e.paginate("users.list",{limit:200})){const i=s.members??[];for(const a of i){if(a.deleted||!a.id)continue;if([a.name,a.profile?.display_name,a.profile?.real_name].some(l=>l?.toLowerCase()===r))return a.id}}}async function M(e,t,r={}){try{return await A(e,t,r)}catch(s){throw s instanceof o?s:T(s,t)}}function T(e,t){const r=e,s=r?.data?.error;return s==="missing_scope"?new o(`Slack won't let this app look up ${t}: it is missing the ${r?.data?.needed??"required"} scope. Bishop's manifest asks for it, so an app installed before this existed has to be reinstalled: run \`bishop slack setup\` again.`):new o(`Slack could not resolve ${t}: ${s??(e instanceof Error?e.message:String(e))}`)}async function A(e,t,r){const s=t.trim();if(!s)throw new o("a Slack destination needs a channel or a person");const[i,a]=s.split(":"),n=(i??"").replace(/^[#@]/,"");if(!n)throw new o(`not a Slack destination: ${t}`);const l=s.startsWith("@")||w(n);if(g(n))return{interface:"slack",handle:h({channel:n,...a?{threadTs:a}:{}}),label:n};if(b(n)){const d=(await e.conversations.info({channel:n})).channel;if(!d?.id)throw new o(`Slack has no conversation ${n}`);return k(d,n),{interface:"slack",handle:h({channel:d.id,...a?{threadTs:a}:{}}),label:d.name?`#${d.name}`:d.id}}if(l){const f=w(n)?n:await x(e,n);if(!f)throw new o(`no Slack user matches @${n}`);if(r.allows&&!r.allows(f))throw new o(`@${n} is not on this agent's allow list, so Bishop won't start sending them messages they never asked for. Add them to slack.allow in .bishop/config.json, or post to a channel they are in instead.`);const p=(await e.conversations.open({users:f})).channel?.id;if(!p)throw new o(`could not open a DM with @${n}`);return{interface:"slack",handle:h({channel:p,...a?{threadTs:a}:{}}),label:`@${n}`}}const c=await v(e,n);if(!c?.id)throw new o(`no Slack channel named #${n} that this app can see. A private channel has to have the app in it.`);return k(c,`#${n}`),{interface:"slack",handle:h({channel:c.id,...a?{threadTs:a}:{}}),label:`#${c.name??n}`}}function k(e,t){if(!(e.is_im||e.is_member!==!1))throw new o(`this app is not in ${t}. Invite it there first, or nothing sent to it will arrive.`)}export{D as deliverToSlack,E as describeSlackHandle,m as parseSlackHandle,M as resolveSlackDestination,h as slackHandle,_ as slackRetryable};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{log as l}from"../log.js";import{classifyEntry as t,entriesNeedingUserList as u}from"./authorize.js";import{listMembers as d,toUser as c}from"./members.js";async function h(e,a){const i=[],s=[],m=a.map(t).filter(r=>r.kind==="email");if(u(a))for(const r of await d(e))i.push(n(r));else for(const{value:r}of m)try{const o=await e.users.lookupByEmail({email:r});o.user&&i.push(n(c(o.user)))}catch(o){l.debug({email:r,err:o},"email lookup failed")}if(a.some(r=>t(r).kind==="name"))try{const r=await e.usergroups.list({include_users:!0});for(const o of r.usergroups??[])o.id&&s.push({id:o.id,...o.handle?{handle:o.handle}:{},...o.name?{name:o.name}:{},memberIds:o.users??[]})}catch(r){l.warn({err:r},"could not list usergroups; group entries will not resolve")}return{users:i,groups:s}}function n(e){return{id:e.userId,...e.handle?{name:e.handle}:{},...e.displayName?{displayName:e.displayName}:{},...e.realName?{realName:e.realName}:{},...e.email?{email:e.email}:{}}}export{h as loadDirectory};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{renderTag as d}from"../harness/principal.js";const f=30*6e4,l=32,p=512;function g(s=f){const e=new Map;function c(i,t,o){for(const[n,a]of t)a<=o-s&&t.delete(n);t.size===0&&e.delete(i)}return{get size(){return e.size},saw(i,t,o){const n=e.get(i)??new Map;e.delete(i);const a=n.get(t);for((a===void 0||o>a)&&(n.delete(t),n.set(t,o)),e.set(i,n);n.size>l;){const r=n.keys().next();if(r.done)break;n.delete(r.value)}for(;e.size>p;){const r=e.keys().next();if(r.done)break;e.delete(r.value)}},count(i,t=Date.now()){const o=e.get(i);return o?(c(i,o,t),1+Math.max(o.size,1)):2}}}function w(s){if(!(s.mentioned||s.participants<=2))return d("thread",{participants:String(s.participants),reply:"optional"})}export{w as etiquetteNote,g as threadParticipants};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
const l=3e5,s={file_not_found:"Slack no longer has this file",check_file_info:"this agent's Slack app cannot see this file",access_denied:"this agent's Slack app is not allowed to open this file"};function c(e,n){return/^text\/html/i.test(e)?!/html/i.test(n.mimetype??""):!1}function i(e){return e.name?.trim()||e.title?.trim()||e.id||"file"}function d(e){const n=e.url_private_download??e.url_private,r=e.file_access==="visible"?void 0:e.file_access,a=(r?s[r]??r:void 0)??(e.mode==="external"?"shared as a link, so Slack holds no copy of it":void 0)??(n?void 0:"Slack gave no download link for it");return{id:e.id??n??i(e),name:i(e),...e.mimetype?{mimeType:e.mimetype}:{},...e.size===void 0?{}:{size:e.size},...e.permalink?{url:e.permalink}:{},...a?{unavailable:a}:{}}}function u(e){return JSON.stringify({id:e.id,name:e.name,title:e.title,mimetype:e.mimetype,size:e.size,url_private_download:e.url_private_download,url_private:e.url_private,permalink:e.permalink,mode:e.mode,file_access:e.file_access})}function p(e){try{const n=JSON.parse(e);return!n||typeof n!="object"||Array.isArray(n)?void 0:n}catch{return}}function m(e,n){const r=e.url_private_download??e.url_private,t=d(e),a=t.unavailable;return a?{...t,fetch:()=>Promise.reject(new Error(a))}:{...t,async fetch(){const o=await fetch(r,{headers:{authorization:`Bearer ${n}`},signal:AbortSignal.timeout(3e5)});if(!o.ok)throw new Error(`Slack returned ${o.status} for ${t.name}`);if(c(o.headers.get("content-type")??"",e))throw new Error(`Slack served a sign-in page instead of ${t.name}, which means the bot token has no files:read scope`);if(!o.body)throw new Error(`Slack sent no body for ${t.name}`);return o.body}}}export{d as describeSlackFile,p as parseSlackFile,m as slackAttachment,u as slackFilePayload};
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
const f=12e3;function l(t,s=12e3){if(t.length<=s)return[t];const r=[];let e=t;for(;e.length>s;){const c=e.slice(0,s);let n=c.lastIndexOf(`
|
|
2
|
+
|
|
3
|
+
`);n<s*.5&&(n=c.lastIndexOf(`
|
|
4
|
+
`)),n<s*.5&&(n=c.lastIndexOf(" ")),n<s*.5&&(n=s),r.push(e.slice(0,n).trimEnd()),e=e.slice(n).trimStart()}return e&&r.push(e),r}export{l as splitForSlack};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{log as a}from"../log.js";import{markHeld as N,releaseHeld as F}from"./pending.js";import{FILE_SHARE as L,isBotEvent as M,isSelfEvent as O,routeAppMention as P,routeMessage as R}from"./route.js";import{say as W}from"./say.js";import{mentionsBot as Y,messageKeyString as $,spokenAt as j,threadKeyOf as A,threadKeyString as H}from"./thread.js";function X(I,S){const{identity:d,botName:f,threads:x,events:q,run:z,stop:D,allows:w,bots:p,participants:T,command:B}=S,l=new Map,y=new Map;async function U(t,s,r){const n=l.get(s);if(!n?.length)return[];l.set(s,[]),await y.get(s);const e=await F(r.client,t,n,{botUserId:d.botUserId,...f?{botName:f}:{}}),u=[];for(const h of e)w(h.userId)?u.push(h):a.warn({thread:s,user:h.userId},"dropping a held message");return u}async function E(t,s,r){const n=A(t,d.teamId);if(!n){a.debug({source:r},"dropping event with no usable thread key");return}const e=H(n),u=await x.has(e),h={botUserId:d.botUserId,botId:d.botId,...f?{botName:f}:{},threadKnown:u},g=r==="app_mention"?P(t,h):R(t,h);if((u||g.action==="run")&&t.user&&!O(t,h)&&(!t.subtype||t.subtype===L)&&T.saw(e,t.user,j(t.ts)),g.action==="ignore"){t.user&&t.user!==d.botUserId&&!M(t)&&p.clear(e),a.debug({source:r,thread:e,reason:g.reason},"ignoring event");return}const m=M(t);if(m&&!p.answersBots){a.debug({source:r,thread:e},"ignoring a bot: this agent answers none");return}const v=$(n,t);if(v&&await q.seen(`slack:${v}`)){a.info({source:r,thread:e,message:v,eventId:t.event_id},"duplicate message, skipping");return}const o=t.user??"";if(!w(o)){if(m){a.debug({source:r,thread:e,user:o},"ignoring a bot off the allow list");return}a.warn({source:r,thread:e,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){a.debug({err:i},"could not post the refusal")}return}if(B&&await B({key:n,keyString:e,text:g.text},s)){m||p.clear(e),a.info({source:r,thread:e,user:o},"ran a command");return}const b=p.admit(e,m);if(!b.run){const i={source:r,thread:e,user:o},c="holding off: too many bot turns in a row in this thread";if(b.say?a.warn(i,c):a.debug(i,c),b.say)try{await W(s.client,{channel:n.channelId,threadTs:n.threadTs},b.say)}catch(k){a.debug({err:k},"could not say why Bishop is holding off")}return}const _={text:g.text,userId:o,ts:t.ts??n.threadTs,isBot:m,mentionsAgent:Y(t.text??"",d.botUserId),files:g.files},K=l.get(e);if(K){K.push(_),a.info({source:r,thread:e,user:o},"holding a message until the turn ends");const i=(y.get(e)??Promise.resolve()).then(()=>N(s.client,n.channelId,_.ts));y.set(e,i),await i;return}l.set(e,[]);try{await x.remember(e);let i=[_],c=!u;for(;i.length||l.get(e)?.length;){if(!i.length){i=await U(n,e,s);continue}a.info({thread:e,messages:i.length,newThread:c},"running a turn");try{await z({key:n,keyString:e,channelId:n.channelId,threadTs:n.threadTs,messages:i,isNewThread:c,participants:T.count(e),isDirectMessage:t.channel_type==="im"||n.channelId.startsWith("D")},s)}catch(k){a.error({thread:e,err:k},"turn failed")}c=!1,i=await U(n,e,s)}}finally{l.delete(e),y.delete(e)}}I.event("app_mention",async({event:t,body:s,client:r})=>{await E({...t,event_id:s.event_id},{client:r},"app_mention")}),I.event("message",async({event:t,body:s,client:r})=>{await E({...t,event_id:s.event_id},{client:r},"message")}),I.event("agent_session_stopped",async({event:t})=>{const s=t,r=A(s,d.teamId);if(!r)return;if(!w(s.user??"")){a.warn({user:s.user},"ignoring a stop from an unauthorized user");return}const n=H(r),e=D(n);a.info({thread:n,user:s.user,aborted:e},"stop requested")})}export{X as registerHandlers};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{describedFile as h}from"../files.js";import{trimHistory as b,withinFileBudget as I}from"../interface/history.js";import{log as c}from"../log.js";import{describeSlackFile as g,slackFilePayload as k}from"./files.js";import{mentionableName as p,renderMentions as y}from"./mentions.js";import{FILE_SHARE as w}from"./route.js";import{nameBotMentions as d,spokenAt as x}from"./thread.js";const T=200,B=3;function O(e){const t=e.messages[0]?.ts;if(!(!t||t===e.threadTs))return t}async function W(e){const t=await v(e);if(!t)return c.warn({thread:e.key.threadTs},"could not read the thread to catch the agent up"),[{kind:"omitted",reason:"unavailable"}];for(const s of t.messages)s.userId&&!f(s,e.self)&&e.saw?.(s.userId,x(s.ts));const r=A(t.messages,e),n=I(b(r.entries));return F(t.complete?n:_(n,r.opensWithParent),e)}function _(e,t){const r=t?1:0,n=e[r];return n?.kind==="omitted"&&n.reason==="trimmed"?e.map((s,a)=>a===r?{kind:"omitted",reason:"trimmed"}:s):[...e.slice(0,r),{kind:"omitted",reason:"unavailable"},...e.slice(r)]}async function v(e){const t=[],r=new Set;let n;try{for(let s=0;s<B;s+=1){const a=await e.client.conversations.replies({channel:e.key.channelId,ts:e.key.threadTs,latest:e.before,inclusive:!1,limit:T,...n?{cursor:n}:{}});for(const o of a.messages??[]){if(!o.ts||o.ts>=e.before||r.has(o.ts))continue;r.add(o.ts);const{subtype:i,files:u}=o;t.push({ts:o.ts,...o.user?{userId:o.user}:{},...o.bot_id?{botId:o.bot_id}:{},...i?{subtype:i}:{},text:o.text??"",files:u??[]})}if(n=a.response_metadata?.next_cursor,!n)return l(t,!0)}return l(t,!1)}catch(s){return c.warn({thread:e.key.threadTs,read:t.length,err:s},"could not read the whole thread"),t.length?l(t,!1):void 0}}function l(e,t){return{messages:[...e].sort((n,s)=>n.ts<s.ts?-1:n.ts>s.ts?1:0),complete:t}}function N(e,t){return e.botId&&!t.answersBots?!1:!!(e.userId&&t.allows(e.userId))}function f(e,t){return e.userId&&e.userId===t.botUserId?!0:!!t.botId&&e.botId===t.botId}function P(e,t){return e.map(r=>{const n=g(r),s=h(n);if(n.unavailable)return s;const a=t.remember({id:n.id,name:n.name,payload:k(r)});return a?{...s,ref:a}:{...s,unavailable:"Bishop could not record a reference for it"}})}function A(e,t){const r=[];let n=!1,s=0;const a=()=>{s&&(r.push({kind:"omitted",reason:"unauthorized",count:s}),s=0)};for(const o of e){const i=f(o,t.self),u=o.ts===t.key.threadTs;if(o.subtype&&o.subtype!==w&&!i)continue;if(!i&&!N(o,t)){u&&(n=!0),s+=1;continue}if(!d(o.text,t.self.botUserId)&&o.files.length===0)continue;const m=d(o.text,t.self.botUserId,t.self.botName);u&&(n=!0),a(),r.push({kind:"message",principal:{id:o.userId??"",...o.botId&&!i?{isBot:!0}:{}},text:m,...i?{self:!0}:{},...o.files.length>0?{shared:o.files}:{}})}return a(),{entries:r,opensWithParent:n}}async function F(e,t){return Promise.all(e.map(async r=>{if(r.kind!=="message")return r;const{shared:n,...s}=r,a=n?P(n,t):[];return{...s,principal:await M(r.principal,t),text:await y(r.text,t.users),...a.length>0?{files:a}:{}}}))}async function M(e,t){if(!e.id)return e;const r=await t.users.lookup(e.id),n=p(r);return{...e,...n?{displayName:n}:{},...r.realName?{realName:r.realName}:{},...r.email?{email:r.email}:{}}}export{O as landedMidThread,W as threadHistory};
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import{WebClient as s}from"@slack/web-api";import{BishopError as c}from"../errors.js";import{BOT_SCOPES as p}from"./manifest.js";class a extends c{constructor(r){super(r,"InspectError")}}function i(t){return t?/^https?:\/\/([^./]+)\./.exec(t.trim())?.[1]:void 0}async function u(t){const r=new s(t);let e;try{e=await r.auth.test()}catch(n){throw new a(`Slack rejected the bot token: ${n instanceof Error?n.message:String(n)}`)}if(!e.team_id||!e.user_id)throw new a("auth.test returned no team or bot user; the token may not be a bot token");const o=i(e.url);return{...e.app_id?{appId:e.app_id}:{},...e.app_name?{appName:e.app_name}:{},teamId:e.team_id,...o?{workspace:o}:{},...e.team?{teamName:e.team}:{},botUserId:e.user_id,...e.bot_id?{botId:e.bot_id}:{},...e.response_metadata?.scopes?{grantedScopes:e.response_metadata.scopes}:{}}}async function f(t){const r=new s(t);try{await r.apps.connections.open()}catch(e){const o=e instanceof Error?e.message:String(e);throw new a(`Slack rejected the app-level token: ${o}
|
|
2
|
+
It should start with xapp- and have the connections:write scope.`)}}function w(t){if(!t)return[];const r=new Set(t);return p.filter(e=>!r.has(e))}export{a as InspectError,u as inspectBotToken,w as missingScopes,f as verifyAppToken,i as workspaceFromUrl};
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import{log as y}from"../log.js";import{createSlackApp as C}from"./app.js";import{createAuthorizer as D}from"./authorizer.js";import{botTurnCap as L}from"./bots.js";import{runCommand as O}from"./commands/run.js";import{commandLine as E}from"./commands/syntax.js";import{deliverToSlack as H,describeSlackHandle as F,parseSlackHandle as b,resolveSlackDestination as K,slackHandle as _}from"./destination.js";import{etiquetteNote as U,threadParticipants as z}from"./etiquette.js";import{describeSlackFile as R,parseSlackFile as W,slackAttachment as v,slackFilePayload as j}from"./files.js";import{registerHandlers as q}from"./handlers.js";import{landedMidThread as G,threadHistory as V}from"./history.js";import{linkMentions as I,mentionableName as S,renderMentions as T}from"./mentions.js";import{createPresence as Y}from"./presence.js";import{say as N}from"./say.js";import{threadKeyString as $}from"./thread.js";import{userDirectory as J}from"./users.js";import{threadVerbosity as Q}from"./verbosity.js";const X={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
|
+
`)},Z=3e3;async function ee(t,s,i=Z){const l=await ae(t.botUserId,s,i),r=S(l)??t.username;if(!r)return;const h=[t.appName,l.realName].filter(w=>!!w?.trim()&&w?.toLowerCase()!==r.toLowerCase());return{address:`@${r}`,...h.length>0?{aka:[...new Set(h)]}:{}}}async function ae(t,s,i){const l={userId:t};let r;const h=new Promise(w=>{r=setTimeout(()=>w(l),i)});try{return await Promise.race([s.lookup(t).catch(()=>l),h])}finally{clearTimeout(r)}}async function ge(t){const{app:s,identity:i}=await C(t.credentials),l=await D(s.client,t.allow),r=J(s.client,{self:i.botUserId}),h=L(t.maxBotTurns),w=z(),k=Q(t.meta,t.verbosity),c=await ee(i,r),f={...X,...c?{self:c}:{}};c?y.info({address:c.address,aka:c.aka},"the agent's name in Slack"):y.warn("could not read the bot's own name; the agent will not know what it is called");const u=new Map,x=async({key:e,keyString:a,text:o},p)=>{const m=E(o);if(!m)return!1;const n=O(m,{threadKey:a},{verbosity:k});try{await N(p.client,{channel:e.channelId,threadTs:e.threadTs},n),u.get(a)?.interrupted()}catch(d){y.warn({thread:a,err:d},"couldn't answer a command")}return!0};async function B(e){const a=await r.lookup(e.userId),o=S(a);return{id:e.userId,...o?{displayName:o}:{},...a.realName?{realName:a.realName}:{},...a.email?{email:a.email}:{},...e.isBot?{isBot:!0}:{}}}function M(e,a){const o=R(a);if(!o.unavailable)return t.attachments.remember(e,f.name,{id:o.id,name:o.name,payload:j(a)})}async function P(e,a){return a.files.length===0?[]:Promise.all(a.files.map(async o=>{const p=M(e,o),m=await t.files.save(e,v(o,t.credentials.botToken));return p?{...m,ref:p}:m}))}function A(e,a){const o=T(e.messages[0]?.text??"",r),p=G(e),m=U({participants:e.participants,mentioned:e.messages.some(n=>n.mentionsAgent)});return{threadKey:e.keyString,isNewThread:e.isNewThread,origin:f,...m?{note:m}:{},...p?{history:()=>V({client:a.client,key:e.key,before:p,allows:n=>l.allows(n),answersBots:h.answersBots,self:{botUserId:i.botUserId,...i.botId?{botId:i.botId}:{},...c?{botName:c.address}:{}},users:r,remember:n=>t.attachments.remember(e.keyString,f.name,n),saw:(n,d)=>w.saw(e.keyString,n,d)})}:{},async messages(){return Promise.all(e.messages.map(async(n,d)=>{const g=await P(e.keyString,n);return{principal:await B(n),text:d===0?await o:await T(n.text,r),...g.length>0?{files:g}:{}}}))},presence:()=>{const n=Y(e,a,{outgoing:d=>I(d,r),title:()=>o,verbosity:()=>k.of(e.keyString).level,...m?{mayDecline:!0}:{},...t.taskIntervalMs===void 0?{}:{taskIntervalMs:t.taskIntervalMs}});return u.set(e.keyString,n),{handle:d=>n.handle(d),finish:async()=>{try{await n.finish()}finally{u.delete(e.keyString)}}}},async notify(n){await N(a.client,{channel:e.channelId,threadTs:e.threadTs},n)},async destination(){const n=_({channel:e.channelId,threadTs:e.threadTs});return{interface:"slack",handle:n,label:await F(s.client,n)}}}}return q(s,{identity:i,...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:h,participants:w,command:x}),{name:"slack",async start(){await s.start(),i.scopes.includes("files:read")||y.warn("this Slack app has no files:read scope, so it cannot open files people share; reinstall it to grant one"),r.prime(),y.info("listening on Slack via Socket Mode")},async stop(){l.stop(),await s.stop()},allows:e=>l.allows(e),resolveDestination:(e,a)=>K(s.client,e,{...a?.optedInOnly?{allows:o=>l.allows(o)}:{}}),sameConversation:(e,a)=>b(e).channel===b(a).channel,deliver:async e=>{const a=await H(s.client,{...e,text:await I(e.text,r)});return a?{threadKey:$({teamId:i.teamId,channelId:a.channel,threadTs:a.threadTs})}:{}},attachment:e=>{const a=W(e);return a?v(a,t.credentials.botToken):void 0}}}export{X as SLACK_ORIGIN,ge as createSlackInterface,ee as slackIdentity};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{displayNameOf as t}from"../config.js";const n=["app_mentions:read","chat:write","assistant:write","im:history","channels:history","groups:history","files:read","reactions:write","users:read","users:read.email","usergroups:read","channels:read","groups:read","im:write"],i=["app_mention","agent_session_stopped","app_home_opened","app_context_changed","message.im","message.channels","message.groups"];function r(a){const e=t(a),s=a.description?.trim();return{display_information:{name:e.slice(0,35),...s?{description:s.slice(0,140)}:{}},features:{bot_user:{display_name:e.slice(0,80),always_online:!0},app_home:{messages_tab_enabled:!0,messages_tab_read_only_enabled:!1,home_tab_enabled:!1},agent_view:{agent_description:(s||e).slice(0,300)}},oauth_config:{scopes:{bot:[...n]}},settings:{event_subscriptions:{bot_events:[...i]},interactivity:{is_enabled:!1},socket_mode_enabled:!0,org_deploy_enabled:!1,token_rotation_enabled:!1}}}export{i as BOT_EVENTS,n as BOT_SCOPES,r as buildManifest};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{log as n}from"../log.js";function a(e){const s=e.profile?.display_name?.trim(),o=e.profile?.real_name?.trim(),t=e.profile?.email?.trim(),r=e.name?.trim();return{userId:e.id??"",...s?{displayName:s}:{},...o?{realName:o}:{},...t?{email:t}:{},...r?{handle:r}:{}}}const l="USLACKBOT";function u(e){return!!e.id&&!e.deleted&&e.id!==l}async function c(e,s=()=>!0){const o=[];let t;do{const r=await e.users.list({limit:200,...t?{cursor:t}:{}});for(const i of r.members??[])i.id&&s(i)&&o.push(a(i));t=r.response_metadata?.next_cursor||void 0}while(t);return n.debug({users:o.length},"paged the workspace member list"),o}export{u as isMentionable,c as listMembers,a as toUser};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{log as h}from"../log.js";function N(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 E=2e3,w=/<@([UW][A-Z0-9]+)(?:\|([^>]*))?>|<!(here|channel|everyone)(?:\|[^>]*)?>|<!subteam\^([A-Z0-9]+)(?:\|([^>]*))?>|<#(C[A-Z0-9]+)(?:\|([^>]*))?>/g;async function O(e,r,n=E){return f(e,async()=>{const s=g(e,r).catch(()=>{});let a;const o=new Promise(t=>{a=setTimeout(()=>t(void 0),n)}),l=await Promise.race([s,o]);return clearTimeout(a),l!==void 0?l:(h.debug("naming the people mentioned took too long; using the labels Slack sent"),g(e,L))})}const A=/<((?:https?|mailto):[^|>\s]+)(?:\|([^>]*))?>/g;function M(e){return e.replace(A,(r,n,s)=>s?.trim()||I(n)).replaceAll("<","<").replaceAll(">",">").replaceAll("&","&")}function I(e){return e.startsWith("mailto:")?e.slice(7):e}const L={lookup:async e=>({userId:e}),findByName:async()=>{}};async function f(e,r){try{return await r()}catch(n){return h.debug({err:n},"could not translate mentions; passing the text through"),e}}async function g(e,r){const n=[...e.matchAll(w)];if(n.length===0)return e;const s=await Promise.all(n.map(async l=>{const[,t,c,i,u,p,d,y]=l;if(t){const m=await r.lookup(t),b=c?.trim().replace(/^@+/,"");return`@${N(m)||b||t}`}return i?`@${i}`: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)+s[t],o=c+l[0].length}),a+e.slice(o)}const k=/(```[\s\S]*?```|`[^`\n]*`)/,S=/(^|[^\p{L}\p{N}@/<])@([\p{L}\p{N}][\p{L}\p{N}._-]{0,79})/gu,T=new Set(["here","channel","everyone","group"]);async function P(e,r){return f(e,async()=>{const n=e.split(k);return(await Promise.all(n.map((a,o)=>o%2===1?a:_(a,r)))).join("")})}async function _(e,r){const n=[...e.matchAll(S)];if(n.length===0)return e;const s=await Promise.all(n.map(async t=>{const c=t[1]??"",i=t[2]??"",u=i.replace(/[._-]+$/,""),p=i.slice(u.length);if(!u||T.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 i=t.index??0;a+=e.slice(o,i)+s[c],o=i+t[0].length});const l=a+e.slice(o);return l!==e&&h.debug("linked a mention in agent text"),l}export{P as linkMentions,N as mentionableName,M as plainText,O as renderMentions};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{log as m}from"../log.js";import{addressedText as l,mentionsBot as u}from"./thread.js";const f="hourglass_flowing_sand",g=200,h=20;async function T(a,t,s){try{await a.reactions.add({channel:t,timestamp:s,name:f})}catch(n){m.debug({channel:t,ts:s,err:n},"could not mark a held message")}}async function b(a,t,s,n){for(const r of s)try{await a.reactions.remove({channel:t.channelId,timestamp:r.ts,name:f})}catch(e){m.debug({ts:r.ts,err:e},"could not take the mark off a held message")}const o=await p(a,t,s);if(!o)return m.warn({thread:t.threadTs,held:s.length},"sending held messages as they arrived"),s;const i=[];for(const r of s){const e=o.get(r.ts);if(!e){m.info({thread:t.threadTs,ts:r.ts},"a held message is gone; dropping it");continue}const c=l(e.text,n.botUserId,n.botName);(c||e.files.length>0)&&i.push({...r,text:c,mentionsAgent:u(e.text,n.botUserId),files:e.files})}return i}async function p(a,t,s){const n=new Set(s.map(e=>e.ts)),o=s[0]?.ts;if(!o)return new Map;const i=new Map;let r;try{for(let e=0;e<h;e+=1){const c=await a.conversations.replies({channel:t.channelId,ts:t.threadTs,oldest:o,inclusive:!0,limit:g,...r?{cursor:r}:{}});for(const d of c.messages??[])d.ts&&(i.set(d.ts,{text:d.text??"",files:d.files??[]}),n.delete(d.ts));if(r=c.response_metadata?.next_cursor,!r||n.size===0)break}return i}catch(e){m.warn({thread:t.threadTs,err:e},"could not re-read held messages");return}}export{T as markHeld,b as releaseHeld};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{FETCH_FILE_TOOL as r}from"../harness/principal.js";const a={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"},s={schedule_create:"Setting up a schedule",schedule_list:"Checking what is scheduled",schedule_delete:"Removing a schedule",[r]:"Fetching a file from this conversation"};function c(e){if(!e.startsWith("mcp__"))return;const[,n,...t]=e.split("__");return n==="bishop"?s[t.join("__")]??"Sorting something out":n?`Calling ${n}`:"Calling a tool"}function l(e){const n=e.replaceAll(/[_-]+/g," ").replaceAll(/([a-z\d])([A-Z])/g,"$1 $2").trim();if(!n)return"Working";const t=n.toLowerCase();return t.charAt(0).toUpperCase()+t.slice(1)}const i=100;function g(e){const n=e.trim();return n.length>i?`${n.slice(0,i-3)}...`:n}function h(e){const n=e.summary?.trim();if(n)return g(n);const t=a[e.name];if(t)return t(e.detail);const o=c(e.name);return o||l(e.name)}function m(e){const n=Math.max(0,Math.round(e/1e3)),t=Math.floor(n/60),o=n%60;return t?`${t}m ${o}s`:`${o}s`}export{m as elapsed,l as humanizeToolName,g as summaryPhrase,h as toolPhrase};
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import{DEFAULT_VERBOSITY as M}from"../config.js";import{saysNoOutput as g}from"../harness/principal.js";import{log as y}from"../log.js";import{renameSession as P,setSessionStatus as $}from"./agent-session.js";import{elapsed as F,summaryPhrase as L,toolPhrase as O}from"./phrases.js";import{say as R}from"./say.js";import{taskMessage as j}from"./tasks.js";import{showsProgress as H,showsTasks as p}from"./verbosity.js";const N=3e4;function K(a,A,r={}){const s=A.client,f={channel:a.channelId,threadTs:a.threadTs},B=Date.now(),D=()=>r.verbosity?.()??M;let n,S,k="",o=!1,c="",x=!1,i,T=!1;async function d(t,e){try{return await e()}catch(l){y.debug({thread:a.keyString,what:t,err:l},"presence call failed");return}}let u=E();function m(t){return u=u.then(()=>$(s,a,t)),u}async function E(){const t=a.messages[0];if(await $(s,a,"processing",t?.userId),!(!r.title||t?.ts!==a.threadTs))try{await P(s,a,await r.title())}catch(e){y.debug({thread:a.keyString,err:e},"could not read the session title")}}async function h(){const t=n;t&&(n=void 0,await d("task.close",()=>t.close()))}async function _(t){await h();const e=await d("say",()=>R(s,f,t));if(!e){o=!1;return}S=e,k=t,o=!0}const I=setInterval(()=>{m("processing")},r.heartbeatMs??N);return I.unref(),{interrupted(){o=!1},async handle(t){const e=D();switch(p(e)||await h(),t.type){case"text":{if(g(t.text)||!t.text.trim()||!H(e))return;await _(r.outgoing?await r.outgoing(t.text):t.text);return}case"tool":if(!p(e))return;n||(n=j(s,f,r.taskIntervalMs),o=!1),n.note(O(t));return;case"tool_summary":if(!p(e))return;n?.retitle(L(t.summary));return;case"done":x=!0,c=g(t.text)?"":t.text,T=!!r.mayDecline&&g(t.text);return;case"error":i=t.message;return;default:return}},async finish(){if(clearInterval(I),await h(),T&&!i){y.info({thread:a.keyString},"the agent left this message alone"),await m("active");return}const t=F(Date.now()-B),e=x||i?`Worked for ${t}`:`Stopped after ${t}`,l=c.trim()&&r.outgoing?await r.outgoing(c):c,b=i?`Something went wrong: ${i}`:l.trim()===k.trim()?"":l.trim(),w=S;!b&&w&&o&&await d("coda",()=>s.chat.update({channel:f.channel,ts:w.ts,markdown_text:`${w.text}
|
|
2
|
+
|
|
3
|
+
_${e}_`}))||await _([b,`_${e}_`].filter(Boolean).join(`
|
|
4
|
+
|
|
5
|
+
`)),await m("active")}}}export{K as createPresence};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import{spawn as k}from"node:child_process";import{mkdir as S,mkdtemp as y,readFile as f,rm as T,writeFile as l}from"node:fs/promises";import{homedir as j,tmpdir as x}from"node:os";import{join as i}from"node:path";import{BishopError as v}from"../errors.js";import{exec as m}from"../exec.js";import{log as p}from"../log.js";import{buildManifest as w}from"./manifest.js";class g extends v{constructor(e){super(e,"ProvisionError")}}async function M(t){const e=await y(i(x(),"bishop-provision-")),n=i(e,"captured.json");try{await O(e,t.app,n),await A(e,t.teamId,n,t.timeoutMs??12e4);const o=JSON.parse(await f(n,"utf8"));if(!o.botToken||!o.appToken)throw new g("The Slack CLI started the app but supplied no tokens.");let r;try{const c=JSON.parse(await f(i(e,".slack","apps.dev.json"),"utf8"));r=Object.values(c).find(u=>u.app_id)?.app_id}catch{}let a=!1;return r&&(a=await N(r,t.app,t.teamId)),{...r?{appId:r}:{},botToken:o.botToken,appToken:o.appToken,manifestCorrected:a}}finally{await T(e,{recursive:!0,force:!0})}}async function O(t,e,n){await S(i(t,".slack"),{recursive:!0}),await l(i(t,".slack","hooks.json"),JSON.stringify({hooks:{"get-hooks":"node get-hooks.js"}},null,2)),await l(i(t,".slack","config.json"),JSON.stringify({manifest:{source:"local"}},null,2)),await l(i(t,"get-hooks.js"),`console.log(JSON.stringify({
|
|
2
|
+
hooks: { 'get-manifest': 'node get-manifest.js', start: 'node capture.js' },
|
|
3
|
+
config: { 'sdk-managed-connection-enabled': true },
|
|
4
|
+
runtime: 'node',
|
|
5
|
+
}))
|
|
6
|
+
`),await l(i(t,"manifest.json"),JSON.stringify(w(e))),await l(i(t,"get-manifest.js"),`const fs = require('node:fs')
|
|
7
|
+
process.stdout.write(fs.readFileSync(__dirname + '/manifest.json', 'utf8'))
|
|
8
|
+
`),await l(i(t,"capture.js"),`const fs = require('node:fs')
|
|
9
|
+
fs.writeFileSync(${JSON.stringify(n)}, JSON.stringify({
|
|
10
|
+
botToken: process.env.SLACK_BOT_TOKEN,
|
|
11
|
+
appToken: process.env.SLACK_APP_TOKEN,
|
|
12
|
+
}), { mode: 0o600 })
|
|
13
|
+
process.exit(0)
|
|
14
|
+
`)}async function b(t){try{const e=await f(i(j(),".slack","credentials.json"),"utf8"),n=JSON.parse(e);return t&&n[t]?.token?n[t].token:Object.values(n).find(o=>o.token)?.token}catch{return}}async function N(t,e,n){const o=await b(n);if(!o)return p.warn(`could not read the Slack config token; app names keep the CLI's "(local)" suffix`),!1;const r=JSON.stringify({app_id:t,manifest:w(e)}),a=await m("slack",["api","apps.manifest.update","--no-color","--token",o,"--json",r]),c=/\{[\s\S]*\}/.exec(a.output);if(c)try{const u=JSON.parse(c[0]);return u.ok?(p.info({appId:t},"pushed the generated manifest over the CLI's copy"),!0):(p.warn({appId:t,error:u.error},"could not update the stored manifest"),!1)}catch{}return p.warn({appId:t},"could not update the stored manifest"),!1}async function A(t,e,n,o){const r=["run","--no-color","--force"];e&&r.push("--team",e);const a=k("slack",r,{cwd:t,stdio:["ignore","pipe","pipe"]});let c="";a.stdout.on("data",s=>{c+=s.toString()}),a.stderr.on("data",s=>{c+=s.toString()});const u=await new Promise(s=>{const d=setInterval(()=>{f(n,"utf8").then(()=>{clearInterval(d),s("captured")},()=>{})},250),h=setTimeout(()=>{clearInterval(d),s("timeout")},o);a.on("error",()=>{clearInterval(d),clearTimeout(h),s("exited")}),a.on("exit",()=>{clearInterval(d),clearTimeout(h),f(n,"utf8").then(()=>s("captured"),()=>s("exited"))})});if(a.killed||a.kill("SIGTERM"),u!=="captured"){p.debug({output:c},"slack run output");const s=c.trim().split(`
|
|
15
|
+
`).slice(-4).join(`
|
|
16
|
+
`);throw new g(u==="timeout"?`The Slack CLI didn't finish within ${Math.round(o/1e3)}s.
|
|
17
|
+
${s}`:`The Slack CLI exited before installing the app.
|
|
18
|
+
${s}`)}}function J(t){const e=[];for(const n of t.matchAll(/^\s*(\S+)\s+\(Team ID:\s*(T[A-Z0-9]+)\)/gm)){const o=n[1],r=n[2];r&&e.push({id:r,...o?{domain:o}:{}})}if(e.length===0)for(const n of t.matchAll(/Team ID:\s*(T[A-Z0-9]+)/g)){const o=n[1];o&&e.push({id:o})}return e}async function $(){const t=await m("slack",["version","--no-color"]);return t.code===null?void 0:/v?(\d+\.\d+\.\d+)/.exec(t.output)?.[1]??t.output.trim().split(`
|
|
19
|
+
`)[0]}async function q(){const t=await m("slack",["auth","list","--no-color"]);return t.code!==0?[]:J(t.output)}async function B(){return new Promise(t=>{const e=k("slack",["auth","login"],{stdio:"inherit"});e.on("error",()=>t(!1)),e.on("exit",n=>t(n===0))})}export{g as ProvisionError,J as parseAuthList,M as provisionSlackApp,B as slackAuthLogin,q as slackAuthTeams,$ as slackCliVersion};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{addressedText as h}from"./thread.js";const p="file_share";function i(n){return n.files??[]}function s(n,o){return h(n.text??"",o.botUserId,o.botName)}function a(n){return!!n.bot_id}function u(n,o){return n.user&&n.user===o.botUserId?!0:!!o.botId&&n.bot_id===o.botId}function f(n){return a(n)&&!n.user}function c(n,o){return a(n)&&!o.threadKnown}function l(n,o){if(u(n,o))return{action:"ignore",reason:"from self"};if(f(n))return{action:"ignore",reason:"from a bot with no account"};if(c(n,o))return{action:"ignore",reason:"from a bot, in a thread with no session"};const t=s(n,o),r=i(n);return!t&&r.length===0?{action:"ignore",reason:"nothing left once the mention was stripped"}:{action:"run",text:t,files:r}}function g(n,o){if(n.subtype&&n.subtype!==p)return{action:"ignore",reason:`subtype ${n.subtype}`};if(u(n,o))return{action:"ignore",reason:"from self"};if(f(n))return{action:"ignore",reason:"from a bot with no account"};if(!(n.channel_type==="im")){if(!n.thread_ts)return{action:"ignore",reason:"top-level channel message; app_mention covers those"};if(!o.threadKnown)return{action:"ignore",reason:"reply in a thread with no session"}}if(c(n,o))return{action:"ignore",reason:"from a bot, in a thread with no session"};const r=s(n,o),e=i(n);return!r&&e.length===0?{action:"ignore",reason:"nothing in it"}:{action:"run",text:r,files:e}}export{p as FILE_SHARE,a as isBotEvent,u as isSelfEvent,l as routeAppMention,g as routeMessage};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{splitForSlack as r}from"./format.js";async function l(n,t,e){let s;for(const a of r(e)){const o=await n.chat.postMessage({channel:t.channel,thread_ts:t.threadTs,markdown_text:a});o.ts&&(s={ts:o.ts,text:a})}return s}export{l as say};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
function c(r,l=1e3){let a,t,n,o=0,i=!1,s=Promise.resolve();const d=e=>{a=e,o=Date.now(),s=s.then(()=>r(e)).catch(()=>{})};return{set(e){if(i||!e||e===a||e===t)return;const u=Date.now()-o;if(u>=l&&!n){d(e);return}t=e,n||(n=setTimeout(()=>{n=void 0;const f=t;t=void 0,f&&!i&&d(f)},Math.max(0,l-u)))},async close(){i=!0,n&&(clearTimeout(n),n=void 0),t=void 0,await s}}}export{c as statusLine};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{statusLine as l}from"./status.js";const i="t1";function c(t){return{type:"task_card",task_id:i,title:t,status:"in_progress"}}function d(t){return t===1?"Used 1 tool":`Used ${t} tools`}function h(t,e,r=1e3){let s,o=0;const n=l(async a=>{if(s){await t.chat.update({channel:e.channel,ts:s,text:a,blocks:[c(a)]});return}s=(await t.chat.postMessage({channel:e.channel,thread_ts:e.threadTs,text:a,blocks:[c(a)]})).ts},r);return{note(a){o+=1,n.set(a)},retitle(a){n.set(a)},async close(){await n.close(),s&&await t.chat.update({channel:e.channel,ts:s,markdown_text:`_${d(o)}_`})}}}export{h as taskMessage};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
function s(n,t){const e=n.thread_ts??n.ts;if(!(!e||!n.channel))return{teamId:t,channelId:n.channel,threadTs:e}}function a(n){const t=Number.parseFloat(n??"");return Number.isFinite(t)?t*1e3:Date.now()}function c(n){return`${n.teamId}:${n.channelId}:${n.threadTs}`}function o(n){return new RegExp(`<@${n}(?:\\|[^>]*)?>`,"g")}function u(n,t,e){if(e)return n.replaceAll(o(t),()=>e).trim();const r=new RegExp(`[ \\t]*<@${t}(?:\\|[^>]*)?>[ \\t]*`,"g");return n.replaceAll(r," ").trim()}function i(n){return new RegExp(`^(?:\\s*<@${n}(?:\\|[^>]*)?>[ \\t]*[,:]?)+[ \\t]*`)}function d(n,t){return new RegExp(`<@${t}(?:\\|[^>]*)?>`).test(n)}function p(n,t,e){return u(n.replace(i(t),""),t,e)}function f(n,t){return t.ts?`${n.teamId}:${n.channelId}:${t.ts}`:t.event_id}export{p as addressedText,d as mentionsBot,f as messageKeyString,u as nameBotMentions,a as spokenAt,s as threadKeyOf,c as threadKeyString};
|
|
@@ -0,0 +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,s){return l(i.handle)===s||l(i.displayName)===s||l(i.realName)===s}function k(i,s={}){const{ttlMs:f=_,deadlineMs:p=N,self:g}=s,c=new Map;let o,m;const u=new Map;async function w(e){let t={userId:e};try{const r=(await i.users.info({user:e})).user;if(r){const h=r.real_name?.trim();t={...b(r),userId:e,...!r.profile?.real_name?.trim()&&h?{realName:h}:{}}}}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 m??=M(i,e=>D(e)&&e.id!==g).then(e=>{o={users:e,expires:Date.now()+f}}).catch(e=>{o={users:[],expires:Date.now()+x},a.debug({err:e},"could not list the workspace members")}).finally(()=>{m=void 0}),m}async function y(){if(o&&o.expires>Date.now())return o.users;const e=d();let t;const n=new Promise(r=>{t=setTimeout(r,p)});return await Promise.race([e,n]),clearTimeout(t),o?o.users:(a.debug("the member list is still loading; leaving this name unlinked"),[])}return{get size(){return c.size},prime(){d()},async lookup(e){if(!e)return{userId:e};const t=c.get(e);if(t&&t.expires>Date.now())return t.user;const n=u.get(e);if(n)return n;const r=w(e).finally(()=>u.delete(e));return u.set(e,r),r},async findByName(e){const t=l(e.replace(/^@+/,""));if(!t)return;const n=(await y()).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{DEFAULT_VERBOSITY as l,VERBOSITY as f}from"../config.js";import{log as u}from"../log.js";const n="slack.verbosity";function a(e){const t=e.trim().toLowerCase();return f.includes(t)?t:void 0}function h(e,t=l){const s=new Set;return{of(r){const o=e.get(r,n);if(o===void 0)return{level:t,fromThread:!1};const i=a(o);return i?{level:i,fromThread:!0}:(s.has(`${r} ${o}`)||(s.add(`${r} ${o}`),u.warn({thread:r,stored:o},"unknown verbosity on this thread; using the default")),{level:t,fromThread:!1})},set(r,o){e.set(r,n,o)},clear(r){e.clear(r,n)}}}function p(e){return e!=="low"}function m(e){return e==="high"}export{a as parseVerbosity,p as showsProgress,m as showsTasks,h as threadVerbosity};
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import{z as a}from"zod";import{FETCH_FILE_TOOL as h,fileTag as f}from"../harness/principal.js";import{defineTool as s,ToolRefusal as o}from"./types.js";function c(i){return[s({name:h,description:["Downloads a file somebody shared in this conversation and gives you the path to it.","","Takes the ref from an <attachment> tag. Ask for a file when you need to open it, rather","than for everything in the conversation. Asking for one already downloaded hands back","the same path without fetching it again, so ask rather than reusing a path from earlier","in the conversation. It fails, with the reason, when there is no file to be had."].join(`
|
|
2
|
+
`),input:{ref:a.string().trim().min(1).describe("The ref attribute of the file tag naming the file you want")},async run(t,n){const e=await i.fetch(n.threadKey,t.ref);if(!e)throw new o(`No file with the ref ${t.ref} was shared in this conversation. Refs come from the file tags Bishop writes, and one only works in the conversation it was written in.`);if(!e.path){const r=e.unavailable??"the download produced no file";throw new o(`Bishop could not fetch the file with ref ${t.ref}: ${r}.`+(e.url?` The original is at ${e.url}`:""))}return f({...e,ref:t.ref})}})]}export{c as fileTools};
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import{z as a}from"zod";import{BishopError as y}from"../errors.js";import{NO_OUTPUT as T}from"../harness/principal.js";import{log as p}from"../log.js";import{describeTime as w,nextRun as R,systemTimeZone as g,validateCron as z,validateTimeZone as _}from"../schedule/cron.js";import{defineTool as l,ToolRefusal as d}from"./types.js";const A=20;function m(e){return`${e.origin.name}:${e.principal.id}`}function $(e){try{return e()}catch(n){throw n instanceof y?new d(n.message):n}}async function k(e){try{return await e()}catch(n){throw n instanceof y?new d(n.message):n}}function B(e,n){const t=[`${e.id} ${e.name}${e.enabled?"":" (disabled)"}`,` runs ${e.cron} (${e.tz})`,` next ${e.enabled?w(e.nextRunAt,n):"never, while it is off"}`,` to ${e.destination.label}`,` prompt ${e.prompt}`,` by ${e.createdBy}`];return e.lastError&&t.push(` error ${e.lastError} (${e.consecutiveFailures} in a row)`),t.join(`
|
|
2
|
+
`)}async function b(e,n){const t=await n.destination(),i=m(n);return e.schedules.all().filter(o=>o.createdBy===i||e.sameConversation(o.destination,t))}function E(e){const n=e.now??Date.now;return l({name:"schedule_create",description:["Runs a prompt on a cron schedule and posts the answer where you say.","Each firing is a fresh session with no memory of the last one, so the prompt has to","carry everything it needs.","",`The prompt must say when to stay quiet, and say that answering ${T} is how.`,"A schedule that reports every time it runs is one people learn to ignore."].join(`
|
|
3
|
+
`),input:{name:a.string().trim().min(1).describe("What a person sees in a list, and an email subject"),cron:a.string().trim().min(1).describe("Five-field cron, or @daily and friends. No more than once every five minutes"),prompt:a.string().trim().min(1).describe("What the agent is asked at each firing, including when to say nothing"),to:a.string().trim().min(1).optional().describe("Where the answer goes: #channel or @person for Slack, or an email address. Leave it out to post back into this conversation."),tz:a.string().trim().min(1).optional().describe("IANA zone the expression is read in, like Europe/London. Defaults to Bishop's")},async run(t,i){const o=m(i);if(!e.authorized(o))throw new d(`Bishop can't run a schedule created by ${o}, so making one would write something that never fires. Ask an operator to add it with \`bishop schedules add\`.`);const s=e.schedules.all().filter(v=>v.createdBy===o).length;if(s>=A)throw new d(`You already have ${s} schedules, which is the limit. Remove one with schedule_delete before adding another.`);const u=t.tz,c=u?$(()=>_(u)):g();$(()=>z(t.cron,c));const f=t.to,h=f?await k(()=>e.resolve(f)):await i.destination(),r=e.schedules.add({name:t.name,cron:t.cron,tz:c,prompt:t.prompt,destination:h,createdBy:o,nextRunAt:R(t.cron,c,n())});return p.info({schedule:r.id,name:r.name,cron:r.cron,tz:r.tz,by:r.createdBy,to:h.label},"the agent created a schedule"),`Created ${r.id} "${r.name}", running ${r.cron} (${c}).
|
|
4
|
+
Next ${w(r.nextRunAt,c)}, posting to ${h.label}.`}})}function N(e){return l({name:"schedule_list",description:"Lists the schedules you created, and any that post into this conversation. Not schedules other people aimed elsewhere.",input:{},async run(n,t){const i=await b(e,t);if(i.length===0)return"Nothing is scheduled here, and you have not made any.";const o=g();return`${i.map(s=>B(s,o)).join(`
|
|
5
|
+
|
|
6
|
+
`)}
|
|
7
|
+
|
|
8
|
+
Times in ${o}.`}})}function x(e){return l({name:"schedule_delete",description:"Removes a schedule by id. Only ones schedule_list shows you.",input:{id:a.string().trim().min(1).describe("The id schedule_list reports")},async run(n,t){const o=(await b(e,t)).find(s=>s.id===n.id);if(!o)throw new d(`No schedule ${n.id} that you can remove. schedule_list has the ones you can.`);return e.schedules.remove(o.id),p.info({schedule:o.id,name:o.name,by:m(t)},"the agent removed a schedule"),`Removed ${o.id} "${o.name}".`}})}function F(e){return[E(e),N(e),x(e)]}export{A as MAX_PER_CREATOR,m as createdBy,F as scheduleTools};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{randomBytes as v,randomUUID as S}from"node:crypto";import{createServer as E}from"node:http";import{McpServer as T}from"@modelcontextprotocol/sdk/server/mcp.js";import{StreamableHTTPServerTransport as b}from"@modelcontextprotocol/sdk/server/streamableHttp.js";import{isInitializeRequest as B}from"@modelcontextprotocol/sdk/types.js";import{log as h}from"../log.js";import{ToolRefusal as g}from"./types.js";const O="bishop",y="/mcp",_="BISHOP_TOOL_TOKEN",x=1048576;function k(i){const r=i.headers.authorization;return r&&/^Bearer\s+(.+)$/i.exec(r.trim())?.[1]?.trim()||void 0}function u(i,r,s){i.writeHead(r,{"content-type":"application/json"}),i.end(JSON.stringify({jsonrpc:"2.0",id:null,error:{code:-32001,message:s}}))}async function R(i){const r=[];let s=0;for await(const n of i){if(s+=n.length,s>x)throw new Error("request body is too large");r.push(n)}if(r.length!==0)return JSON.parse(Buffer.concat(r).toString("utf8"))}const A="Bishop's tools answer to a person, and this message came from another bot. Ask someone in the conversation to do it.";function N(i,r){const s=new T({name:O,version:"1"});for(const n of i)s.registerTool(n.name,{description:n.description,inputSchema:n.input},async m=>{try{if(r.principal.isBot)throw new g(A);return{content:[{type:"text",text:await n.run(m,r)}]}}catch(a){const d=a instanceof Error?a.message:String(a);return a instanceof g?h.info({tool:n.name,thread:r.threadKey,reason:d},"a tool refused"):h.error({tool:n.name,thread:r.threadKey,err:a},"a tool failed"),{content:[{type:"text",text:d}],isError:!0}}});return s}async function L(i){const r=i.host??"127.0.0.1",s=new Map,n=new Map;async function m(e,t){const o=new b({sessionIdGenerator:()=>S(),onsessioninitialized:c=>{if(s.get(e)!==t){o.close().catch(()=>{});return}n.set(c,{token:e,transport:o}),t.sessions.add(c)}});return o.onclose=()=>{const c=o.sessionId;c&&(n.delete(c),t.sessions.delete(c))},await N(i.tools,t.context).connect(o),o}const a=E((e,t)=>{(async()=>{try{if(!e.url?.startsWith(y))return u(t,404,"no such endpoint");const o=k(e),c=o?s.get(o):void 0;if(!c||!o)return u(t,401,"this call carries no valid turn credential");const l=e.method==="POST"?await R(e):void 0,f=e.headers["mcp-session-id"],p=typeof f=="string"?n.get(f):void 0;if(p){if(p.token!==o)return u(t,404,"no such session");await p.transport.handleRequest(e,t,l);return}if(typeof f=="string")return u(t,404,"no such session");if(!B(l))return u(t,400,"the first call on a connection must be initialize");await(await m(o,c)).handleRequest(e,t,l)}catch(o){h.error({err:o},"a tool call could not be handled"),t.headersSent?t.end():u(t,400,"that request could not be handled")}})()});await new Promise((e,t)=>{a.once("error",t),a.listen(0,r,()=>{a.removeListener("error",t),e()})});const d=a.address();if(!d||typeof d=="string")throw new Error("the tool endpoint started without a port");const w=`http://${r}:${d.port}${y}`;return h.info({url:w,tools:i.tools.map(e=>e.name)},"serving the agent its own tools"),{url:w,toolNames:i.tools.map(e=>e.name),grant(e){const t=v(32).toString("base64url"),o={context:e,sessions:new Set};return s.set(t,o),{access:{url:w,tokenEnvVar:_,token:t},revoke:()=>{if(s.delete(t)){for(const l of o.sessions){const f=n.get(l);n.delete(l),f?.transport.close().catch(p=>{h.debug({err:p},"a tool connection did not close cleanly")})}o.sessions.clear()}}}},async stop(){for(const{transport:e}of n.values())await e.close().catch(()=>{});n.clear(),s.clear(),a.closeAllConnections(),await new Promise(e=>a.close(()=>e()))}}}export{y as PATH,O as SERVER_NAME,_ as TOKEN_ENV_VAR,L as startToolServer};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
function r(o){return o}class s extends Error{constructor(e){super(e),this.name="ToolRefusal"}}export{s as ToolRefusal,r as defineTool};
|
package/dist/turn.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{log as i}from"./log.js";function u(o){const d=new Map,l=new Set;async function g(e){const r=o.sharedDirNotice;if(!(!r||l.has(e.threadKey))){l.add(e.threadKey);try{await e.notify(r)}catch(n){i.debug({thread:e.threadKey,err:n},"could not deliver the shared-directory notice")}}}function w(e,r){if(!(!o.tools||e.tools===!1))return o.tools.grant({threadKey:e.threadKey,principal:r,origin:e.origin,destination:()=>e.destination()})}async function K(e){await g(e);const r=new AbortController,n=e.presence();let h;try{d.set(e.threadKey,r);const a=e.freshSession?void 0:await o.sessions.get(e.threadKey),s=await o.workspaceFor?.(e.threadKey)??e.threadKey,m=await o.cwd(s),[p,y]=await Promise.all([e.messages(),a?void 0:e.history?.().catch(t=>{i.warn({thread:e.threadKey,err:t},"could not read the history")})]),[c,...f]=p;if(!c)throw new Error("a turn with no messages");const b=f.at(-1)?.principal??c.principal;if(r.signal.aborted){i.info({thread:e.threadKey},"turn stopped before the agent started");return}h=w(e,b);for await(const t of o.harness.runTurn({messages:[c,...f],cwd:m,threadKey:e.threadKey,...s===e.threadKey?{}:{workspaceKey:s},...a?{sessionId:a}:{},...e.note?{note:e.note}:{},...y?.length?{history:y}:{},origin:e.origin,...h?{tools:h.access}:{},signal:r.signal}))t.type==="session"&&t.sessionId!==a&&await o.sessions.set(e.threadKey,t.sessionId),t.type==="text"&&i.debug({thread:e.threadKey,chars:t.text.length},"agent said something"),t.type==="tool"&&i.debug({thread:e.threadKey,tool:t.name,detail:t.summary??t.detail},"tool activity"),t.type==="done"&&i.info({thread:e.threadKey,...t.usage},"turn complete"),t.type==="error"&&i.error({thread:e.threadKey,kind:t.kind,err:t.message},"turn errored"),await n.handle(t)}catch(a){const s=a instanceof Error?a.message:String(a);throw i.error({thread:e.threadKey,err:a},"turn failed before the agent answered"),await n.handle({type:"error",kind:"turn_failed",message:s}),a}finally{h?.revoke(),d.get(e.threadKey)===r&&d.delete(e.threadKey),await n.finish()}}return{run:K,stop(e){const r=d.get(e);return r?(r.abort(),!0):!1}}}export{u as makeTurnRunner};
|
package/dist/version.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{readFileSync as e}from"node:fs";import{dirname as t,join as n}from"node:path";import{fileURLToPath as i}from"node:url";function a(){try{const r=t(i(import.meta.url)),o=e(n(r,"..","package.json"),"utf8");return JSON.parse(o).version??"0.0.0"}catch{return"0.0.0"}}const p=a();export{p as version};
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import{createHash as w}from"node:crypto";import{existsSync as u,mkdirSync as p}from"node:fs";import{join as h,resolve as s}from"node:path";import{BishopError as d}from"./errors.js";import{exec as i}from"./exec.js";const l=64,c="bishop-",m=".claude/worktrees";function b(t){const r=t.replaceAll(/[^a-zA-Z0-9]+/g,"-").replaceAll(/-+/g,"-").replaceAll(/^-+|-+$/g,""),e=`${c}${r}`;if(e.length<=l)return e;const o=w("sha256").update(t).digest("hex").slice(0,8),n=l-c.length-o.length-1;return`${c}${r.slice(0,n)}-${o}`}function g(t,r){return s(t,m,r)}function I(t){return`\`${t}\` isn't a git repository, so I'm working in it directly. Another thread changing the same files at the same time can conflict.`}function N(t){return!!(t?.enabled&&t.isRepo)}function x(t){return u(h(t,".git"))}async function y(t){const r=await i("git",["rev-parse","--show-prefix"],{cwd:t});return r.code===0?r.stdout.trim():""}async function M(t,r){const e=g(t,r),o=await y(t);if(x(e))return f(e,o);if(u(e))throw new d(`${e} exists but is not a git worktree, which usually means an earlier attempt failed partway. Remove the directory and this thread will get a fresh one.`);const n=await i("git",["worktree","add","-b",r,e,"HEAD"],{cwd:t});if(n.code!==0){await i("git",["worktree","prune"],{cwd:t});const a=await i("git",["worktree","add",e,r],{cwd:t});if(a.code!==0)throw new d(`Couldn't create a git worktree for this thread at ${e}: ${k(n,a)}`)}return f(e,o)}function k(t,r){const e=t.stderr.trim(),o=r.stderr.trim();return!o||o===e?e:`${e}
|
|
2
|
+
and, retrying with the existing branch: ${o}`}function f(t,r){if(!r)return t;const e=s(t,r);return p(e,{recursive:!0}),e}async function P(t){const r=await i("git",["rev-parse","--git-common-dir"],{cwd:t}),e=r.stdout.trim();if(!(r.code!==0||!e))return s(t,e)}async function W(t){return(await i("git",["rev-parse","--show-toplevel"],{cwd:t})).code===0}async function _(t){const r=await i("git",["config","--get","user.email"],{cwd:t}),e=await i("git",["config","--get","user.name"],{cwd:t});return r.code===0&&r.stdout.trim()!==""&&e.code===0&&e.stdout.trim()!==""}export{m as WORKTREE_DIR,M as ensureWorktree,P as gitCommonDir,_ as hasGitIdentity,W as isGitRepo,N as isolates,I as sharedDirNotice,b as worktreeName,g as worktreePath};
|
package/package.json
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@agentdeploymentco/bishop",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "An agent gateway. Connects an agent to Slack and email.",
|
|
5
|
+
"license": "UNLICENSED",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"bin": {
|
|
8
|
+
"bishop": "dist/cli.js"
|
|
9
|
+
},
|
|
10
|
+
"engines": {
|
|
11
|
+
"node": ">=24"
|
|
12
|
+
},
|
|
13
|
+
"files": [
|
|
14
|
+
"dist"
|
|
15
|
+
],
|
|
16
|
+
"publishConfig": {
|
|
17
|
+
"access": "public",
|
|
18
|
+
"provenance": false
|
|
19
|
+
},
|
|
20
|
+
"scripts": {
|
|
21
|
+
"build": "rm -rf dist && tsc -p tsconfig.build.json && chmod +x dist/cli.js",
|
|
22
|
+
"build:release": "rm -rf dist && tsc -p tsconfig.release.json && node scripts/minify.mjs && chmod +x dist/cli.js",
|
|
23
|
+
"prepublishOnly": "npm run build:release && node scripts/check-package.mjs",
|
|
24
|
+
"dev": "tsx src/cli.ts",
|
|
25
|
+
"typecheck": "tsc --noEmit",
|
|
26
|
+
"test": "vitest run",
|
|
27
|
+
"test:watch": "vitest",
|
|
28
|
+
"lint": "biome check src test",
|
|
29
|
+
"format": "biome check --write src test",
|
|
30
|
+
"check": "npm run lint && npm run typecheck && npm test"
|
|
31
|
+
},
|
|
32
|
+
"dependencies": {
|
|
33
|
+
"@anthropic-ai/claude-agent-sdk": "^0.3",
|
|
34
|
+
"@modelcontextprotocol/sdk": "^1.30.0",
|
|
35
|
+
"@openai/codex-sdk": "^0.149.1",
|
|
36
|
+
"@slack/bolt": "^5.0.0",
|
|
37
|
+
"@slack/web-api": "^8",
|
|
38
|
+
"commander": "^13",
|
|
39
|
+
"cron-parser": "^5.10.0",
|
|
40
|
+
"dotenv": "^17",
|
|
41
|
+
"pino": "^9",
|
|
42
|
+
"zod": "^4"
|
|
43
|
+
},
|
|
44
|
+
"devDependencies": {
|
|
45
|
+
"@biomejs/biome": "^2.5.10",
|
|
46
|
+
"@types/node": "^24",
|
|
47
|
+
"esbuild": "^0.28.2",
|
|
48
|
+
"tsx": "^4",
|
|
49
|
+
"typescript": "^5",
|
|
50
|
+
"vitest": "^3"
|
|
51
|
+
}
|
|
52
|
+
}
|