@agentdeploymentco/bishop 0.5.1 → 0.5.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -1,35 +1,35 @@
1
1
  #!/usr/bin/env node
2
- import{stat as de}from"node:fs/promises";import{join as me,resolve as he}from"node:path";import{Command as le}from"commander";import{agentSnapshots as ue,fixedAgentDir as fe}from"./agent-dir.js";import{looksRemote as j,parseRepoSpec as Y}from"./agent-source.js";import{everyStore as W,storePath as J}from"./agent-store.js";import{attachmentStore as we}from"./attachments.js";import{HARNESSES as ge,loadConfig as G,nonEmptyFlag as Q,parseHarnessName as ye,positiveIntFlag as Se,resolveAgent as ke,sandboxConfig as ve,writeConfig as be}from"./config.js";import{dbPath as V,openDb as X}from"./db.js";import{describeMissing as Te,loadEnv as v,readCredentials as Ie}from"./env.js";import{BishopError as b}from"./errors.js";import{fileStore as Ee}from"./files.js";import{DEFAULT_MAX_AGE_DAYS as Ae,makeCollector as _e,sweep as xe}from"./gc.js";import{ghAuthLogin as Re,ghCliIn as Ne,gitAuthenticatesToGithub as Oe,withTemporaryGhConfig as Ge}from"./github/gh.js";import{AGENT_SCOPES as $e,checkGithubSetup as Le}from"./github/setup.js";import{createGmail as Ce}from"./gmail/create.js";import{checkGmailSetup as He}from"./gmail/setup.js";import{createHarness as Pe}from"./harness/create.js";import{sharedFileFetcher as Me}from"./interface/attachments.js";import{principalCheck as Z}from"./interface/authorize.js";import{deliveryRouter as De}from"./interface/delivery.js";import{conversationMatch as Be,destinationResolver as Ue}from"./interface/destination.js";import{messageReader as Fe}from"./interface/messages.js";import{log as m,requestedLevel as Ke}from"./log.js";import{DEFAULT_PROFILE as ze,setupNono as qe}from"./nono-setup.js";import{principalStore as je}from"./principals.js";import{checkSandbox as Ye,clearedStaging as We,sandboxPolicy as Je,writeLauncher as Qe}from"./sandbox.js";import{addSchedule as Ve,listSchedules as Xe,removeSchedule as Ze,setScheduleEnabled as ee}from"./schedule/cli.js";import{authorizedToRun as et,makeScheduler as tt}from"./schedule/runner.js";import{scheduleStore as ot}from"./schedule/store.js";import{threadQueue as nt}from"./sessions/queue.js";import{interfaceState as st,retentionStore as te,sessionStore as rt,threadContexts as at,threadHandover as it,threadMeta as ct}from"./sessions/store.js";import{ensureGitignore as pt,openBrowser as dt,resolveAppDetails as mt,SetupError as oe,setupSlack as ht,setupSlackManually as lt,withPrompt as ne,writeEnv as se}from"./setup.js";import{createSlackInterface as ut}from"./slack/interface.js";import{buildManifest as ft}from"./slack/manifest.js";import{createTeams as wt}from"./teams/create.js";import{administratorInstructions as gt,repointTeamsEndpoint as yt,setupTeams as St,setupTeamsManually as kt}from"./teams/setup.js";import{threadModel as vt}from"./thread-model.js";import{fileTools as bt}from"./tools/files.js";import{messageTools as Tt}from"./tools/messages.js";import{scheduleTools as It}from"./tools/schedules.js";import{startToolServer as Et}from"./tools/server.js";import{transcriptStore as At}from"./transcript.js";import{makeTurnRunner as _t}from"./turn.js";import{version as xt}from"./version.js";import{hasGitIdentity as Rt,isGitRepo as Nt,sharedDirNotice as Ot,WORKTREE_DIR as Gt}from"./worktrees.js";function u(e){process.stderr.write(`${e}
3
- `),process.exit(1)}async function $t(e){const t=he(e??".");let n;try{n=(await de(t)).isDirectory()}catch{throw new b(`--agent directory doesn't exist: ${t}`)}if(!n)throw new b(`--agent is not a directory: ${t}`);return t}async function Lt(e){const t=await Nt(e),n={enabled:!0,isRepo:t};return t?(m.info({agentDir:e,worktrees:me(e,Gt)},"worktree mode is on"),await Rt(e)||m.warn({agentDir:e},"git has no user.name or user.email here, so the agent cannot commit what it does in a worktree"),n):(m.warn({agentDir:e},"worktree mode is on but the agent directory is not a git repository; threads share it"),n)}function Ct(e){if(e.kind==="options-replaced"){m.warn({harness:e.harness},`agent.harnesses.${e.harness} replaces agent.options, which is ignored for this run`);return}m.warn({harness:e.harness,configured:e.configured},`--harness overrides the configured harness, but agent.options is still written for that one; move it to agent.harnesses.${e.configured} and give ${e.harness} its own block`)}async function Ht(e=[],t={}){e.length>0&&u(`bishop: unknown command "${e[0]}"
2
+ import{stat as me}from"node:fs/promises";import{join as le,resolve as he}from"node:path";import{Command as ue}from"commander";import{agentSnapshots as fe,fixedAgentDir as we}from"./agent-dir.js";import{looksRemote as j,parseRepoSpec as Y}from"./agent-source.js";import{everyStore as W,storePath as J}from"./agent-store.js";import{attachmentStore as ge}from"./attachments.js";import{HARNESSES as ye,loadConfig as $,nonEmptyFlag as Q,parseHarnessName as ke,positiveIntFlag as Se,resolveAgent as be,sandboxConfig as ve,writeConfig as Te}from"./config.js";import{dbPath as V,openDb as X}from"./db.js";import{describeMissing as Ie,loadEnv as b,readCredentials as Ee}from"./env.js";import{BishopError as v}from"./errors.js";import{fileStore as xe}from"./files.js";import{DEFAULT_MAX_AGE_DAYS as Ae,makeCollector as _e,sweep as Re}from"./gc.js";import{ghAuthLogin as Ne,ghCliIn as Oe,gitAuthenticatesToGithub as Ge,withTemporaryGhConfig as $e}from"./github/gh.js";import{AGENT_SCOPES as Le,checkGithubSetup as Ce}from"./github/setup.js";import{createGmail as He}from"./gmail/create.js";import{checkGmailSetup as Pe}from"./gmail/setup.js";import{createHarness as Me}from"./harness/create.js";import{sharedFileFetcher as De}from"./interface/attachments.js";import{principalCheck as Z}from"./interface/authorize.js";import{deliveryRouter as Be}from"./interface/delivery.js";import{conversationMatch as Ue,destinationResolver as Fe}from"./interface/destination.js";import{messageReader as Ke}from"./interface/messages.js";import{log as d,requestedLevel as ze}from"./log.js";import{DEFAULT_PROFILE as qe,setupNono as je}from"./nono-setup.js";import{principalStore as Ye}from"./principals.js";import{startReviewer as We}from"./review.js";import{checkSandbox as Je,clearedStaging as Qe,sandboxPolicy as Ve,writeLauncher as Xe}from"./sandbox.js";import{addSchedule as Ze,listSchedules as et,removeSchedule as tt,setScheduleEnabled as ee}from"./schedule/cli.js";import{authorizedToRun as ot,makeScheduler as nt}from"./schedule/runner.js";import{scheduleStore as st}from"./schedule/store.js";import{threadQueue as it}from"./sessions/queue.js";import{interfaceState as rt,retentionStore as te,sessionStore as at,threadContexts as ct,threadHandover as pt,threadMeta as dt}from"./sessions/store.js";import{ensureGitignore as mt,openBrowser as lt,resolveAppDetails as ht,SetupError as oe,setupSlack as ut,setupSlackManually as ft,withPrompt as ne,writeEnv as se}from"./setup.js";import{createSlackInterface as wt}from"./slack/interface.js";import{buildManifest as gt}from"./slack/manifest.js";import{createTeams as yt}from"./teams/create.js";import{administratorInstructions as kt,repointTeamsEndpoint as St,setupTeams as bt,setupTeamsManually as vt}from"./teams/setup.js";import{threadModel as Tt}from"./thread-model.js";import{fileTools as It}from"./tools/files.js";import{messageTools as Et}from"./tools/messages.js";import{scheduleTools as xt}from"./tools/schedules.js";import{startToolServer as At}from"./tools/server.js";import{transcriptStore as _t}from"./transcript.js";import{makeTurnRunner as Rt}from"./turn.js";import{version as Nt}from"./version.js";import{hasGitIdentity as Ot,isGitRepo as Gt,sharedDirNotice as $t,WORKTREE_DIR as Lt}from"./worktrees.js";function f(e){process.stderr.write(`${e}
3
+ `),process.exit(1)}async function Ct(e){const t=he(e??".");let n;try{n=(await me(t)).isDirectory()}catch{throw new v(`--agent directory doesn't exist: ${t}`)}if(!n)throw new v(`--agent is not a directory: ${t}`);return t}async function Ht(e){const t=await Gt(e),n={enabled:!0,isRepo:t};return t?(d.info({agentDir:e,worktrees:le(e,Lt)},"worktree mode is on"),await Ot(e)||d.warn({agentDir:e},"git has no user.name or user.email here, so the agent cannot commit what it does in a worktree"),n):(d.warn({agentDir:e},"worktree mode is on but the agent directory is not a git repository; threads share it"),n)}function Pt(e){if(e.kind==="options-replaced"){d.warn({harness:e.harness},`agent.harnesses.${e.harness} replaces agent.options, which is ignored for this run`);return}d.warn({harness:e.harness,configured:e.configured},`--harness overrides the configured harness, but agent.options is still written for that one; move it to agent.harnesses.${e.configured} and give ${e.harness} its own block`)}async function Mt(e=[],t={}){e.length>0&&f(`bishop: unknown command "${e[0]}"
4
4
 
5
- Run \`bishop --help\` to see what's available.`);const n=process.cwd(),c=t.agent&&j(t.agent)?Y(t.agent):void 0;t.autoUpdate&&!c&&u("--auto-update needs --agent to be a git URL. Bishop never updates a directory it was pointed at; that state is yours to manage."),v(n);const s=await G(n),r=Ie();r.ok||u(Te(r));const a=ke(s,t);for(const o of a.warnings)Ct(o);const h=ve(s);if(h){const o=await Ye(h);m.info({nono:o,...h.profile?{profile:h.profile}:{}},"sandbox is on")}const l=h?(o,d)=>Je({home:n,cwd:o,...d?{dirKey:d}:{},...h.profile?{profile:h.profile}:{}}):void 0,i=X(V(n)),p=rt(i,a.harness);let g;const $=async o=>await g?.check(o,await l?.(o))??{ok:!0},_=c?ue({spec:c,store:J(n,c.slug),dirs:p,autoUpdate:!!t.autoUpdate,recheck:$}):void 0,y=_?await _.start():await $t(t.agent),ae=_?_.forTurn:fe(y,p);m.info({bishopDir:n,agentDir:y,...y===n?{}:{separate:!0},...c?{source:c.cloneUrl,autoUpdate:!!t.autoUpdate}:{},app:s.slack?.app?.name},"bishop starting");const x=s.agent?.worktrees?await Lt(y):void 0,T=Pe(a.harness,{...a.options?{options:a.options}:{},...a.model?{model:a.model}:{},...a.effort?{effort:a.effort}:{},...x?{worktrees:x}:{},...h?{launcher:await Qe(n)}:{},credentials:r.credentials}),R=await T.check(y,await l?.(y));R.ok||u(T.describeStartupFailure(y,R.error));const L=R.model??a.model;m.info({harness:T.name,credentials:R.credentialSource??"unreported",model:L,...a.effort?{effort:a.effort}:{},...a.options?{agentOptions:Object.keys(a.options)}:{}},"harness ready"),g=T,m.info({sessions:p.count()},"opened session store");const P=_e({retention:te(i),stores:()=>W(n),home:n,...s.gc?.maxAgeDays===void 0?{}:{maxAgeDays:s.gc.maxAgeDays}});P.start();let f;const ie=o=>f?Z(f,k)(o):et(o),ce=async(o,d)=>{if(!f)throw new b("no interface is running, so there is nowhere to send");return Ue(f,{optedInOnly:!0})(o,d)},pe=(o,d)=>f?Be(f)(o,d):!1,M=Ee({home:n,...s.files?.maxMb===void 0?{}:{maxMb:s.files.maxMb}}),D=we(i),k=je(i),B=ot(i),U=await Et({tools:[...It({schedules:B,authorized:ie,resolve:ce,sameConversation:pe,principals:k}),...bt({fetch:Me({interfaces:()=>f??[],store:D,files:M})}),...r.credentials.slack?Tt({read:Fe({interfaces:()=>f??[]})}):[]]}),N=ct(i),C=vt(N),F={...L?{model:L}:{},...a.effort?{effort:a.effort}:{}},K=_t({harness:T,cwd:ae,sessions:p,model:C,tools:U,contexts:at(i,N),transcript:At(i),principals:k,...x&&!x.isRepo?{sharedDirNotice:Ot(y)}:{},...l?{sandbox:{policy:(o,d)=>l(o,d.dirKey),staging:await We(n)}}:{}}),z=nt(),I={threads:p,events:p,files:M,attachments:D,principals:k,run:o=>z.run(o.thread.threadKey,()=>K.run(o)),stop:o=>K.stop(o)},w=[];if(r.credentials.slack&&w.push(await ut({...I,credentials:r.credentials.slack,meta:N,model:C,configuredModel:F,...s.slack?.verbosity?{verbosity:s.slack.verbosity}:{},...s.slack?.allow?{allow:s.slack.allow}:{},...s.slack?.maxBotTurns===void 0?{}:{maxBotTurns:s.slack.maxBotTurns}})),r.credentials.gmail)try{w.push(await Ce({...I,credentials:r.credentials.gmail,...s.gmail?{config:s.gmail}:{},state:st(i)}))}catch(o){o instanceof b&&u(o.message);const d=r.credentials.gmail,O=d.kind==="oauth"?"the authorized Gmail account":`${d.user}`;u(`Couldn't reach the mailbox for ${O}:
5
+ Run \`bishop --help\` to see what's available.`);const n=process.cwd(),c=t.agent&&j(t.agent)?Y(t.agent):void 0;t.autoUpdate&&!c&&f("--auto-update needs --agent to be a git URL. Bishop never updates a directory it was pointed at; that state is yours to manage."),b(n);const s=await $(n),i=Ee();i.ok||f(Ie(i));const r=be(s,t);for(const o of r.warnings)Pt(o);const l=ve(s);if(l){const o=await Je(l);d.info({nono:o,...l.profile?{profile:l.profile}:{}},"sandbox is on"),l.profile||d.warn("sandbox is enabled but agent.sandbox.profile is missing; run `bishop nono setup`")}const u=s.review?await We(s.review,{home:n}):void 0;u&&d.warn("review is configured, but this version only checks the reviewer at startup; no message is reviewed yet (#254)");const m=l?(o,p)=>Ve({home:n,cwd:o,...p?{dirKey:p}:{},...l.profile?{profile:l.profile}:{},...u?.unwritable.length?{unwritable:u.unwritable}:{}}):void 0,a=X(V(n)),h=at(a,r.harness);let A;const re=async o=>await A?.check(o,await m?.(o))??{ok:!0},_=c?fe({spec:c,store:J(n,c.slug),dirs:h,autoUpdate:!!t.autoUpdate,recheck:re}):void 0,y=_?await _.start():await Ct(t.agent),ae=_?_.forTurn:we(y,h);d.info({bishopDir:n,agentDir:y,...y===n?{}:{separate:!0},...c?{source:c.cloneUrl,autoUpdate:!!t.autoUpdate}:{},app:s.slack?.app?.name},"bishop starting");const R=s.agent?.worktrees?await Ht(y):void 0,T=Me(r.harness,{...r.options?{options:r.options}:{},...r.model?{model:r.model}:{},...r.effort?{effort:r.effort}:{},...R?{worktrees:R}:{},...l?{launcher:await Xe(n)}:{},credentials:i.credentials}),N=await T.check(y,await m?.(y));N.ok||f(T.describeStartupFailure(y,N.error));const L=N.model??r.model;d.info({harness:T.name,credentials:N.credentialSource??"unreported",model:L,...r.effort?{effort:r.effort}:{},...r.options?{agentOptions:Object.keys(r.options)}:{}},"harness ready"),A=T,d.info({sessions:h.count()},"opened session store");const P=_e({retention:te(a),stores:()=>W(n),home:n,...s.gc?.maxAgeDays===void 0?{}:{maxAgeDays:s.gc.maxAgeDays}});P.start();let w;const ce=o=>w?Z(w,S)(o):ot(o),pe=async(o,p)=>{if(!w)throw new v("no interface is running, so there is nowhere to send");return Fe(w,{optedInOnly:!0})(o,p)},de=(o,p)=>w?Ue(w)(o,p):!1,M=xe({home:n,...s.files?.maxMb===void 0?{}:{maxMb:s.files.maxMb}}),D=ge(a),S=Ye(a),B=st(a),U=await At({tools:[...xt({schedules:B,authorized:ce,resolve:pe,sameConversation:de,principals:S}),...It({fetch:De({interfaces:()=>w??[],store:D,files:M})}),...i.credentials.slack?Et({read:Ke({interfaces:()=>w??[]})}):[]]}),O=dt(a),C=Tt(O),F={...L?{model:L}:{},...r.effort?{effort:r.effort}:{}},K=Rt({harness:T,cwd:ae,sessions:h,model:C,tools:U,contexts:ct(a,O),transcript:_t(a),principals:S,...R&&!R.isRepo?{sharedDirNotice:$t(y)}:{},...m?{sandbox:{policy:(o,p)=>m(o,p.dirKey),staging:await Qe(n)}}:{}}),z=it(),I={threads:h,events:h,files:M,attachments:D,principals:S,run:o=>z.run(o.thread.threadKey,()=>K.run(o)),stop:o=>K.stop(o)},g=[];if(i.credentials.slack&&g.push(await wt({...I,credentials:i.credentials.slack,meta:O,model:C,configuredModel:F,...s.slack?.verbosity?{verbosity:s.slack.verbosity}:{},...s.slack?.allow?{allow:s.slack.allow}:{},...s.slack?.maxBotTurns===void 0?{}:{maxBotTurns:s.slack.maxBotTurns}})),i.credentials.gmail)try{g.push(await He({...I,credentials:i.credentials.gmail,...s.gmail?{config:s.gmail}:{},state:rt(a)}))}catch(o){o instanceof v&&f(o.message);const p=i.credentials.gmail,G=p.kind==="oauth"?"the authorized Gmail account":`${p.user}`;f(`Couldn't reach the mailbox for ${G}:
6
6
  ${o instanceof Error?o.message:String(o)}
7
7
 
8
- Run \`bishop gmail setup\` to check the credentials.`)}if(r.credentials.teams)try{w.push(await wt({...I,credentials:r.credentials.teams,meta:N,model:C,configuredModel:F,...s.teams?{config:s.teams}:{}}))}catch(o){o instanceof b&&u(o.message),u(`Couldn't start the Teams app for client ${r.credentials.teams.clientId}:
8
+ Run \`bishop gmail setup\` to check the credentials.`)}if(i.credentials.teams)try{g.push(await yt({...I,credentials:i.credentials.teams,meta:O,model:C,configuredModel:F,...s.teams?{config:s.teams}:{}}))}catch(o){o instanceof v&&f(o.message),f(`Couldn't start the Teams app for client ${i.credentials.teams.clientId}:
9
9
  ${o instanceof Error?o.message:String(o)}
10
10
 
11
- Check the credentials in .env and the \`teams\` block in .bishop/config.json.`)}f=w;for(const o of w)await o.start();m.info({interfaces:w.map(o=>o.name)},"bishop is listening");const q=tt({schedules:B,run:I.run,threads:p,cancel:I.stop,deliver:De(w),authorized:Z(w,k),principals:k,attach:it(p)});q.start();for(const o of["SIGINT","SIGTERM"])process.once(o,()=>{m.info({signal:o,inFlight:z.size},"shutting down"),q.stop().catch(d=>m.error({err:d},"the scheduler failed to stop cleanly")).then(()=>Promise.allSettled(w.map(d=>d.stop()))).then(async d=>{const O=d.filter(E=>E.status==="rejected");for(const E of O)m.error({err:E.reason},"an interface failed to stop cleanly");await U.stop().catch(E=>{m.error({err:E},"the tool endpoint failed to stop cleanly")}),await P.stop(),i.close(),process.exit(O.length>0?1:0)})})}function Pt(e){for(const t of e.snapshots)process.stdout.write(`removed snapshot ${t.commit} (unused for ${t.ageDays} days)
11
+ Check the credentials in .env and the \`teams\` block in .bishop/config.json.`)}w=g;for(const o of g)await o.start();d.info({interfaces:g.map(o=>o.name)},"bishop is listening");const q=nt({schedules:B,run:I.run,threads:h,cancel:I.stop,deliver:Be(g),authorized:Z(g,S),principals:S,attach:pt(h)});q.start();for(const o of["SIGINT","SIGTERM"])process.once(o,()=>{d.info({signal:o,inFlight:z.size},"shutting down"),q.stop().catch(p=>d.error({err:p},"the scheduler failed to stop cleanly")).then(()=>Promise.allSettled(g.map(p=>p.stop()))).then(async p=>{const G=p.filter(E=>E.status==="rejected");for(const E of G)d.error({err:E.reason},"an interface failed to stop cleanly");await U.stop().catch(E=>{d.error({err:E},"the tool endpoint failed to stop cleanly")}),await P.stop(),a.close(),process.exit(G.length>0?1:0)})})}function Dt(e){for(const t of e.snapshots)process.stdout.write(`removed snapshot ${t.commit} (unused for ${t.ageDays} days)
12
12
  `);for(const t of e.worktrees){const n=t.branch?`, branch ${t.branch}`:"";process.stdout.write(`released ${t.path}${n}
13
13
  `)}process.stdout.write(`snapshots: removed ${e.snapshots.length}, kept ${e.keptSnapshots}
14
14
  worktrees: released ${e.worktrees.length}
15
15
  files: removed ${e.files} download directories
16
16
  rows: forgot ${e.threads} threads, ${e.events} events
17
- `)}async function Mt(e){const t=process.cwd();v(t),e.agent&&!j(e.agent)&&u("gc --agent takes the git URL Bishop cloned, not a path. It narrows the pass to the files Bishop owns for one agent, and it owns none for a directory agent. Run `bishop gc` with no --agent to collect what its threads left behind.");const n=e.agent?[J(t,Y(e.agent).slug)]:await W(t),c=await G(t),s=e.maxAgeDays??c.gc?.maxAgeDays;Ke()||(m.level="error");const r=X(V(t));try{Pt(await xe({retention:te(r),stores:n,home:t,...e.agent?{only:n}:{},...s===void 0?{}:{maxAgeDays:s}}))}finally{r.close()}}function Dt(){return!(process.env.SSH_CONNECTION||process.env.SSH_TTY||process.env.SSH_CLIENT||process.platform==="linux"&&!process.env.DISPLAY&&!process.env.WAYLAND_DISPLAY)}async function Bt(e={}){const t=process.cwd();v(t);const n=process.env.BISHOP_GMAIL_CLIENT_ID?.trim(),c=process.env.BISHOP_GMAIL_CLIENT_SECRET?.trim(),s=process.env.BISHOP_GMAIL_REFRESH_TOKEN?.trim(),r=process.env.BISHOP_GMAIL_USER?.trim(),a=process.env.BISHOP_GMAIL_SERVICE_ACCOUNT?.trim();n&&!c&&u("BISHOP_GMAIL_CLIENT_ID is set but BISHOP_GMAIL_CLIENT_SECRET is not.");const h=e.browser===!1||!Dt()?"paste":"browser",l=await ne(!0,async i=>He({...n&&c?{client:{clientId:n,clientSecret:c}}:{},...s?{refreshToken:s}:{},...r?{user:r}:{},...a?{serviceAccount:a}:{},mode:h,openBrowser:dt,readPaste:async p=>(process.stdout.write(`Open this in a browser and approve it:
17
+ `)}async function Bt(e){const t=process.cwd();b(t),e.agent&&!j(e.agent)&&f("gc --agent takes the git URL Bishop cloned, not a path. It narrows the pass to the files Bishop owns for one agent, and it owns none for a directory agent. Run `bishop gc` with no --agent to collect what its threads left behind.");const n=e.agent?[J(t,Y(e.agent).slug)]:await W(t),c=await $(t),s=e.maxAgeDays??c.gc?.maxAgeDays;ze()||(d.level="error");const i=X(V(t));try{Dt(await Re({retention:te(i),stores:n,home:t,...e.agent?{only:n}:{},...s===void 0?{}:{maxAgeDays:s}}))}finally{i.close()}}function Ut(){return!(process.env.SSH_CONNECTION||process.env.SSH_TTY||process.env.SSH_CLIENT||process.platform==="linux"&&!process.env.DISPLAY&&!process.env.WAYLAND_DISPLAY)}async function Ft(e={}){const t=process.cwd();b(t);const n=process.env.BISHOP_GMAIL_CLIENT_ID?.trim(),c=process.env.BISHOP_GMAIL_CLIENT_SECRET?.trim(),s=process.env.BISHOP_GMAIL_REFRESH_TOKEN?.trim(),i=process.env.BISHOP_GMAIL_USER?.trim(),r=process.env.BISHOP_GMAIL_SERVICE_ACCOUNT?.trim();n&&!c&&f("BISHOP_GMAIL_CLIENT_ID is set but BISHOP_GMAIL_CLIENT_SECRET is not.");const l=e.browser===!1||!Ut()?"paste":"browser",u=await ne(!0,async m=>Pe({...n&&c?{client:{clientId:n,clientSecret:c}}:{},...s?{refreshToken:s}:{},...i?{user:i}:{},...r?{serviceAccount:r}:{},mode:l,openBrowser:lt,readPaste:async a=>(process.stdout.write(`Open this in a browser and approve it:
18
18
 
19
- ${p}
19
+ ${a}
20
20
 
21
21
  It will finish on a http://127.0.0.1 address that fails to load. That is expected.
22
22
  Copy the whole address out of the browser and paste it here.
23
23
 
24
- `),i("Redirect URL",""))}));if(process.stdout.write(`${l.report}
25
- `),l.refreshToken){const i=await se(t,{BISHOP_GMAIL_REFRESH_TOKEN:l.refreshToken});process.stdout.write(`
26
- Saved BISHOP_GMAIL_REFRESH_TOKEN to ${i}
27
- `)}l.ok||process.exit(1)}async function Ut(e){const t=process.cwd(),n=process.env.GH_TOKEN?.trim();process.env.GH_TOKEN!==void 0&&!n&&delete process.env.GH_TOKEN,v(t);const c=process.env.GH_TOKEN?.trim(),s=!!c&&!n,r=e.prompt&&process.stdin.isTTY===!0,a=e.print?process.stderr:process.stdout,h=p=>a.write(p),l=p=>Le({...c?{token:c,tokenIsSaved:s}:{},...r?{login:()=>Re($e.map(g=>g.scope),p),confirm:async g=>/^y/i.test(await ne(!0,$=>$(g,"y"),a))}:{},gitConfigured:Oe,gh:Ne(p),...e.temporary?{temporary:!0}:{}}),i=e.temporary&&!c?await Ge(l):await l();if(h(`${i.report}
28
- `),e.print)i.token&&process.stdout.write(`${JSON.stringify({GH_TOKEN:i.token})}
29
- `);else if(i.token){if(i.source==="gh"){const g=await se(t,{GH_TOKEN:i.token});h(`
30
- Saved GH_TOKEN to ${g}
31
- `)}const p=await pt(t,[".env"]);p.length>0&&h(`Added ${p.join(", ")} to .gitignore
32
- `)}i.ok||process.exit(1)}async function Ft(e){const t=process.cwd(),n=await qe({config:await G(t),name:e.name});process.stdout.write(`${n.report}
33
- `),n.config&&process.stdout.write(`Saved ${await be(n.config,t)}
34
- `),n.ok||process.exit(1)}async function Kt(e){v(process.cwd());const t={...e.name===void 0?{}:{name:e.name},...e.displayName===void 0?{}:{displayName:e.displayName},...e.description===void 0?{}:{description:e.description},prompt:e.prompt};e.manual?await lt(t):await ht(t)}async function zt(e){if(e.appId!==void 0&&!e.manual)throw new oe("--app-id adopts an app somebody else created, so it needs --manual too.");if(e.endpoint!==void 0&&e.manual)throw new oe("--endpoint sets the URL on an app Bishop is creating, so it cannot be used\nwith --manual. Point an adopted app with `bishop teams endpoint <url>`,\nor ask whoever created it to.");v(process.cwd());const t={...e.name===void 0?{}:{name:e.name},...e.displayName===void 0?{}:{displayName:e.displayName},...e.description===void 0?{}:{description:e.description},...e.endpoint===void 0?{}:{endpoint:e.endpoint},...e.appId===void 0?{}:{appId:e.appId},prompt:e.prompt};e.manual?await kt(t):await St(t)}async function qt(e){const t=process.cwd(),n=await G(t),c=await mt({name:e.name??n.slack?.app?.name,displayName:e.displayName??n.slack?.app?.displayName,description:e.description??n.slack?.app?.description,prompt:!1},t,async(s,r)=>r);process.stdout.write(`${JSON.stringify(ft(c),null,2)}
35
- `)}const S=new le().name("bishop").description("An agent gateway. Runs an agent and connects it to Slack, email, and Teams.").version(xt,"-v, --version").enablePositionalOptions().option("--agent <path-or-url>","the agent's directory, or a git URL to clone; defaults to the current directory").option(`--harness <${ge.join("|")}>`,"which SDK runs the agent",ye).option("--model <model>","the model to run, in the harness's own naming",Q("--model")).option("--effort <effort>","how hard the model should think, in the harness's own naming",Q("--effort")).option("--auto-update","for a git-URL --agent, check for a new commit at the start of every new thread").allowExcessArguments(!0).action((e,t)=>Ht(t.args,e));S.command("gc").description("Collect the snapshots, worktrees, and rows old conversations left behind").option("--max-age-days <days>",`how long a quiet thread's remains are kept; defaults to gc.maxAgeDays, then ${Ae}`,Se("--max-age-days")).option("--agent <url>","only this agent's snapshots; defaults to every agent Bishop has cloned here").action(Mt);const A=S.command("schedules").description("Prompts the agent runs on a clock, and where their answers go");A.command("list").description("Everything scheduled here, in your own time zone").action(()=>Xe(process.cwd())),A.command("add").description("Add a schedule").requiredOption("--name <name>","what a person sees in a list, and the subject of an email").requiredOption("--cron <expression>","five-field cron, or @daily and friends").requiredOption("--prompt <text>","what the agent is asked, including when to stay quiet").requiredOption("--to <destination>","#channel or @person for Slack, an address for Gmail").option("--tz <zone>","IANA zone the expression is read in; defaults to this machine").option("--interface <name>","which interface delivers, when --to is ambiguous").action(e=>Ve(process.cwd(),e)),A.command("rm <id>").description("Remove a schedule").action(e=>Ze(process.cwd(),e)),A.command("disable <id>").description("Stop a schedule firing, keeping it").action(e=>ee(process.cwd(),e,!1)),A.command("enable <id>").description("Start a disabled schedule firing again, from now").action(e=>ee(process.cwd(),e,!0));const jt=S.command("gmail").description("Gmail interface commands");jt.command("setup").description("Authorize a Gmail mailbox, or check the one already configured").option("--no-browser","print a URL to open elsewhere instead of opening one here").action(Bt);const Yt=S.command("github").description("GitHub credentials for the agent");Yt.command("setup").description("Give the agent a GitHub token, or check the one it's using").option("--no-prompt","don't offer to run `gh auth login`").option("--print","write the credential to stdout as JSON instead of to .env").option("--temporary","log in without touching this machine's own gh login").action(Ut);const Wt=S.command("nono").description("The nono sandbox the agent runs under");Wt.command("setup").description("Write a nono profile that lets a sandboxed agent push").option("--name <name>","the profile name",ze).action(Ft);const re=S.command("slack").description("Slack interface commands");re.command("setup").description("Create and install a Slack app, and save its credentials here").option("--name <name>","app name; defaults to the current directory name").option("--display-name <name>","bot display name in Slack; defaults to --name").option("--description <text>","short description shown in Slack").option("--no-prompt","don't prompt for anything left off").option("--manual","adopt an app you created in Slack yourself, using its tokens").action(Kt),re.command("manifest").description("Print the app manifest, for creating an app in Slack by hand").option("--name <name>","app name; defaults to config or the directory name").option("--display-name <name>","bot display name; defaults to --name").option("--description <text>","short description shown in Slack").action(qt);const H=S.command("teams").description("Microsoft Teams interface commands");H.command("setup").description("Create a Teams app and save its credentials here, or check the one configured").option("--name <name>","app name; defaults to the current directory name").option("--display-name <name>","app name shown in Teams; defaults to --name").option("--description <text>","short description shown in Teams").option("--endpoint <url>","the URL Teams POSTs to; can be set later").option("--app-id <id>","with --manual, the Teams app ID you were given").option("--no-prompt","don't prompt for anything left off").option("--manual","adopt an app somebody else created, using its credentials").action(zt),H.command("endpoint <url>").description("Point the Teams app at a different URL, changing nothing else").option("--app-id <id>","the Teams app to point; defaults to the one in config").action((e,t)=>yt(e,t.appId===void 0?{}:{appId:t.appId})),H.command("instructions").description("Print what to send whoever creates the Teams app, when that is an administrator").option("--endpoint <url>","the URL Teams should POST to, if you already know it").action(e=>{process.stdout.write(gt(e.endpoint))});try{await S.parseAsync()}catch(e){throw e instanceof b&&u(e.message),e}
24
+ `),m("Redirect URL",""))}));if(process.stdout.write(`${u.report}
25
+ `),u.refreshToken){const m=await se(t,{BISHOP_GMAIL_REFRESH_TOKEN:u.refreshToken});process.stdout.write(`
26
+ Saved BISHOP_GMAIL_REFRESH_TOKEN to ${m}
27
+ `)}u.ok||process.exit(1)}async function Kt(e){const t=process.cwd(),n=process.env.GH_TOKEN?.trim();process.env.GH_TOKEN!==void 0&&!n&&delete process.env.GH_TOKEN,b(t);const c=process.env.GH_TOKEN?.trim(),s=!!c&&!n,i=e.prompt&&process.stdin.isTTY===!0,r=e.print?process.stderr:process.stdout,l=a=>r.write(a),u=a=>Ce({...c?{token:c,tokenIsSaved:s}:{},...i?{login:()=>Ne(Le.map(h=>h.scope),a),confirm:async h=>/^y/i.test(await ne(!0,A=>A(h,"y"),r))}:{},gitConfigured:Ge,gh:Oe(a),...e.temporary?{temporary:!0}:{}}),m=e.temporary&&!c?await $e(u):await u();if(l(`${m.report}
28
+ `),e.print)m.token&&process.stdout.write(`${JSON.stringify({GH_TOKEN:m.token})}
29
+ `);else if(m.token){if(m.source==="gh"){const h=await se(t,{GH_TOKEN:m.token});l(`
30
+ Saved GH_TOKEN to ${h}
31
+ `)}const a=await mt(t,[".env"]);a.length>0&&l(`Added ${a.join(", ")} to .gitignore
32
+ `)}m.ok||process.exit(1)}async function zt(e){const t=process.cwd(),n=await je({config:await $(t),name:e.name});process.stdout.write(`${n.report}
33
+ `),n.config&&process.stdout.write(`Saved ${await Te(n.config,t)}
34
+ `),n.ok||process.exit(1)}async function qt(e){b(process.cwd());const t={...e.name===void 0?{}:{name:e.name},...e.displayName===void 0?{}:{displayName:e.displayName},...e.description===void 0?{}:{description:e.description},prompt:e.prompt};e.manual?await ft(t):await ut(t)}async function jt(e){if(e.appId!==void 0&&!e.manual)throw new oe("--app-id adopts an app somebody else created, so it needs --manual too.");if(e.endpoint!==void 0&&e.manual)throw new oe("--endpoint sets the URL on an app Bishop is creating, so it cannot be used\nwith --manual. Point an adopted app with `bishop teams endpoint <url>`,\nor ask whoever created it to.");b(process.cwd());const t={...e.name===void 0?{}:{name:e.name},...e.displayName===void 0?{}:{displayName:e.displayName},...e.description===void 0?{}:{description:e.description},...e.endpoint===void 0?{}:{endpoint:e.endpoint},...e.appId===void 0?{}:{appId:e.appId},prompt:e.prompt};e.manual?await vt(t):await bt(t)}async function Yt(e){const t=process.cwd(),n=await $(t),c=await ht({name:e.name??n.slack?.app?.name,displayName:e.displayName??n.slack?.app?.displayName,description:e.description??n.slack?.app?.description,prompt:!1},t,async(s,i)=>i);process.stdout.write(`${JSON.stringify(gt(c),null,2)}
35
+ `)}const k=new ue().name("bishop").description("An agent gateway. Runs an agent and connects it to Slack, email, and Teams.").version(Nt,"-v, --version").enablePositionalOptions().option("--agent <path-or-url>","the agent's directory, or a git URL to clone; defaults to the current directory").option(`--harness <${ye.join("|")}>`,"which SDK runs the agent",ke).option("--model <model>","the model to run, in the harness's own naming",Q("--model")).option("--effort <effort>","how hard the model should think, in the harness's own naming",Q("--effort")).option("--auto-update","for a git-URL --agent, check for a new commit at the start of every new thread").allowExcessArguments(!0).action((e,t)=>Mt(t.args,e));k.command("gc").description("Collect the snapshots, worktrees, and rows old conversations left behind").option("--max-age-days <days>",`how long a quiet thread's remains are kept; defaults to gc.maxAgeDays, then ${Ae}`,Se("--max-age-days")).option("--agent <url>","only this agent's snapshots; defaults to every agent Bishop has cloned here").action(Bt);const x=k.command("schedules").description("Prompts the agent runs on a clock, and where their answers go");x.command("list").description("Everything scheduled here, in your own time zone").action(()=>et(process.cwd())),x.command("add").description("Add a schedule").requiredOption("--name <name>","what a person sees in a list, and the subject of an email").requiredOption("--cron <expression>","five-field cron, or @daily and friends").requiredOption("--prompt <text>","what the agent is asked, including when to stay quiet").requiredOption("--to <destination>","#channel or @person for Slack, an address for Gmail").option("--tz <zone>","IANA zone the expression is read in; defaults to this machine").option("--interface <name>","which interface delivers, when --to is ambiguous").action(e=>Ze(process.cwd(),e)),x.command("rm <id>").description("Remove a schedule").action(e=>tt(process.cwd(),e)),x.command("disable <id>").description("Stop a schedule firing, keeping it").action(e=>ee(process.cwd(),e,!1)),x.command("enable <id>").description("Start a disabled schedule firing again, from now").action(e=>ee(process.cwd(),e,!0));const Wt=k.command("gmail").description("Gmail interface commands");Wt.command("setup").description("Authorize a Gmail mailbox, or check the one already configured").option("--no-browser","print a URL to open elsewhere instead of opening one here").action(Ft);const Jt=k.command("github").description("GitHub credentials for the agent");Jt.command("setup").description("Give the agent a GitHub token, or check the one it's using").option("--no-prompt","don't offer to run `gh auth login`").option("--print","write the credential to stdout as JSON instead of to .env").option("--temporary","log in without touching this machine's own gh login").action(Kt);const Qt=k.command("nono").description("The nono sandbox the agent runs under");Qt.command("setup").description("Write a nono profile that lets a sandboxed agent push").option("--name <name>","the profile name",qe).action(zt);const ie=k.command("slack").description("Slack interface commands");ie.command("setup").description("Create and install a Slack app, and save its credentials here").option("--name <name>","app name; defaults to the current directory name").option("--display-name <name>","bot display name in Slack; defaults to --name").option("--description <text>","short description shown in Slack").option("--no-prompt","don't prompt for anything left off").option("--manual","adopt an app you created in Slack yourself, using its tokens").action(qt),ie.command("manifest").description("Print the app manifest, for creating an app in Slack by hand").option("--name <name>","app name; defaults to config or the directory name").option("--display-name <name>","bot display name; defaults to --name").option("--description <text>","short description shown in Slack").action(Yt);const H=k.command("teams").description("Microsoft Teams interface commands");H.command("setup").description("Create a Teams app and save its credentials here, or check the one configured").option("--name <name>","app name; defaults to the current directory name").option("--display-name <name>","app name shown in Teams; defaults to --name").option("--description <text>","short description shown in Teams").option("--endpoint <url>","the URL Teams POSTs to; can be set later").option("--app-id <id>","with --manual, the Teams app ID you were given").option("--no-prompt","don't prompt for anything left off").option("--manual","adopt an app somebody else created, using its credentials").action(jt),H.command("endpoint <url>").description("Point the Teams app at a different URL, changing nothing else").option("--app-id <id>","the Teams app to point; defaults to the one in config").action((e,t)=>St(e,t.appId===void 0?{}:{appId:t.appId})),H.command("instructions").description("Print what to send whoever creates the Teams app, when that is an administrator").option("--endpoint <url>","the URL Teams should POST to, if you already know it").action(e=>{process.stdout.write(kt(e.endpoint))});try{await k.parseAsync()}catch(e){throw e instanceof v&&f(e.message),e}
package/dist/config.js CHANGED
@@ -1,4 +1,4 @@
1
- import{mkdir as h,readFile as b,writeFile as w}from"node:fs/promises";import{dirname as x,join as O}from"node:path";import{z as t}from"zod";import{BishopError as p}from"./errors.js";const y=".bishop",j="config.json",k=t.strictObject({id:t.string().optional(),name:t.string().min(1),displayName:t.string().optional(),description:t.string().optional()}),$=["low","medium","high"],J="high",f=t.string().transform(n=>n.trim().toLowerCase()).pipe(t.enum($)),N=t.strictObject({workspace:t.string().optional(),app:k.optional(),verbosity:f.optional(),allow:t.array(t.string().min(1)).optional(),maxBotTurns:t.number().int().nonnegative().optional()}),v=t.strictObject({allow:t.array(t.string().min(1)).optional(),pollSeconds:t.number().int().positive().optional(),labelPrefix:t.string().trim().min(1).optional(),markRead:t.boolean().optional(),internalDomains:t.array(t.string().trim().toLowerCase().regex(/^[^\s@]+\.[^\s@]+$/,"must be a domain like example.com")).optional()}),E=t.strictObject({cert:t.string().trim().min(1),key:t.string().trim().min(1)}),S=t.strictObject({id:t.string().optional(),name:t.string().min(1),displayName:t.string().optional(),description:t.string().optional()}),C=t.strictObject({app:S.optional(),allow:t.array(t.string().min(1)).optional(),verbosity:f.optional(),port:t.number().int().min(1).max(65535).optional(),host:t.string().trim().min(1).optional(),tls:E.optional()}),a=["claude","codex"],F=t.strictObject({model:t.string().trim().min(1).optional(),effort:t.string().trim().min(1).optional(),options:t.record(t.string(),t.unknown()).optional()}),T=t.strictObject({harness:t.enum(a).optional(),model:t.string().trim().min(1).optional(),effort:t.string().trim().min(1).optional(),harnesses:t.partialRecord(t.enum(a),F,{error:()=>`not a harness Bishop runs; supported: ${a.join(", ")}`}).optional(),worktrees:t.boolean().optional(),sandbox:t.union([t.boolean(),t.strictObject({profile:t.string().trim().min(1).optional()})]).optional(),options:t.record(t.string(),t.unknown()).optional()}),I=t.strictObject({maxAgeDays:t.number().int().positive().optional()}),A=t.strictObject({maxMb:t.number().positive().optional()}),B=t.strictObject({slack:N.optional(),gmail:v.optional(),teams:C.optional(),agent:T.optional(),files:A.optional(),gc:I.optional()});class l extends p{path;constructor(o,r){super(o,"ConfigError"),this.path=r}}function P(n,o={}){const r=n.agent,s=r?.harness??"claude",e=o.harness??s,i=r?.harnesses?.[e],c=[];r?.options&&(i&&e===s?c.push({kind:"options-replaced",harness:e}):!i&&e!==s&&c.push({kind:"options-foreign",harness:e,configured:s}));const m=o.model??i?.model??r?.model,u=o.effort??i?.effort??r?.effort,d=i?i.options:r?.options;return{harness:e,...m?{model:m}:{},...u?{effort:u}:{},...d?{options:d}:{},warnings:c}}function V(n){const o=n.agent?.sandbox;if(o)return o===!0||!o.profile?{}:{profile:o.profile}}function _(n){return o=>{const r=o.trim();if(!r)throw new p(`${n} needs a value`);return r}}function Y(n){return o=>{const r=Number(o.trim());if(!Number.isInteger(r)||r<=0)throw new p(`${n} needs a positive whole number, got "${o}"`);return r}}function z(n){const o=n.trim();if(a.includes(o))return o;throw new p(`unknown harness "${n}"; supported: ${a.join(", ")}`)}function g(n=process.cwd()){return O(n,y,j)}function R(n){return n.issues.map(o=>`${o.path.length>0?o.path.join("."):"(root)"}: ${o.message}`).join(`
2
- `)}async function M(n=process.cwd()){const o=g(n);let r;try{r=await b(o,"utf8")}catch(i){if(i.code==="ENOENT")return{};throw new l(`Couldn't read ${o}: ${i.message}`,o)}let s;try{s=JSON.parse(r)}catch(i){throw new l(`${o} isn't valid JSON: ${i.message}`,o)}const e=B.safeParse(s);if(!e.success)throw new l(`${o} has invalid values:
3
- ${R(e.error)}`,o);return e.data}async function U(n,o=process.cwd()){const r=g(o);return await h(x(r),{recursive:!0}),await w(r,`${JSON.stringify(n,null,2)}
4
- `,"utf8"),r}function q(n){return n.displayName?.trim()||n.name}export{y as CONFIG_DIR,j as CONFIG_FILE,l as ConfigError,B as ConfigSchema,J as DEFAULT_VERBOSITY,a as HARNESSES,$ as VERBOSITY,g as configPath,q as displayNameOf,M as loadConfig,_ as nonEmptyFlag,z as parseHarnessName,Y as positiveIntFlag,P as resolveAgent,V as sandboxConfig,U as writeConfig};
1
+ import{mkdir as b,readFile as w,writeFile as x}from"node:fs/promises";import{dirname as O,join as y}from"node:path";import{z as o}from"zod";import{BishopError as p}from"./errors.js";const v=".bishop",j="config.json",E=o.strictObject({id:o.string().optional(),name:o.string().min(1),displayName:o.string().optional(),description:o.string().optional()}),k=["low","medium","high"],J="high",f=o.string().transform(n=>n.trim().toLowerCase()).pipe(o.enum(k)),$=o.strictObject({workspace:o.string().optional(),app:E.optional(),verbosity:f.optional(),allow:o.array(o.string().min(1)).optional(),maxBotTurns:o.number().int().nonnegative().optional()}),R=o.strictObject({allow:o.array(o.string().min(1)).optional(),pollSeconds:o.number().int().positive().optional(),labelPrefix:o.string().trim().min(1).optional(),markRead:o.boolean().optional(),internalDomains:o.array(o.string().trim().toLowerCase().regex(/^[^\s@]+\.[^\s@]+$/,"must be a domain like example.com")).optional()}),N=o.strictObject({cert:o.string().trim().min(1),key:o.string().trim().min(1)}),I=o.strictObject({id:o.string().optional(),name:o.string().min(1),displayName:o.string().optional(),description:o.string().optional()}),S=o.strictObject({app:I.optional(),allow:o.array(o.string().min(1)).optional(),verbosity:f.optional(),port:o.number().int().min(1).max(65535).optional(),host:o.string().trim().min(1).optional(),tls:N.optional()}),a=["claude","codex"],C=o.strictObject({model:o.string().trim().min(1).optional(),effort:o.string().trim().min(1).optional(),options:o.record(o.string(),o.unknown()).optional()}),A=o.strictObject({harness:o.enum(a).optional(),model:o.string().trim().min(1).optional(),effort:o.string().trim().min(1).optional(),harnesses:o.partialRecord(o.enum(a),C,{error:()=>`not a harness Bishop runs; supported: ${a.join(", ")}`}).optional(),worktrees:o.boolean().optional(),sandbox:o.union([o.boolean(),o.strictObject({profile:o.string().trim().min(1).optional()})]).optional(),options:o.record(o.string(),o.unknown()).optional()}),F=o.strictObject({maxAgeDays:o.number().int().positive().optional()}),T=o.strictObject({maxMb:o.number().positive().optional()}),_=["all","external"],L=["reject","approve"],g=o.strictObject({scope:o.enum(_).optional(),onError:o.enum(L).optional()});function B(n){let t;try{t=new URL(n)}catch{return!1}return t.protocol==="https:"?!0:t.protocol!=="http:"?!1:t.hostname==="localhost"||t.hostname==="[::1]"||/^127(\.\d{1,3}){3}$/.test(t.hostname)}const D=o.strictObject({command:o.array(o.string().min(1)).min(1).optional(),url:o.string().trim().refine(B,"must be an https URL, or http on loopback").optional(),inbound:g.optional(),outbound:g.optional(),timeoutSeconds:o.number().positive().max(300).optional(),env:o.array(o.string().regex(/^[A-Za-z_][A-Za-z0-9_]*$/,"must be an environment variable name")).optional()}).superRefine((n,t)=>{n.command&&n.url?t.addIssue({code:"custom",message:"names both command and url; pick one"}):!n.command&&!n.url&&t.addIssue({code:"custom",message:"needs a command or a url"}),n.url&&n.env&&t.addIssue({code:"custom",path:["env"],message:"is only for a command reviewer"}),!n.inbound&&!n.outbound&&t.addIssue({code:"custom",message:"reviews nothing without inbound or outbound"})}),V=o.strictObject({slack:$.optional(),gmail:R.optional(),teams:S.optional(),agent:A.optional(),files:T.optional(),gc:F.optional(),review:D.optional()}).superRefine((n,t)=>{n.review&&!n.agent?.sandbox&&t.addIssue({code:"custom",path:["review"],message:"needs agent.sandbox on"})});class l extends p{path;constructor(t,e){super(t,"ConfigError"),this.path=e}}function W(n,t={}){const e=n.agent,s=e?.harness??"claude",i=t.harness??s,r=e?.harnesses?.[i],c=[];e?.options&&(r&&i===s?c.push({kind:"options-replaced",harness:i}):!r&&i!==s&&c.push({kind:"options-foreign",harness:i,configured:s}));const m=t.model??r?.model??e?.model,u=t.effort??r?.effort??e?.effort,d=r?r.options:e?.options;return{harness:i,...m?{model:m}:{},...u?{effort:u}:{},...d?{options:d}:{},warnings:c}}function Y(n){const t=n.agent?.sandbox;if(t)return t===!0||!t.profile?{}:{profile:t.profile}}function Z(n){return t=>{const e=t.trim();if(!e)throw new p(`${n} needs a value`);return e}}function M(n){return t=>{const e=Number(t.trim());if(!Number.isInteger(e)||e<=0)throw new p(`${n} needs a positive whole number, got "${t}"`);return e}}function q(n){const t=n.trim();if(a.includes(t))return t;throw new p(`unknown harness "${n}"; supported: ${a.join(", ")}`)}function h(n=process.cwd()){return y(n,v,j)}function G(n){return n.issues.map(t=>`${t.path.length>0?t.path.join("."):"(root)"}: ${t.message}`).join(`
2
+ `)}async function K(n=process.cwd()){const t=h(n);let e;try{e=await w(t,"utf8")}catch(r){if(r.code==="ENOENT")return{};throw new l(`Couldn't read ${t}: ${r.message}`,t)}let s;try{s=JSON.parse(e)}catch(r){throw new l(`${t} isn't valid JSON: ${r.message}`,t)}const i=V.safeParse(s);if(!i.success)throw new l(`${t} has invalid values:
3
+ ${G(i.error)}`,t);return i.data}async function Q(n,t=process.cwd()){const e=h(t);return await b(O(e),{recursive:!0}),await x(e,`${JSON.stringify(n,null,2)}
4
+ `,"utf8"),e}function X(n){return n.displayName?.trim()||n.name}export{v as CONFIG_DIR,j as CONFIG_FILE,l as ConfigError,V as ConfigSchema,J as DEFAULT_VERBOSITY,a as HARNESSES,L as REVIEW_ON_ERROR,_ as REVIEW_SCOPES,k as VERBOSITY,h as configPath,X as displayNameOf,K as loadConfig,Z as nonEmptyFlag,q as parseHarnessName,M as positiveIntFlag,W as resolveAgent,Y as sandboxConfig,Q as writeConfig};
package/dist/exec.js CHANGED
@@ -1 +1 @@
1
- import{spawn as a}from"node:child_process";function l(u,c,e={}){return new Promise(i=>{const r=a(u,c,{stdio:["ignore","pipe","pipe"],...e.cwd?{cwd:e.cwd}:{},...e.env?{env:e.env}:{}});let o="",d="",n="";r.stdout.on("data",t=>{o+=t.toString(),n+=t.toString()}),r.stderr.on("data",t=>{d+=t.toString(),n+=t.toString()}),r.on("error",()=>i({code:null,stdout:o,stderr:d,output:n})),r.on("exit",t=>i({code:t,stdout:o,stderr:d,output:n}))})}export{l as exec};
1
+ import{spawn as g}from"node:child_process";const m=1e3;function w(a,c,r={}){return new Promise(i=>{const t=g(a,c,{stdio:"pipe",...r.cwd?{cwd:r.cwd}:{},...r.env?{env:r.env}:{},...r.argv0?{argv0:r.argv0}:{},...r.signal?{signal:r.signal,killSignal:"SIGKILL"}:{}});t.stdin.on("error",()=>{}),t.stdin.end(r.input);let o="",u="",n="";t.stdout.setEncoding("utf8"),t.stderr.setEncoding("utf8"),t.stdout.on("data",e=>{o+=e,n+=e}),t.stderr.on("data",e=>{u+=e,n+=e});let l;const s=(e,d)=>{clearTimeout(l),i({code:e,stdout:o,stderr:u,output:n,...d?{signal:d}:{}})};t.on("error",()=>{t.stdout.destroy(),t.stderr.destroy(),s(null)}),t.on("exit",(e,d)=>{l=setTimeout(()=>{t.stdout.destroy(),t.stderr.destroy(),s(e,d)},m)}),t.on("close",(e,d)=>s(e,d))})}export{m as DRAIN_MS,w as exec};
package/dist/review.js ADDED
@@ -0,0 +1 @@
1
+ import{constants as $}from"node:fs";import{access as x,realpath as P,stat as E}from"node:fs/promises";import{delimiter as S,join as O,resolve as b}from"node:path";import{z as d}from"zod";import{BishopError as f}from"./errors.js";import{exec as R}from"./exec.js";import{log as v}from"./log.js";import{retry as _}from"./retry.js";import{unwritablePaths as j}from"./sandbox.js";const y="BISHOP_REVIEWER_TOKEN",A=1,I=5,N=d.discriminatedUnion("verdict",[d.object({verdict:d.literal("approve")}),d.object({verdict:d.literal("reject"),reason:d.string().trim().min(1)}),d.object({verdict:d.literal("rewrite"),text:d.string().min(1)})]);class p extends Error{}async function g(e,r){const a=r.env??process.env,i=(e.timeoutSeconds??I)*1e3;let n,o=[];if(e.url){const t=a[y]?.trim();if(!t)throw new f(`review.url needs ${y} in .env, sent as a bearer token`);n=U(e.url,t)}else{const[t,...s]=e.command??[];if(!t)throw new f("review needs a command or a url");const c=await k(t,r.home,a.PATH),l=a.PATH?{PATH:a.PATH}:{};for(const w of e.env??[]){const m=a[w];if(m===void 0)throw new f(`review.env names ${w}, which isn't set`);l[w]=m}const h=await Promise.all(s.map(async w=>{const m=b(r.home,w);return await E(m).catch(()=>{})?m:void 0}));o=[c,...h.filter(w=>w!==void 0)].flatMap(j),n=M(c,t,s,r.home,l)}const u={covers:(t,s)=>{const c=e[t];return c?(c.scope??"all")==="all"||s!=="internal":!1},review:async t=>{const s=e[t.direction],c=JSON.stringify({version:A,...t});try{const l=await n(c,AbortSignal.timeout(i)),h=N.safeParse(l);if(!h.success)throw new Error("answered something that isn't a verdict");return h.data}catch(l){return H(t,s,V(l,i))}}};return v.info({reviewer:e.url?new URL(e.url).origin:e.command?.[0],...e.inbound?{inbound:e.inbound.scope??"all"}:{},...e.outbound?{outbound:e.outbound.scope??"all"}:{},timeoutMs:i},"message reviewer found"),{reviewer:u,unwritable:o}}function H(e,r,a){const i={direction:e.direction,thread:e.thread,error:a};return r?.onError==="approve"?(v.warn(i,"the reviewer failed, and onError approves the message unreviewed"),{verdict:"approve",error:a}):(v.warn(i,"the reviewer failed, so the message is rejected"),{verdict:"reject",reason:"The reviewer didn't answer.",error:a})}function V(e,r){return e instanceof Error&&(e.name==="TimeoutError"||e.name==="AbortError")?`no answer within ${r/1e3}s`:e instanceof Error?e.message:String(e)}async function k(e,r,a){const i=e.includes("/")?[b(r,e)]:(a??"").split(S).filter(Boolean).map(n=>O(n,e));for(const n of i){const o=await B(n);if(o)return o}throw new f(`review.command names ${e}, which isn't an executable program`)}async function B(e){try{const r=await P(e);return(await E(r)).isFile()?(await x(r,$.X_OK),r):void 0}catch{return}}function M(e,r,a,i,n){return async(o,u)=>{const t=await R(e,a,{cwd:i,env:n,input:o,signal:u,argv0:r});u.throwIfAborted();const s=t.stderr.trim().slice(-500),c=s?`: ${s}`:"";if(t.signal)throw new Error(`${e} was killed by ${t.signal}${c}`);if(t.code===null)throw new Error(`${e} couldn't be started`);if(t.code!==0)throw new Error(`${e} exited with code ${t.code}${c}`);return T(t.stdout)}}function U(e,r){const a=async(i,n)=>{let o;try{o=await fetch(e,{method:"POST",headers:{authorization:`Bearer ${r}`,"content-type":"application/json"},body:i,signal:n,redirect:"error"})}catch(t){if(n.aborted)throw n.reason;const s=t.cause,c=s instanceof Error?s.message:t.message;throw new p(`couldn't reach the reviewer: ${c}`)}const u=await o.text();if(o.status>=500)throw new p(`the reviewer answered HTTP ${o.status}`);if(!o.ok)throw new Error(`the reviewer answered HTTP ${o.status}`);return T(u)};return(i,n)=>_(()=>a(i,n),{what:"message review",attempts:2,baseDelayMs:100,retryable:o=>o instanceof p&&!n.aborted})}function T(e){try{return JSON.parse(e)}catch{throw new Error("answered something that isn't JSON")}}export{I as DEFAULT_REVIEW_TIMEOUT_SECONDS,y as REVIEWER_TOKEN_ENV,A as REVIEW_VERSION,g as startReviewer};
package/dist/sandbox.js CHANGED
@@ -1,11 +1,11 @@
1
- import{randomBytes as D}from"node:crypto";import{constants as S,createWriteStream as R,existsSync as O,realpathSync as C}from"node:fs";import{chmod as M,mkdir as g,open as H,realpath as I,rm as L,stat as T,writeFile as k}from"node:fs/promises";import{homedir as E,platform as P,tmpdir as W}from"node:os";import{basename as N,dirname as _,isAbsolute as v,join as d,resolve as f,sep as A}from"node:path";import{pipeline as z}from"node:stream/promises";import{CONFIG_DIR as b}from"./config.js";import{BishopError as l}from"./errors.js";import{exec as $}from"./exec.js";import{ensureThreadFilesDir as K}from"./files.js";import{MAX_SHARE_BYTES as U,ShareRefused as u}from"./outgoing.js";import{gitDirs as X}from"./worktrees.js";const w="nono",j=["linux","darwin"];async function ct(t,e=P()){if(!j.includes(e))throw new l(`agent.sandbox is on, but nono can't sandbox on ${e}. It runs on Linux and macOS.`);const o=await $(w,["--version"]);if(o.code!==0)throw new l("agent.sandbox is on, but nono is not installed or not on PATH. See https://nono.sh/docs/cli/getting_started/installation for how to install it.");const n=await $(w,["setup","--check-only"]);if(n.code!==0)throw new l(`agent.sandbox is on, but nono says this system can't enforce its sandbox:
2
- ${n.output.trim()}`);if(t.profile){const r=await $(w,["profile","show",t.profile]);if(r.code!==0)throw new l(`agent.sandbox.profile names "${t.profile}", which nono can't load:
3
- ${r.output.trim()}`)}return o.stdout.trim()}function V(t){return[f(t,b),f(t,".env")]}function m(t){const e=f(t);try{return C(e)}catch{const o=_(e);return o===e?e:d(m(o),N(e))}}function F(t,e){const o=m(t),n=m(e);return n===o||n.startsWith(o.endsWith(A)?o:o+A)}function B(t,e){for(const o of t){const n=e.find(r=>F(o,r));if(n)throw new l(`The sandbox would have to grant ${o}, which contains ${n}. Bishop's own state can't sit inside the agent's directory with agent.sandbox on: run Bishop from a directory of its own and point --agent at the agent.`)}}async function dt(t){const e=t.userHome??E(),o=[f(t.cwd),...await X(t.cwd),t.tmp??W()],n=[d(e,".config","git")].filter(O);t.dirKey&&n.push(await K(t.home,t.dirKey));const r=[d(e,".gitconfig")].filter(O),i=V(t.home);return B([...o,...n,...r],i),{allow:o,read:n,readFiles:r,...t.profile?{profile:t.profile}:{},protect:i}}function ft(t,e,o,n){const r=h([...t.allow,...e.allow??[]]),i=h([...t.read,...e.read??[]]),s=h(e.allowFiles??[]),c=h(t.readFiles);return B([...r,...i,...s,...c],t.protect),{command:w,args:["-s","wrap",...t.profile?["--profile",t.profile]:[],...r.flatMap(a=>["--allow",a]),...i.flatMap(a=>["--read",a]),...s.flatMap(a=>["--allow-file",a]),...c.flatMap(a=>["--read-file",a]),"--",o,...n]}}function h(t){return[...new Set(t)]}function Y(t,e){return[...t.allow,...t.read].some(o=>F(o,e))}async function lt(t){const e=f(t,b,"outgoing");return await L(e,{recursive:!0,force:!0}),await g(e,{recursive:!0,mode:448}),e}async function ut(t,e,o){if(!v(e))return e;const n=new u(`${e} is outside the directories you can work in, so nothing was sent. Copy it into your working directory first.`),r=await H(e,S.O_RDONLY|S.O_NONBLOCK).catch(()=>{throw new u(`There is no file at ${e}.`)});try{const i=await r.stat();if(!i.isFile())throw new u(`There is no file at ${e}.`);const s=await I(e).catch(()=>{}),c=s?await T(s).catch(()=>{}):void 0;if(!s||!c||c.ino!==i.ino||c.dev!==i.dev||!Y(t,s))throw n;if(i.size>U)throw new u(`${e} is too large to send.`);const a=d(o,D(6).toString("hex"));await g(a,{recursive:!0,mode:448});const y=d(a,N(e));return await z(r.createReadStream({autoClose:!1}),R(y,{mode:384})),y}finally{await r.close()}}function wt(t){return v(t)?_(m(t)):void 0}const p="BISHOP_SANDBOX_COMMAND",x="BISHOP_SANDBOX_DIR",q=`#!/bin/sh
4
- # Written by Bishop at startup. Runs the command in ${p}, one
1
+ import{randomBytes as R}from"node:crypto";import{constants as N,createWriteStream as C,existsSync as _,realpathSync as M}from"node:fs";import{chmod as H,mkdir as b,open as T,realpath as I,rm as L,stat as P,writeFile as k}from"node:fs/promises";import{homedir as E,platform as W,tmpdir as U}from"node:os";import{basename as $,dirname as y,isAbsolute as A,join as d,resolve as f,sep as h}from"node:path";import{pipeline as z}from"node:stream/promises";import{CONFIG_DIR as S}from"./config.js";import{BishopError as l}from"./errors.js";import{exec as v}from"./exec.js";import{ensureThreadFilesDir as K}from"./files.js";import{MAX_SHARE_BYTES as X,ShareRefused as m}from"./outgoing.js";import{gitDirs as j}from"./worktrees.js";const p="nono",V=["linux","darwin"];async function dt(t,e=W()){if(!V.includes(e))throw new l(`agent.sandbox is on, but nono can't sandbox on ${e}. It runs on Linux and macOS.`);const o=await v(p,["--version"]);if(o.code!==0)throw new l("agent.sandbox is on, but nono is not installed or not on PATH. See https://nono.sh/docs/cli/getting_started/installation for how to install it.");const n=await v(p,["setup","--check-only"]);if(n.code!==0)throw new l(`agent.sandbox is on, but nono says this system can't enforce its sandbox:
2
+ ${n.output.trim()}`);if(t.profile){const r=await v(p,["profile","show",t.profile]);if(r.code!==0)throw new l(`agent.sandbox.profile names "${t.profile}", which nono can't load:
3
+ ${r.output.trim()}`)}return o.stdout.trim()}function Y(t){return[f(t,S),f(t,".env")]}function u(t){const e=f(t);try{return M(e)}catch{const o=y(e);return o===e?e:d(u(o),$(e))}}function F(t,e){const o=u(t),n=u(e);return n===o||n.startsWith(o.endsWith(h)?o:o+h)}function ft(t){const e=f(t);return w([u(e),d(u(y(e)),$(e))])}function B(t,e){for(const o of t){const n=u(o),r=e.find(a=>{const i=(c,s)=>c.startsWith(s.endsWith(h)?s:s+h);return a===n||i(a,n)||i(n,a)});if(r)throw new l(`The sandbox would let the agent write ${o}, which contains ${r}. An agent that can change its message reviewer can approve itself: move the reviewer, and anything it reads, outside every directory the agent can write.`)}}function D(t,e){for(const o of t){const n=e.find(r=>F(o,r));if(n)throw new l(`The sandbox would have to grant ${o}, which contains ${n}. Bishop's own state can't sit inside the agent's directory with agent.sandbox on: run Bishop from a directory of its own and point --agent at the agent.`)}}async function lt(t){const e=t.userHome??E(),o=[f(t.cwd),...await j(t.cwd),t.tmp??U()],n=[d(e,".config","git")].filter(_);t.dirKey&&n.push(await K(t.home,t.dirKey));const r=[d(e,".gitconfig")].filter(_),a=Y(t.home);D([...o,...n,...r],a);const i=t.unwritable??[];return B(o,i),{allow:o,read:n,readFiles:r,...t.profile?{profile:t.profile}:{},protect:a,...i.length>0?{unwritable:i}:{}}}function ut(t,e,o,n){const r=w([...t.allow,...e.allow??[]]),a=w([...t.read,...e.read??[]]),i=w(e.allowFiles??[]),c=w(t.readFiles);return D([...r,...a,...i,...c],t.protect),B([...r,...i],t.unwritable??[]),{command:p,args:["-s","wrap",...t.profile?["--profile",t.profile]:[],...r.flatMap(s=>["--allow",s]),...a.flatMap(s=>["--read",s]),...i.flatMap(s=>["--allow-file",s]),...c.flatMap(s=>["--read-file",s]),"--",o,...n]}}function w(t){return[...new Set(t)]}function q(t,e){return[...t.allow,...t.read].some(o=>F(o,e))}async function wt(t){const e=f(t,S,"outgoing");return await L(e,{recursive:!0,force:!0}),await b(e,{recursive:!0,mode:448}),e}async function ht(t,e,o){if(!A(e))return e;const n=new m(`${e} is outside the directories you can work in, so nothing was sent. Copy it into your working directory first.`),r=await T(e,N.O_RDONLY|N.O_NONBLOCK).catch(()=>{throw new m(`There is no file at ${e}.`)});try{const a=await r.stat();if(!a.isFile())throw new m(`There is no file at ${e}.`);const i=await I(e).catch(()=>{}),c=i?await P(i).catch(()=>{}):void 0;if(!i||!c||c.ino!==a.ino||c.dev!==a.dev||!q(t,i))throw n;if(a.size>X)throw new m(`${e} is too large to send.`);const s=d(o,R(6).toString("hex"));await b(s,{recursive:!0,mode:448});const O=d(s,$(e));return await z(r.createReadStream({autoClose:!1}),C(O,{mode:384})),O}finally{await r.close()}}function mt(t){return A(t)?y(u(t)):void 0}const g="BISHOP_SANDBOX_COMMAND",x="BISHOP_SANDBOX_DIR",G=`#!/bin/sh
4
+ # Written by Bishop at startup. Runs the command in ${g}, one
5
5
  # argument per line, from ${x}, followed by this script's arguments.
6
- command=$${p}
6
+ command=$${g}
7
7
  dir=$${x}
8
- unset ${p} ${x}
8
+ unset ${g} ${x}
9
9
  if [ -z "$command" ] || [ -z "$dir" ]; then
10
10
  echo "bishop: the sandbox launcher was started without a command" >&2
11
11
  exit 1
@@ -15,6 +15,6 @@ set -f
15
15
  IFS='
16
16
  '
17
17
  exec $command "$@"
18
- `;async function mt(t){const e=f(t,b,"sandbox");await g(e,{recursive:!0,mode:448});const o=d(e,"launch");return await k(o,q,{mode:448}),await M(o,448),o}function ht(t,e){const o=[t.command,...t.args],n=[...o,e].find(r=>r===""||r.includes(`
19
- `));if(n!==void 0)throw new l(`The sandbox can't pass ${JSON.stringify(n)} to its launcher, which takes one argument per line.`);return{[p]:o.join(`
20
- `),[x]:e}}export{p as LAUNCH_COMMAND_ENV,x as LAUNCH_DIR_ENV,w as NONO,ct as checkSandbox,lt as clearedStaging,wt as executableDir,ht as launchEnv,ft as nonoCommand,V as protectedPaths,dt as sandboxPolicy,Y as sandboxReadable,ut as stageFile,mt as writeLauncher};
18
+ `;async function pt(t){const e=f(t,S,"sandbox");await b(e,{recursive:!0,mode:448});const o=d(e,"launch");return await k(o,G,{mode:448}),await H(o,448),o}function gt(t,e){const o=[t.command,...t.args],n=[...o,e].find(r=>r===""||r.includes(`
19
+ `));if(n!==void 0)throw new l(`The sandbox can't pass ${JSON.stringify(n)} to its launcher, which takes one argument per line.`);return{[g]:o.join(`
20
+ `),[x]:e}}export{g as LAUNCH_COMMAND_ENV,x as LAUNCH_DIR_ENV,p as NONO,dt as checkSandbox,wt as clearedStaging,mt as executableDir,gt as launchEnv,ut as nonoCommand,Y as protectedPaths,lt as sandboxPolicy,q as sandboxReadable,ht as stageFile,ft as unwritablePaths,pt as writeLauncher};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agentdeploymentco/bishop",
3
- "version": "0.5.1",
3
+ "version": "0.5.3",
4
4
  "description": "An agent gateway. Connects an agent to Slack, email, and Microsoft Teams.",
5
5
  "license": "UNLICENSED",
6
6
  "type": "module",
@@ -45,10 +45,10 @@
45
45
  "docs:preview": "npm --prefix docs run preview --"
46
46
  },
47
47
  "dependencies": {
48
- "@anthropic-ai/claude-agent-sdk": "0.3.287",
48
+ "@anthropic-ai/claude-agent-sdk": "0.3.292",
49
49
  "@microsoft/teams.apps": "^2.0.16",
50
50
  "@modelcontextprotocol/sdk": "^1.30.0",
51
- "@openai/codex-sdk": "0.160.0",
51
+ "@openai/codex-sdk": "0.160.1",
52
52
  "@slack/bolt": "^5.0.0",
53
53
  "@slack/web-api": "^8",
54
54
  "commander": "^13",