@agentdeploymentco/bishop 0.7.1 → 0.8.1

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,20 +1,20 @@
1
1
  #!/usr/bin/env node
2
- import{stat as me}from"node:fs/promises";import{join as le,resolve as he}from"node:path";import{Command as ue}from"commander";import{agentSnapshots as fe,fixedAgentDir as we}from"./agent-dir.js";import{looksRemote as q,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 Se,positiveIntFlag as ke,resolveAgent as be,sandboxConfig as ve,writeConfig as Te}from"./config.js";import{dbPath as V,openDb as X}from"./db.js";import{describeMissing as Ie,loadEnv as b,readCredentials as Ee}from"./env.js";import{BishopError as v}from"./errors.js";import{fileStore as xe}from"./files.js";import{DEFAULT_MAX_AGE_DAYS as Ae,makeCollector as _e,sweep as Re}from"./gc.js";import{ghAuthLogin as Ne,ghCliIn as Oe,gitAuthenticatesToGithub as Ge,withTemporaryGhConfig as $e}from"./github/gh.js";import{AGENT_SCOPES as Le,checkGithubSetup as Ce}from"./github/setup.js";import{createGmail as He}from"./gmail/create.js";import{checkGmailSetup as Pe}from"./gmail/setup.js";import{createHarness as De}from"./harness/create.js";import{sharedFileFetcher as Me}from"./interface/attachments.js";import{principalCheck as Z}from"./interface/authorize.js";import{deliveryRouter as Be}from"./interface/delivery.js";import{conversationMatch as Ue,destinationResolver as Fe,directDestination as Ke}from"./interface/destination.js";import{messageReader as ze}from"./interface/messages.js";import{peopleFinder as je}from"./interface/people.js";import{log as d,requestedLevel as qe}from"./log.js";import{DEFAULT_PROFILE as Ye,setupNono as We}from"./nono-setup.js";import{principalStore as Je}from"./principals.js";import{startReviewer as Qe}from"./review.js";import{checkSandbox as Ve,clearedStaging as Xe,sandboxPolicy as Ze,writeLauncher as et}from"./sandbox.js";import{addSchedule as tt,listSchedules as ot,removeSchedule as nt,setScheduleEnabled as ee}from"./schedule/cli.js";import{authorizedToRun as st,makeScheduler as it}from"./schedule/runner.js";import{scheduleStore as rt}from"./schedule/store.js";import{threadQueue as at}from"./sessions/queue.js";import{interfaceState as ct,retentionStore as te,sessionStore as pt,threadContexts as dt,threadHandover as mt,threadMeta as lt}from"./sessions/store.js";import{ensureGitignore as ht,openBrowser as ut,resolveAppDetails as ft,SetupError as oe,setupSlack as wt,setupSlackManually as gt,withPrompt as ne,writeEnv as se}from"./setup.js";import{createSlackInterface as yt}from"./slack/interface.js";import{buildManifest as St}from"./slack/manifest.js";import{createTeams as kt}from"./teams/create.js";import{administratorInstructions as bt,repointTeamsEndpoint as vt,setupTeams as Tt,setupTeamsManually as It}from"./teams/setup.js";import{threadModel as Et}from"./thread-model.js";import{fileTools as xt}from"./tools/files.js";import{messageTools as At}from"./tools/messages.js";import{peopleTools as _t}from"./tools/people.js";import{scheduleTools as Rt}from"./tools/schedules.js";import{startToolServer as Nt}from"./tools/server.js";import{transcriptStore as Ot}from"./transcript.js";import{makeTurnRunner as Gt}from"./turn.js";import{version as $t}from"./version.js";import{hasGitIdentity as Lt,isGitRepo as Ct,sharedDirNotice as Ht,WORKTREE_DIR as Pt}from"./worktrees.js";function f(e){process.stderr.write(`${e}
3
- `),process.exit(1)}async function Dt(e){const t=he(e??".");let n;try{n=(await me(t)).isDirectory()}catch{throw new v(`--agent directory doesn't exist: ${t}`)}if(!n)throw new v(`--agent is not a directory: ${t}`);return t}async function Mt(e){const t=await Ct(e),n={enabled:!0,isRepo:t};return t?(d.info({agentDir:e,worktrees:le(e,Pt)},"worktree mode is on"),await Lt(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 Bt(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 Ut(e=[],t={}){e.length>0&&f(`bishop: unknown command "${e[0]}"
2
+ import{stat as le}from"node:fs/promises";import{join as he,resolve as ue}from"node:path";import{Command as fe}from"commander";import{agentSnapshots as we,fixedAgentDir as ge}from"./agent-dir.js";import{looksRemote as Y,parseRepoSpec as W}from"./agent-source.js";import{everyStore as J,storePath as Q}from"./agent-store.js";import{attachmentStore as ye}from"./attachments.js";import{HARNESSES as ke,loadConfig as $,nonEmptyFlag as V,parseHarnessName as Se,positiveIntFlag as ve,resolveAgent as be,sandboxConfig as Te,writeConfig as Ie}from"./config.js";import{dbPath as X,openDb as Z}from"./db.js";import{describeMissing as Ee,loadEnv as T,readCredentials as Ae}from"./env.js";import{BishopError as I}from"./errors.js";import{fileStore as xe}from"./files.js";import{DEFAULT_MAX_AGE_DAYS as _e,makeCollector as Re,sweep as Ne}from"./gc.js";import{ghAuthLogin as Oe,ghCliIn as Ge,gitAuthenticatesToGithub as $e,withTemporaryGhConfig as Le}from"./github/gh.js";import{AGENT_SCOPES as Ce,checkGithubSetup as He}from"./github/setup.js";import{createGmail as Pe}from"./gmail/create.js";import{checkGmailSetup as De}from"./gmail/setup.js";import{createHarness as Me}from"./harness/create.js";import{sharedFileFetcher as Be}from"./interface/attachments.js";import{principalCheck as ee}from"./interface/authorize.js";import{deliveryRouter as Ue}from"./interface/delivery.js";import{conversationMatch as Fe,destinationResolver as Ke,directDestination as ze}from"./interface/destination.js";import{messageReader as je}from"./interface/messages.js";import{peopleFinder as qe}from"./interface/people.js";import{log as p,requestedLevel as Ye}from"./log.js";import{DEFAULT_PROFILE as We,setupNono as Je}from"./nono-setup.js";import{principalStore as Qe}from"./principals.js";import{startReviewer as Ve}from"./review.js";import{checkSandbox as Xe,clearedStaging as Ze,sandboxPolicy as et,writeLauncher as tt}from"./sandbox.js";import{addSchedule as ot,listSchedules as nt,removeSchedule as it,setScheduleEnabled as te}from"./schedule/cli.js";import{authorizedToRun as st,makeScheduler as rt}from"./schedule/runner.js";import{scheduleStore as at}from"./schedule/store.js";import{threadQueue as ct}from"./sessions/queue.js";import{interfaceState as pt,retentionStore as oe,sessionStore as dt,threadContexts as mt,threadHandover as lt,threadMeta as ht}from"./sessions/store.js";import{ensureGitignore as ut,openBrowser as ft,resolveAppDetails as wt,SetupError as ne,setupSlack as gt,setupSlackManually as yt,withPrompt as ie,writeEnv as se}from"./setup.js";import{createSlackInterface as kt}from"./slack/interface.js";import{buildManifest as St}from"./slack/manifest.js";import{createTeams as vt}from"./teams/create.js";import{administratorInstructions as bt,repointTeamsEndpoint as Tt,setupTeams as It,setupTeamsManually as Et}from"./teams/setup.js";import{threadModel as At}from"./thread-model.js";import{fileTools as xt}from"./tools/files.js";import{messageTools as _t}from"./tools/messages.js";import{peopleTools as Rt}from"./tools/people.js";import{scheduleTools as Nt}from"./tools/schedules.js";import{startToolServer as Ot}from"./tools/server.js";import{transcriptStore as Gt}from"./transcript.js";import{makeTurnRunner as $t}from"./turn.js";import{version as Lt}from"./version.js";import{hasGitIdentity as Ct,isGitRepo as Ht,sharedDirNotice as Pt,WORKTREE_DIR as Dt}from"./worktrees.js";function f(e){process.stderr.write(`${e}
3
+ `),process.exit(1)}async function Mt(e){const t=ue(e??".");let n;try{n=(await le(t)).isDirectory()}catch{throw new I(`--agent directory doesn't exist: ${t}`)}if(!n)throw new I(`--agent is not a directory: ${t}`);return t}async function Bt(e){const t=await Ht(e),n={enabled:!0,isRepo:t};return t?(p.info({agentDir:e,worktrees:he(e,Dt)},"worktree mode is on"),await Ct(e)||p.warn({agentDir:e},"git has no user.name or user.email here, so the agent cannot commit what it does in a worktree"),n):(p.warn({agentDir:e},"worktree mode is on but the agent directory is not a git repository; threads share it"),n)}function Ut(e){if(e.kind==="options-replaced"){p.warn({harness:e.harness},`agent.harnesses.${e.harness} replaces agent.options, which is ignored for this run`);return}p.warn({harness:e.harness,configured:e.configured},`--harness overrides the configured harness, but agent.options is still written for that one; move it to agent.harnesses.${e.configured} and give ${e.harness} its own block`)}async function Ft(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&&q(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)Bt(o);const l=ve(s);if(l){const o=await Ve(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 Qe(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)=>Ze({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=pt(a,r.harness);let A;const re=async o=>await A?.check(o,await m?.(o))??{ok:!0},_=c?fe({spec:c,store:J(n,c.slug),dirs:h,autoUpdate:!!t.autoUpdate,recheck:re}):void 0,y=_?await _.start():await Dt(t.agent),ae=_?_.forTurn:we(y,h);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 Mt(y):void 0,T=De(r.harness,{...r.options?{options:r.options}:{},...r.model?{model:r.model}:{},...r.effort?{effort:r.effort}:{},...R?{worktrees:R}:{},...s.agent?.projectSettings===!1?{projectSettings:!1}:{},...l?{launcher:await et(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,k)(o):st(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,D=xe({home:n,...s.files?.maxMb===void 0?{}:{maxMb:s.files.maxMb}}),M=ge(a),k=Je(a),B=rt(a),U=await Nt({tools:[...Rt({schedules:B,authorized:ce,resolve:pe,sameConversation:de,principals:k}),...xt({fetch:Me({interfaces:()=>w??[],store:M,files:D})}),...i.credentials.slack?At({read:ze({interfaces:()=>w??[]})}):[],...i.credentials.teams?_t({find:je({interfaces:()=>w??[]})}):[]]}),O=lt(a),C=Et(O),F={...L?{model:L}:{},...r.effort?{effort:r.effort}:{}},K=Gt({harness:T,cwd:ae,sessions:h,model:C,tools:U,contexts:dt(a,O),transcript:Ot(a),principals:k,...R&&!R.isRepo?{sharedDirNotice:Ht(y)}:{},...m?{sandbox:{policy:(o,p)=>m(o,p.dirKey),staging:await Xe(n)}}:{}}),z=at(),I={threads:h,events:h,files:D,attachments:M,principals:k,run:o=>z.run(o.thread.threadKey,()=>K.run(o)),stop:o=>K.stop(o)},g=[];if(i.credentials.slack&&g.push(await yt({...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:ct(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}:
5
+ Run \`bishop --help\` to see what's available.`);const n=process.cwd(),c=t.agent&&Y(t.agent)?W(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."),T(n);const i=await $(n),r=Ae();r.ok||f(Ee(r));const s=be(i,t);for(const o of s.warnings)Ut(o);const l=Te(i);if(l){const o=await Xe(l);p.info({nono:o,...l.profile?{profile:l.profile}:{}},"sandbox is on"),l.profile||p.warn("sandbox is enabled but agent.sandbox.profile is missing; run `bishop nono setup`")}const u=i.review?await Ve(i.review,{home:n}):void 0;u&&p.warn("review is configured, but this version only checks the reviewer at startup; no message is reviewed yet (#254)");const m=l?(o,d)=>et({home:n,cwd:o,...d?{dirKey:d}:{},...l.profile?{profile:l.profile}:{},...u?.unwritable.length?{unwritable:u.unwritable}:{}}):void 0,a=Z(X(n)),h=dt(a,s.harness);let x;const ae=async o=>await x?.check(o,await m?.(o))??{ok:!0},_=c?we({spec:c,store:Q(n,c.slug),dirs:h,autoUpdate:!!t.autoUpdate,recheck:ae}):void 0,y=_?await _.start():await Mt(t.agent),ce=_?_.forTurn:ge(y,h);p.info({bishopDir:n,agentDir:y,...y===n?{}:{separate:!0},...c?{source:c.cloneUrl,autoUpdate:!!t.autoUpdate}:{},app:i.slack?.app?.name},"bishop starting");const R=i.agent?.worktrees?await Bt(y):void 0,S=Me(s.harness,{...s.options?{options:s.options}:{},...s.model?{model:s.model}:{},...s.effort?{effort:s.effort}:{},...R?{worktrees:R}:{},...i.agent?.projectSettings===!1?{projectSettings:!1}:{},...l?{launcher:await tt(n)}:{},...s.keepAlive!==void 0?{keepAlive:s.keepAlive}:{},credentials:r.credentials}),N=await S.check(y,await m?.(y));N.ok||f(S.describeStartupFailure(y,N.error));const L=N.model??s.model;p.info({harness:S.name,credentials:N.credentialSource??"unreported",model:L,...s.effort?{effort:s.effort}:{},...s.options?{agentOptions:Object.keys(s.options)}:{},keepAlive:!!S.keepAlive},"harness ready"),x=S,p.info({sessions:h.count()},"opened session store");const D=Re({retention:oe(a),stores:()=>J(n),home:n,...i.gc?.maxAgeDays===void 0?{}:{maxAgeDays:i.gc.maxAgeDays}});D.start();let w,C;const pe=o=>w?ee(w,v)(o):st(o),de=async(o,d)=>{if(!w)throw new I("no interface is running, so there is nowhere to send");return Ke(w,{optedInOnly:!0})(o,d)},me=(o,d)=>w?Fe(w)(o,d):!1,M=xe({home:n,...i.files?.maxMb===void 0?{}:{maxMb:i.files.maxMb}}),B=ye(a),v=Qe(a),U=at(a),F=await Ot({tools:[...Nt({schedules:U,authorized:pe,resolve:de,sameConversation:me,principals:v}),...xt({fetch:Be({interfaces:()=>w??[],store:B,files:M})}),...r.credentials.slack?_t({read:je({interfaces:()=>w??[]})}):[],...r.credentials.teams?Rt({find:qe({interfaces:()=>w??[]})}):[]]}),O=ht(a),H=At(O),K={...L?{model:L}:{},...s.effort?{effort:s.effort}:{}},z=$t({harness:S,cwd:ce,sessions:h,model:H,tools:F,contexts:mt(a,O),transcript:Gt(a),principals:v,deliver:()=>C,...R&&!R.isRepo?{sharedDirNotice:Pt(y)}:{},...m?{sandbox:{policy:(o,d)=>m(o,d.dirKey),staging:await Ze(n)}}:{}}),j=ct(),E={threads:h,events:h,files:M,attachments:B,principals:v,run:o=>j.run(o.thread.threadKey,()=>z.run(o)),stop:o=>z.stop(o)},g=[];if(r.credentials.slack&&g.push(await kt({...E,credentials:r.credentials.slack,meta:O,model:H,configuredModel:K,...i.slack?.verbosity?{verbosity:i.slack.verbosity}:{},...i.slack?.allow?{allow:i.slack.allow}:{},...i.slack?.maxBotTurns===void 0?{}:{maxBotTurns:i.slack.maxBotTurns}})),r.credentials.gmail)try{g.push(await Pe({...E,credentials:r.credentials.gmail,...i.gmail?{config:i.gmail}:{},state:pt(a)}))}catch(o){o instanceof I&&f(o.message);const d=r.credentials.gmail,G=d.kind==="oauth"?"the authorized Gmail account":`${d.user}`;f(`Couldn't reach the mailbox for ${G}:
6
6
  ${o instanceof Error?o.message:String(o)}
7
7
 
8
- Run \`bishop gmail setup\` to check the credentials.`)}if(i.credentials.teams)try{g.push(await kt({...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}:
8
+ Run \`bishop gmail setup\` to check the credentials.`)}if(r.credentials.teams)try{g.push(await vt({...E,credentials:r.credentials.teams,meta:O,model:H,configuredModel:K,...i.teams?{config:i.teams}:{}}))}catch(o){o instanceof I&&f(o.message),f(`Couldn't start the Teams app for client ${r.credentials.teams.clientId}:
9
9
  ${o instanceof Error?o.message:String(o)}
10
10
 
11
- Check the credentials in .env and the \`teams\` block in .bishop/config.json.`)}w=g;for(const o of g)await o.start();d.info({interfaces:g.map(o=>o.name)},"bishop is listening");const j=it({schedules:B,run:I.run,threads:h,cancel:I.stop,deliver:Be(g),authorized:Z(g,k),reach:Ke(g,k),principals:k,attach:mt(h)});j.start();for(const o of["SIGINT","SIGTERM"])process.once(o,()=>{d.info({signal:o,inFlight:z.size},"shutting down"),j.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 Ft(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,C=Ue(g);for(const o of g)await o.start();p.info({interfaces:g.map(o=>o.name)},"bishop is listening");const q=rt({schedules:U,run:E.run,threads:h,cancel:E.stop,deliver:C,authorized:ee(g,v),reach:ze(g,v),principals:v,attach:lt(h)});q.start();for(const o of["SIGINT","SIGTERM"])process.once(o,()=>{p.info({signal:o,inFlight:j.size},"shutting down"),q.stop().catch(d=>p.error({err:d},"the scheduler failed to stop cleanly")).then(()=>Promise.allSettled(g.map(d=>d.stop()))).then(async d=>{const G=d.filter(b=>b.status==="rejected");for(const b of G)p.error({err:b.reason},"an interface failed to stop cleanly");await S.keepAlive?.close().catch(b=>{p.error({err:b},"the processes kept alive failed to close cleanly")}),await F.stop().catch(b=>{p.error({err:b},"the tool endpoint failed to stop cleanly")}),await D.stop(),a.close(),process.exit(G.length>0?1:0)})})}function Kt(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 Kt(e){const t=process.cwd();b(t),e.agent&&!q(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;qe()||(d.level="error");const i=X(V(t));try{Ft(await Re({retention:te(i),stores:n,home:t,...e.agent?{only:n}:{},...s===void 0?{}:{maxAgeDays:s}}))}finally{i.close()}}function zt(){return!(process.env.SSH_CONNECTION||process.env.SSH_TTY||process.env.SSH_CLIENT||process.platform==="linux"&&!process.env.DISPLAY&&!process.env.WAYLAND_DISPLAY)}async function jt(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||!zt()?"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:ut,readPaste:async a=>(process.stdout.write(`Open this in a browser and approve it:
17
+ `)}async function zt(e){const t=process.cwd();T(t),e.agent&&!Y(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?[Q(t,W(e.agent).slug)]:await J(t),c=await $(t),i=e.maxAgeDays??c.gc?.maxAgeDays;Ye()||(p.level="error");const r=Z(X(t));try{Kt(await Ne({retention:oe(r),stores:n,home:t,...e.agent?{only:n}:{},...i===void 0?{}:{maxAgeDays:i}}))}finally{r.close()}}function jt(){return!(process.env.SSH_CONNECTION||process.env.SSH_TTY||process.env.SSH_CLIENT||process.platform==="linux"&&!process.env.DISPLAY&&!process.env.WAYLAND_DISPLAY)}async function qt(e={}){const t=process.cwd();T(t);const n=process.env.BISHOP_GMAIL_CLIENT_ID?.trim(),c=process.env.BISHOP_GMAIL_CLIENT_SECRET?.trim(),i=process.env.BISHOP_GMAIL_REFRESH_TOKEN?.trim(),r=process.env.BISHOP_GMAIL_USER?.trim(),s=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||!jt()?"paste":"browser",u=await ie(!0,async m=>De({...n&&c?{client:{clientId:n,clientSecret:c}}:{},...i?{refreshToken:i}:{},...r?{user:r}:{},...s?{serviceAccount:s}:{},mode:l,openBrowser:ft,readPaste:async a=>(process.stdout.write(`Open this in a browser and approve it:
18
18
 
19
19
  ${a}
20
20
 
@@ -24,12 +24,12 @@ Copy the whole address out of the browser and paste it here.
24
24
  `),m("Redirect URL",""))}));if(process.stdout.write(`${u.report}
25
25
  `),u.refreshToken){const m=await se(t,{BISHOP_GMAIL_REFRESH_TOKEN:u.refreshToken});process.stdout.write(`
26
26
  Saved BISHOP_GMAIL_REFRESH_TOKEN to ${m}
27
- `)}u.ok||process.exit(1)}async function qt(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}
27
+ `)}u.ok||process.exit(1)}async function Yt(e){const t=process.cwd(),n=process.env.GH_TOKEN?.trim();process.env.GH_TOKEN!==void 0&&!n&&delete process.env.GH_TOKEN,T(t);const c=process.env.GH_TOKEN?.trim(),i=!!c&&!n,r=e.prompt&&process.stdin.isTTY===!0,s=e.print?process.stderr:process.stdout,l=a=>s.write(a),u=a=>He({...c?{token:c,tokenIsSaved:i}:{},...r?{login:()=>Oe(Ce.map(h=>h.scope),a),confirm:async h=>/^y/i.test(await ie(!0,x=>x(h,"y"),s))}:{},gitConfigured:$e,gh:Ge(a),...e.temporary?{temporary:!0}:{}}),m=e.temporary&&!c?await Le(u):await u();if(l(`${m.report}
28
28
  `),e.print)m.token&&process.stdout.write(`${JSON.stringify({GH_TOKEN:m.token})}
29
29
  `);else if(m.token){if(m.source==="gh"){const h=await se(t,{GH_TOKEN:m.token});l(`
30
30
  Saved GH_TOKEN to ${h}
31
- `)}const a=await ht(t,[".env"]);a.length>0&&l(`Added ${a.join(", ")} to .gitignore
32
- `)}m.ok||process.exit(1)}async function Yt(e){const t=process.cwd(),n=await We({config:await $(t),name:e.name});process.stdout.write(`${n.report}
33
- `),n.config&&process.stdout.write(`Saved ${await Te(n.config,t)}
34
- `),n.ok||process.exit(1)}async function Wt(e){b(process.cwd());const t={...e.name===void 0?{}:{name:e.name},...e.displayName===void 0?{}:{displayName:e.displayName},...e.description===void 0?{}:{description:e.description},prompt:e.prompt};e.manual?await gt(t):await wt(t)}async function Jt(e){if(e.appId!==void 0&&!e.manual)throw new oe("--app-id adopts an app somebody else created, so it needs --manual too.");if(e.endpoint!==void 0&&e.manual)throw new oe("--endpoint sets the URL on an app Bishop is creating, so it cannot be used\nwith --manual. Point an adopted app with `bishop teams endpoint <url>`,\nor ask whoever created it to.");b(process.cwd());const t={...e.name===void 0?{}:{name:e.name},...e.displayName===void 0?{}:{displayName:e.displayName},...e.description===void 0?{}:{description:e.description},...e.endpoint===void 0?{}:{endpoint:e.endpoint},...e.appId===void 0?{}:{appId:e.appId},prompt:e.prompt};e.manual?await It(t):await Tt(t)}async function Qt(e){const t=process.cwd(),n=await $(t),c=await ft({name:e.name??n.slack?.app?.name,displayName:e.displayName??n.slack?.app?.displayName,description:e.description??n.slack?.app?.description,prompt:!1},t,async(s,i)=>i);process.stdout.write(`${JSON.stringify(St(c),null,2)}
35
- `)}const S=new ue().name("bishop").description("An agent gateway. Runs an agent and connects it to Slack, email, and Teams.").version($t,"-v, --version").enablePositionalOptions().option("--agent <path-or-url>","the agent's directory, or a git URL to clone; defaults to the current directory").option(`--harness <${ye.join("|")}>`,"which SDK runs the agent",Se).option("--model <model>","the model to run, in the harness's own naming",Q("--model")).option("--effort <effort>","how hard the model should think, in the harness's own naming",Q("--effort")).option("--auto-update","for a git-URL --agent, check for a new commit at the start of every new thread").allowExcessArguments(!0).action((e,t)=>Ut(t.args,e));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}`,ke("--max-age-days")).option("--agent <url>","only this agent's snapshots; defaults to every agent Bishop has cloned here").action(Kt);const x=S.command("schedules").description("Prompts the agent runs on a clock, and where their answers go");x.command("list").description("Everything scheduled here, in your own time zone").action(()=>ot(process.cwd())),x.command("add").description("Add a schedule, or with --at a reminder that runs once and is then deleted").requiredOption("--name <name>","what a person sees in a list, and the subject of an email").option("--cron <expression>","five-field cron, or @daily and friends").option("--at <time>","run once instead, at an ISO 8601 time like 2026-10-09T15:00").requiredOption("--prompt <text>","what the agent is asked, including when to stay quiet").requiredOption("--to <destination>","#channel or @person for Slack, an address for Gmail").option("--tz <zone>","IANA zone --cron or --at is read in; defaults to this machine").option("--interface <name>","which interface delivers, when --to is ambiguous").action(e=>tt(process.cwd(),e)),x.command("rm <id>").description("Remove a schedule").action(e=>nt(process.cwd(),e)),x.command("disable <id>").description("Stop a schedule firing, keeping it").action(e=>ee(process.cwd(),e,!1)),x.command("enable <id>").description("Start a disabled schedule firing again, from now").action(e=>ee(process.cwd(),e,!0));const Vt=S.command("gmail").description("Gmail interface commands");Vt.command("setup").description("Authorize a Gmail mailbox, or check the one already configured").option("--no-browser","print a URL to open elsewhere instead of opening one here").action(jt);const Xt=S.command("github").description("GitHub credentials for the agent");Xt.command("setup").description("Give the agent a GitHub token, or check the one it's using").option("--no-prompt","don't offer to run `gh auth login`").option("--print","write the credential to stdout as JSON instead of to .env").option("--temporary","log in without touching this machine's own gh login").action(qt);const Zt=S.command("nono").description("The nono sandbox the agent runs under");Zt.command("setup").description("Write a nono profile that lets a sandboxed agent push").option("--name <name>","the profile name",Ye).action(Yt);const ie=S.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(Wt),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(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(Jt),H.command("endpoint <url>").description("Point the Teams app at a different URL, changing nothing else").option("--app-id <id>","the Teams app to point; defaults to the one in config").action((e,t)=>vt(e,t.appId===void 0?{}:{appId:t.appId})),H.command("instructions").description("Print what to send whoever creates the Teams app, when that is an administrator").option("--endpoint <url>","the URL Teams should POST to, if you already know it").action(e=>{process.stdout.write(bt(e.endpoint))});try{await S.parseAsync()}catch(e){throw e instanceof v&&f(e.message),e}
31
+ `)}const a=await ut(t,[".env"]);a.length>0&&l(`Added ${a.join(", ")} to .gitignore
32
+ `)}m.ok||process.exit(1)}async function Wt(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 Ie(n.config,t)}
34
+ `),n.ok||process.exit(1)}async function Jt(e){T(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 yt(t):await gt(t)}async function Qt(e){if(e.appId!==void 0&&!e.manual)throw new ne("--app-id adopts an app somebody else created, so it needs --manual too.");if(e.endpoint!==void 0&&e.manual)throw new ne("--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.");T(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 Et(t):await It(t)}async function Vt(e){const t=process.cwd(),n=await $(t),c=await wt({name:e.name??n.slack?.app?.name,displayName:e.displayName??n.slack?.app?.displayName,description:e.description??n.slack?.app?.description,prompt:!1},t,async(i,r)=>r);process.stdout.write(`${JSON.stringify(St(c),null,2)}
35
+ `)}const k=new fe().name("bishop").description("An agent gateway. Runs an agent and connects it to Slack, email, and Teams.").version(Lt,"-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 <${ke.join("|")}>`,"which SDK runs the agent",Se).option("--model <model>","the model to run, in the harness's own naming",V("--model")).option("--effort <effort>","how hard the model should think, in the harness's own naming",V("--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)=>Ft(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 ${_e}`,ve("--max-age-days")).option("--agent <url>","only this agent's snapshots; defaults to every agent Bishop has cloned here").action(zt);const A=k.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(()=>nt(process.cwd())),A.command("add").description("Add a schedule, or with --at a reminder that runs once and is then deleted").requiredOption("--name <name>","what a person sees in a list, and the subject of an email").option("--cron <expression>","five-field cron, or @daily and friends").option("--at <time>","run once instead, at an ISO 8601 time like 2026-10-09T15:00").requiredOption("--prompt <text>","what the agent is asked, including when to stay quiet").requiredOption("--to <destination>","#channel or @person for Slack, an address for Gmail").option("--tz <zone>","IANA zone --cron or --at is read in; defaults to this machine").option("--interface <name>","which interface delivers, when --to is ambiguous").action(e=>ot(process.cwd(),e)),A.command("rm <id>").description("Remove a schedule").action(e=>it(process.cwd(),e)),A.command("disable <id>").description("Stop a schedule firing, keeping it").action(e=>te(process.cwd(),e,!1)),A.command("enable <id>").description("Start a disabled schedule firing again, from now").action(e=>te(process.cwd(),e,!0));const Xt=k.command("gmail").description("Gmail interface commands");Xt.command("setup").description("Authorize a Gmail mailbox, or check the one already configured").option("--no-browser","print a URL to open elsewhere instead of opening one here").action(qt);const Zt=k.command("github").description("GitHub credentials for the agent");Zt.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(Yt);const eo=k.command("nono").description("The nono sandbox the agent runs under");eo.command("setup").description("Write a nono profile that lets a sandboxed agent push").option("--name <name>","the profile name",We).action(Wt);const re=k.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(Jt),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(Vt);const P=k.command("teams").description("Microsoft Teams interface commands");P.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(Qt),P.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)=>Tt(e,t.appId===void 0?{}:{appId:t.appId})),P.command("instructions").description("Print what to send whoever creates the Teams app, when that is an administrator").option("--endpoint <url>","the URL Teams should POST to, if you already know it").action(e=>{process.stdout.write(bt(e.endpoint))});try{await k.parseAsync()}catch(e){throw e instanceof I&&f(e.message),e}
package/dist/config.js CHANGED
@@ -1,4 +1,4 @@
1
- import{mkdir as h,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 j=".bishop",v="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)}),S=o.strictObject({id:o.string().optional(),name:o.string().min(1),displayName:o.string().optional(),description:o.string().optional()}),I=o.strictObject({app:S.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(),projectSettings: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:I.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 b(n=process.cwd()){return y(n,j,v)}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=b(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=b(t);return await h(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{j as CONFIG_DIR,v 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,b 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};
1
+ import{mkdir as v,readFile as y,writeFile as O}from"node:fs/promises";import{dirname as j,join as $}from"node:path";import{z as t}from"zod";import{BishopError as c}from"./errors.js";const E=".bishop",R="config.json",I=t.strictObject({id:t.string().optional(),name:t.string().min(1),displayName:t.string().optional(),description:t.string().optional()}),N=["low","medium","high"],K="high",h=t.string().transform(n=>n.trim().toLowerCase()).pipe(t.enum(N)),S=t.strictObject({workspace:t.string().optional(),app:I.optional(),verbosity:h.optional(),allow:t.array(t.string().min(1)).optional(),maxBotTurns:t.number().int().nonnegative().optional()}),A=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()}),C=t.strictObject({cert:t.string().trim().min(1),key:t.string().trim().min(1)}),_=t.strictObject({id:t.string().optional(),name:t.string().min(1),displayName:t.string().optional(),description:t.string().optional()}),F=t.strictObject({app:_.optional(),allow:t.array(t.string().min(1)).optional(),verbosity:h.optional(),port:t.number().int().min(1).max(65535).optional(),host:t.string().trim().min(1).optional(),tls:C.optional()}),p=["claude","codex"],T=t.union([t.literal(!1),t.strictObject({idleMinutes:t.number().positive().max(10080).optional(),max:t.number().int().positive().optional()})]),L=t.strictObject({model:t.string().trim().min(1).optional(),effort:t.string().trim().min(1).optional(),options:t.record(t.string(),t.unknown()).optional(),keepAlive:T.optional()}),z=t.strictObject({harness:t.enum(p).optional(),model:t.string().trim().min(1).optional(),effort:t.string().trim().min(1).optional(),harnesses:t.partialRecord(t.enum(p),L,{error:()=>`not a harness Bishop runs; supported: ${p.join(", ")}`}).superRefine((n,o)=>{n.codex?.keepAlive!==void 0&&o.addIssue({code:"custom",path:["codex","keepAlive"],message:"only the claude harness keeps an agent running between turns"})}).optional(),worktrees:t.boolean().optional(),projectSettings: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()}),B=t.strictObject({maxAgeDays:t.number().int().positive().optional()}),D=t.strictObject({maxMb:t.number().positive().optional()}),U=["all","external"],V=["reject","approve"],b=t.strictObject({scope:t.enum(U).optional(),onError:t.enum(V).optional()});function G(n){let o;try{o=new URL(n)}catch{return!1}return o.protocol==="https:"?!0:o.protocol!=="http:"?!1:o.hostname==="localhost"||o.hostname==="[::1]"||/^127(\.\d{1,3}){3}$/.test(o.hostname)}const P=t.strictObject({command:t.array(t.string().min(1)).min(1).optional(),url:t.string().trim().refine(G,"must be an https URL, or http on loopback").optional(),inbound:b.optional(),outbound:b.optional(),timeoutSeconds:t.number().positive().max(300).optional(),env:t.array(t.string().regex(/^[A-Za-z_][A-Za-z0-9_]*$/,"must be an environment variable name")).optional()}).superRefine((n,o)=>{n.command&&n.url?o.addIssue({code:"custom",message:"names both command and url; pick one"}):!n.command&&!n.url&&o.addIssue({code:"custom",message:"needs a command or a url"}),n.url&&n.env&&o.addIssue({code:"custom",path:["env"],message:"is only for a command reviewer"}),!n.inbound&&!n.outbound&&o.addIssue({code:"custom",message:"reviews nothing without inbound or outbound"})}),H=t.strictObject({slack:S.optional(),gmail:A.optional(),teams:F.optional(),agent:z.optional(),files:D.optional(),gc:B.optional(),review:P.optional()}).superRefine((n,o)=>{n.review&&!n.agent?.sandbox&&o.addIssue({code:"custom",path:["review"],message:"needs agent.sandbox on"})});class m extends c{path;constructor(o,e){super(o,"ConfigError"),this.path=e}}function q(n,o={}){const e=n.agent,i=e?.harness??"claude",r=o.harness??i,s=e?.harnesses?.[r],{keepAlive:x,...k}=s??{},a=s&&Object.keys(k).length>0?s:void 0,l=[];e?.options&&(a&&r===i?l.push({kind:"options-replaced",harness:r}):!a&&r!==i&&l.push({kind:"options-foreign",harness:r,configured:i}));const u=o.model??a?.model??e?.model,d=o.effort??a?.effort??e?.effort,g=a?a.options:e?.options,f=r==="claude"?x:void 0;return{harness:r,...u?{model:u}:{},...d?{effort:d}:{},...g?{options:g}:{},...f!==void 0?{keepAlive:f}:{},warnings:l}}function Q(n){const o=n.agent?.sandbox;if(o)return o===!0||!o.profile?{}:{profile:o.profile}}function X(n){return o=>{const e=o.trim();if(!e)throw new c(`${n} needs a value`);return e}}function tt(n){return o=>{const e=Number(o.trim());if(!Number.isInteger(e)||e<=0)throw new c(`${n} needs a positive whole number, got "${o}"`);return e}}function ot(n){const o=n.trim();if(p.includes(o))return o;throw new c(`unknown harness "${n}"; supported: ${p.join(", ")}`)}function w(n=process.cwd()){return $(n,E,R)}function J(n){return n.issues.flatMap(o=>{if(o.code==="unrecognized_keys"&&o.keys.length>0){const e=o.message.startsWith("Unrecognized key")?"unrecognized key":o.message;return o.keys.map(i=>`${[...o.path,i].join(".")}: ${e}`)}return[`${o.path.length>0?o.path.join("."):"(root)"}: ${o.message}`]}).join(`
2
+ `)}async function nt(n=process.cwd()){const o=w(n);let e;try{e=await y(o,"utf8")}catch(s){if(s.code==="ENOENT")return{};throw new m(`Couldn't read ${o}: ${s.message}`,o)}let i;try{i=JSON.parse(e)}catch(s){throw new m(`${o} isn't valid JSON: ${s.message}`,o)}const r=H.safeParse(i);if(!r.success)throw new m(`${o} has invalid values:
3
+ ${J(r.error)}`,o);return r.data}async function et(n,o=process.cwd()){const e=w(o);return await v(j(e),{recursive:!0}),await O(e,`${JSON.stringify(n,null,2)}
4
+ `,"utf8"),e}function rt(n){return n.displayName?.trim()||n.name}export{E as CONFIG_DIR,R as CONFIG_FILE,m as ConfigError,H as ConfigSchema,K as DEFAULT_VERBOSITY,p as HARNESSES,V as REVIEW_ON_ERROR,U as REVIEW_SCOPES,N as VERBOSITY,w as configPath,rt as displayNameOf,nt as loadConfig,X as nonEmptyFlag,ot as parseHarnessName,tt as positiveIntFlag,q as resolveAgent,Q as sandboxConfig,et as writeConfig};
@@ -1 +1 @@
1
- import{SERVER_NAME as l}from"../tools/server.js";import{mergeSettings as a}from"./claude-settings.js";import{layerOptions as c,mergeEnv as i,PLUGIN_INSTALL_TIMEOUT_MS as m}from"./layers.js";const f={permissionMode:"auto"},y=["autoMemoryEnabled"],d=["cwd","resume","forkSession","sessionId","systemPrompt","abortController","canUseTool"];function g(e,o){const r=e??void 0;return o?{...r,[l]:{type:"http",url:o.url,headers:{Authorization:`Bearer \${${o.tokenEnvVar}}`},alwaysLoad:!0}}:r}function S(e,o){const r=e??void 0;if(!o)return r;const t={...r};for(const[s,n]of Object.entries(o))t[s]=[...n,...r?.[s]??[]];return t}function E(e,o,r){if(typeof e=="string")return e;const t={autoMemoryEnabled:!1};return r&&(t.worktree={baseRef:"head"}),a(t,o,e)}function N(e){const o=c({defaults:f,...e.configOptions?{configOptions:e.configOptions}:{},...e.general?{general:e.general}:{},controlledKeys:d}),r=S(o.hooks,e.hooks);r&&(o.hooks=r);const t=g(o.mcpServers,e.tools);t&&(o.mcpServers=t),o.settings=E(o.settings,e.agentSettings,e.worktrees===!0),o.permissionMode==="bypassPermissions"&&(o.allowDangerouslySkipPermissions=!0);const s=i(o.env,{...e.apiKey?{ANTHROPIC_API_KEY:e.apiKey}:{},...e.tools?{[e.tools.tokenEnvVar]:e.tools.token}:{},...e.installPlugins?{CLAUDE_CODE_SYNC_PLUGIN_INSTALL:"1",CLAUDE_CODE_SYNC_PLUGIN_INSTALL_TIMEOUT_MS:String(m)}:{}});return s&&(o.env=s),o.cwd=e.controlled.cwd,o.systemPrompt=e.controlled.systemPrompt,o.abortController=e.controlled.abortController,e.controlled.resume&&(o.resume=e.controlled.resume),o}export{d as CONTROLLED_KEYS,f as DEFAULT_AGENT_OPTIONS,y as SETTINGS_ONLY_KEYS,N as buildAgentOptions,g as mergeMcpServers};
1
+ import{SERVER_NAME as n}from"../tools/server.js";import{mergeSettings as i}from"./claude-settings.js";import{layerOptions as a,mergeEnv as c,PLUGIN_INSTALL_TIMEOUT_MS as d,unionLists as m}from"./layers.js";const E={permissionMode:"auto"},_={CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC:"1"},f=["CronCreate","CronDelete","CronList","ScheduleWakeup"],g=["autoMemoryEnabled"],S=["cwd","resume","forkSession","sessionId","systemPrompt","abortController","canUseTool"];function N(e){return{[n]:{type:"http",url:e.url,headers:{Authorization:`Bearer ${e.token}`},alwaysLoad:!0}}}function u(e){if(!e||typeof e!="object")return;const{[n]:o,...r}=e;return Object.keys(r).length>0?r:void 0}function L(e,o){const r=e??void 0;if(!o)return r;const t={...r};for(const[s,l]of Object.entries(o))t[s]=[...l,...r?.[s]??[]];return t}function p(e,o,r){if(typeof e=="string")return e;const t={autoMemoryEnabled:!1};return r&&(t.worktree={baseRef:"head"}),i(t,o,e)}function y(e){const o=a({defaults:E,...e.configOptions?{configOptions:e.configOptions}:{},...e.general?{general:e.general}:{},controlledKeys:S}),r=L(o.hooks,e.hooks);r&&(o.hooks=r);const t=u(o.mcpServers);t?o.mcpServers=t:delete o.mcpServers,o.disallowedTools=m([...f],Array.isArray(o.disallowedTools)?o.disallowedTools:[]),o.settings=p(o.settings,e.agentSettings,e.worktrees===!0),o.permissionMode==="bypassPermissions"&&(o.allowDangerouslySkipPermissions=!0);const s=c(o.env,{...e.apiKey?{ANTHROPIC_API_KEY:e.apiKey}:{},...e.installPlugins?{CLAUDE_CODE_SYNC_PLUGIN_INSTALL:"1",CLAUDE_CODE_SYNC_PLUGIN_INSTALL_TIMEOUT_MS:String(d)}:{}});return o.env={..._,...s??process.env},o.cwd=e.controlled.cwd,o.systemPrompt=e.controlled.systemPrompt,o.abortController=e.controlled.abortController,e.controlled.resume&&(o.resume=e.controlled.resume),o}export{S as CONTROLLED_KEYS,_ as DEFAULT_AGENT_ENV,E as DEFAULT_AGENT_OPTIONS,f as REPLACED_TOOLS,g as SETTINGS_ONLY_KEYS,N as bishopServer,y as buildAgentOptions,u as withoutBishopServer};
@@ -0,0 +1 @@
1
+ import{randomUUID as P}from"node:crypto";import{log as a}from"../log.js";const $={idleMs:10*6e4,max:4},O=5e3;class _{items=[];waiting;done=!1;failure;push(o){if(!this.done)if(this.waiting){const f=this.waiting;this.waiting=void 0,f({value:o,done:!1})}else this.items.push(o)}end(o){if(!this.done&&(this.done=!0,this.failure=o,this.waiting)){const f=this.waiting;this.waiting=void 0,f({value:void 0,done:!0})}}async*[Symbol.asyncIterator](){for(;;){const o=this.items.shift();if(o!==void 0){yield o;continue}if(this.done){if(this.failure)throw this.failure;return}const f=await new Promise(w=>{this.waiting=w});if(f.done){if(this.failure)throw this.failure;return}yield f.value}}}function q(d){return d.type==="system"&&d.subtype==="init"||d.type==="assistant"||d.type==="result"}function C(d){return d.type==="assistant"&&d.parent_tool_use_id===null}function I(d){return!!(d.turn||d.current||d.background.size>0)}function L(d,o){const f=JSON.parse(d),w=JSON.parse(o);return[...new Set([...Object.keys(f),...Object.keys(w)])].filter(g=>JSON.stringify(f[g])!==JSON.stringify(w[g]))}function F(d){const o=new Map;let f=0;const w=new WeakMap;let g;const{keepAlive:k}=d;function h(e,t){if(e.closing)return;e.closing=t,e.idleTimer&&clearTimeout(e.idleTimer),o.get(e.threadId)===e&&o.delete(e.threadId),e.keptAlive&&g?.ended(e.threadId,e.id);const n={thread:e.threadKey,reason:t,turns:e.turns,aliveMs:Date.now()-e.startedAt,...e.background.size>0?{background:e.background.size}:{}};e.keptAlive?a.info(n,"closing a thread's Claude Code process"):a.debug(n,"closing a thread's Claude Code process"),e.inbox.end(),e.query.close()}function K(e){if(e.idleTimer&&clearTimeout(e.idleTimer),e.idleTimer=void 0,!(!k||!e.keptAlive||e.closing)){if(I(e)){!e.turn&&!e.current&&a.debug({thread:e.threadKey,background:e.background.size},"keeping Claude Code running while its background work runs");return}e.idleTimer=setTimeout(()=>h(e,"idle"),k.idleMs),e.idleTimer.unref()}}function D(e){if(!k)return;const t=[...o.values()].filter(s=>s.keptAlive&&!s.closing);let n=t.length-k.max;if(n<=0)return;const r=t.filter(s=>s!==e&&!I(s)).sort((s,i)=>s.lastUsed-i.lastUsed);for(const s of r){if(n<=0)break;h(s,"over the cap"),n--}n>0&&a.debug({kept:t.length,max:k.max},"over the keep-alive cap, but the rest are busy")}function M(e){const t=e.turn?.onOwned;!t||!e.turn||(e.turn.onOwned=void 0,t())}function S(e,t,n){if(t.owner=n,n==="turn"&&e.turn)t.channel=e.turn.channel,M(e);else{const r=new _;t.channel=r;const s=t.buffer[0];a.info({thread:e.threadKey,...s?{opened:"subtype"in s?`${s.type}/${s.subtype}`:s.type}:{}},"the agent started a follow-up"),g?g.followUp(e.threadId,d.translate(r)):(a.warn({thread:e.threadKey},"nothing is listening for a follow-up; dropping it"),(async()=>{for await(const i of r);})())}for(const r of t.buffer.splice(0))t.channel.push(r)}function A(e,t){if(!e.current){if(!q(t)||e.turns===0&&!e.turn){e.turn?e.turn.channel.push(t):e.turns===0&&e.early.push(t);return}e.idleTimer&&clearTimeout(e.idleTimer),e.idleTimer=void 0,e.current={buffer:[]}}const n=e.current;if(t.type==="system"&&t.subtype==="init"&&(n.init??=t,w.set(t,Date.now())),n.owner)e.turn&&n.channel!==e.turn.channel&&C(t)&&t.user_message_uuid===e.turn.uuid?(n.channel?.end(),n.owner==="followUp"&&(e.notified=!1),n.owner="turn",n.channel=e.turn.channel,M(e),n.init&&n.channel.push(n.init),n.channel.push(t)):n.channel?.push(t);else if(n.buffer.push(t),C(t)||t.type==="result"){const r=C(t),s=r?t.user_message_uuid:void 0,i=!!e.turn&&(s?s===e.turn?.uuid:!r||!e.notified);S(e,n,i?"turn":"followUp")}t.type==="result"&&(n.channel?.end(),e.current=void 0,e.lastUsed=Date.now(),n.owner==="turn"?e.turn?.channel===n.channel&&(e.turn=void 0):(e.notified=!1,a.info({thread:e.threadKey,outcome:t.subtype},"a follow-up finished")),K(e))}async function E(e){let t;try{for await(const s of e.query)if(s.type==="system"&&s.subtype==="init"&&(e.sessionId=s.session_id),s.type==="system"&&s.subtype==="task_notification"&&!s.ambient&&(e.notified=!0),A(e,s),s.type==="system"&&s.subtype==="background_tasks_changed"){const i=e.background.size;e.background=new Set(s.tasks.filter(l=>!l.ambient).map(l=>l.task_id)),e.background.size!==i&&a.debug({thread:e.threadKey,background:e.background.size},"background work changed"),K(e)}}catch(s){t=s}e.closing||(a.warn({thread:e.threadKey,turns:e.turns,aliveMs:Date.now()-e.startedAt,...t?{err:t}:{}},"a thread's Claude Code process exited on its own"),h(e,"exited"));const n=t instanceof Error?t.message:"Claude Code exited before the turn finished",r=new Error(e.explain?e.explain(n):n);e.current?.channel?.end(r),e.turn?.channel.end(r),e.current=void 0,e.turn=void 0}async function z(e,t,n){const r=Date.now(),s=new _,i=`cc_${++f}`,l=t?g?.started(e.threadId,i):e.tools;let v,x;try{v=await e.options(),x=d.start({prompt:s,options:v.options})}catch(p){throw t&&g?.ended(e.threadId,i),p}const{explain:b}=v,c={id:i,threadId:e.threadId,threadKey:e.threadKey,query:x,inbox:s,fingerprint:JSON.stringify(e.fingerprint),keptAlive:t,...e.sessionId?{sessionId:e.sessionId}:{},startedAt:r,lastUsed:r,turns:0,notified:!1,background:new Set,exited:Promise.resolve(),...b?{explain:b}:{},early:[]};if(c.exited=E(c),t&&o.set(e.threadId,c),l){const p=Date.now();try{const y=await x.setMcpServers(e.server(l)),m=Object.entries(y.errors??{});m.length>0&&(a.warn({thread:e.threadKey,errors:Object.fromEntries(m)},"the agent couldn't connect to Bishop's tools; this turn runs without them"),c.toolless=!0)}catch(y){a.warn({thread:e.threadKey,err:y},"couldn't hand the agent Bishop's tools; this turn runs without them"),c.toolless=!0}a.debug({thread:e.threadKey,startMs:Date.now()-r,toolsMs:Date.now()-p},"Claude Code is up")}const T={thread:e.threadKey,reason:n,startMs:Date.now()-r};return t?a.info(T,"started Claude Code for this thread"):a.debug(T,"started Claude Code for one turn"),c}async function B(e,t){const n=o.get(e.threadId);if(!n)return"new";if(n.closing)return"exited";const r=JSON.stringify(e.fingerprint);let s;if(!t)s="one-off turn";else if(n.fingerprint!==r){const i=L(n.fingerprint,r);if(I(n))return a.info({thread:e.threadKey,changed:i},"the agent's configuration changed, but it is still running something; keeping it until it isn't"),n;a.info({thread:e.threadKey,changed:i},"the agent's configuration changed since its process started; starting a new one"),s="configuration changed"}else n.sessionId!==e.sessionId&&(s="session changed");return s?(h(n,s),await n.exited,s):n}async function*N(e){const t=Date.now(),n=!!k&&e.keepAlive,r=await B(e,n),s=typeof r!="string";s&&a.debug({thread:e.threadKey,idleMs:t-r.lastUsed,...r.current?{behind:"a follow-up"}:{}},"reusing the thread's Claude Code process");const i=s?r:await z(e,n,r);if(i.closing){await i.exited;const u="Claude Code exited before the turn started";throw new Error(i.explain?i.explain(u):u)}if(e.signal.aborted){n?i.toolless?h(i,"Bishop's tools didn't attach"):(K(i),D(i)):h(i,"one-off turn");return}i.idleTimer&&clearTimeout(i.idleTimer),i.idleTimer=void 0;const l=new _,v=P();i.turn={uuid:v,channel:l},i.turns++;for(const u of i.early.splice(0))l.push(u);const x=Date.now()-t;i.inbox.push({type:"user",message:{role:"user",content:e.text},parent_tool_use_id:null,session_id:i.sessionId??"",uuid:v});let b=!1,c;const T=()=>{a.debug({thread:e.threadKey},"interrupting the agent"),i.query.interrupt().catch(u=>{a.debug({thread:e.threadKey,err:u},"the interrupt was not delivered")}),c=setTimeout(()=>{a.warn({thread:e.threadKey},"the agent didn't stop when asked; closing its process"),h(i,"didn't stop when asked")},O),c.unref()},p=()=>{b=!0;const u=i.turn?.channel===l,J=i.current&&i.current.channel!==l;if(u&&J&&i.turn){a.debug({thread:e.threadKey},i.current?.owner==="followUp"?"holding the stop until the follow-up ends":"holding the stop until the running turn's first reply says whose it is"),i.turn.onOwned=T;return}T()};e.signal.aborted?p():e.signal.addEventListener("abort",p,{once:!0});let y,m,U=!1;try{for await(const u of l)u.type==="result"&&(U=!0),!b&&(y===void 0&&u.type==="system"&&u.subtype==="init"&&(y=(w.get(u)??Date.now())-t),m===void 0&&u.type==="assistant"&&(m=Date.now()-t),yield u)}finally{e.signal.removeEventListener("abort",p),c&&clearTimeout(c),a.debug({thread:e.threadKey,process:s?"kept":"new",readyMs:x,...y===void 0?{}:{initMs:y},...m===void 0?{}:{firstOutputMs:m},totalMs:Date.now()-t,...b?{stopped:!0}:{}},"process timing"),!U&&i.turn?.channel===l&&!i.closing&&(b||(a.debug({thread:e.threadKey},"the turn ended before the agent did; interrupting it"),i.query.interrupt().catch(()=>{})),setTimeout(()=>{i.turn?.channel===l&&h(i,"didn't stop when asked")},O).unref()),n?i.toolless?h(i,"Bishop's tools didn't attach"):(K(i),D(i)):h(i,"one-off turn")}}return{run:N,attach(e){g=e},has(e){return o.has(e)},async close(){const e=[...o.values()];for(const t of e)h(t,"shutting down");await Promise.allSettled(e.map(t=>t.exited))}}}export{$ as DEFAULT_KEEP_ALIVE,F as claudeProcesses};
@@ -1,12 +1,12 @@
1
- import{spawn as D}from"node:child_process";import{existsSync as K,mkdirSync as j}from"node:fs";import{mkdir as F}from"node:fs/promises";import{homedir as L}from"node:os";import{join as S,resolve as k}from"node:path";import{query as I}from"@anthropic-ai/claude-agent-sdk";import{log as h}from"../log.js";import{executableDir as N,nonoCommand as P}from"../sandbox.js";import{isolates as x,repoRoot as $,WORKTREE_DIR as G,worktreePath as U}from"../worktrees.js";import{buildAgentOptions as W,CONTROLLED_KEYS as B,SETTINGS_ONLY_KEYS as H}from"./claude-options.js";import{pluginInstallWatcher as Y,readAgentSettings as M}from"./claude-settings.js";import{ignoredKeys as A,shadowedKeys as z}from"./layers.js";import{buildPrompt as J,principalInstructions as Q}from"./principal.js";function*V(e){switch(e.type){case"system":e.subtype==="init"&&(yield{type:"session",sessionId:e.session_id,...e.model?{model:e.model}:{}});return;case"assistant":for(const t of e.message.content)if(t.type==="text"&&t.text.trim())yield{type:"text",text:t.text};else if(t.type==="tool_use"){const r=X(t.input);yield{type:"tool",name:t.name,...r?{detail:r}:{}}}else t.type==="thinking"&&(yield{type:"thinking"});return;case"tool_use_summary":yield{type:"tool_summary",summary:e.summary};return;case"result":e.subtype==="success"?yield{type:"done",text:e.result,usage:{turns:e.num_turns,costUsd:e.total_cost_usd}}:yield{type:"error",kind:e.subtype,message:O(e.subtype)};return;default:return}}function X(e){if(!e||typeof e!="object")return;const t=e,r=t.file_path??t.notebook_path??t.path;if(typeof r=="string"&&r)return r.split("/").pop()??r;const o=t.url;if(typeof o=="string"&&o)try{return new URL(o).hostname}catch{return o.slice(0,40)}for(const a of["pattern","query","description"]){const d=t[a];if(typeof d=="string"&&d.trim()){const c=d.trim().split(`
2
- `)[0]??"";return c.length>48?`${c.slice(0,45)}...`:c}}}function Z(e){switch(e){case"none":return"existing claude login or cloud provider";case"/login managed key":return"key managed by claude login";default:return e}}function O(e){switch(e){case"error_max_turns":return"the agent hit its turn limit before finishing";case"error_max_budget_usd":return"the agent hit its cost limit before finishing";case"error_during_execution":return"the agent stopped on an error";default:return`the agent stopped: ${e}`}}const q=new Set(["Edit","Write","NotebookEdit"]);function ee(e,t){return["This directory is shared with other conversations that may be running at the same time.",`Before you edit, create, or delete any file here, call EnterWorktree with name "${t}"`,"so your changes land in a git worktree of your own. Load the tool with ToolSearch first if","it isn't available. If a worktree by that name already exists, enter it by path instead:",`${U(e,t)}.`,"","Once you are in the worktree, work normally. Relative paths resolve inside it, and you","stay in it for every later message in this thread. Commit what you finish on the","worktree's branch and say which branch it is, since that's how a person finds your work.","Don't call ExitWorktree."].join(`
3
- `)}function te(e){const t=k(e);return({toolName:r,cwd:o})=>{if(q.has(r)&&k(o)===t)return"This is the shared checkout, which other conversations are using. Call EnterWorktree with the name given in your instructions, then make this edit inside the worktree."}}function ne(e,t,r,o){const a=x(e)?ee(o,r):void 0;return[Q(t),a].filter(d=>!!d?.trim()).join(`
1
+ import{spawn as P}from"node:child_process";import{existsSync as F,mkdirSync as N}from"node:fs";import{mkdir as $}from"node:fs/promises";import{homedir as L}from"node:os";import{join as S,resolve as x}from"node:path";import{query as K}from"@anthropic-ai/claude-agent-sdk";import{log as f}from"../log.js";import{executableDir as U,nonoCommand as B}from"../sandbox.js";import{SERVER_NAME as E}from"../tools/server.js";import{isolates as v,repoRoot as G,WORKTREE_DIR as W,worktreePath as H}from"../worktrees.js";import{bishopServer as Y,buildAgentOptions as M,CONTROLLED_KEYS as V,SETTINGS_ONLY_KEYS as z}from"./claude-options.js";import{claudeProcesses as J,DEFAULT_KEEP_ALIVE as Q}from"./claude-processes.js";import{pluginInstallWatcher as X,readAgentSettings as Z}from"./claude-settings.js";import{ignoredKeys as D,shadowedKeys as O}from"./layers.js";import{buildPrompt as q,principalInstructions as ee}from"./principal.js";function*C(e){switch(e.type){case"system":e.subtype==="init"&&(yield{type:"session",sessionId:e.session_id,...e.model?{model:e.model}:{}});return;case"assistant":for(const n of e.message.content)if(n.type==="text"&&n.text.trim())yield{type:"text",text:n.text};else if(n.type==="tool_use"){const o=te(n.input);yield{type:"tool",name:n.name,...o?{detail:o}:{}}}else n.type==="thinking"&&(yield{type:"thinking"});return;case"tool_use_summary":yield{type:"tool_summary",summary:e.summary};return;case"result":e.subtype==="success"?yield{type:"done",text:e.result,usage:{turns:e.num_turns,costUsd:e.total_cost_usd}}:yield{type:"error",kind:e.subtype,message:oe(e.subtype)};return;default:return}}function te(e){if(!e||typeof e!="object")return;const n=e,o=n.file_path??n.notebook_path??n.path;if(typeof o=="string"&&o)return o.split("/").pop()??o;const r=n.url;if(typeof r=="string"&&r)try{return new URL(r).hostname}catch{return r.slice(0,40)}for(const l of["pattern","query","description"]){const c=n[l];if(typeof c=="string"&&c.trim()){const p=c.trim().split(`
2
+ `)[0]??"";return p.length>48?`${p.slice(0,45)}...`:p}}}function ne(e){switch(e){case"none":return"existing claude login or cloud provider";case"/login managed key":return"key managed by claude login";default:return e}}function oe(e){switch(e){case"error_max_turns":return"the agent hit its turn limit before finishing";case"error_max_budget_usd":return"the agent hit its cost limit before finishing";case"error_during_execution":return"the agent stopped on an error";default:return`the agent stopped: ${e}`}}const re=new Set(["Edit","Write","NotebookEdit"]);function ie(e,n){return["This directory is shared with other conversations that may be running at the same time.",`Before you edit, create, or delete any file here, call EnterWorktree with name "${n}"`,"so your changes land in a git worktree of your own. Load the tool with ToolSearch first if","it isn't available. If a worktree by that name already exists, enter it by path instead:",`${H(e,n)}.`,"","Once you are in the worktree, work normally. Relative paths resolve inside it, and you","stay in it for every later message in this thread. Commit what you finish on the","worktree's branch and say which branch it is, since that's how a person finds your work.","Don't call ExitWorktree."].join(`
3
+ `)}function se(e){const n=x(e);return({toolName:o,cwd:r})=>{if(re.has(o)&&x(r)===n)return"This is the shared checkout, which other conversations are using. Call EnterWorktree with the name given in your instructions, then make this edit inside the worktree."}}function ae(e,n,o,r){const l=v(e)?ie(r,o):void 0;return[ee(n),l].filter(c=>!!c?.trim()).join(`
4
4
 
5
- `)}function re(e,t){return{model:t?.model??e.model,effort:t?.effort??e.effort}}function oe(e){return async t=>{const r=t,o=e({toolName:r.tool_name,cwd:r.cwd});return o?(h.info({tool:r.tool_name,cwd:r.cwd},"refusing an edit in the shared checkout"),{continue:!0,hookSpecificOutput:{hookEventName:"PreToolUse",permissionDecision:"deny",permissionDecisionReason:o}}):{continue:!0}}}function ie(e,t=L()){const r=e.CLAUDE_CONFIG_DIR;return r?{allow:[r]}:{allow:[S(t,".claude")],allowFiles:[S(t,".claude.json")].filter(K)}}async function se(e){const t=await $(e);if(!t||k(t)===k(e))return[];const r=S(t,G);return await F(r,{recursive:!0}),[r]}const ae=2048;function v(e,t){const r=t.text.trim();return r?`${e}
6
- ${r}`:e}function de(e,t,r){return o=>{const a=ie(o.env);for(const f of a.allow??[])j(f,{recursive:!0});const d=N(o.command),c=P(e,{allow:[...a.allow??[],...t],read:d?[d]:[],...a.allowFiles?{allowFiles:a.allowFiles}:{}},o.command,o.args),m=D(c.command,c.args,{...o.cwd?{cwd:o.cwd}:{},env:o.env,stdio:["pipe","pipe","pipe"],signal:o.signal});return m.stderr.on("data",f=>{r.text=(r.text+f.toString()).slice(-ae)}),m.on("exit",f=>{const g=r.text.trim();f&&g&&h.warn({code:f,stderr:g},"the sandboxed agent exited with an error")}),m}}function _e(e={}){const t=A(e.options,B);t.length>0&&h.warn({ignored:t},"these agent.options are set by Bishop and cannot be overridden; ignoring them");const r=n=>re(e,n),o=r(),a=z(e.options,o);a.length>0&&h.warn({shadowed:a},"agent.model and agent.effort override these agent.options");const d=A(e.options,H);d.length>0&&h.warn({misplaced:d},"the harness reads these from agent.options.settings only and ignores them here");const c=e.worktrees,m=e.projectSettings!==!1;if(typeof e.options?.settings=="string"){const n=["auto memory is left to that file and the agent's own settings"];x(c)&&n.push("new worktrees branch from origin/<default-branch> rather than HEAD"),m&&n.push("the agent's .claude/settings.json gets only what headless Claude trusts of it, without its allow rules or plugins"),h.warn({settings:e.options.settings},`agent.options.settings names a file, so Bishop applies no settings defaults: ${n.join("; ")}`)}const f=(n,s,u,p,l,i)=>{const w=x(c)?te(n.cwd):void 0,y=w?{PreToolUse:[{hooks:[oe(w)]}]}:void 0,b=W({...e.options?{configOptions:e.options}:{},general:r(u),...e.apiKey?{apiKey:e.apiKey}:{},...y?{hooks:y,worktrees:!0}:{},...s?{tools:s}:{},...l?{agentSettings:l}:{},...i?{installPlugins:i}:{},controlled:n});return p?{...b,spawnClaudeCodeProcess:p}:b},g=m&&typeof e.options?.settings!="string",E=(n,s)=>g?M(n,s):Promise.resolve(void 0),T=async(n,s,u)=>de(s,x(c)?await se(n):[],u);return{name:"claude",describeStartupFailure(n,s){return`The Claude agent couldn't start in ${n}:
5
+ `)}function ce(e,n){return{model:n?.model??e.model,effort:n?.effort??e.effort}}function le(e){return async n=>{const o=n,r=e({toolName:o.tool_name,cwd:o.cwd});return r?(f.info({tool:o.tool_name,cwd:o.cwd},"refusing an edit in the shared checkout"),{continue:!0,hookSpecificOutput:{hookEventName:"PreToolUse",permissionDecision:"deny",permissionDecisionReason:r}}):{continue:!0}}}function de(e,n=L()){const o=e.CLAUDE_CONFIG_DIR;return o?{allow:[o]}:{allow:[S(n,".claude")],allowFiles:[S(n,".claude.json")].filter(F)}}async function ue(e){const n=await G(e);if(!n||x(n)===x(e))return[];const o=S(n,W);return await $(o,{recursive:!0}),[o]}const fe=2048;function A(e,n){const o=n.text.trim();return o?`${e}
6
+ ${o}`:e}function pe(e){return n=>A(n,e)}function me(e,n,o){return r=>{const l=de(r.env);for(const d of l.allow??[])N(d,{recursive:!0});const c=U(r.command),p=B(e,{allow:[...l.allow??[],...n],read:c?[c]:[],...l.allowFiles?{allowFiles:l.allowFiles}:{}},r.command,r.args),h=P(p.command,p.args,{...r.cwd?{cwd:r.cwd}:{},env:r.env,stdio:["pipe","pipe","pipe"],signal:r.signal});return h.stderr.on("data",d=>{o.text=(o.text+d.toString()).slice(-fe)}),h.on("exit",d=>{const g=o.text.trim();d&&g&&f.warn({code:d,stderr:g},"the sandboxed agent exited with an error")}),h}}function De(e={}){const n=D(e.options,V);n.length>0&&f.warn({ignored:n},"these agent.options are set by Bishop and cannot be overridden; ignoring them");const o=e.options?.mcpServers;o&&typeof o=="object"&&Object.hasOwn(o,E)&&f.warn({server:E},`agent.options.mcpServers names a server "${E}", which is Bishop's own; ignoring it`);const r=t=>ce(e,t),l=r(),c=O(e.options,l);c.length>0&&f.warn({shadowed:c},"agent.model and agent.effort override these agent.options");const p=D(e.options,z);p.length>0&&f.warn({misplaced:p},"the harness reads these from agent.options.settings only and ignores them here");const h=e.worktrees,d=e.projectSettings!==!1;if(typeof e.options?.settings=="string"){const t=["auto memory is left to that file and the agent's own settings"];v(h)&&t.push("new worktrees branch from origin/<default-branch> rather than HEAD"),d&&t.push("the agent's .claude/settings.json gets only what headless Claude trusts of it, without its allow rules or plugins"),f.warn({settings:e.options.settings},`agent.options.settings names a file, so Bishop applies no settings defaults: ${t.join("; ")}`)}const g=(t,s,i,m,u)=>{const a=v(h)?se(t.cwd):void 0,y=a?{PreToolUse:[{hooks:[le(a)]}]}:void 0,k=M({...e.options?{configOptions:e.options}:{},general:r(s),...e.apiKey?{apiKey:e.apiKey}:{},...y?{hooks:y,worktrees:!0}:{},...m?{agentSettings:m}:{},...u?{installPlugins:u}:{},controlled:t});return i?{...k,spawnClaudeCodeProcess:i}:k},j=d&&typeof e.options?.settings!="string",I=(t,s)=>j?Z(t,s):Promise.resolve(void 0),T=e.keepAlive??Q,_=J({start:e.start??(t=>K(t)),keepAlive:T,async*translate(t){for await(const s of t)yield*C(s)}}),R=async(t,s,i)=>me(s,v(h)?await ue(t):[],i);return{name:"claude",describeStartupFailure(t,s){return`The Claude agent couldn't start in ${t}:
7
7
  ${s}
8
8
 
9
9
  Bishop does not manage Anthropic credentials. The agent authenticates the
10
10
  same way \`claude\` does in this directory, so make sure that works first:
11
11
  ANTHROPIC_API_KEY in the environment or .env, or
12
- an existing \`claude\` login on this machine.`},async check(n,s){const u=new AbortController,p={text:""};try{const l=s?await T(n,s,p):void 0;await E(n,s);for await(const i of I({prompt:"ping",options:{...f({cwd:n,systemPrompt:void 0,abortController:u},void 0,void 0,l),maxTurns:1,persistSession:!1}})){if(i.type==="system"&&i.subtype==="init")return u.abort(),{ok:!0,credentialSource:Z(i.apiKeySource),...i.model?{model:i.model}:{}};if(i.type==="result")break}return{ok:!1,error:v("the harness exited without starting a session",p)}}catch(l){if(u.signal.aborted)return{ok:!0};const i=l instanceof Error?l.message:String(l);return{ok:!1,error:v(i,p)}}},async*runTurn(n){const s=new AbortController,u=()=>s.abort();n.signal.addEventListener("abort",u,{once:!0});let p=!1;const l={text:""};try{const i=n.sandbox?await T(n.cwd,n.sandbox,l):void 0,w=await E(n.cwd,n.sandbox),y=m&&!n.sessionId,b=y?Y({thread:n.threadKey}):void 0,C=I({prompt:J(n),options:f({cwd:n.cwd,systemPrompt:{type:"preset",preset:"claude_code",append:ne(c,n.origin,n.worktree,n.cwd)},abortController:s,...n.sessionId?{resume:n.sessionId}:{}},n.tools,n,i,w,y)});for await(const R of C){b?.(R);for(const _ of V(R))(_.type==="done"||_.type==="error")&&(p=!0),yield _}}catch(i){s.signal.aborted?h.debug({thread:n.threadKey},"claude turn aborted"):p?h.debug({err:i},"query() threw after reporting a result"):(h.error({err:i},"harness turn failed"),yield{type:"error",kind:"harness_failure",message:v(i instanceof Error?i.message:String(i),l)})}finally{n.signal.removeEventListener("abort",u)}}}}export{re as chosenGeneral,_e as claudeHarness,ie as claudeStateGrants,te as editGuard,ne as systemPromptAppend,X as toolDetail,ee as worktreeNote};
12
+ an existing \`claude\` login on this machine.`},async check(t,s){const i=new AbortController,m={text:""};try{const u=s?await R(t,s,m):void 0;await I(t,s);for await(const a of K({prompt:"ping",options:{...g({cwd:t,systemPrompt:void 0,abortController:i},void 0,u),maxTurns:1,persistSession:!1}})){if(a.type==="system"&&a.subtype==="init")return i.abort(),{ok:!0,credentialSource:ne(a.apiKeySource),...a.model?{model:a.model}:{}};if(a.type==="result")break}return{ok:!1,error:A("the harness exited without starting a session",m)}}catch(u){if(i.signal.aborted)return{ok:!0};const a=u instanceof Error?u.message:String(u);return{ok:!1,error:A(a,m)}}},async*runTurn(t){let s=!1;try{const i=await I(t.cwd,t.sandbox),m=d&&!t.sessionId,u=m?X({thread:t.threadKey}):void 0,a=ae(h,t.origin,t.worktree,t.cwd),y=r(t),k=_.run({threadId:t.threadId,threadKey:t.threadKey,text:q(t),fingerprint:{cwd:t.cwd,origin:{name:t.origin.name,self:t.origin.self},model:y.model,effort:y.effort,settings:i,sandbox:t.sandbox},...t.sessionId?{sessionId:t.sessionId}:{},keepAlive:t.keepAlive===!0,...t.tools?{tools:t.tools}:{},server:Y,signal:t.signal,options:async()=>{const w={text:""},b=t.sandbox?await R(t.cwd,t.sandbox,w):void 0;return{options:g({cwd:t.cwd,systemPrompt:{type:"preset",preset:"claude_code",append:a},abortController:new AbortController,...t.sessionId?{resume:t.sessionId}:{}},t,b,i,m),explain:pe(w)}}});for await(const w of k){u?.(w);for(const b of C(w))(b.type==="done"||b.type==="error")&&(s=!0),yield b}}catch(i){t.signal.aborted?f.debug({thread:t.threadKey},"claude turn aborted"):s?f.debug({err:i},"the agent failed after reporting a result"):(f.error({err:i},"harness turn failed"),yield{type:"error",kind:"harness_failure",message:i instanceof Error?i.message:String(i)})}},...T?{keepAlive:{attach:t=>_.attach(t),close:()=>_.close()}}:{}}}export{ce as chosenGeneral,De as claudeHarness,de as claudeStateGrants,se as editGuard,ae as systemPromptAppend,te as toolDetail,ie as worktreeNote};
@@ -1 +1 @@
1
- import{claudeHarness as s}from"./claude.js";import{codexHarness as c}from"./codex.js";function d(o,e){const r={...e.options?{options:e.options}:{},...e.model?{model:e.model}:{},...e.effort?{effort:e.effort}:{},...e.worktrees?{worktrees:e.worktrees}:{},...e.projectSettings===!1?{projectSettings:!1}:{}};switch(o){case"codex":return c({...r,...e.launcher?{launcher:e.launcher}:{},...e.credentials.codexApiKey?{apiKey:e.credentials.codexApiKey}:{}});case"claude":return s({...r,...e.credentials.anthropicApiKey?{apiKey:e.credentials.anthropicApiKey}:{}})}}export{d as createHarness};
1
+ import{claudeHarness as a}from"./claude.js";import{DEFAULT_KEEP_ALIVE as s}from"./claude-processes.js";import{codexHarness as l}from"./codex.js";function t(r){return r===!1?!1:{idleMs:r?.idleMinutes===void 0?s.idleMs:r.idleMinutes*6e4,max:r?.max??s.max}}function f(r,e){const o={...e.options?{options:e.options}:{},...e.model?{model:e.model}:{},...e.effort?{effort:e.effort}:{},...e.worktrees?{worktrees:e.worktrees}:{},...e.projectSettings===!1?{projectSettings:!1}:{}};switch(r){case"codex":return l({...o,...e.launcher?{launcher:e.launcher}:{},...e.credentials.codexApiKey?{apiKey:e.credentials.codexApiKey}:{}});case"claude":return a({...o,keepAlive:t(e.keepAlive),...e.credentials.anthropicApiKey?{apiKey:e.credentials.anthropicApiKey}:{}})}}export{f as createHarness,t as keepAliveOf};
@@ -2,6 +2,6 @@ import{log as a}from"../log.js";import{SHARE_FILE_TOOL as z}from"../outgoing.js"
2
2
 
3
3
  Remove it, or add it again with a cron expression that works.`),!1}}async function u(e,t,i=e.destination){const n=r.deliver(i,{text:t,title:e.name}).then(()=>{}).catch(o=>{a.error({schedule:e.id,to:i.label,err:o},"could not say that a schedule failed")}).finally(()=>{l.delete(n)});l.add(n),await n}async function S(e,t,i){if(!e.cron){a.error({schedule:e.id,name:e.name,err:i},"a reminder failed"),await u(e,`The reminder "${e.name}" failed: ${i}`);return}const n=r.schedules.recordFailure(e.id,t,i);if(a.error({schedule:e.id,name:e.name,failures:n,err:i},"a scheduled run failed"),n>=P){r.schedules.setEnabled(e.id,!1),a.error({schedule:e.id,name:e.name,failures:n},"disabling a schedule after too many failures in a row"),await u(e,`The schedule "${e.name}" has failed ${n} times in a row and is now off. The last failure was: ${i}
4
4
 
5
- Nothing more will run until \`bishop schedules enable ${e.id}\`.`);return}n===1&&await u(e,`The schedule "${e.name}" failed: ${i}`)}async function x(e,t,i){const n=A();a.info({schedule:e.id,name:e.name},"a schedule is firing");let o,f;try{f=await r.threads.remember(i)}catch(s){await S(e,d(),s instanceof Error?s.message:String(s));return}const k={thread:f,isNewThread:!0,freshSession:!0,tools:[z],origin:M,note:C(e,t,r.principals?I(e.createdBy,r.principals):e.createdBy),async messages(){return[{principal:_,text:e.prompt}]},presence:()=>n,async notify(s){n.note(s)},async destination(){return e.destination}};try{await r.run(k)}catch(s){o=s instanceof Error?s.message:String(s)}const m=n.outcome(),E=m.failure??o,w=d();if(E){await S(e,w,E);return}if(m.silent){r.schedules.recordSuccess(e.id,w),a.info({schedule:e.id,name:e.name},"a schedule had nothing to report");return}let g;try{g=await r.deliver(e.destination,{text:m.text,title:e.name,...m.files.length>0?{files:m.files}:{}})}catch(s){await S(e,w,s instanceof Error?s.message:String(s));return}if(g.threadKey&&r.attach)try{await r.attach(g.threadKey,i)}catch(s){a.error({schedule:e.id,thread:g.threadKey,err:s},"a report was delivered but nothing will answer a reply to it")}r.schedules.recordSuccess(e.id,w),a.info({schedule:e.id,name:e.name,to:e.destination.label},"a schedule reported")}function $(e){r.schedules.remove(e.id);const t=R(e.nextRunAt,e.tz);if(!b(e.createdBy)){a.warn({schedule:e.id,createdBy:e.createdBy,due:t},"deleted a missed reminder whose creator can no longer be authorized");return}const i=`Bishop wasn't running at ${t}, when your reminder "${e.name}" was due, so it didn't run. It would have asked:
5
+ Nothing more will run until \`bishop schedules enable ${e.id}\`.`);return}n===1&&await u(e,`The schedule "${e.name}" failed: ${i}`)}async function x(e,t,i){const n=A();a.info({schedule:e.id,name:e.name},"a schedule is firing");let o,f;try{f=await r.threads.remember(i)}catch(s){await S(e,d(),s instanceof Error?s.message:String(s));return}const k={thread:f,isNewThread:!0,freshSession:!0,unattended:!0,tools:[z],origin:M,note:C(e,t,r.principals?I(e.createdBy,r.principals):e.createdBy),async messages(){return[{principal:_,text:e.prompt}]},presence:()=>n,async notify(s){n.note(s)},async destination(){return e.destination}};try{await r.run(k)}catch(s){o=s instanceof Error?s.message:String(s)}const m=n.outcome(),E=m.failure??o,w=d();if(E){await S(e,w,E);return}if(m.silent){r.schedules.recordSuccess(e.id,w),a.info({schedule:e.id,name:e.name},"a schedule had nothing to report");return}let g;try{g=await r.deliver(e.destination,{text:m.text,title:e.name,...m.files.length>0?{files:m.files}:{}})}catch(s){await S(e,w,s instanceof Error?s.message:String(s));return}if(g.threadKey&&r.attach)try{await r.attach(g.threadKey,i)}catch(s){a.error({schedule:e.id,thread:g.threadKey,err:s},"a report was delivered but nothing will answer a reply to it")}r.schedules.recordSuccess(e.id,w),a.info({schedule:e.id,name:e.name,to:e.destination.label},"a schedule reported")}function $(e){r.schedules.remove(e.id);const t=R(e.nextRunAt,e.tz);if(!b(e.createdBy)){a.warn({schedule:e.id,createdBy:e.createdBy,due:t},"deleted a missed reminder whose creator can no longer be authorized");return}const i=`Bishop wasn't running at ${t}, when your reminder "${e.name}" was due, so it didn't run. It would have asked:
6
6
 
7
7
  ${e.prompt}`,n=(async()=>{let o;try{o=await r.reach?.(e.createdBy)}catch(f){a.warn({schedule:e.id,createdBy:e.createdBy,err:f},"can't message a reminder's creator directly, so telling its destination instead"),o=e.destination}if(!o){a.warn({schedule:e.id,name:e.name,createdBy:e.createdBy,due:t},"deleted a reminder that came due while Bishop was not running");return}a.info({schedule:e.id,name:e.name,due:t,to:o.label},"telling the creator about a reminder that came due while Bishop was not running"),await u(e,i,o)})().catch(o=>{a.error({schedule:e.id,err:o},"could not report a missed reminder")}).finally(()=>{l.delete(n)});l.add(n)}async function B(){if(v)return;const e=d();for(const t of r.schedules.due(e)){const i=!t.cron;if(!b(t.createdBy)){a.warn({schedule:t.id,createdBy:t.createdBy},i?"deleting a reminder whose creator can no longer be authorized":"skipping a schedule whose creator can no longer be authorized"),i?r.schedules.remove(t.id):p(t,e);continue}if(i){if(c.has(t.id))continue;r.schedules.remove(t.id)}else if(!p(t,e))continue;if(c.has(t.id)){a.warn({schedule:t.id,name:t.name},"skipping a firing while the previous one is still running");continue}const n=U(t.id,e);y.set(t.id,n);const o=x(t,e,n).catch(f=>{a.error({schedule:t.id,err:f},"a schedule firing threw")}).finally(()=>{c.delete(t.id),y.delete(t.id)});c.set(t.id,o)}}return{start(){const e=d(),t=r.schedules.due(e);for(const n of t){if(!n.cron){$(n);continue}a.info({schedule:n.id,name:n.name,due:new Date(n.nextRunAt).toISOString()},"skipping a firing that came due while Bishop was not running"),p(n,e)}const i=r.schedules.all().filter(n=>n.enabled);a.info({schedules:i.length,skipped:t.length},"watching the schedules"),h=setInterval(()=>{B().catch(n=>{a.error({err:n},"a scheduler pass failed; trying again on the next tick")})},T),h.unref()},tick:B,async settle(){await Promise.allSettled([...c.values(),...l])},async stop(){if(v=!0,h&&clearInterval(h),c.size>0){a.info({firings:c.size},"stopping scheduled runs still going");for(const n of c.keys()){const o=y.get(n);o&&r.cancel?.(o)}}const e=Promise.allSettled([...c.values(),...l]);let t;const i=new Promise(n=>{t=setTimeout(()=>{a.warn({firings:c.size},"giving up on scheduled runs that would not stop"),n()},F)});await Promise.race([e,i]),t&&clearTimeout(t)}}}export{P as MAX_CONSECUTIVE_FAILURES,D as authorizedToRun,U as firingThread,j as makeScheduler};
@@ -1 +1 @@
1
- import{BishopError as l}from"../errors.js";import{log as p}from"../log.js";import{retry as m}from"../retry.js";import{splitForSlack as b}from"./format.js";import{attachToSlack as S}from"./upload.js";function w(e){return e.threadTs?`${e.channel}:${e.threadTs}`:e.channel}function k(e){const[n,a]=e.split(":");if(!n)throw new l(`not a Slack destination: ${e}`);return{channel:n,...a?{threadTs:a}:{}}}function v(e){return/^[CG][A-Z0-9]{2,}$/.test(e)}function x(e){return/^D[A-Z0-9]{2,}$/.test(e)}function $(e){return/^[UW][A-Z0-9]{2,}$/.test(e)}const T=new Set(["ratelimited","rate_limited"]);function _(e){const n=e;if(!n)return!1;if(n.code==="slack_webapi_rate_limited_error")return{...n.retryAfter?{retryAfterMs:n.retryAfter*1e3}:{}};const a=n.data?.error;return!!(a&&T.has(a))}async function L(e,n){const{channel:a,threadTs:r}=k(n.handle),d=b(n.text),s=n.files??[],t=s.length>0?d.pop()??"":void 0;let i;for(const h of d){const c=r??i,f=await m(()=>e.chat.postMessage({channel:a,markdown_text:h,...c?{thread_ts:c}:{}}),{what:"chat.postMessage",retryable:_});i??=f.ts}const o=r??i;if(t!==void 0){const h={channel:a,...o?{threadTs:o}:{}};try{const c=await S(e,h,s,t);if(i??=c?.ts,!c&&t){const f=await m(()=>e.chat.postMessage({channel:a,markdown_text:t,...o?{thread_ts:o}:{}}),{what:"chat.postMessage",retryable:_});i??=f.ts}}catch(c){const f=s.map(u=>u.name).join(", ");p.error({channel:a,files:f,err:c},"could not send a file with a report");for(const u of[t,`_Could not upload ${f}._`]){if(!u)continue;const y=await e.chat.postMessage({channel:a,markdown_text:u,...o?{thread_ts:o}:{}}).catch(()=>{});i??=y?.ts}}}return p.info({channel:a,...o?{thread:o}:{},parts:d.length},"delivered to Slack"),r||!i?void 0:{channel:a,threadTs:i}}async function F(e,n){const{channel:a,threadTs:r}=k(n),d=r?" (thread)":"";try{const t=(await e.conversations.info({channel:a})).channel;if(t?.name)return`#${t.name}${d}`;if(t?.is_im&&t.user){const i=await e.users.info({user:t.user}),o=i.user?.profile?.display_name?.trim()||i.user?.name;if(o)return`@${o}`}}catch(s){p.debug({channel:a,err:s},"could not name a Slack destination")}return`${a}${d}`}async function A(e,n){const a=n.toLowerCase();for await(const r of e.paginate("conversations.list",{types:"public_channel,private_channel",exclude_archived:!0,limit:200})){const s=(r.channels??[]).find(t=>t.name?.toLowerCase()===a);if(s)return s}}async function C(e,n){const a=n.toLowerCase();if(a.includes("@"))try{const r=await e.users.lookupByEmail({email:n});if(r.user?.id)return r.user.id}catch(r){p.debug({email:n,err:r},"no Slack user with that address")}for await(const r of e.paginate("users.list",{limit:200})){const d=r.members??[];for(const s of d){if(s.deleted||!s.id)continue;if([s.name,s.profile?.display_name,s.profile?.real_name].some(i=>i?.toLowerCase()===a))return s.id}}}async function R(e,n,a={}){try{return await I(e,n,a)}catch(r){throw r instanceof l?r:M(r,n)}}function M(e,n){const a=e,r=a?.data?.error;return r==="missing_scope"?new l(`Slack won't let this app look up ${n}: it is missing the ${a?.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 l(`Slack could not resolve ${n}: ${r??(e instanceof Error?e.message:String(e))}`)}async function I(e,n,a){const r=n.trim();if(!r)throw new l("a Slack destination needs a channel or a person");const[d,s]=r.split(":"),t=(d??"").replace(/^[#@]/,"");if(!t)throw new l(`not a Slack destination: ${n}`);const i=r.startsWith("@")||$(t);if(x(t))return{interface:"slack",handle:w({channel:t,...s?{threadTs:s}:{}}),label:t};if(v(t)){const c=(await e.conversations.info({channel:t})).channel;if(!c?.id)throw new l(`Slack has no conversation ${t}`);return g(c,t),{interface:"slack",handle:w({channel:c.id,...s?{threadTs:s}:{}}),label:c.name?`#${c.name}`:c.id}}if(i){const h=$(t)?t:await C(e,t);if(!h)throw new l(`no Slack user matches @${t}`);if(a.allows&&!a.allows(h))throw new l(`@${t} 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 f=(await e.conversations.open({users:h})).channel?.id;if(!f)throw new l(`could not open a DM with @${t}`);return{interface:"slack",handle:w({channel:f,...s?{threadTs:s}:{}}),label:`@${t}`}}const o=await A(e,t);if(!o?.id)throw new l(`no Slack channel named #${t} that this app can see. A private channel has to have the app in it.`);return g(o,`#${t}`),{interface:"slack",handle:w({channel:o.id,...s?{threadTs:s}:{}}),label:`#${o.name??t}`}}function g(e,n){if(!(e.is_im||e.is_member!==!1))throw new l(`this app is not in ${n}. Invite it there first, or nothing sent to it will arrive.`)}export{L as deliverToSlack,F as describeSlackHandle,k as parseSlackHandle,R as resolveSlackDestination,w as slackHandle,_ as slackRetryable};
1
+ import{BishopError as l}from"../errors.js";import{log as p}from"../log.js";import{retry as w}from"../retry.js";import{splitForSlack as y}from"./format.js";import{attachToSlack as S}from"./upload.js";function m(e){return e.threadTs?`${e.channel}:${e.threadTs}`:e.channel}function k(e){const[n,a]=e.split(":");if(!n)throw new l(`not a Slack destination: ${e}`);return{channel:n,...a?{threadTs:a}:{}}}function v(e){return/^[CG][A-Z0-9]{2,}$/.test(e)}function x(e){return/^D[A-Z0-9]{2,}$/.test(e)}function $(e){return/^[UW][A-Z0-9]{2,}$/.test(e)}const T=new Set(["ratelimited","rate_limited"]);function _(e){const n=e;if(!n)return!1;if(n.code==="slack_webapi_rate_limited_error")return{...n.retryAfter?{retryAfterMs:n.retryAfter*1e3}:{}};const a=n.data?.error;return!!(a&&T.has(a))}async function L(e,n){const{channel:a,threadTs:r}=k(n.handle),d=y(n.text),s=n.files??[],t=s.length>0?d.pop()??"":void 0;let c;for(const f of d){const i=r??c,h=await w(()=>e.chat.postMessage({channel:a,markdown_text:f,...i?{thread_ts:i}:{}}),{what:"chat.postMessage",retryable:_});c??=h.ts}const o=r??c;if(t!==void 0){const f={channel:a,...o?{threadTs:o}:{}};try{const i=await S(e,f,s,t);if(c??=i?.ts,!i&&t){const h=await w(()=>e.chat.postMessage({channel:a,markdown_text:t,...o?{thread_ts:o}:{}}),{what:"chat.postMessage",retryable:_});c??=h.ts}}catch(i){const h=s.map(u=>u.name).join(", ");p.error({channel:a,files:h,err:i},"could not send a file with a report");for(const u of[t,`_Could not upload ${h}._`]){if(!u)continue;const g=await e.chat.postMessage({channel:a,markdown_text:u,...o?{thread_ts:o}:{}}).catch(()=>{});c??=g?.ts}}}return p.info({channel:a,...o?{thread:o}:{},parts:d.length},"delivered to Slack"),r||!c?void 0:{channel:a,threadTs:c}}async function F(e,n){const{channel:a,threadTs:r}=k(n),d=r?" (thread)":"";try{const t=(await e.conversations.info({channel:a})).channel;if(t?.name)return`#${t.name}${d}`;if(t?.is_im&&t.user){const o=(await e.users.info({user:t.user})).user,f=o?.profile?.display_name?.trim()||(o?.is_bot?o.profile?.real_name?.trim():void 0)||o?.name;if(f)return`@${f}`}}catch(s){p.debug({channel:a,err:s},"could not name a Slack destination")}return`${a}${d}`}async function A(e,n){const a=n.toLowerCase();for await(const r of e.paginate("conversations.list",{types:"public_channel,private_channel",exclude_archived:!0,limit:200})){const s=(r.channels??[]).find(t=>t.name?.toLowerCase()===a);if(s)return s}}async function C(e,n){const a=n.toLowerCase();if(a.includes("@"))try{const r=await e.users.lookupByEmail({email:n});if(r.user?.id)return r.user.id}catch(r){p.debug({email:n,err:r},"no Slack user with that address")}for await(const r of e.paginate("users.list",{limit:200})){const d=r.members??[];for(const s of d){if(s.deleted||!s.id)continue;if([s.name,s.profile?.display_name,s.profile?.real_name].some(c=>c?.toLowerCase()===a))return s.id}}}async function R(e,n,a={}){try{return await I(e,n,a)}catch(r){throw r instanceof l?r:M(r,n)}}function M(e,n){const a=e,r=a?.data?.error;return r==="missing_scope"?new l(`Slack won't let this app look up ${n}: it is missing the ${a?.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 l(`Slack could not resolve ${n}: ${r??(e instanceof Error?e.message:String(e))}`)}async function I(e,n,a){const r=n.trim();if(!r)throw new l("a Slack destination needs a channel or a person");const[d,s]=r.split(":"),t=(d??"").replace(/^[#@]/,"");if(!t)throw new l(`not a Slack destination: ${n}`);const c=r.startsWith("@")||$(t);if(x(t))return{interface:"slack",handle:m({channel:t,...s?{threadTs:s}:{}}),label:t};if(v(t)){const i=(await e.conversations.info({channel:t})).channel;if(!i?.id)throw new l(`Slack has no conversation ${t}`);return b(i,t),{interface:"slack",handle:m({channel:i.id,...s?{threadTs:s}:{}}),label:i.name?`#${i.name}`:i.id}}if(c){const f=$(t)?t:await C(e,t);if(!f)throw new l(`no Slack user matches @${t}`);if(a.allows&&!a.allows(f))throw new l(`@${t} 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 h=(await e.conversations.open({users:f})).channel?.id;if(!h)throw new l(`could not open a DM with @${t}`);return{interface:"slack",handle:m({channel:h,...s?{threadTs:s}:{}}),label:`@${t}`}}const o=await A(e,t);if(!o?.id)throw new l(`no Slack channel named #${t} that this app can see. A private channel has to have the app in it.`);return b(o,`#${t}`),{interface:"slack",handle:m({channel:o.id,...s?{threadTs:s}:{}}),label:`#${o.name??t}`}}function b(e,n){if(!(e.is_im||e.is_member!==!1))throw new l(`this app is not in ${n}. Invite it there first, or nothing sent to it will arrive.`)}export{L as deliverToSlack,F as describeSlackHandle,k as parseSlackHandle,R as resolveSlackDestination,m as slackHandle,_ as slackRetryable};
@@ -1 +1 @@
1
- import{log as a}from"../log.js";function d(t){const n=t.profile?.display_name?.trim(),s=t.profile?.real_name?.trim(),e=t.profile?.email?.trim(),r=t.name?.trim(),i=t.enterprise_user?.enterprise_id,o=t.is_restricted||t.is_ultra_restricted||t.is_stranger;return{userId:t.id??"",...n?{displayName:n}:{},...s?{realName:s}:{},...e?{email:e}:{},...r?{handle:r}:{},...t.team_id?{teamId:t.team_id}:{},...i?{enterpriseId:i}:{},...o?{outsider:!0}:{}}}const l="USLACKBOT";function c(t){return!!t.id&&!t.deleted&&t.id!==l}async function p(t,n=()=>!0){const s=[];let e;do{const r=await t.users.list({limit:200,...e?{cursor:e}:{}});for(const i of r.members??[])i.id&&n(i)&&s.push(d(i));e=r.response_metadata?.next_cursor||void 0}while(e);return a.debug({users:s.length},"paged the workspace member list"),s}export{c as isMentionable,p as listMembers,d as toUser};
1
+ import{log as a}from"../log.js";function d(t){const o=t.profile?.display_name?.trim(),s=t.profile?.real_name?.trim(),e=t.profile?.email?.trim(),r=t.name?.trim(),i=t.enterprise_user?.enterprise_id,n=t.is_restricted||t.is_ultra_restricted||t.is_stranger;return{userId:t.id??"",...o?{displayName:o}:{},...s?{realName:s}:{},...e?{email:e}:{},...r?{handle:r}:{},...t.team_id?{teamId:t.team_id}:{},...i?{enterpriseId:i}:{},...n?{outsider:!0}:{},...t.is_bot?{bot:!0}:{}}}const l="USLACKBOT";function c(t){return!!t.id&&!t.deleted&&t.id!==l}async function p(t,o=()=>!0){const s=[];let e;do{const r=await t.users.list({limit:200,...e?{cursor:e}:{}});for(const i of r.members??[])i.id&&o(i)&&s.push(d(i));e=r.response_metadata?.next_cursor||void 0}while(e);return a.debug({users:s.length},"paged the workspace member list"),s}export{c as isMentionable,p as listMembers,d as toUser};
@@ -1 +1 @@
1
- import{CODE as N}from"../interface/mentions.js";import{log as f}from"../log.js";function E(e){const r=e.displayName?.trim();if(r&&!/\s/.test(r))return r;const n=e.handle?.trim();return n&&!/\s/.test(n)?n:void 0}const w=2e3,A=/<@([UW][A-Z0-9]+)(?:\|([^>]*))?>|<!(here|channel|everyone)(?:\|[^>]*)?>|<!subteam\^([A-Z0-9]+)(?:\|([^>]*))?>|<#(C[A-Z0-9]+)(?:\|([^>]*))?>/g;async function M(e,r,n=w){return h(e,async()=>{const i=g(e,r).catch(()=>{});let a;const o=new Promise(t=>{a=setTimeout(()=>t(void 0),n)}),l=await Promise.race([i,o]);return clearTimeout(a),l!==void 0?l:(f.debug("naming the people mentioned took too long; using the labels Slack sent"),g(e,k))})}const I=/<((?:https?|mailto):[^|>\s]+)(?:\|([^>]*))?>/g;function P(e){return e.replace(I,(r,n,i)=>i?.trim()||L(n)).replaceAll("&lt;","<").replaceAll("&gt;",">").replaceAll("&amp;","&")}function L(e){return e.startsWith("mailto:")?e.slice(7):e}const k={lookup:async e=>({userId:e}),findByName:async()=>{}};async function h(e,r){try{return await r()}catch(n){return f.debug({err:n},"could not translate mentions; passing the text through"),e}}async function g(e,r){const n=[...e.matchAll(A)];if(n.length===0)return e;const i=await Promise.all(n.map(async l=>{const[,t,c,s,u,p,d,y]=l;if(t){const m=await r.lookup(t),b=c?.trim().replace(/^@+/,"");return`@${E(m)||b||t}`}return s?`@${s}`:u?`@${p?.trim().replace(/^@+/,"")||u}`:d?`#${y?.trim().replace(/^#+/,"")||d}`:l[0]}));let a="",o=0;return n.forEach((l,t)=>{const c=l.index??0;a+=e.slice(o,c)+i[t],o=c+l[0].length}),a+e.slice(o)}const T=/(^|[^\p{L}\p{N}@/<])@([\p{L}\p{N}][\p{L}\p{N}._-]{0,79})/gu,_=new Set(["here","channel","everyone","group"]);async function D(e,r){return h(e,async()=>{const n=e.split(N);return(await Promise.all(n.map((a,o)=>o%2===1?a:$(a,r)))).join("")})}async function $(e,r){const n=[...e.matchAll(T)];if(n.length===0)return e;const i=await Promise.all(n.map(async t=>{const c=t[1]??"",s=t[2]??"",u=s.replace(/[._-]+$/,""),p=s.slice(u.length);if(!u||_.has(u.toLowerCase()))return t[0];const d=await r.findByName(u);return d?`${c}<@${d.userId}>${p}`:t[0]}));let a="",o=0;n.forEach((t,c)=>{const s=t.index??0;a+=e.slice(o,s)+i[c],o=s+t[0].length});const l=a+e.slice(o);return l!==e&&f.debug("linked a mention in agent text"),l}export{D as linkMentions,E as mentionableName,P as plainText,M as renderMentions};
1
+ import{CODE as b}from"../interface/mentions.js";import{log as f}from"../log.js";function L(e){const r=e.displayName?.trim();if(r&&!/\s/.test(r))return r;const n=e.bot?e.realName?.trim():void 0;if(n&&_.test(n))return n;const a=e.handle?.trim();return a&&!/\s/.test(a)?a:void 0}const E=2e3,w=/<@([UW][A-Z0-9]+)(?:\|([^>]*))?>|<!(here|channel|everyone)(?:\|[^>]*)?>|<!subteam\^([A-Z0-9]+)(?:\|([^>]*))?>|<#(C[A-Z0-9]+)(?:\|([^>]*))?>/g;async function P(e,r,n=E){return h(e,async()=>{const a=g(e,r).catch(()=>{});let o;const l=new Promise(t=>{o=setTimeout(()=>t(void 0),n)}),i=await Promise.race([a,l]);return clearTimeout(o),i!==void 0?i:(f.debug("naming the people mentioned took too long; using the labels Slack sent"),g(e,k))})}const A=/<((?:https?|mailto):[^|>\s]+)(?:\|([^>]*))?>/g;function R(e){return e.replace(A,(r,n,a)=>a?.trim()||I(n)).replaceAll("&lt;","<").replaceAll("&gt;",">").replaceAll("&amp;","&")}function I(e){return e.startsWith("mailto:")?e.slice(7):e}const k={lookup:async e=>({userId:e}),findByName:async()=>{}};async function h(e,r){try{return await r()}catch(n){return f.debug({err:n},"could not translate mentions; passing the text through"),e}}async function g(e,r){const n=[...e.matchAll(w)];if(n.length===0)return e;const a=await Promise.all(n.map(async i=>{const[,t,c,s,u,d,p,N]=i;if(t){const m=await r.lookup(t),y=c?.trim().replace(/^@+/,"");return`@${L(m)||y||t}`}return s?`@${s}`:u?`@${d?.trim().replace(/^@+/,"")||u}`:p?`#${N?.trim().replace(/^#+/,"")||p}`:i[0]}));let o="",l=0;return n.forEach((i,t)=>{const c=i.index??0;o+=e.slice(l,c)+a[t],l=c+i[0].length}),o+e.slice(l)}const T=/(^|[^\p{L}\p{N}@/<])@([\p{L}\p{N}][\p{L}\p{N}._-]{0,79})/gu,_=/^[\p{L}\p{N}](?:[\p{L}\p{N}._-]{0,78}[\p{L}\p{N}])?$/u,$=new Set(["here","channel","everyone","group"]);async function W(e,r){return h(e,async()=>{const n=e.split(b);return(await Promise.all(n.map((o,l)=>l%2===1?o:O(o,r)))).join("")})}async function O(e,r){const n=[...e.matchAll(T)];if(n.length===0)return e;const a=await Promise.all(n.map(async t=>{const c=t[1]??"",s=t[2]??"",u=s.replace(/[._-]+$/,""),d=s.slice(u.length);if(!u||$.has(u.toLowerCase()))return t[0];const p=await r.findByName(u);return p?`${c}<@${p.userId}>${d}`:t[0]}));let o="",l=0;n.forEach((t,c)=>{const s=t.index??0;o+=e.slice(l,s)+a[c],l=s+t[0].length});const i=o+e.slice(l);return i!==e&&f.debug("linked a mention in agent text"),i}export{W as linkMentions,L as mentionableName,R as plainText,P as renderMentions};
@@ -1 +1 @@
1
- import{randomBytes as T,randomUUID as b}from"node:crypto";import{createServer as k}from"node:http";import{McpServer as O}from"@modelcontextprotocol/sdk/server/mcp.js";import{StreamableHTTPServerTransport as _}from"@modelcontextprotocol/sdk/server/streamableHttp.js";import{isInitializeRequest as x}from"@modelcontextprotocol/sdk/types.js";import{log as h}from"../log.js";import{ToolRefusal as y}from"./types.js";const B="bishop",v="/mcp",R="BISHOP_TOOL_TOKEN",A=1048576;function N(i){const r=i.headers.authorization;return r&&/^Bearer\s+(.+)$/i.exec(r.trim())?.[1]?.trim()||void 0}function p(i,r,a){i.writeHead(r,{"content-type":"application/json"}),i.end(JSON.stringify({jsonrpc:"2.0",id:null,error:{code:-32001,message:a}}))}async function I(i){const r=[];let a=0;for await(const n of i){if(a+=n.length,a>A)throw new Error("request body is too large");r.push(n)}if(r.length!==0)return JSON.parse(Buffer.concat(r).toString("utf8"))}const P="Bishop's tools answer to a person, and this message came from another bot. Ask someone in the conversation to do it.";function z(i,r){const a=new O({name:B,version:"1"});for(const n of i)a.registerTool(n.name,{description:n.description,inputSchema:n.input},async w=>{try{if(r.principal.kind==="bot")throw new y(P);return{content:[{type:"text",text:await n.run(w,r)}]}}catch(c){const f=c instanceof Error?c.message:String(c);return c instanceof y?h.info({tool:n.name,thread:r.thread.threadKey,reason:f},"a tool refused"):h.error({tool:n.name,thread:r.thread.threadKey,err:c},"a tool failed"),{content:[{type:"text",text:f}],isError:!0}}});return a}async function D(i){const r=i.host??"127.0.0.1",a=new Map,n=new Map;async function w(e,t){const o=new _({sessionIdGenerator:()=>b(),onsessioninitialized:s=>{if(a.get(e)!==t){o.close().catch(()=>{});return}n.set(s,{token:e,transport:o}),t.sessions.add(s)}});return o.onclose=()=>{const s=o.sessionId;s&&(n.delete(s),t.sessions.delete(s))},await z(t.tools,t.context).connect(o),o}const c=k((e,t)=>{(async()=>{try{if(!e.url?.startsWith(v))return p(t,404,"no such endpoint");const o=N(e),s=o?a.get(o):void 0;if(!s||!o)return p(t,401,"this call carries no valid turn credential");const d=e.method==="POST"?await I(e):void 0,l=e.headers["mcp-session-id"],m=typeof l=="string"?n.get(l):void 0;if(m){if(m.token!==o)return p(t,404,"no such session");await m.transport.handleRequest(e,t,d);return}if(typeof l=="string")return p(t,404,"no such session");if(!x(d))return p(t,400,"the first call on a connection must be initialize");await(await w(o,s)).handleRequest(e,t,d)}catch(o){h.error({err:o},"a tool call could not be handled"),t.headersSent?t.end():p(t,400,"that request could not be handled")}})()});await new Promise((e,t)=>{c.once("error",t),c.listen(0,r,()=>{c.removeListener("error",t),e()})});const f=c.address();if(!f||typeof f=="string")throw new Error("the tool endpoint started without a port");const g=`http://${r}:${f.port}${v}`;return h.info({url:g,tools:i.tools.map(e=>e.name)},"serving the agent its own tools"),{url:g,toolNames:i.tools.map(e=>e.name),grant(e,t){const o=T(32).toString("base64url"),s=t?.only,d=s?i.tools.filter(u=>s.includes(u.name)):i.tools;s&&d.length!==s.length&&h.error({asked:[...s],serving:d.map(u=>u.name)},"a turn asked for tools this endpoint does not serve");const l={context:e,tools:d,sessions:new Set};return a.set(o,l),{access:{url:g,tokenEnvVar:R,token:o},revoke:()=>{if(a.delete(o)){for(const u of l.sessions){const S=n.get(u);n.delete(u),S?.transport.close().catch(E=>{h.debug({err:E},"a tool connection did not close cleanly")})}l.sessions.clear()}}}},async stop(){for(const{transport:e}of n.values())await e.close().catch(()=>{});n.clear(),a.clear(),c.closeAllConnections(),await new Promise(e=>c.close(()=>e()))}}}export{v as PATH,B as SERVER_NAME,R as TOKEN_ENV_VAR,D as startToolServer};
1
+ import{randomBytes as S,randomUUID as k}from"node:crypto";import{createServer as E}from"node:http";import{McpServer as x}from"@modelcontextprotocol/sdk/server/mcp.js";import{StreamableHTTPServerTransport as T}from"@modelcontextprotocol/sdk/server/streamableHttp.js";import{isInitializeRequest as N}from"@modelcontextprotocol/sdk/types.js";import{log as m}from"../log.js";import{ToolRefusal as y}from"./types.js";const _="bishop",b="/mcp",v="BISHOP_TOOL_TOKEN",B=1048576;function O(t){const a=t.headers.authorization;return a&&/^Bearer\s+(.+)$/i.exec(a.trim())?.[1]?.trim()||void 0}function w(t,a,s){t.writeHead(a,{"content-type":"application/json"}),t.end(JSON.stringify({jsonrpc:"2.0",id:null,error:{code:-32001,message:s}}))}async function R(t){const a=[];let s=0;for await(const c of t){if(s+=c.length,s>B)throw new Error("request body is too large");a.push(c)}if(a.length!==0)return JSON.parse(Buffer.concat(a).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.",I="No turn is running in this conversation, so Bishop's tools have nobody to act for. Say what you need in your reply, and it can be done when someone next writes.";function P(t){return t.length>0?`Nobody asked for this turn, so only ${t.join(", ")} can be used in it.`:"Nobody asked for this turn, so none of Bishop's tools can be used in it."}function z(t){const a=new x({name:_,version:"1"});for(const s of t.tools)a.registerTool(s.name,{description:s.description,inputSchema:s.input},async c=>{const f=t.binding;try{if(!f)throw new y(I);if(f.only&&!f.only.includes(s.name))throw new y(P(f.only));const{context:i}=f;if(i.principal.kind==="bot")throw new y(A);return{content:[{type:"text",text:await s.run(c,i)}]}}catch(i){const h=i instanceof Error?i.message:String(i),p=f?.context.thread.threadKey;return i instanceof y?m.info({tool:s.name,thread:p,reason:h},"a tool refused"):m.error({tool:s.name,thread:p,err:i},"a tool failed"),{content:[{type:"text",text:h}],isError:!0}}});return a}async function j(t){const a=t.host??"127.0.0.1",s=new Map,c=new Map;async function f(e,n){const o=new T({sessionIdGenerator:()=>k(),onsessioninitialized:r=>{if(s.get(e)!==n){o.close().catch(()=>{});return}c.set(r,{token:e,transport:o}),n.sessions.add(r)}});return o.onclose=()=>{const r=o.sessionId;r&&(c.delete(r),n.sessions.delete(r))},await z(n).connect(o),o}const i=E((e,n)=>{(async()=>{try{if(!e.url?.startsWith(b))return w(n,404,"no such endpoint");const o=O(e),r=o?s.get(o):void 0;if(!r||!o)return w(n,401,"this call carries no valid credential");const u=e.method==="POST"?await R(e):void 0,d=e.headers["mcp-session-id"],l=typeof d=="string"?c.get(d):void 0;if(l){if(l.token!==o)return w(n,404,"no such session");await l.transport.handleRequest(e,n,u);return}if(typeof d=="string")return w(n,404,"no such session");if(!N(u))return w(n,400,"the first call on a connection must be initialize");await(await f(o,r)).handleRequest(e,n,u)}catch(o){m.error({err:o},"a tool call could not be handled"),n.headersSent?n.end():w(n,400,"that request could not be handled")}})()});await new Promise((e,n)=>{i.once("error",n),i.listen(0,a,()=>{i.removeListener("error",n),e()})});const h=i.address();if(!h||typeof h=="string")throw new Error("the tool endpoint started without a port");const p=`http://${a}:${h.port}${b}`;m.info({url:p,tools:t.tools.map(e=>e.name)},"serving the agent its own tools");function g(e){const n=S(32).toString("base64url");return s.set(n,e),{token:n,revoke:()=>{if(s.delete(n)){e.binding=void 0;for(const r of e.sessions){const u=c.get(r);c.delete(r),u?.transport.close().catch(d=>{m.debug({err:d},"a tool connection did not close cleanly")})}e.sessions.clear()}}}}return{url:p,toolNames:t.tools.map(e=>e.name),grant(e,n){const o=n?.only,r=o?t.tools.filter(l=>o.includes(l.name)):t.tools;o&&r.length!==o.length&&m.error({asked:[...o],serving:r.map(l=>l.name)},"a turn asked for tools this endpoint does not serve");const{token:u,revoke:d}=g({binding:{context:e},tools:r,sessions:new Set});return{access:{url:p,tokenEnvVar:v,token:u},revoke:d}},lease(){const e={binding:void 0,tools:t.tools,sessions:new Set},{token:n,revoke:o}=g(e);return{access:{url:p,tokenEnvVar:v,token:n},bind(r,u){const d=u?.only,l={context:r,...d?{only:d}:{}};return e.binding=l,()=>{e.binding===l&&(e.binding=void 0)}},revoke:o}},async stop(){for(const{transport:e}of c.values())await e.close().catch(()=>{});c.clear(),s.clear(),i.closeAllConnections(),await new Promise(e=>i.close(()=>e()))}}}export{b as PATH,_ as SERVER_NAME,v as TOKEN_ENV_VAR,j as startToolServer};
package/dist/turn.js CHANGED
@@ -1,3 +1,3 @@
1
- import{createHash as q,randomUUID as z}from"node:crypto";import{rm as G}from"node:fs/promises";import{join as L}from"node:path";import{contextChangeNote as Q,guidanceChangeNote as V}from"./harness/principal.js";import{mint as W}from"./ids.js";import{log as o}from"./log.js";import{stageFile as X}from"./sandbox.js";import{inbound as Z,outbound as $}from"./transcript.js";import{worktreeName as ee}from"./worktrees.js";function te(r){const{location:s,visibility:b,audience:I}=r;return{...s?{location:{kind:s.kind,...s.name?{name:s.name}:{}}}:{},visibility:b,audience:I}}const D=Symbol("not yet");function re(r){if(r)try{return JSON.parse(r)}catch{return}}function le(r){const s=new Map,b=new Set;async function I(e){const a=r.sharedDirNotice;if(!(!a||b.has(e.thread.threadKey))){b.add(e.thread.threadKey);try{await e.notify(a)}catch(i){o.debug({thread:e.thread.threadKey,err:i},"could not deliver the shared-directory notice")}}}function J(e,a,i,g,f){if(!r.tools||e.tools===!1)return;const c=Array.isArray(e.tools)?e.tools:void 0;if(c&&c.length===0)return;const l=i.share?.bind(i),n=l&&(async d=>{const m=await l(d);return g.set(m.ref,m),m});return r.tools.grant({thread:e.thread,principal:a,origin:e.origin,destination:()=>e.destination(),...n?{share:n}:{},...f?{stage:f}:{}},...c?[{only:c}]:[])}async function M(e){const a=r.principals;if(!a)return;const i=a.agent(e.origin.name);if(i)return i;try{return a.agent((await e.destination()).interface)}catch(g){o.debug({thread:e.thread.threadKey,err:g},"couldn't name the agent's account");return}}async function U(e){await I(e);const a=new AbortController,i=W("trn"),g=new Map,f=e.presence();let c,l;try{s.set(e.thread.threadKey,a);const n=e.observe?.().catch(t=>(o.warn({thread:e.thread.threadKey,err:t},"could not read where this conversation takes place"),{visibility:"unknown",audience:"unknown"})),d=e.freshSession?void 0:await r.sessions.get(e.thread.id),m=await r.cwd(e.thread),[_,p]=await Promise.all([e.messages(),d?void 0:e.history?.().catch(t=>{o.warn({thread:e.thread.threadKey,err:t},"could not read the history")})]),N=n&&await Promise.race([n,Promise.resolve(D)]),S=N===D?void 0:N;N===D&&n&&(o.debug({thread:e.thread.threadKey},"where this conversation takes place isn't read yet; not waiting for it"),n.then(t=>r.contexts?.record(e.thread.id,e.origin.name,t)).catch(t=>{o.warn({thread:e.thread.threadKey,err:t},"could not record where this conversation takes place")}));const[x,...O]=_;if(!x)throw new Error("a turn with no messages");const H=O.at(-1)?.principal??x.principal,R=r.transcript?await M(e):void 0;if(a.signal.aborted){o.info({thread:e.thread.threadKey},"turn stopped before the agent started");return}const K=await r.sandbox?.policy(m,e.thread);let A;if(K&&r.sandbox){const t=L(r.sandbox.staging,z());l=t,A=C=>X(K,C,t)}c=J(e,H,f,g,A),r.contexts?.record(e.thread.id,e.origin.name,S);const B=r.contexts?.told(e.thread.id),h=S?te(S):n&&re(B),y=h&&JSON.stringify(h),E=!!(d&&y&&B!==y);E&&h&&o.info({thread:e.thread.threadKey,...h},"this conversation changed since the agent was told about it; telling it again");const w=e.origin,T=q("sha256").update(w.guidance).digest("hex"),P=r.contexts?.guided(e.thread.id),j=!!(d&&P&&P!==T);j&&o.info({thread:e.thread.threadKey,interface:w.name},"the interface's guidance changed since the agent was given it; giving it again");const v=[e.note,E&&h?Q(h):void 0,j?V(w.guidance):void 0].filter(Boolean).join(`
1
+ import{createHash as se,randomUUID as V}from"node:crypto";import{rm as W}from"node:fs/promises";import{join as X}from"node:path";import{contextChangeNote as de,guidanceChangeNote as ce}from"./harness/principal.js";import{mint as Z}from"./ids.js";import{log as a}from"./log.js";import{SHARE_FILE_TOOL as le}from"./outgoing.js";import{SYSTEM_PRINCIPAL as he}from"./principals.js";import{stageFile as $}from"./sandbox.js";import{schedulePresence as fe}from"./schedule/presence.js";import{inbound as ge,outbound as ee}from"./transcript.js";import{worktreeName as ue}from"./worktrees.js";function we(o){const{location:y,visibility:O,audience:U}=o;return{...y?{location:{kind:y.kind,...y.name?{name:y.name}:{}}}:{},visibility:O,audience:U}}const j=Symbol("not yet");function ye(o){if(o)try{return JSON.parse(o)}catch{return}}function Ne(o){const y=new Map,O=new Set;async function U(e){const n=o.sharedDirNotice;if(!(!n||O.has(e.thread.threadKey))){O.add(e.thread.threadKey);try{await e.notify(n)}catch(r){a.debug({thread:e.thread.threadKey,err:r},"could not deliver the shared-directory notice")}}}const v=new Map;o.harness.keepAlive?.attach({started(e,n){v.get(e)?.lease?.revoke();const r=o.tools?.lease();return v.set(e,{processId:n,...r?{lease:r}:{}}),r?.access},ended(e,n){const r=v.get(e);r?.processId===n&&(r.lease?.revoke(),v.delete(e))},followUp(e,n){oe(e,n).catch(r=>{a.error({threadId:e,err:r},"a follow-up could not be handled")})}});function H(e,n,r,s,l,f,g){const u=l.share?.bind(l),h=u&&(async w=>{const m=await u(w);return f.set(m.ref,m),m});return{thread:e,principal:s,origin:n,destination:r,...h?{share:h}:{},...g?{stage:g}:{}}}function te(e){if(!o.tools||e.tools===!1)return!1;const n=Array.isArray(e.tools)?e.tools:void 0;return n&&n.length===0?!1:n}async function oe(e,n){const r=v.get(e),s=r?.followable,l=s?.thread.threadKey??e,f=Z("trn"),g=fe(),u=new Map;let h,w;const m=Date.now();a.info({thread:l,turn:f},"the agent is following up on its own");const d={thread:l,turn:f};try{if(s){let i;const D=s.sandbox;if(D&&o.sandbox){const C=X(o.sandbox.staging,V());h=C,i=S=>$(D,S,C)}w=r?.lease?.bind(H(s.thread,s.origin,s.destination,he,g,u,i),{only:[le]})}let c;try{for await(const i of n)s&&(i.type==="text"||i.type==="done")&&o.transcript?.record([ee({threadId:s.thread.id,turnId:f,kind:i.type==="done"?"result":"progress",agent:s.agent,text:i.text,shared:u,at:Date.now()})]),i.type==="tool"&&a.debug({thread:l,tool:i.name,detail:i.summary??i.detail},"tool activity"),await g.handle(i)}catch(i){c=i instanceof Error?i.message:String(i)}w?.(),w=void 0;const p=g.outcome(),T=c??p.failure,b={...d,totalMs:Date.now()-m};if(T){a.error({...b,err:T},"the agent failed while following up");return}if(p.silent||!p.text){a.info(b,"the agent followed up with nothing to say");return}const K=o.deliver?.();if(!s||!K){a.warn(b,"the agent followed up, but there's nowhere to post it; dropping it");return}const k=await s.destination();await K(k,{text:p.text,title:"Following up",...p.files.length>0?{files:p.files}:{}}),a.info({...b,to:k.label},"posted the agent's follow-up")}catch(c){a.error({...d,err:c},"couldn't post the agent's follow-up")}finally{w?.(),h&&await W(h,{recursive:!0,force:!0}).catch(c=>a.warn({dir:h,err:c},"couldn't remove a turn's shared-file copies"))}}async function ne(e){const n=o.principals;if(!n)return;const r=n.agent(e.origin.name);if(r)return r;try{return n.agent((await e.destination()).interface)}catch(s){a.debug({thread:e.thread.threadKey,err:s},"couldn't name the agent's account");return}}async function re(e){await U(e);const n=new AbortController,r=Z("trn"),s=new Map,l=e.presence();let f,g,u;const h=Date.now();let w,m;try{y.set(e.thread.threadKey,n);const d=e.observe?.().catch(t=>(a.warn({thread:e.thread.threadKey,err:t},"could not read where this conversation takes place"),{visibility:"unknown",audience:"unknown"})),c=e.freshSession?void 0:await o.sessions.get(e.thread.id),p=await o.cwd(e.thread),[T,b]=await Promise.all([e.messages(),c?void 0:e.history?.().catch(t=>{a.warn({thread:e.thread.threadKey,err:t},"could not read the history")})]),K=d&&await Promise.race([d,Promise.resolve(j)]),k=K===j?void 0:K;K===j&&d&&(a.debug({thread:e.thread.threadKey},"where this conversation takes place isn't read yet; not waiting for it"),d.then(t=>o.contexts?.record(e.thread.id,e.origin.name,t)).catch(t=>{a.warn({thread:e.thread.threadKey,err:t},"could not record where this conversation takes place")}));const[i,...D]=T;if(!i)throw new Error("a turn with no messages");const C=D.at(-1)?.principal??i.principal,S=o.transcript?await ne(e):void 0;if(n.signal.aborted){a.info({thread:e.thread.threadKey},"turn stopped before the agent started");return}const I=await o.sandbox?.policy(p,e.thread);let J;if(I&&o.sandbox){const t=X(o.sandbox.staging,V());u=t,J=R=>$(I,R,t)}const P=!!o.harness.keepAlive&&!e.unattended,M=te(e),F=M===!1?void 0:H(e.thread,e.origin,()=>e.destination(),C,l,s,J);F&&!P&&(f=o.tools?.grant(F,...M?[{only:M}]:[]));const ae=()=>{if(!P||g)return;const t=v.get(e.thread.id);t&&(t.followable={thread:e.thread,origin:e.origin,destination:()=>e.destination(),...S?{agent:S}:{},...I?{sandbox:I}:{}},F&&(g=t.lease?.bind(F,...M?[{only:M}]:[])))};w=Date.now()-h,o.contexts?.record(e.thread.id,e.origin.name,k);const Y=o.contexts?.told(e.thread.id),x=k?we(k):d&&ye(Y),A=x&&JSON.stringify(x),q=!!(c&&A&&Y!==A);q&&x&&a.info({thread:e.thread.threadKey,...x},"this conversation changed since the agent was told about it; telling it again");const N=e.origin,L=se("sha256").update(N.guidance).digest("hex"),z=o.contexts?.guided(e.thread.id),G=!!(c&&z&&z!==L);G&&a.info({thread:e.thread.threadKey,interface:N.name},"the interface's guidance changed since the agent was given it; giving it again");const _=[e.note,q&&x?de(x):void 0,G?ce(N.guidance):void 0].filter(Boolean).join(`
2
2
 
3
- `),Y=h?{...w,context:h}:w,k=r.model?.of(e.thread.id)??{};let u;const F=(t,C)=>r.transcript?.record([$({threadId:e.thread.id,turnId:i,kind:t,agent:R,text:C,shared:g,at:Date.now()})]);r.transcript?.record(Z({threadId:e.thread.id,turnId:i,messages:[x,...O],...v?{note:v}:{},...p?.length?{history:p}:{},at:Date.now()}));for await(const t of r.harness.runTurn({messages:[x,...O],cwd:m,threadKey:e.thread.threadKey,worktree:ee(e.thread.dirKey),...d?{sessionId:d}:{},...k.model?{model:k.model}:{},...k.effort?{effort:k.effort}:{},...v?{note:v}:{},...p?.length?{history:p}:{},origin:Y,...c?{tools:c.access}:{},...K?{sandbox:K}:{},signal:a.signal}))t.type==="session"&&(t.model&&(u=t.model),t.sessionId!==d&&await r.sessions.set(e.thread.id,t.sessionId),y&&r.contexts?.told(e.thread.id)!==y&&r.contexts?.tell(e.thread.id,y),r.contexts?.guided(e.thread.id)!==T&&r.contexts?.guide(e.thread.id,T)),t.type==="text"&&(F("progress",t.text),o.debug({thread:e.thread.threadKey,chars:t.text.length},"agent said something")),t.type==="tool"&&o.debug({thread:e.thread.threadKey,tool:t.name,detail:t.summary??t.detail},"tool activity"),t.type==="done"&&(F("result",t.text),o.info({thread:e.thread.threadKey,turn:i,...u?{model:u}:{},...t.usage},"turn complete")),t.type==="error"&&o.error({thread:e.thread.threadKey,turn:i,...u?{model:u}:{},kind:t.kind,err:t.message},"turn errored"),await f.handle(t)}catch(n){const d=n instanceof Error?n.message:String(n);throw o.error({thread:e.thread.threadKey,turn:i,err:n},"turn failed before the agent answered"),await f.handle({type:"error",kind:"turn_failed",message:d}),n}finally{c?.revoke(),s.get(e.thread.threadKey)===a&&s.delete(e.thread.threadKey),await f.finish(),l&&await G(l,{recursive:!0,force:!0}).catch(n=>o.warn({dir:l,err:n},"couldn't remove a turn's shared-file copies"))}}return{run:U,stop(e){const a=s.get(e);return a?(a.abort(),!0):!1}}}export{le as makeTurnRunner};
3
+ `),ie=x?{...N,context:x}:N,B=o.model?.of(e.thread.id)??{};let E;const Q=(t,R)=>o.transcript?.record([ee({threadId:e.thread.id,turnId:r,kind:t,agent:S,text:R,shared:s,at:Date.now()})]);o.transcript?.record(ge({threadId:e.thread.id,turnId:r,messages:[i,...D],..._?{note:_}:{},...b?.length?{history:b}:{},at:Date.now()}));for await(const t of o.harness.runTurn({messages:[i,...D],cwd:p,threadKey:e.thread.threadKey,worktree:ue(e.thread.dirKey),...c?{sessionId:c}:{},...B.model?{model:B.model}:{},...B.effort?{effort:B.effort}:{},..._?{note:_}:{},...b?.length?{history:b}:{},origin:ie,...f?{tools:f.access}:{},...I?{sandbox:I}:{},threadId:e.thread.id,...P?{keepAlive:P}:{},signal:n.signal}))ae(),t.type==="session"&&(m??=Date.now()-h,t.model&&(E=t.model),t.sessionId!==c&&await o.sessions.set(e.thread.id,t.sessionId),A&&o.contexts?.told(e.thread.id)!==A&&o.contexts?.tell(e.thread.id,A),o.contexts?.guided(e.thread.id)!==L&&o.contexts?.guide(e.thread.id,L)),t.type==="text"&&(Q("progress",t.text),a.debug({thread:e.thread.threadKey,chars:t.text.length},"agent said something")),t.type==="tool"&&a.debug({thread:e.thread.threadKey,tool:t.name,detail:t.summary??t.detail},"tool activity"),t.type==="done"&&(Q("result",t.text),a.info({thread:e.thread.threadKey,turn:r,...E?{model:E}:{},...t.usage},"turn complete")),t.type==="error"&&a.error({thread:e.thread.threadKey,turn:r,...E?{model:E}:{},kind:t.kind,err:t.message},"turn errored"),await l.handle(t)}catch(d){const c=d instanceof Error?d.message:String(d);throw a.error({thread:e.thread.threadKey,turn:r,err:d},"turn failed before the agent answered"),await l.handle({type:"error",kind:"turn_failed",message:c}),d}finally{a.debug({thread:e.thread.threadKey,turn:r,...w===void 0?{}:{preparedMs:w},...m===void 0?{}:{sessionMs:m},totalMs:Date.now()-h,...n.signal.aborted?{stopped:!0}:{}},"turn timing"),f?.revoke(),g?.(),y.get(e.thread.threadKey)===n&&y.delete(e.thread.threadKey),await l.finish(),u&&await W(u,{recursive:!0,force:!0}).catch(d=>a.warn({dir:u,err:d},"couldn't remove a turn's shared-file copies"))}}return{run:re,stop(e){const n=y.get(e);return n?(n.abort(),!0):!1}}}export{Ne as makeTurnRunner};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agentdeploymentco/bishop",
3
- "version": "0.7.1",
3
+ "version": "0.8.1",
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.300",
48
+ "@anthropic-ai/claude-agent-sdk": "0.3.310",
49
49
  "@microsoft/teams.apps": "^2.0.16",
50
50
  "@modelcontextprotocol/sdk": "^1.30.0",
51
- "@openai/codex-sdk": "0.162.1",
51
+ "@openai/codex-sdk": "0.164.0",
52
52
  "@slack/bolt": "^5.0.0",
53
53
  "@slack/web-api": "^8",
54
54
  "commander": "^13",