@agentdeploymentco/bishop 0.3.0 → 0.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -405,6 +405,8 @@ Each thread is its own agent session with its own history. Two threads are two i
405
405
 
406
406
  **Turn it down.** A thread showing every tool call is a thread that's hard to read when you already trust the agent. Send `` `! verbosity medium` `` to drop the tool card, or `` `! verbosity low` `` to leave only the answer. Send `` `! verbosity` `` to see where a thread stands, and `` `! verbosity default` `` to put it back. It applies to the turn that's running, so you can quiet one mid-flight. `slack.verbosity` in the config sets where every thread starts, and Slack always marks the thread as working whatever the level.
407
407
 
408
+ **Run one thread on another model.** Send `` `! model opus` `` to move a thread onto a different model, `` `! model terra high` `` to set the model and the effort together, or `` `! model --effort high` `` to change how hard it thinks and leave the model alone. Send `` `! model` `` to see where a thread stands, and `default` in either position to put it back: `` `! model default default` ``. Names go to the harness as you type them, in its own naming, so `opus` or `gpt-5.1-codex-max` means whatever it means there; Bishop only checks that it looks like a model name. A name the harness doesn't know does what a bad `--model` does, which differs by harness and is described under `agent.model` below: Codex fails the turn, Claude says so in prose about a model and silently ignores an effort it doesn't recognize. `` `! model` `` is how you see what a thread is being run with, which is the only sign of a typo in the silent case. Unlike verbosity this takes effect on the thread's next turn, not the one running, and it lasts for the life of the thread.
409
+
408
410
  **Commands go to Bishop, not the agent.** Anything whose whole message is code opening with `! ` is a command: Bishop runs it, answers in the thread, and the agent is never told about either. `` `! help` `` lists them, and any of them takes `--help`. What follows the `!` has to look like a command name, so code and pasted output reach the agent as they always did.
409
411
 
410
412
  **It doesn't answer every message in a busy thread.** In a thread where more than one other person is talking, the agent can leave a message alone when it wasn't for the agent, the way anyone else in the thread would. Slack still shows it thinking for a moment, the way somebody starts typing and then doesn't send. It always answers a message that @-mentions it, and always answers in a thread where you're the only other person talking, so a DM and a thread between you and the agent are unchanged.
@@ -729,7 +731,9 @@ Under Claude, Bishop refuses `Edit`, `Write`, and `NotebookEdit` in the shared d
729
731
 
730
732
  **When the agent directory isn't a git repository**, there's nothing to isolate. Bishop warns at startup and says so once in each thread, then edits the directory directly.
731
733
 
732
- ### Shared files
734
+ ### Files people share
735
+
736
+ Files go both ways: people attach them for the agent, and the agent sends them back. This half is the first.
733
737
 
734
738
  Bishop downloads what people attach and saves it under `.bishop/files`, one directory per thread, collected with everything else that thread left behind. The agent reads it from disk, so any file type works.
735
739
 
@@ -745,7 +749,15 @@ A file shared earlier in the conversation is named to the agent rather than down
745
749
 
746
750
  The default is 100. A file over the limit isn't refused: the agent is told its name and that it was too large, so it can say so. There is no restriction on type and won't be.
747
751
 
748
- **Slack needs the `files:read` scope**, which is new. An app set up before this has to be reinstalled to grant it, and until then Bishop warns at startup and the agent sees a note in place of each file. Run `bishop slack manifest` for the current manifest, or reinstall from App Settings. Gmail needs nothing new. **Teams needs `supportsFiles`** on the app's bot, without which it offers no attach button in a chat with the agent and only pasted images arrive. `bishop teams setup` sets it on a new app and tells you when an existing one is missing it, which needs the Teams CLI logged in.
752
+ **Slack needs the `files:read` and `files:write` scopes**, which are new. An app set up before these has to be reinstalled to grant them, and until then Bishop warns at startup about each one it is missing, the agent sees a note in place of each file, and a file it sends fails to upload. Run `bishop slack manifest` for the current manifest, or reinstall from App Settings. Gmail needs nothing new. **Teams needs `supportsFiles`** on the app's bot, without which it offers no attach button in a chat with the agent and only pasted images arrive. `bishop teams setup` sets it on a new app and tells you when an existing one is missing it, which needs the Teams CLI logged in.
753
+
754
+ ### Files the agent sends
755
+
756
+ The other half. The agent can send a file back: an upload in the Slack thread, an attachment on the mail in email. It registers the file with a tool, which checks it and hands back a reference, and writes that reference into its reply where the file should appear. In Slack the file arrives on the message carrying the words around it, the way a person sends one. A file it registers and never places is still sent, after the answer, so nothing is silently dropped.
757
+
758
+ Teams cannot carry one yet. An agent that tries in a Teams conversation is told so and can say what it made instead, and a scheduled report bound for Teams arrives with a line naming the file it could not attach.
759
+
760
+ The limit is 25MB per answer in Slack and 4MB by email, across everything one answer sends. Neither is configurable, and the email one is lower because of how Bishop talks to Gmail rather than what Gmail accepts. Anything past it is dropped with a line in the log and the message still goes. A scheduled report can carry files too, and an agent that reports nothing sends nothing, attachment included.
749
761
 
750
762
  ### Credentials
751
763
 
@@ -765,7 +777,7 @@ Anything else in `.env` reaches the agent's environment too, which is how `GH_TO
765
777
 
766
778
  ## Running it for real
767
779
 
768
- Bishop is one process that logs JSON to stdout and errors to stderr, so any process supervisor works. It needs only outbound access to Slack, since it uses Socket Mode: no public URL and no inbound port unless necessary. Teams is what makes it necessary, since Teams POSTs activities to an endpoint rather than holding a socket open. Bishop binds a port when Teams credentials are present and only then, and a tunnel gives that port a public hostname and a certificate without an inbound firewall rule.
780
+ Bishop is one process that logs JSON to stdout and errors to stderr, so any process supervisor works. Slack and Gmail need outbound access only, since one holds a socket open and the other polls. Teams is the exception: it POSTs activities to an endpoint rather than holding a socket open, so Bishop serves one when Teams credentials are present. That endpoint binds loopback like every other port Bishop opens, and a tunnel gives it a public hostname and a certificate without an inbound firewall rule.
769
781
 
770
782
  ```
771
783
  $ BISHOP_LOG_LEVEL=info bishop
package/dist/cli.js CHANGED
@@ -1,20 +1,20 @@
1
1
  #!/usr/bin/env node
2
- import{stat as ne}from"node:fs/promises";import{join as se,resolve as ae}from"node:path";import{Command as re}from"commander";import{agentSnapshots as ie,fixedAgentDir as ce}from"./agent-dir.js";import{looksRemote as M,parseRepoSpec as B}from"./agent-source.js";import{everyStore as U,storePath as K}from"./agent-store.js";import{attachmentStore as de}from"./attachments.js";import{HARNESSES as pe,loadConfig as N,nonEmptyFlag as F,parseHarnessName as me,positiveIntFlag as he,resolveAgent as le}from"./config.js";import{dbPath as z,openDb as q}from"./db.js";import{describeMissing as ue,loadEnv as S,readCredentials as fe}from"./env.js";import{BishopError as v}from"./errors.js";import{fileStore as we}from"./files.js";import{DEFAULT_MAX_AGE_DAYS as ge,makeCollector as ye,sweep as ke}from"./gc.js";import{ghAuthLogin as Se,ghCliIn as ve,gitAuthenticatesToGithub as be,withTemporaryGhConfig as Te}from"./github/gh.js";import{AGENT_SCOPES as Ie,checkGithubSetup as Ee}from"./github/setup.js";import{createGmail as Ae}from"./gmail/create.js";import{checkGmailSetup as _e}from"./gmail/setup.js";import{createHarness as Re}from"./harness/create.js";import{sharedFileFetcher as Ne}from"./interface/attachments.js";import{principalCheck as j}from"./interface/authorize.js";import{deliveryRouter as Oe}from"./interface/delivery.js";import{conversationMatch as Ge,destinationResolver as $e}from"./interface/destination.js";import{log as p,requestedLevel as xe}from"./log.js";import{addSchedule as Le,listSchedules as Ce,removeSchedule as He,setScheduleEnabled as Y}from"./schedule/cli.js";import{authorizedToRun as Pe,makeScheduler as De}from"./schedule/runner.js";import{scheduleStore as Me}from"./schedule/store.js";import{threadQueue as Be}from"./sessions/queue.js";import{interfaceState as Ue,retentionStore as W,sessionStore as Ke,threadHandover as Fe,threadMeta as ze}from"./sessions/store.js";import{ensureGitignore as qe,openBrowser as je,resolveAppDetails as Ye,SetupError as J,setupSlack as We,setupSlackManually as Je,withPrompt as Q,writeEnv as V}from"./setup.js";import{createSlackInterface as Qe}from"./slack/interface.js";import{buildManifest as Ve}from"./slack/manifest.js";import{createTeams as Xe}from"./teams/create.js";import{administratorInstructions as Ze,repointTeamsEndpoint as et,setupTeams as tt,setupTeamsManually as ot}from"./teams/setup.js";import{fileTools as nt}from"./tools/files.js";import{scheduleTools as st}from"./tools/schedules.js";import{startToolServer as at}from"./tools/server.js";import{makeTurnRunner as rt}from"./turn.js";import{version as it}from"./version.js";import{hasGitIdentity as ct,isGitRepo as dt,sharedDirNotice as pt,WORKTREE_DIR as mt}from"./worktrees.js";function u(e){process.stderr.write(`${e}
3
- `),process.exit(1)}async function ht(e){const t=ae(e??".");let n;try{n=(await ne(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 lt(e){const t=await dt(e),n={enabled:!0,isRepo:t};return t?(p.info({agentDir:e,worktrees:se(e,mt)},"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&&u(`bishop: unknown command "${e[0]}"
2
+ import{stat as ie}from"node:fs/promises";import{join as ce,resolve as de}from"node:path";import{Command as pe}from"commander";import{agentSnapshots as me,fixedAgentDir as he}from"./agent-dir.js";import{looksRemote as K,parseRepoSpec as F}from"./agent-source.js";import{everyStore as z,storePath as q}from"./agent-store.js";import{attachmentStore as le}from"./attachments.js";import{HARNESSES as ue,loadConfig as O,nonEmptyFlag as j,parseHarnessName as fe,positiveIntFlag as we,resolveAgent as ge}from"./config.js";import{dbPath as Y,openDb as W}from"./db.js";import{describeMissing as ye,loadEnv as S,readCredentials as ke}from"./env.js";import{BishopError as v}from"./errors.js";import{fileStore as Se}from"./files.js";import{DEFAULT_MAX_AGE_DAYS as ve,makeCollector as be,sweep as Te}from"./gc.js";import{ghAuthLogin as Ie,ghCliIn as Ee,gitAuthenticatesToGithub as Ae,withTemporaryGhConfig as _e}from"./github/gh.js";import{AGENT_SCOPES as Re,checkGithubSetup as Ne}from"./github/setup.js";import{createGmail as Oe}from"./gmail/create.js";import{checkGmailSetup as Ge}from"./gmail/setup.js";import{createHarness as $e}from"./harness/create.js";import{sharedFileFetcher as xe}from"./interface/attachments.js";import{principalCheck as J}from"./interface/authorize.js";import{deliveryRouter as Le}from"./interface/delivery.js";import{conversationMatch as Ce,destinationResolver as He}from"./interface/destination.js";import{log as p,requestedLevel as Me}from"./log.js";import{addSchedule as Pe,listSchedules as De,removeSchedule as Be,setScheduleEnabled as Q}from"./schedule/cli.js";import{authorizedToRun as Ue,makeScheduler as Ke}from"./schedule/runner.js";import{scheduleStore as Fe}from"./schedule/store.js";import{threadQueue as ze}from"./sessions/queue.js";import{interfaceState as qe,retentionStore as V,sessionStore as je,threadHandover as Ye,threadMeta as We}from"./sessions/store.js";import{ensureGitignore as Je,openBrowser as Qe,resolveAppDetails as Ve,SetupError as X,setupSlack as Xe,setupSlackManually as Ze,withPrompt as Z,writeEnv as ee}from"./setup.js";import{createSlackInterface as et}from"./slack/interface.js";import{buildManifest as tt}from"./slack/manifest.js";import{createTeams as ot}from"./teams/create.js";import{administratorInstructions as nt,repointTeamsEndpoint as st,setupTeams as rt,setupTeamsManually as at}from"./teams/setup.js";import{threadModel as it}from"./thread-model.js";import{fileTools as ct}from"./tools/files.js";import{scheduleTools as dt}from"./tools/schedules.js";import{startToolServer as pt}from"./tools/server.js";import{makeTurnRunner as mt}from"./turn.js";import{version as ht}from"./version.js";import{hasGitIdentity as lt,isGitRepo as ut,sharedDirNotice as ft,WORKTREE_DIR as wt}from"./worktrees.js";function u(e){process.stderr.write(`${e}
3
+ `),process.exit(1)}async function gt(e){const t=de(e??".");let n;try{n=(await ie(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 yt(e){const t=await ut(e),n={enabled:!0,isRepo:t};return t?(p.info({agentDir:e,worktrees:ce(e,wt)},"worktree mode is on"),await lt(e)||p.warn({agentDir:e},"git has no user.name or user.email here, so the agent cannot commit what it does in a worktree"),n):(p.warn({agentDir:e},"worktree mode is on but the agent directory is not a git repository; threads share it"),n)}function kt(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 St(e=[],t={}){e.length>0&&u(`bishop: unknown command "${e[0]}"
4
4
 
5
- Run \`bishop --help\` to see what's available.`);const n=process.cwd(),i=t.agent&&M(t.agent)?B(t.agent):void 0;t.autoUpdate&&!i&&u("--auto-update needs --agent to be a git URL. Bishop never updates a directory it was pointed at; that state is yours to manage."),S(n);const s=await N(n),a=fe();a.ok||u(ue(a));const r=le(s,t);for(const o of r.warnings)ut(o);const m=q(z(n)),c=Ke(m,r.harness);let d;const l=async o=>await d?.check(o)??{ok:!0},f=i?ie({spec:i,store:K(n,i.slug),dirs:c,autoUpdate:!!t.autoUpdate,recheck:l}):void 0,w=f?await f.start():await ht(t.agent),Z=f?f.forTurn:ce(w,c);p.info({bishopDir:n,agentDir:w,...w===n?{}:{separate:!0},...i?{source:i.cloneUrl,autoUpdate:!!t.autoUpdate}:{},app:s.slack?.app?.name},"bishop starting");const A=s.agent?.worktrees?await lt(w):void 0,b=Re(r.harness,{...r.options?{options:r.options}:{},...r.model?{model:r.model}:{},...r.effort?{effort:r.effort}:{},...A?{worktrees:A}:{},credentials:a.credentials}),_=await b.check(w);_.ok||u(b.describeStartupFailure(w,_.error)),p.info({harness:b.name,credentials:_.credentialSource??"unreported",model:_.model??r.model,...r.effort?{effort:r.effort}:{},...r.options?{agentOptions:Object.keys(r.options)}:{}},"harness ready"),d=b,p.info({sessions:c.count()},"opened session store");const G=ye({retention:W(m),stores:()=>U(n),home:n,...s.gc?.maxAgeDays===void 0?{}:{maxAgeDays:s.gc.maxAgeDays}});G.start();let y;const ee=o=>y?j(y)(o):Pe(o),te=async(o,h)=>{if(!y)throw new v("no interface is running, so there is nowhere to send");return $e(y,{optedInOnly:!0})(o,h)},oe=(o,h)=>y?Ge(y)(o,h):!1,$=we({home:n,...s.files?.maxMb===void 0?{}:{maxMb:s.files.maxMb}}),x=de(m),L=Me(m),C=await at({tools:[...st({schedules:L,authorized:ee,resolve:te,sameConversation:oe}),...nt({fetch:Ne({interfaces:()=>y??[],store:x,files:$})})]}),H=rt({harness:b,cwd:Z,sessions:c,tools:C,...A&&!A.isRepo?{sharedDirNotice:pt(w)}:{}}),P=Be(),T={threads:c,events:c,files:$,attachments:x,run:o=>P.run(o.thread.threadKey,()=>H.run(o)),stop:o=>H.stop(o)},g=[];if(a.credentials.slack&&g.push(await Qe({...T,credentials:a.credentials.slack,meta:ze(m),...s.slack?.verbosity?{verbosity:s.slack.verbosity}:{},...s.slack?.allow?{allow:s.slack.allow}:{},...s.slack?.maxBotTurns===void 0?{}:{maxBotTurns:s.slack.maxBotTurns}})),a.credentials.gmail)try{g.push(await Ae({...T,credentials:a.credentials.gmail,...s.gmail?{config:s.gmail}:{},state:Ue(m)}))}catch(o){o instanceof v&&u(o.message);const h=a.credentials.gmail,R=h.kind==="oauth"?"the authorized Gmail account":`${h.user}`;u(`Couldn't reach the mailbox for ${R}:
5
+ Run \`bishop --help\` to see what's available.`);const n=process.cwd(),i=t.agent&&K(t.agent)?F(t.agent):void 0;t.autoUpdate&&!i&&u("--auto-update needs --agent to be a git URL. Bishop never updates a directory it was pointed at; that state is yours to manage."),S(n);const s=await O(n),a=ke();a.ok||u(ye(a));const r=ge(s,t);for(const o of r.warnings)kt(o);const m=W(Y(n)),c=je(m,r.harness);let d;const l=async o=>await d?.check(o)??{ok:!0},f=i?me({spec:i,store:q(n,i.slug),dirs:c,autoUpdate:!!t.autoUpdate,recheck:l}):void 0,w=f?await f.start():await gt(t.agent),oe=f?f.forTurn:he(w,c);p.info({bishopDir:n,agentDir:w,...w===n?{}:{separate:!0},...i?{source:i.cloneUrl,autoUpdate:!!t.autoUpdate}:{},app:s.slack?.app?.name},"bishop starting");const A=s.agent?.worktrees?await yt(w):void 0,b=$e(r.harness,{...r.options?{options:r.options}:{},...r.model?{model:r.model}:{},...r.effort?{effort:r.effort}:{},...A?{worktrees:A}:{},credentials:a.credentials}),_=await b.check(w);_.ok||u(b.describeStartupFailure(w,_.error));const N=_.model??r.model;p.info({harness:b.name,credentials:_.credentialSource??"unreported",model:N,...r.effort?{effort:r.effort}:{},...r.options?{agentOptions:Object.keys(r.options)}:{}},"harness ready"),d=b,p.info({sessions:c.count()},"opened session store");const $=be({retention:V(m),stores:()=>z(n),home:n,...s.gc?.maxAgeDays===void 0?{}:{maxAgeDays:s.gc.maxAgeDays}});$.start();let y;const ne=o=>y?J(y)(o):Ue(o),se=async(o,h)=>{if(!y)throw new v("no interface is running, so there is nowhere to send");return He(y,{optedInOnly:!0})(o,h)},re=(o,h)=>y?Ce(y)(o,h):!1,x=Se({home:n,...s.files?.maxMb===void 0?{}:{maxMb:s.files.maxMb}}),L=le(m),C=Fe(m),H=await pt({tools:[...dt({schedules:C,authorized:ne,resolve:se,sameConversation:re}),...ct({fetch:xe({interfaces:()=>y??[],store:L,files:x})})]}),M=We(m),P=it(M),ae={...N?{model:N}:{},...r.effort?{effort:r.effort}:{}},D=mt({harness:b,cwd:oe,sessions:c,model:P,tools:H,...A&&!A.isRepo?{sharedDirNotice:ft(w)}:{}}),B=ze(),T={threads:c,events:c,files:x,attachments:L,run:o=>B.run(o.thread.threadKey,()=>D.run(o)),stop:o=>D.stop(o)},g=[];if(a.credentials.slack&&g.push(await et({...T,credentials:a.credentials.slack,meta:M,model:P,configuredModel:ae,...s.slack?.verbosity?{verbosity:s.slack.verbosity}:{},...s.slack?.allow?{allow:s.slack.allow}:{},...s.slack?.maxBotTurns===void 0?{}:{maxBotTurns:s.slack.maxBotTurns}})),a.credentials.gmail)try{g.push(await Oe({...T,credentials:a.credentials.gmail,...s.gmail?{config:s.gmail}:{},state:qe(m)}))}catch(o){o instanceof v&&u(o.message);const h=a.credentials.gmail,R=h.kind==="oauth"?"the authorized Gmail account":`${h.user}`;u(`Couldn't reach the mailbox for ${R}:
6
6
  ${o instanceof Error?o.message:String(o)}
7
7
 
8
- Run \`bishop gmail setup\` to check the credentials.`)}if(a.credentials.teams)try{g.push(await Xe({...T,credentials:a.credentials.teams,...s.teams?{config:s.teams}:{}}))}catch(o){o instanceof v&&u(o.message),u(`Couldn't start the Teams app for client ${a.credentials.teams.clientId}:
8
+ Run \`bishop gmail setup\` to check the credentials.`)}if(a.credentials.teams)try{g.push(await ot({...T,credentials:a.credentials.teams,...s.teams?{config:s.teams}:{}}))}catch(o){o instanceof v&&u(o.message),u(`Couldn't start the Teams app for client ${a.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.`)}y=g;for(const o of g)await o.start();p.info({interfaces:g.map(o=>o.name)},"bishop is listening");const D=De({schedules:L,run:T.run,threads:c,cancel:T.stop,deliver:Oe(g),authorized:j(g),attach:Fe(c)});D.start();for(const o of["SIGINT","SIGTERM"])process.once(o,()=>{p.info({signal:o,inFlight:P.size},"shutting down"),D.stop().catch(h=>p.error({err:h},"the scheduler failed to stop cleanly")).then(()=>Promise.allSettled(g.map(h=>h.stop()))).then(async h=>{const R=h.filter(I=>I.status==="rejected");for(const I of R)p.error({err:I.reason},"an interface failed to stop cleanly");await C.stop().catch(I=>{p.error({err:I},"the tool endpoint failed to stop cleanly")}),await G.stop(),m.close(),process.exit(R.length>0?1:0)})})}function wt(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.`)}y=g;for(const o of g)await o.start();p.info({interfaces:g.map(o=>o.name)},"bishop is listening");const U=Ke({schedules:C,run:T.run,threads:c,cancel:T.stop,deliver:Le(g),authorized:J(g),attach:Ye(c)});U.start();for(const o of["SIGINT","SIGTERM"])process.once(o,()=>{p.info({signal:o,inFlight:B.size},"shutting down"),U.stop().catch(h=>p.error({err:h},"the scheduler failed to stop cleanly")).then(()=>Promise.allSettled(g.map(h=>h.stop()))).then(async h=>{const R=h.filter(I=>I.status==="rejected");for(const I of R)p.error({err:I.reason},"an interface failed to stop cleanly");await H.stop().catch(I=>{p.error({err:I},"the tool endpoint failed to stop cleanly")}),await $.stop(),m.close(),process.exit(R.length>0?1:0)})})}function vt(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 gt(e){const t=process.cwd();S(t),e.agent&&!M(e.agent)&&u("gc --agent takes the git URL Bishop cloned, not a path. It narrows the pass to the files Bishop owns for one agent, and it owns none for a directory agent. Run `bishop gc` with no --agent to collect what its threads left behind.");const n=e.agent?[K(t,B(e.agent).slug)]:await U(t),i=await N(t),s=e.maxAgeDays??i.gc?.maxAgeDays;xe()||(p.level="error");const a=q(z(t));try{wt(await ke({retention:W(a),stores:n,home:t,...e.agent?{only:n}:{},...s===void 0?{}:{maxAgeDays:s}}))}finally{a.close()}}function yt(){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 kt(e={}){const t=process.cwd();S(t);const n=process.env.BISHOP_GMAIL_CLIENT_ID?.trim(),i=process.env.BISHOP_GMAIL_CLIENT_SECRET?.trim(),s=process.env.BISHOP_GMAIL_REFRESH_TOKEN?.trim(),a=process.env.BISHOP_GMAIL_USER?.trim(),r=process.env.BISHOP_GMAIL_SERVICE_ACCOUNT?.trim();n&&!i&&u("BISHOP_GMAIL_CLIENT_ID is set but BISHOP_GMAIL_CLIENT_SECRET is not.");const m=e.browser===!1||!yt()?"paste":"browser",c=await Q(!0,async d=>_e({...n&&i?{client:{clientId:n,clientSecret:i}}:{},...s?{refreshToken:s}:{},...a?{user:a}:{},...r?{serviceAccount:r}:{},mode:m,openBrowser:je,readPaste:async l=>(process.stdout.write(`Open this in a browser and approve it:
17
+ `)}async function bt(e){const t=process.cwd();S(t),e.agent&&!K(e.agent)&&u("gc --agent takes the git URL Bishop cloned, not a path. It narrows the pass to the files Bishop owns for one agent, and it owns none for a directory agent. Run `bishop gc` with no --agent to collect what its threads left behind.");const n=e.agent?[q(t,F(e.agent).slug)]:await z(t),i=await O(t),s=e.maxAgeDays??i.gc?.maxAgeDays;Me()||(p.level="error");const a=W(Y(t));try{vt(await Te({retention:V(a),stores:n,home:t,...e.agent?{only:n}:{},...s===void 0?{}:{maxAgeDays:s}}))}finally{a.close()}}function Tt(){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 It(e={}){const t=process.cwd();S(t);const n=process.env.BISHOP_GMAIL_CLIENT_ID?.trim(),i=process.env.BISHOP_GMAIL_CLIENT_SECRET?.trim(),s=process.env.BISHOP_GMAIL_REFRESH_TOKEN?.trim(),a=process.env.BISHOP_GMAIL_USER?.trim(),r=process.env.BISHOP_GMAIL_SERVICE_ACCOUNT?.trim();n&&!i&&u("BISHOP_GMAIL_CLIENT_ID is set but BISHOP_GMAIL_CLIENT_SECRET is not.");const m=e.browser===!1||!Tt()?"paste":"browser",c=await Z(!0,async d=>Ge({...n&&i?{client:{clientId:n,clientSecret:i}}:{},...s?{refreshToken:s}:{},...a?{user:a}:{},...r?{serviceAccount:r}:{},mode:m,openBrowser:Qe,readPaste:async l=>(process.stdout.write(`Open this in a browser and approve it:
18
18
 
19
19
  ${l}
20
20
 
@@ -22,12 +22,12 @@ It will finish on a http://127.0.0.1 address that fails to load. That is expecte
22
22
  Copy the whole address out of the browser and paste it here.
23
23
 
24
24
  `),d("Redirect URL",""))}));if(process.stdout.write(`${c.report}
25
- `),c.refreshToken){const d=await V(t,{BISHOP_GMAIL_REFRESH_TOKEN:c.refreshToken});process.stdout.write(`
25
+ `),c.refreshToken){const d=await ee(t,{BISHOP_GMAIL_REFRESH_TOKEN:c.refreshToken});process.stdout.write(`
26
26
  Saved BISHOP_GMAIL_REFRESH_TOKEN to ${d}
27
- `)}c.ok||process.exit(1)}async function St(e){const t=process.cwd(),n=process.env.GH_TOKEN?.trim();process.env.GH_TOKEN!==void 0&&!n&&delete process.env.GH_TOKEN,S(t);const i=process.env.GH_TOKEN?.trim(),s=!!i&&!n,a=e.prompt&&process.stdin.isTTY===!0,r=e.print?process.stderr:process.stdout,m=l=>r.write(l),c=l=>Ee({...i?{token:i,tokenIsSaved:s}:{},...a?{login:()=>Se(Ie.map(f=>f.scope),l),confirm:async f=>/^y/i.test(await Q(!0,w=>w(f,"y"),r))}:{},gitConfigured:be,gh:ve(l),...e.temporary?{temporary:!0}:{}}),d=e.temporary&&!i?await Te(c):await c();if(m(`${d.report}
27
+ `)}c.ok||process.exit(1)}async function Et(e){const t=process.cwd(),n=process.env.GH_TOKEN?.trim();process.env.GH_TOKEN!==void 0&&!n&&delete process.env.GH_TOKEN,S(t);const i=process.env.GH_TOKEN?.trim(),s=!!i&&!n,a=e.prompt&&process.stdin.isTTY===!0,r=e.print?process.stderr:process.stdout,m=l=>r.write(l),c=l=>Ne({...i?{token:i,tokenIsSaved:s}:{},...a?{login:()=>Ie(Re.map(f=>f.scope),l),confirm:async f=>/^y/i.test(await Z(!0,w=>w(f,"y"),r))}:{},gitConfigured:Ae,gh:Ee(l),...e.temporary?{temporary:!0}:{}}),d=e.temporary&&!i?await _e(c):await c();if(m(`${d.report}
28
28
  `),e.print)d.token&&process.stdout.write(`${JSON.stringify({GH_TOKEN:d.token})}
29
- `);else if(d.token){if(d.source==="gh"){const f=await V(t,{GH_TOKEN:d.token});m(`
29
+ `);else if(d.token){if(d.source==="gh"){const f=await ee(t,{GH_TOKEN:d.token});m(`
30
30
  Saved GH_TOKEN to ${f}
31
- `)}const l=await qe(t,[".env"]);l.length>0&&m(`Added ${l.join(", ")} to .gitignore
32
- `)}d.ok||process.exit(1)}async function vt(e){S(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 Je(t):await We(t)}async function bt(e){if(e.appId!==void 0&&!e.manual)throw new J("--app-id adopts an app somebody else created, so it needs --manual too.");if(e.endpoint!==void 0&&e.manual)throw new J("--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.");S(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 ot(t):await tt(t)}async function Tt(e){const t=process.cwd(),n=await N(t),i=await Ye({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,a)=>a);process.stdout.write(`${JSON.stringify(Ve(i),null,2)}
33
- `)}const k=new re().name("bishop").description("An agent gateway. Runs an agent and connects it to Slack and email.").version(it,"-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 <${pe.join("|")}>`,"which SDK runs the agent",me).option("--model <model>","the model to run, in the harness's own naming",F("--model")).option("--effort <effort>","how hard the model should think, in the harness's own naming",F("--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 ${ge}`,he("--max-age-days")).option("--agent <url>","only this agent's snapshots; defaults to every agent Bishop has cloned here").action(gt);const E=k.command("schedules").description("Prompts the agent runs on a clock, and where their answers go");E.command("list").description("Everything scheduled here, in your own time zone").action(()=>Ce(process.cwd())),E.command("add").description("Add a schedule").requiredOption("--name <name>","what a person sees in a list, and the subject of an email").requiredOption("--cron <expression>","five-field cron, or @daily and friends").requiredOption("--prompt <text>","what the agent is asked, including when to stay quiet").requiredOption("--to <destination>","#channel or @person for Slack, an address for Gmail").option("--tz <zone>","IANA zone the expression is read in; defaults to this machine").option("--interface <name>","which interface delivers, when --to is ambiguous").action(e=>Le(process.cwd(),e)),E.command("rm <id>").description("Remove a schedule").action(e=>He(process.cwd(),e)),E.command("disable <id>").description("Stop a schedule firing, keeping it").action(e=>Y(process.cwd(),e,!1)),E.command("enable <id>").description("Start a disabled schedule firing again, from now").action(e=>Y(process.cwd(),e,!0));const It=k.command("gmail").description("Gmail interface commands");It.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(kt);const Et=k.command("github").description("GitHub credentials for the agent");Et.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(St);const X=k.command("slack").description("Slack interface commands");X.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(vt),X.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(Tt);const O=k.command("teams").description("Microsoft Teams interface commands");O.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(bt),O.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)=>et(e,t.appId===void 0?{}:{appId:t.appId})),O.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(Ze(e.endpoint))});try{await k.parseAsync()}catch(e){throw e instanceof v&&u(e.message),e}
31
+ `)}const l=await Je(t,[".env"]);l.length>0&&m(`Added ${l.join(", ")} to .gitignore
32
+ `)}d.ok||process.exit(1)}async function At(e){S(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 Ze(t):await Xe(t)}async function _t(e){if(e.appId!==void 0&&!e.manual)throw new X("--app-id adopts an app somebody else created, so it needs --manual too.");if(e.endpoint!==void 0&&e.manual)throw new X("--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.");S(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 at(t):await rt(t)}async function Rt(e){const t=process.cwd(),n=await O(t),i=await Ve({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,a)=>a);process.stdout.write(`${JSON.stringify(tt(i),null,2)}
33
+ `)}const k=new pe().name("bishop").description("An agent gateway. Runs an agent and connects it to Slack and email.").version(ht,"-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 <${ue.join("|")}>`,"which SDK runs the agent",fe).option("--model <model>","the model to run, in the harness's own naming",j("--model")).option("--effort <effort>","how hard the model should think, in the harness's own naming",j("--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)=>St(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 ${ve}`,we("--max-age-days")).option("--agent <url>","only this agent's snapshots; defaults to every agent Bishop has cloned here").action(bt);const E=k.command("schedules").description("Prompts the agent runs on a clock, and where their answers go");E.command("list").description("Everything scheduled here, in your own time zone").action(()=>De(process.cwd())),E.command("add").description("Add a schedule").requiredOption("--name <name>","what a person sees in a list, and the subject of an email").requiredOption("--cron <expression>","five-field cron, or @daily and friends").requiredOption("--prompt <text>","what the agent is asked, including when to stay quiet").requiredOption("--to <destination>","#channel or @person for Slack, an address for Gmail").option("--tz <zone>","IANA zone the expression is read in; defaults to this machine").option("--interface <name>","which interface delivers, when --to is ambiguous").action(e=>Pe(process.cwd(),e)),E.command("rm <id>").description("Remove a schedule").action(e=>Be(process.cwd(),e)),E.command("disable <id>").description("Stop a schedule firing, keeping it").action(e=>Q(process.cwd(),e,!1)),E.command("enable <id>").description("Start a disabled schedule firing again, from now").action(e=>Q(process.cwd(),e,!0));const Nt=k.command("gmail").description("Gmail interface commands");Nt.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(It);const Ot=k.command("github").description("GitHub credentials for the agent");Ot.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(Et);const te=k.command("slack").description("Slack interface commands");te.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(At),te.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(Rt);const G=k.command("teams").description("Microsoft Teams interface commands");G.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(_t),G.command("endpoint <url>").description("Point the Teams app at a different URL, changing nothing else").option("--app-id <id>","the Teams app to point; defaults to the one in config").action((e,t)=>st(e,t.appId===void 0?{}:{appId:t.appId})),G.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(nt(e.endpoint))});try{await k.parseAsync()}catch(e){throw e instanceof v&&u(e.message),e}
@@ -1,5 +1,7 @@
1
- import{INBOX as g}from"./api.js";function I(u){const d=Buffer.from(u,"base64url").toString("utf8"),l=d.indexOf(`\r
1
+ import{INBOX as $}from"./api.js";function y(c){const r={};for(const o of c.replaceAll(/\r\n[ \t]+/g," ").split(`\r
2
+ `)){const n=o.indexOf(":");n>0&&(r[o.slice(0,n).toLowerCase()]=o.slice(n+1).trim())}return r}function b(c,r){return c["content-transfer-encoding"]==="base64"?Buffer.from(r.replaceAll(`\r
3
+ `,""),"base64").toString("utf8"):r}const x=/filename="([^"]*)"/;function w(c){const r=Buffer.from(c,"base64url").toString("utf8"),o=r.indexOf(`\r
2
4
  \r
3
- `),i=d.slice(0,l),c=d.slice(l+4),o={};for(const a of i.split(`\r
4
- `)){const f=a.indexOf(":");f>0&&(o[a.slice(0,f).toLowerCase()]=a.slice(f+1).trim())}return{text:o["content-transfer-encoding"]==="base64"?Buffer.from(c.replaceAll(`\r
5
- `,""),"base64").toString("utf8"):c,headers:o}}function $(u="agent@acme.com",d={}){const l=new Map,i=new Map,c=new Map,o=[],t=[],a=[],f=new Map;let m=!1,y=1;function b(e,s){const r=i.get(e)??new Set;for(const n of s.removeLabelIds??[])r.delete(n);for(const n of s.addLabelIds??[])r.add(n);i.set(e,r)}return{calls:t,sent:o,deliver(e){const s=e.id??`m${y++}`,r=e.threadId??s,n=Object.entries(e.headers??{}).map(([h,p])=>({name:h,value:p}));return l.set(s,{id:s,threadId:r,internalDate:e.internalDate??String(Date.now()),labelIds:e.labelIds??[g],payload:e.payload??{mimeType:"text/plain",headers:n,body:{data:Buffer.from(e.body??"","utf8").toString("base64url")}}}),a.push(s),s},attach(e,s){f.set(e,s)},labelsOn(e){return[...i.get(e)??[]]},expireHistory(){m=!0},async profile(){return t.push("profile"),{emailAddress:u,historyId:String(a.length)}},async sendAs(){return t.push("sendAs"),[{email:u,...d.displayName?{displayName:d.displayName}:{},isPrimary:!0},...(d.alsoSendsAs??[]).map(e=>({email:e}))]},async history(e){if(t.push(`history:${e}`),m)return m=!1,{messageIds:[],expired:!0};const s=Number(e);return{messageIds:a.slice(s),historyId:String(a.length),expired:!1}},async search(e,s){return t.push(`search:${e}`),a.slice(-s).reverse()},async message(e){t.push(`message:${e}`);const s=l.get(e);if(!s)throw new Error(`no such message ${e}`);return s},async thread(e){return t.push(`thread:${e}`),a.map(s=>l.get(s)).filter(s=>s?.threadId===e)},async attachment(e,s){t.push(`attachment:${s}`);const r=f.get(s);if(r===void 0)throw new Error(`no such attachment ${s} on ${e}`);return Buffer.from(r)},async send(e){return t.push("send"),o.push({raw:e.raw,...e.threadId?{threadId:e.threadId}:{},...I(e.raw)}),{id:`sent-${o.length}`,threadId:e.threadId??`sent-thread-${o.length}`}},async modifyMessage(e,s){t.push(`modifyMessage:${e}`);const r=l.get(e);if(!r)return;const n=new Set(r.labelIds??[]);for(const h of s.removeLabelIds??[])n.delete(h);for(const h of s.addLabelIds??[])n.add(h);r.labelIds=[...n]},async modifyThread(e,s){t.push(`modifyThread:${e}`),b(e,s)},async labels(){return t.push("labels"),[...c.values()]},async createLabel(e){t.push(`createLabel:${e}`);const s={id:`label-${c.size+1}`,name:e};return c.set(s.id,s),s}}}export{$ as fakeMailbox};
5
+ `),n=y(r.slice(0,o)),u=r.slice(o+4),f=/boundary="([^"]+)"/.exec(n["content-type"]??"")?.[1];if(!f)return{text:b(n,u),headers:n,files:[]};const[t,...i]=u.split(`--${f}`).slice(1,-1).map(l=>{const h=l.indexOf(`\r
6
+ \r
7
+ `),p=y(l.slice(0,h).replace(/^\r\n/,""));return{headers:p,body:b(p,l.slice(h+4))}});return{text:(t?.body??"").replace(/\r\n$/,""),headers:n,files:i.map(l=>({name:x.exec(l.headers["content-disposition"]??"")?.[1]??"",mimeType:(l.headers["content-type"]??"").split(";")[0]?.trim()??"",contents:l.body.replace(/\r\n$/,"")}))}}function S(c="agent@acme.com",r={}){const o=new Map,n=new Map,u=new Map,f=[],t=[],i=[],l=new Map;let h=!1,p=1;function g(e,s){const a=n.get(e)??new Set;for(const d of s.removeLabelIds??[])a.delete(d);for(const d of s.addLabelIds??[])a.add(d);n.set(e,a)}return{calls:t,sent:f,deliver(e){const s=e.id??`m${p++}`,a=e.threadId??s,d=Object.entries(e.headers??{}).map(([m,I])=>({name:m,value:I}));return o.set(s,{id:s,threadId:a,internalDate:e.internalDate??String(Date.now()),labelIds:e.labelIds??[$],payload:e.payload??{mimeType:"text/plain",headers:d,body:{data:Buffer.from(e.body??"","utf8").toString("base64url")}}}),i.push(s),s},attach(e,s){l.set(e,s)},labelsOn(e){return[...n.get(e)??[]]},expireHistory(){h=!0},async profile(){return t.push("profile"),{emailAddress:c,historyId:String(i.length)}},async sendAs(){return t.push("sendAs"),[{email:c,...r.displayName?{displayName:r.displayName}:{},isPrimary:!0},...(r.alsoSendsAs??[]).map(e=>({email:e}))]},async history(e){if(t.push(`history:${e}`),h)return h=!1,{messageIds:[],expired:!0};const s=Number(e);return{messageIds:i.slice(s),historyId:String(i.length),expired:!1}},async search(e,s){return t.push(`search:${e}`),i.slice(-s).reverse()},async message(e){t.push(`message:${e}`);const s=o.get(e);if(!s)throw new Error(`no such message ${e}`);return s},async thread(e){return t.push(`thread:${e}`),i.map(s=>o.get(s)).filter(s=>s?.threadId===e)},async attachment(e,s){t.push(`attachment:${s}`);const a=l.get(s);if(a===void 0)throw new Error(`no such attachment ${s} on ${e}`);return Buffer.from(a)},async send(e){return t.push("send"),f.push({raw:e.raw,...e.threadId?{threadId:e.threadId}:{},...w(e.raw)}),{id:`sent-${f.length}`,threadId:e.threadId??`sent-thread-${f.length}`}},async modifyMessage(e,s){t.push(`modifyMessage:${e}`);const a=o.get(e);if(!a)return;const d=new Set(a.labelIds??[]);for(const m of s.removeLabelIds??[])d.delete(m);for(const m of s.addLabelIds??[])d.add(m);a.labelIds=[...d]},async modifyThread(e,s){t.push(`modifyThread:${e}`),g(e,s)},async labels(){return t.push("labels"),[...u.values()]},async createLabel(e){t.push(`createLabel:${e}`);const s={id:`label-${u.size+1}`,name:e};return u.set(s.id,s),s}}}export{S as fakeMailbox};
@@ -1,2 +1,2 @@
1
- import{BishopError as C}from"../errors.js";import{log as n}from"../log.js";import{retry as B}from"../retry.js";import{GmailApiError as P,gmailSendRetryable as $,UNREAD as q}from"./api.js";import{looksLikeAddress as z}from"./authorize.js";import{resolveGmailDestination as H}from"./destination.js";import{mailEtiquetteNote as U}from"./etiquette.js";import{mailAttachment as R,mailFile as W,mailFileId as J,mailFilePayload as Q,parseMailFile as V}from"./files.js";import{continuesAThread as X,mailHistory as Z}from"./history.js";import{mailIdentity as ee}from"./identity.js";import{buildMessage as te,buildReply as ae,mailHeaders as re,parseMessage as ne}from"./message.js";import{gmailPresence as oe}from"./presence.js";import{participants as ie,routeMessage as se}from"./route.js";const le={name:"gmail",guidance:["This conversation is an email thread. Everything you write during a turn is collected and","sent as one plain-text reply to everyone on the thread, so give the whole answer in a","single message and never say you will follow up separately: there is no way for you to","send anything until someone writes to you again.","","Nobody is watching while you work. None of your tool calls are visible and nothing you say","arrives until the turn is over, so a turn that takes minutes costs nothing. Markdown is not","rendered; write plain prose, and use blank lines rather than formatting for structure.","","A <headers> tag above each message says who it was addressed to. Everyone on the To and Cc","lines receives your reply and there is no way to answer one of them privately. Being on Cc","usually means you are being kept informed rather than asked for something.","","A thread can be a conversation between other people that you were only copied on. Bishop",'marks a message you may leave alone with a <thread> tag carrying reply="optional": you were',"not the only person it was written to. Stay out of those unless you are sure somebody is","asking you to do something. To stay out, say <no-output> and nothing else, and Bishop sends","no mail at all. Every message without that tag is yours to answer.","","Sure means the sender named you, or asked for something you can act on. Work of yours coming","up in conversation is not, and neither is a question somebody else is better placed to","answer, and neither is having something you think worth adding. When you are sure, answer,","however many people are on the thread. A message replying to something you said is a","conversation you are already part of, so lean towards answering that one.","",'When the tag also carries addressed="no", your address is on neither line. You reached this',"through a group, a forwarding alias, or a blind copy, so nobody wrote to you and a single","address can stand for a thousand people. Answer only if the message asks you for something","by name.","","One address can be a thousand people wherever it appears. Judge how many you would be","interrupting by who an address reaches rather than by how many you can count, and treat one","you cannot place as a crowd."].join(`
2
- `)},A="gmail:historyId",G="gmail:attachedAt",D="in:inbox newer_than:2d",S=50;function N(a){return`gmail:${a}`}async function de(a){try{return await a.sendAs()}catch(y){return n.warn({err:y},"the mailbox would not say who it is; the agent will not know its own name or any alias"),[]}}async function Ae(a){const y=await a.api.profile(),h=ee(y.emailAddress,await de(a.api)),w=h.address,g={email:w,...h.name?{name:h.name}:{}},p={...le,self:{address:w,...h.name?{aka:[h.name]}:{}}},O=a.now??Date.now,b=a.state.get(G),f=b?Number(b):O();b||a.state.set(G,String(f));const x=new Set;let v,m,I=!1;const u=new Set;async function T(e,t){if(await a.labels.mark(e.threadId,"refused"),!(!t||x.has(e.threadId)))try{await a.api.send({raw:ae({from:g,to:[e.from],subject:e.subject,inReplyTo:e.messageId,references:e.references,body:t}),threadId:e.threadId}),x.add(e.threadId)}catch(i){n.warn({threadId:e.threadId,err:i},"could not send the refusal")}}function K(e,t){if(t.fetchable)return a.attachments.remember(e,p.name,{id:J(t),name:t.name,payload:Q(t)})}async function L(e,t){return t.attachments.length===0?[]:Promise.all(t.attachments.map(async(i,r)=>{const s=W(t.id,r,i),o=K(e.id,s),l=await a.files.save(e.dirKey,R(a.api,s,i.data));return o?{...l,ref:o}:l}))}function F(e,t,i,r){const s=[],o=ie(t,h).map(d=>({email:d})),l=U(t,h);return{thread:e,isNewThread:r,origin:p,...l?{note:l}:{},...X(t)?{history:()=>Z({api:a.api,message:t,self:h,allow:a.allow,remember:d=>a.attachments.remember(e.id,p.name,d)})}:{},async messages(){const d=await L(e,t);return[{principal:{id:t.from.email,email:t.from.email,...t.from.name?{realName:t.from.name}:{}},text:i,headers:re(t),...d.length>0?{files:d}:{}}]},presence:()=>oe({api:a.api,labels:a.labels,self:g,message:t,to:o,threadKey:e.threadKey,notices:s,...l?{mayDecline:!0}:{}}),async notify(d){s.push(d)},async destination(){return{interface:"gmail",handle:t.from.email,label:t.from.email}}}}async function Y(e){const t=`gmail:${e}`;if(await a.events.seen(t,{record:!1}))return;let i;try{i=await a.api.message(e)}catch(c){if(c instanceof P&&c.status===404){await a.events.seen(t),n.warn({messageId:e},"a message vanished before it could be read; skipping it");return}throw c}await a.events.seen(t);const r=ne(i);if(r.receivedAt<f){n.warn({messageId:e,threadId:r.threadId,receivedAt:new Date(r.receivedAt).toISOString(),floor:new Date(f).toISOString()},"ignoring an email that predates this agent");return}const s=se(r,{self:h,allow:a.allow});if(s.action==="ignore"){n.debug({messageId:e,threadId:r.threadId,reason:s.reason},"ignoring an email");return}if(s.action==="refuse"){n.warn({messageId:e,threadId:r.threadId,from:r.from.email,reason:s.reason},"refusing an unauthorized email"),await T(r,s.note);return}const o=N(r.threadId),l=await a.threads.has(o),d=await a.threads.remember(o);a.markRead&&await a.api.modifyMessage(e,{removeLabelIds:[q]}).catch(c=>{n.debug({messageId:e,err:c},"could not mark the message read")}),n.info({thread:o,from:r.from.email,newThread:!l},"running a turn from email");const M=a.run(F(d,r,s.text,!l)).catch(c=>{n.error({thread:o,err:c},"a turn from email failed")}).finally(()=>{u.delete(M)});u.add(M)}async function _(){const e=a.state.get(A);if(!e){const o=await a.api.profile();a.state.set(A,o.historyId),n.info({historyId:o.historyId},"starting from the current state of the mailbox");return}const t=await a.api.history(e);let i=t.messageIds,r=t.historyId;if(t.expired){n.warn({cursor:e},"the Gmail history cursor is too old; falling back to a search of recent mail");const o=await a.api.profile();i=(await a.api.search(D,S)).reverse(),i.length===S&&n.warn({limit:S,query:D},"the resync search filled its limit, so older mail in the window was not looked at"),r=o.historyId}let s=!0;for(const o of i){if(I)return;try{await Y(o)}catch(l){s=!1,n.error({messageId:o,err:l},"could not handle an email")}}if(!s){n.warn({cursor:e},"holding the Gmail cursor so unhandled mail is offered again");return}r&&a.state.set(A,r)}async function k(){if(!(m||I)){m=(async()=>{await _()})();try{await m}catch(e){n.error({err:e},"a Gmail poll failed; retrying on the next interval")}finally{m=void 0}}}async function E(){await Promise.allSettled([...u])}async function j(e){const t=e.handle.trim().toLowerCase();if(!z(t))throw new C(`"${e.handle}" is not an email address, so nothing was sent`);if(!a.allow.allows(t))throw new C(`${t} is not on this agent's allow list, so nothing was sent to it`);const i=e.title?.trim()||"A message from your agent",r=await B(()=>a.api.send({raw:te({from:g,to:[{email:t}],subject:i,body:e.text})}),{what:"gmail messages.send",retryable:$});return n.info({to:t,chars:e.text.length},"delivered by email"),r.threadId?{threadKey:N(r.threadId)}:(n.warn({to:t},"Gmail named no thread for a sent message, so a reply to it starts fresh"),{})}return{name:"gmail",poll:k,settle:E,deliver:j,allows:e=>a.allow.allows(e),async resolveDestination(e,t){return H(a.allow,e,{...t?.optedInOnly?{optedInOnly:!0}:{}})},sameConversation:(e,t)=>e.trim().toLowerCase()===t.trim().toLowerCase(),attachment:e=>{const t=V(e);return t?R(a.api,t):void 0},async start(){n.info({address:w,everyMs:a.pollIntervalMs,allow:a.allow.open?"anyone":"a list",answeringMailAfter:new Date(f).toISOString()},"watching the Gmail inbox"),await k(),v=setInterval(()=>{k()},a.pollIntervalMs)},async stop(){I=!0,v&&clearInterval(v),await m?.catch(()=>{}),u.size>0&&(n.info({turns:u.size},"waiting for email turns still running"),await E())}}}export{le as GMAIL_ORIGIN,Ae as createGmailInterface,N as threadKeyFor};
1
+ import{BishopError as C}from"../errors.js";import{log as n}from"../log.js";import{retry as B}from"../retry.js";import{GmailApiError as P,gmailSendRetryable as $,UNREAD as q}from"./api.js";import{looksLikeAddress as z}from"./authorize.js";import{resolveGmailDestination as H}from"./destination.js";import{mailEtiquetteNote as U}from"./etiquette.js";import{mailAttachment as R,mailFile as W,mailFileId as J,mailFilePayload as Q,parseMailFile as V}from"./files.js";import{continuesAThread as X,mailHistory as Z}from"./history.js";import{mailIdentity as ee}from"./identity.js";import{buildMessage as te,buildReply as ae,mailHeaders as re,parseMessage as ne,readAttachments as oe}from"./message.js";import{gmailPresence as ie}from"./presence.js";import{participants as se,routeMessage as le}from"./route.js";const de={name:"gmail",guidance:["This conversation is an email thread. Everything you write during a turn is collected and","sent as one plain-text reply to everyone on the thread, so give the whole answer in a","single message and never say you will follow up separately: there is no way for you to","send anything until someone writes to you again.","","Nobody is watching while you work. None of your tool calls are visible and nothing you say","arrives until the turn is over, so a turn that takes minutes costs nothing. Markdown is not","rendered; write plain prose, and use blank lines rather than formatting for structure.","","A <headers> tag above each message says who it was addressed to. Everyone on the To and Cc","lines receives your reply and there is no way to answer one of them privately. Being on Cc","usually means you are being kept informed rather than asked for something.","","A thread can be a conversation between other people that you were only copied on. Bishop",'marks a message you may leave alone with a <thread> tag carrying reply="optional": you were',"not the only person it was written to. Stay out of those unless you are sure somebody is","asking you to do something. To stay out, say <no-output> and nothing else, and Bishop sends","no mail at all. Every message without that tag is yours to answer.","","Sure means the sender named you, or asked for something you can act on. Work of yours coming","up in conversation is not, and neither is a question somebody else is better placed to","answer, and neither is having something you think worth adding. When you are sure, answer,","however many people are on the thread. A message replying to something you said is a","conversation you are already part of, so lean towards answering that one.","",'When the tag also carries addressed="no", your address is on neither line. You reached this',"through a group, a forwarding alias, or a blind copy, so nobody wrote to you and a single","address can stand for a thousand people. Answer only if the message asks you for something","by name.","","One address can be a thousand people wherever it appears. Judge how many you would be","interrupting by who an address reaches rather than by how many you can count, and treat one","you cannot place as a crowd."].join(`
2
+ `)},A="gmail:historyId",G="gmail:attachedAt",D="in:inbox newer_than:2d",S=50;function N(a){return`gmail:${a}`}async function he(a){try{return await a.sendAs()}catch(y){return n.warn({err:y},"the mailbox would not say who it is; the agent will not know its own name or any alias"),[]}}async function Se(a){const y=await a.api.profile(),h=ee(y.emailAddress,await he(a.api)),w=h.address,g={email:w,...h.name?{name:h.name}:{}},p={...de,self:{address:w,...h.name?{aka:[h.name]}:{}}},O=a.now??Date.now,b=a.state.get(G),f=b?Number(b):O();b||a.state.set(G,String(f));const x=new Set;let v,m,I=!1;const u=new Set;async function T(e,t){if(await a.labels.mark(e.threadId,"refused"),!(!t||x.has(e.threadId)))try{await a.api.send({raw:ae({from:g,to:[e.from],subject:e.subject,inReplyTo:e.messageId,references:e.references,body:t}),threadId:e.threadId}),x.add(e.threadId)}catch(s){n.warn({threadId:e.threadId,err:s},"could not send the refusal")}}function K(e,t){if(t.fetchable)return a.attachments.remember(e,p.name,{id:J(t),name:t.name,payload:Q(t)})}async function L(e,t){return t.attachments.length===0?[]:Promise.all(t.attachments.map(async(s,r)=>{const i=W(t.id,r,s),o=K(e.id,i),l=await a.files.save(e.dirKey,R(a.api,i,s.data));return o?{...l,ref:o}:l}))}function F(e,t,s,r){const i=[],o=se(t,h).map(d=>({email:d})),l=U(t,h);return{thread:e,isNewThread:r,origin:p,...l?{note:l}:{},...X(t)?{history:()=>Z({api:a.api,message:t,self:h,allow:a.allow,remember:d=>a.attachments.remember(e.id,p.name,d)})}:{},async messages(){const d=await L(e,t);return[{principal:{id:t.from.email,email:t.from.email,...t.from.name?{realName:t.from.name}:{}},text:s,headers:re(t),...d.length>0?{files:d}:{}}]},presence:()=>ie({api:a.api,labels:a.labels,self:g,message:t,to:o,threadKey:e.threadKey,notices:i,...l?{mayDecline:!0}:{}}),async notify(d){i.push(d)},async destination(){return{interface:"gmail",handle:t.from.email,label:t.from.email}}}}async function Y(e){const t=`gmail:${e}`;if(await a.events.seen(t,{record:!1}))return;let s;try{s=await a.api.message(e)}catch(c){if(c instanceof P&&c.status===404){await a.events.seen(t),n.warn({messageId:e},"a message vanished before it could be read; skipping it");return}throw c}await a.events.seen(t);const r=ne(s);if(r.receivedAt<f){n.warn({messageId:e,threadId:r.threadId,receivedAt:new Date(r.receivedAt).toISOString(),floor:new Date(f).toISOString()},"ignoring an email that predates this agent");return}const i=le(r,{self:h,allow:a.allow});if(i.action==="ignore"){n.debug({messageId:e,threadId:r.threadId,reason:i.reason},"ignoring an email");return}if(i.action==="refuse"){n.warn({messageId:e,threadId:r.threadId,from:r.from.email,reason:i.reason},"refusing an unauthorized email"),await T(r,i.note);return}const o=N(r.threadId),l=await a.threads.has(o),d=await a.threads.remember(o);a.markRead&&await a.api.modifyMessage(e,{removeLabelIds:[q]}).catch(c=>{n.debug({messageId:e,err:c},"could not mark the message read")}),n.info({thread:o,from:r.from.email,newThread:!l},"running a turn from email");const M=a.run(F(d,r,i.text,!l)).catch(c=>{n.error({thread:o,err:c},"a turn from email failed")}).finally(()=>{u.delete(M)});u.add(M)}async function _(){const e=a.state.get(A);if(!e){const o=await a.api.profile();a.state.set(A,o.historyId),n.info({historyId:o.historyId},"starting from the current state of the mailbox");return}const t=await a.api.history(e);let s=t.messageIds,r=t.historyId;if(t.expired){n.warn({cursor:e},"the Gmail history cursor is too old; falling back to a search of recent mail");const o=await a.api.profile();s=(await a.api.search(D,S)).reverse(),s.length===S&&n.warn({limit:S,query:D},"the resync search filled its limit, so older mail in the window was not looked at"),r=o.historyId}let i=!0;for(const o of s){if(I)return;try{await Y(o)}catch(l){i=!1,n.error({messageId:o,err:l},"could not handle an email")}}if(!i){n.warn({cursor:e},"holding the Gmail cursor so unhandled mail is offered again");return}r&&a.state.set(A,r)}async function k(){if(!(m||I)){m=(async()=>{await _()})();try{await m}catch(e){n.error({err:e},"a Gmail poll failed; retrying on the next interval")}finally{m=void 0}}}async function E(){await Promise.allSettled([...u])}async function j(e){const t=e.handle.trim().toLowerCase();if(!z(t))throw new C(`"${e.handle}" is not an email address, so nothing was sent`);if(!a.allow.allows(t))throw new C(`${t} is not on this agent's allow list, so nothing was sent to it`);const s=e.title?.trim()||"A message from your agent",r=await oe(e.files??[],(o,l)=>{n.error({to:t,file:o.name,err:l},"could not read a file to send with a report")}),i=await B(()=>a.api.send({raw:te({from:g,to:[{email:t}],subject:s,body:e.text,...r.length>0?{files:r}:{}})}),{what:"gmail messages.send",retryable:$});return n.info({to:t,chars:e.text.length,...r.length>0?{files:r.length}:{}},"delivered by email"),i.threadId?{threadKey:N(i.threadId)}:(n.warn({to:t},"Gmail named no thread for a sent message, so a reply to it starts fresh"),{})}return{name:"gmail",poll:k,settle:E,deliver:j,allows:e=>a.allow.allows(e),async resolveDestination(e,t){return H(a.allow,e,{...t?.optedInOnly?{optedInOnly:!0}:{}})},sameConversation:(e,t)=>e.trim().toLowerCase()===t.trim().toLowerCase(),attachment:e=>{const t=V(e);return t?R(a.api,t):void 0},async start(){n.info({address:w,everyMs:a.pollIntervalMs,allow:a.allow.open?"anyone":"a list",answeringMailAfter:new Date(f).toISOString()},"watching the Gmail inbox"),await k(),v=setInterval(()=>{k()},a.pollIntervalMs)},async stop(){I=!0,v&&clearInterval(v),await m?.catch(()=>{}),u.size>0&&(n.info({turns:u.size},"waiting for email turns still running"),await E())}}}export{de as GMAIL_ORIGIN,Se as createGmailInterface,N as threadKeyFor};
@@ -1,14 +1,15 @@
1
- function $(e){const t=new Map;for(const{name:o,value:r}of e?.headers??[]){const n=o.toLowerCase(),i=t.get(n);i?i.push(r):t.set(n,[r])}return t}function l(e,t){return e.get(t.toLowerCase())?.[0]??""}function a(e){const t=[];let o="",r=!1;for(const n of e){if(n==='"'&&(r=!r),n===","&&!r){t.push(o),o="";continue}o+=n}return t.push(o),t.map(A).filter(n=>!!n)}function A(e){const t=e.trim();if(!t)return;const o=/^(.*)<([^>]+)>\s*$/.exec(t);if(o){const r=p(o[1]?.trim().replace(/^"|"$/g,"").trim()??""),n=o[2]?.trim().toLowerCase()??"";return n?{...r?{name:r}:{},email:n}:void 0}if(t.includes("@"))return{email:t.toLowerCase()}}function p(e){return e.replaceAll(/=\?([^?]+)\?([BbQq])\?([^?]*)\?=/g,(t,o,r,n)=>{try{const i=r.toUpperCase()==="B"?Buffer.from(n,"base64"):Buffer.from(n.replaceAll("_"," ").replaceAll(/=([0-9A-Fa-f]{2})/g,(c,h)=>String.fromCharCode(Number.parseInt(h,16))),"binary");return new TextDecoder(o.toLowerCase()).decode(i)}catch{return t}})}function d(e){const t=e.body?.data;return t?Buffer.from(t,"base64url").toString("utf8"):""}function*f(e){if(e){yield e;for(const t of e.parts??[])yield*f(t)}}function g(e){const t=[...f(e)].filter(n=>!n.filename),o=t.filter(n=>n.mimeType==="text/plain").map(d).join(`
2
- `);if(o.trim())return o;const r=t.filter(n=>n.mimeType==="text/html").map(d).join(`
3
- `);return r.trim()?x(r):""}function x(e){return e.replaceAll(/<(script|style)[^>]*>[\s\S]*?<\/\1>/gi,"").replaceAll(/<br\s*\/?>/gi,`
1
+ import{randomBytes as g}from"node:crypto";import{readFile as x}from"node:fs/promises";function j(e){const t=new Map;for(const{name:o,value:r}of e?.headers??[]){const n=o.toLowerCase(),i=t.get(n);i?i.push(r):t.set(n,[r])}return t}function c(e,t){return e.get(t.toLowerCase())?.[0]??""}function l(e){const t=[];let o="",r=!1;for(const n of e){if(n==='"'&&(r=!r),n===","&&!r){t.push(o),o="";continue}o+=n}return t.push(o),t.map(C).filter(n=>!!n)}function C(e){const t=e.trim();if(!t)return;const o=/^(.*)<([^>]+)>\s*$/.exec(t);if(o){const r=b(o[1]?.trim().replace(/^"|"$/g,"").trim()??""),n=o[2]?.trim().toLowerCase()??"";return n?{...r?{name:r}:{},email:n}:void 0}if(t.includes("@"))return{email:t.toLowerCase()}}function b(e){return e.replaceAll(/=\?([^?]+)\?([BbQq])\?([^?]*)\?=/g,(t,o,r,n)=>{try{const i=r.toUpperCase()==="B"?Buffer.from(n,"base64"):Buffer.from(n.replaceAll("_"," ").replaceAll(/=([0-9A-Fa-f]{2})/g,(a,T)=>String.fromCharCode(Number.parseInt(T,16))),"binary");return new TextDecoder(o.toLowerCase()).decode(i)}catch{return t}})}function h(e){const t=e.body?.data;return t?Buffer.from(t,"base64url").toString("utf8"):""}function*s(e){if(e){yield e;for(const t of e.parts??[])yield*s(t)}}function B(e){const t=[...s(e)].filter(n=>!n.filename),o=t.filter(n=>n.mimeType==="text/plain").map(h).join(`
2
+ `);if(o.trim())return o;const r=t.filter(n=>n.mimeType==="text/html").map(h).join(`
3
+ `);return r.trim()?S(r):""}function S(e){return e.replaceAll(/<(script|style)[^>]*>[\s\S]*?<\/\1>/gi,"").replaceAll(/<br\s*\/?>/gi,`
4
4
  `).replaceAll(/<\/(p|div|tr|li|h[1-6])>/gi,`
5
5
  `).replaceAll(/<[^>]+>/g,"").replaceAll("&nbsp;"," ").replaceAll("&amp;","&").replaceAll("&lt;","<").replaceAll("&gt;",">").replaceAll("&quot;",'"').replaceAll("&#39;","'").replaceAll(/\n{3,}/g,`
6
6
 
7
- `).trim()}const T=[/^On .+ wrote:$/,/^-{2,}\s*Original Message\s*-{2,}$/i,/^_{10,}$/],j=/^(Sent|Date|To|Subject):\s/i;function S(e){const t=e.split(/\r?\n/),o=[];for(let r=0;r<t.length;r+=1){const n=(t[r]??"").trim();if(T.some(i=>i.test(n))||/^From:\s/i.test(n)&&t.slice(r+1,r+3).map(c=>c.trim()).some(c=>j.test(c))||n.startsWith(">")&&B(t,r))break;o.push(t[r]??"")}return o.join(`
8
- `).trim()}function B(e,t){for(let o=t;o<e.length;o+=1){const r=(e[o]??"").trim();if(!(!r||r.startsWith(">")))return!1}return!0}function w(e){const t=$(e.payload),o=a(l(t,"from"))[0]??{email:""},r=[...f(e.payload)].filter(n=>!!n.filename).map(n=>({name:n.filename,...n.body?.attachmentId?{attachmentId:n.body.attachmentId}:{},...n.body?.data===void 0?{}:{data:n.body.data},...n.mimeType?{mimeType:n.mimeType}:{},...n.body?.size===void 0?{}:{size:n.body.size}}));return{id:e.id,threadId:e.threadId,receivedAt:Number(e.internalDate??0),labelIds:e.labelIds??[],from:o,to:a(l(t,"to")),cc:a(l(t,"cc")),subject:p(l(t,"subject")),messageId:l(t,"message-id"),references:l(t,"references").split(/\s+/).filter(Boolean),body:S(g(e.payload)),attachments:r,headers:t}}function L(e){const t=a(l(e.headers,"reply-to"));return[{name:"To",value:e.to.map(u).join(", ")},{name:"Cc",value:e.cc.map(u).join(", ")},{name:"Reply-To",value:t.map(u).join(", ")},{name:"Date",value:e.receivedAt?new Date(e.receivedAt).toISOString():""},{name:"Subject",value:e.subject}]}function b(e){return/^[\x20-\x7e]*$/.test(e)?e:`=?UTF-8?B?${Buffer.from(e,"utf8").toString("base64")}?=`}const y=/[()<>[\]:;@\\,."]/;function u(e){return e.name?y.test(e.name)?`"${e.name.replaceAll("\\","\\\\").replaceAll('"','\\"')}" <${e.email}>`:`${e.name} <${e.email}>`:e.email}function s(e){if(!e.name)return e.email;const t=b(e.name);return t!==e.name?`${t} <${e.email}>`:y.test(e.name)?`"${e.name.replaceAll("\\","\\\\").replaceAll('"','\\"')}" <${e.email}>`:`${e.name} <${e.email}>`}function I(e){const t=e.trim();return t?/^re:/i.test(t)?t:`Re: ${t}`:"Re: (no subject)"}const m=78;function R(e){if(e.length<=m)return e;const[t="",...o]=e.split(" "),r=[];let n=t;for(const i of o){const c=r.length===0?m:m-1;if(n&&`${n} ${i}`.length>c){r.push(n),n=i;continue}n=n?`${n} ${i}`:i}return n&&r.push(n),r.join(`\r
9
- `)}function M(e){return C({...e,subject:I(e.subject)})}function C(e){const t=[...e.references??[],e.inReplyTo].filter(Boolean),o=[`From: ${s(e.from)}`,`To: ${e.to.map(s).join(", ")}`,...e.cc&&e.cc.length>0?[`Cc: ${e.cc.map(s).join(", ")}`]:[],`Subject: ${b(e.subject)}`,...e.inReplyTo?[`In-Reply-To: ${e.inReplyTo}`]:[],...t.length>0?[`References: ${t.join(" ")}`]:[],`Auto-Submitted: ${e.inReplyTo?"auto-replied":"auto-generated"}`,"MIME-Version: 1.0",'Content-Type: text/plain; charset="UTF-8"',"Content-Transfer-Encoding: base64"],r=Buffer.from(e.body,"utf8").toString("base64").replaceAll(/(.{76})/g,`$1\r
10
- `);return Buffer.from(`${o.map(R).join(`\r
7
+ `).trim()}const I=[/^On .+ wrote:$/,/^-{2,}\s*Original Message\s*-{2,}$/i,/^_{10,}$/],w=/^(Sent|Date|To|Subject):\s/i;function R(e){const t=e.split(/\r?\n/),o=[];for(let r=0;r<t.length;r+=1){const n=(t[r]??"").trim();if(I.some(i=>i.test(n))||/^From:\s/i.test(n)&&t.slice(r+1,r+3).map(a=>a.trim()).some(a=>w.test(a))||n.startsWith(">")&&E(t,r))break;o.push(t[r]??"")}return o.join(`
8
+ `).trim()}function E(e,t){for(let o=t;o<e.length;o+=1){const r=(e[o]??"").trim();if(!(!r||r.startsWith(">")))return!1}return!0}function F(e){const t=j(e.payload),o=l(c(t,"from"))[0]??{email:""},r=[...s(e.payload)].filter(n=>!!n.filename).map(n=>({name:n.filename,...n.body?.attachmentId?{attachmentId:n.body.attachmentId}:{},...n.body?.data===void 0?{}:{data:n.body.data},...n.mimeType?{mimeType:n.mimeType}:{},...n.body?.size===void 0?{}:{size:n.body.size}}));return{id:e.id,threadId:e.threadId,receivedAt:Number(e.internalDate??0),labelIds:e.labelIds??[],from:o,to:l(c(t,"to")),cc:l(c(t,"cc")),subject:b(c(t,"subject")),messageId:c(t,"message-id"),references:c(t,"references").split(/\s+/).filter(Boolean),body:R(B(e.payload)),attachments:r,headers:t}}function O(e){const t=l(c(e.headers,"reply-to"));return[{name:"To",value:e.to.map(u).join(", ")},{name:"Cc",value:e.cc.map(u).join(", ")},{name:"Reply-To",value:t.map(u).join(", ")},{name:"Date",value:e.receivedAt?new Date(e.receivedAt).toISOString():""},{name:"Subject",value:e.subject}]}function f(e){return/^[\x20-\x7e]*$/.test(e)?e:`=?UTF-8?B?${Buffer.from(e,"utf8").toString("base64")}?=`}const $=/[()<>[\]:;@\\,."]/;function u(e){return e.name?$.test(e.name)?`"${e.name.replaceAll("\\","\\\\").replaceAll('"','\\"')}" <${e.email}>`:`${e.name} <${e.email}>`:e.email}function m(e){if(!e.name)return e.email;const t=f(e.name);return t!==e.name?`${t} <${e.email}>`:$.test(e.name)?`"${e.name.replaceAll("\\","\\\\").replaceAll('"','\\"')}" <${e.email}>`:`${e.name} <${e.email}>`}function M(e){const t=e.trim();return t?/^re:/i.test(t)?t:`Re: ${t}`:"Re: (no subject)"}const L=4*1024*1024;async function q(e,t){let o=L;const r=e.filter(i=>i.size<=o?(o-=i.size,!0):(t(i,new Error(`${i.size} bytes is more than this mail can carry`)),!1));return(await Promise.all(r.map(async i=>{try{return{name:i.name,mimeType:i.mimeType,bytes:await x(i.path)}}catch(a){t(i,a);return}}))).filter(i=>i!==void 0)}const p=78;function d(e){if(e.length<=p)return e;const[t="",...o]=e.split(" "),r=[];let n=t;for(const i of o){const a=r.length===0?p:p-1;if(n&&`${n} ${i}`.length>a){r.push(n),n=i;continue}n=n?`${n} ${i}`:i}return n&&r.push(n),r.join(`\r
9
+ `)}function U(e){return D({...e,subject:M(e.subject)})}function y(e){return(typeof e=="string"?Buffer.from(e,"utf8"):Buffer.from(e)).toString("base64").replaceAll(/(.{76})/g,`$1\r
10
+ `)}function A(e){const t=f(e);return t!==e?t:`"${e.replaceAll("\\","\\\\").replaceAll('"','\\"')}"`}function D(e){const t=[...e.references??[],e.inReplyTo].filter(Boolean),o=e.files??[],r=`bishop-${g(8).toString("hex")}`,n=[`From: ${m(e.from)}`,`To: ${e.to.map(m).join(", ")}`,...e.cc&&e.cc.length>0?[`Cc: ${e.cc.map(m).join(", ")}`]:[],`Subject: ${f(e.subject)}`,...e.inReplyTo?[`In-Reply-To: ${e.inReplyTo}`]:[],...t.length>0?[`References: ${t.join(" ")}`]:[],`Auto-Submitted: ${e.inReplyTo?"auto-replied":"auto-generated"}`,"MIME-Version: 1.0",...o.length>0?[`Content-Type: multipart/mixed; boundary="${r}"`]:['Content-Type: text/plain; charset="UTF-8"',"Content-Transfer-Encoding: base64"]],i=o.length>0?[`--${r}`,'Content-Type: text/plain; charset="UTF-8"',"Content-Transfer-Encoding: base64","",y(e.body),...o.flatMap(a=>[`--${r}`,d(`Content-Type: ${a.mimeType}; name=${A(a.name)}`),"Content-Transfer-Encoding: base64",d(`Content-Disposition: attachment; filename=${A(a.name)}`),"",y(a.bytes)]),`--${r}--`].join(`\r
11
+ `):y(e.body);return Buffer.from(`${n.map(d).join(`\r
11
12
  `)}\r
12
13
  \r
13
- ${r}\r
14
- `,"utf8").toString("base64url")}export{C as buildMessage,M as buildReply,p as decodeWords,u as describeAddress,b as encodeHeaderValue,g as extractBody,R as foldHeader,s as formatAddress,l as header,L as mailHeaders,a as parseAddressList,w as parseMessage,I as replySubject,x as stripHtml,S as stripQuotedReply};
14
+ ${i}\r
15
+ `,"utf8").toString("base64url")}export{L as MAX_ATTACHED_BYTES,D as buildMessage,U as buildReply,b as decodeWords,u as describeAddress,f as encodeHeaderValue,B as extractBody,d as foldHeader,m as formatAddress,c as header,O as mailHeaders,l as parseAddressList,F as parseMessage,q as readAttachments,M as replySubject,S as stripHtml,R as stripQuotedReply};
@@ -1,5 +1,5 @@
1
- import{saysNoOutput as n}from"../harness/principal.js";import{log as i}from"../log.js";import{buildReply as c}from"./message.js";const g="The agent finished without saying anything. Nothing went wrong that it reported, but there is no answer to send.";function y(e){const o=[];let a,s="",l=!1;const h=e.labels.mark(e.message.threadId,"working");return{async handle(t){switch(t.type){case"text":if(n(t.text))return;o.push(t.text);return;case"done":s=n(t.text)?"":t.text,l=!!e.mayDecline&&n(t.text);return;case"error":a=t.message;return;default:return}},async finish(){if(await h,l&&!a){i.info({thread:e.threadKey},"the agent left this message alone"),await e.labels.mark(e.message.threadId,"quiet");return}const t=o.join(`
1
+ import{saysNoOutput as s}from"../harness/principal.js";import{log as o}from"../log.js";import{fileShelf as d}from"../outgoing.js";import{buildReply as u,MAX_ATTACHED_BYTES as y,readAttachments as w}from"./message.js";const b="The agent finished without saying anything. Nothing went wrong that it reported, but there is no answer to send.";function A(e){const l=[];let r,h="",m=!1;const n=d({budget:y}),g=e.labels.mark(e.message.threadId,"working");return{share:t=>n.offer(t),async handle(t){switch(t.type){case"text":if(s(t.text))return;l.push(n.strip(t.text));return;case"done":h=s(t.text)?"":n.strip(t.text),m=!!e.mayDecline&&s(t.text);return;case"error":r=t.message;return;default:return}},async finish(){if(await g,m&&!r){o.info({thread:e.threadKey},"the agent left this message alone"),await e.labels.mark(e.message.threadId,"quiet");return}const t=l.join(`
2
2
 
3
- `).trim()||s.trim(),m=[...e.notices,a?`Something went wrong: ${a}`:"",t||(a?"":g)].filter(Boolean).join(`
3
+ `).trim()||h.trim(),f=[...e.notices,r?`Something went wrong: ${r}`:"",t||(r?"":b)].filter(Boolean).join(`
4
4
 
5
- `);try{await e.api.send({raw:c({from:e.self,to:e.to,subject:e.message.subject,inReplyTo:e.message.messageId,references:e.message.references,body:m}),threadId:e.message.threadId}),i.info({thread:e.threadKey,to:e.to.map(r=>r.email),chars:m.length},"replied by email")}catch(r){i.error({thread:e.threadKey,err:r},"could not send the reply"),await e.labels.mark(e.message.threadId,"failed");return}await e.labels.mark(e.message.threadId,a?"failed":"done")}}}export{y as gmailPresence};
5
+ `),i=await w(n.offered(),(a,c)=>{o.error({thread:e.threadKey,file:a.name,err:c},"could not read a file the agent sent, so it is not on the reply")});try{await e.api.send({raw:u({from:e.self,to:e.to,subject:e.message.subject,inReplyTo:e.message.messageId,references:e.message.references,body:f,...i.length>0?{files:i}:{}}),threadId:e.message.threadId}),o.info({thread:e.threadKey,to:e.to.map(a=>a.email),chars:f.length,...i.length>0?{files:i.length}:{}},"replied by email")}catch(a){o.error({thread:e.threadKey,err:a},"could not send the reply"),await e.labels.mark(e.message.threadId,"failed");return}await e.labels.mark(e.message.threadId,r?"failed":"done")}}}export{A as gmailPresence};
@@ -1,11 +1,11 @@
1
- import{resolve as y}from"node:path";import{query as m}from"@anthropic-ai/claude-agent-sdk";import{log as c}from"../log.js";import{isolates as p,worktreePath as b}from"../worktrees.js";import{buildAgentOptions as k,CONTROLLED_KEYS as _,SETTINGS_ONLY_KEYS as x}from"./claude-options.js";import{ignoredKeys as g,shadowedKeys as v}from"./layers.js";import{buildPrompt as E,principalInstructions as S}from"./principal.js";function*T(e){switch(e.type){case"system":e.subtype==="init"&&(yield{type:"session",sessionId:e.session_id});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 n=C(t.input);yield{type:"tool",name:t.name,...n?{detail:n}:{}}}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:A(e.subtype)};return;default:return}}function C(e){if(!e||typeof e!="object")return;const t=e,n=t.file_path??t.notebook_path??t.path;if(typeof n=="string"&&n)return n.split("/").pop()??n;const i=t.url;if(typeof i=="string"&&i)try{return new URL(i).hostname}catch{return i.slice(0,40)}for(const u of["pattern","query","description"]){const a=t[u];if(typeof a=="string"&&a.trim()){const l=a.trim().split(`
2
- `)[0]??"";return l.length>48?`${l.slice(0,45)}...`:l}}}function K(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 A(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 P=new Set(["Edit","Write","NotebookEdit"]);function I(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:",`${b(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 N(e){const t=y(e);return({toolName:n,cwd:i})=>{if(P.has(n)&&y(i)===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 R(e,t,n,i){const u=p(e)?I(i,n):void 0;return[S(t),u].filter(a=>!!a?.trim()).join(`
1
+ import{resolve as m}from"node:path";import{query as g}from"@anthropic-ai/claude-agent-sdk";import{log as d}from"../log.js";import{isolates as p,worktreePath as k}from"../worktrees.js";import{buildAgentOptions as _,CONTROLLED_KEYS as x,SETTINGS_ONLY_KEYS as v}from"./claude-options.js";import{ignoredKeys as w,shadowedKeys as E}from"./layers.js";import{buildPrompt as S,principalInstructions as T}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 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 n=K(t.input);yield{type:"tool",name:t.name,...n?{detail:n}:{}}}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:P(e.subtype)};return;default:return}}function K(e){if(!e||typeof e!="object")return;const t=e,n=t.file_path??t.notebook_path??t.path;if(typeof n=="string"&&n)return n.split("/").pop()??n;const i=t.url;if(typeof i=="string"&&i)try{return new URL(i).hostname}catch{return i.slice(0,40)}for(const u of["pattern","query","description"]){const l=t[u];if(typeof l=="string"&&l.trim()){const c=l.trim().split(`
2
+ `)[0]??"";return c.length>48?`${c.slice(0,45)}...`:c}}}function A(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 P(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 I=new Set(["Edit","Write","NotebookEdit"]);function N(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:",`${k(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 R(e){const t=m(e);return({toolName:n,cwd:i})=>{if(I.has(n)&&m(i)===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 L(e,t,n,i){const u=p(e)?N(i,n):void 0;return[T(t),u].filter(l=>!!l?.trim()).join(`
4
4
 
5
- `)}function L(e){return async t=>{const n=t,i=e({toolName:n.tool_name,cwd:n.cwd});return i?(c.info({tool:n.tool_name,cwd:n.cwd},"refusing an edit in the shared checkout"),{continue:!0,hookSpecificOutput:{hookEventName:"PreToolUse",permissionDecision:"deny",permissionDecisionReason:i}}):{continue:!0}}}function Y(e={}){const t=g(e.options,_);t.length>0&&c.warn({ignored:t},"these agent.options are set by Bishop and cannot be overridden; ignoring them");const n={model:e.model,effort:e.effort},i=v(e.options,n);i.length>0&&c.warn({shadowed:i},"agent.model and agent.effort override these agent.options");const u=g(e.options,x);u.length>0&&c.warn({misplaced:u},"the harness reads these from agent.options.settings only and ignores them here");const a=e.worktrees;if(typeof e.options?.settings=="string"){const r=["auto memory is left to that file and the agent's own settings"];p(a)&&r.push("new worktrees branch from origin/<default-branch> rather than HEAD"),c.warn({settings:e.options.settings},`agent.options.settings names a file, so Bishop applies no settings defaults: ${r.join("; ")}`)}const l=(r,s)=>{const o=p(a)?N(r.cwd):void 0,f=o?{PreToolUse:[{hooks:[L(o)]}]}:void 0;return k({...e.options?{configOptions:e.options}:{},general:n,...e.apiKey?{apiKey:e.apiKey}:{},...f?{hooks:f,worktrees:!0}:{},...s?{tools:s}:{},controlled:r})};return{name:"claude",describeStartupFailure(r,s){return`The Claude agent couldn't start in ${r}:
5
+ `)}function $(e,t){return{model:t?.model??e.model,effort:t?.effort??e.effort}}function D(e){return async t=>{const n=t,i=e({toolName:n.tool_name,cwd:n.cwd});return i?(d.info({tool:n.tool_name,cwd:n.cwd},"refusing an edit in the shared checkout"),{continue:!0,hookSpecificOutput:{hookEventName:"PreToolUse",permissionDecision:"deny",permissionDecisionReason:i}}):{continue:!0}}}function F(e={}){const t=w(e.options,x);t.length>0&&d.warn({ignored:t},"these agent.options are set by Bishop and cannot be overridden; ignoring them");const n=r=>$(e,r),i=n(),u=E(e.options,i);u.length>0&&d.warn({shadowed:u},"agent.model and agent.effort override these agent.options");const l=w(e.options,v);l.length>0&&d.warn({misplaced:l},"the harness reads these from agent.options.settings only and ignores them here");const c=e.worktrees;if(typeof e.options?.settings=="string"){const r=["auto memory is left to that file and the agent's own settings"];p(c)&&r.push("new worktrees branch from origin/<default-branch> rather than HEAD"),d.warn({settings:e.options.settings},`agent.options.settings names a file, so Bishop applies no settings defaults: ${r.join("; ")}`)}const y=(r,s,o)=>{const f=p(c)?R(r.cwd):void 0,a=f?{PreToolUse:[{hooks:[D(f)]}]}:void 0;return _({...e.options?{configOptions:e.options}:{},general:n(o),...e.apiKey?{apiKey:e.apiKey}:{},...a?{hooks:a,worktrees:!0}:{},...s?{tools:s}:{},controlled:r})};return{name:"claude",describeStartupFailure(r,s){return`The Claude agent couldn't start in ${r}:
6
6
  ${s}
7
7
 
8
8
  Bishop does not manage Anthropic credentials. The agent authenticates the
9
9
  same way \`claude\` does in this directory, so make sure that works first:
10
10
  ANTHROPIC_API_KEY in the environment or .env, or
11
- an existing \`claude\` login on this machine.`},async check(r){const s=new AbortController;try{for await(const o of m({prompt:"ping",options:{...l({cwd:r,systemPrompt:void 0,abortController:s}),maxTurns:1,persistSession:!1}})){if(o.type==="system"&&o.subtype==="init")return s.abort(),{ok:!0,credentialSource:K(o.apiKeySource),...o.model?{model:o.model}:{}};if(o.type==="result")break}return{ok:!1,error:"the harness exited without starting a session"}}catch(o){return s.signal.aborted?{ok:!0}:{ok:!1,error:o instanceof Error?o.message:String(o)}}},async*runTurn(r){const s=new AbortController,o=()=>s.abort();r.signal.addEventListener("abort",o,{once:!0});let f=!1;try{const d=m({prompt:E(r),options:l({cwd:r.cwd,systemPrompt:{type:"preset",preset:"claude_code",append:R(a,r.origin,r.worktree,r.cwd)},abortController:s,...r.sessionId?{resume:r.sessionId}:{}},r.tools)});for await(const w of d)for(const h of T(w))(h.type==="done"||h.type==="error")&&(f=!0),yield h}catch(d){s.signal.aborted?c.debug({thread:r.threadKey},"claude turn aborted"):f?c.debug({err:d},"query() threw after reporting a result"):(c.error({err:d},"harness turn failed"),yield{type:"error",kind:"harness_failure",message:d instanceof Error?d.message:String(d)})}finally{r.signal.removeEventListener("abort",o)}}}}export{Y as claudeHarness,N as editGuard,R as systemPromptAppend,C as toolDetail,I as worktreeNote};
11
+ an existing \`claude\` login on this machine.`},async check(r){const s=new AbortController;try{for await(const o of g({prompt:"ping",options:{...y({cwd:r,systemPrompt:void 0,abortController:s}),maxTurns:1,persistSession:!1}})){if(o.type==="system"&&o.subtype==="init")return s.abort(),{ok:!0,credentialSource:A(o.apiKeySource),...o.model?{model:o.model}:{}};if(o.type==="result")break}return{ok:!1,error:"the harness exited without starting a session"}}catch(o){return s.signal.aborted?{ok:!0}:{ok:!1,error:o instanceof Error?o.message:String(o)}}},async*runTurn(r){const s=new AbortController,o=()=>s.abort();r.signal.addEventListener("abort",o,{once:!0});let f=!1;try{const a=g({prompt:S(r),options:y({cwd:r.cwd,systemPrompt:{type:"preset",preset:"claude_code",append:L(c,r.origin,r.worktree,r.cwd)},abortController:s,...r.sessionId?{resume:r.sessionId}:{}},r.tools,r)});for await(const b of a)for(const h of C(b))(h.type==="done"||h.type==="error")&&(f=!0),yield h}catch(a){s.signal.aborted?d.debug({thread:r.threadKey},"claude turn aborted"):f?d.debug({err:a},"query() threw after reporting a result"):(d.error({err:a},"harness turn failed"),yield{type:"error",kind:"harness_failure",message:a instanceof Error?a.message:String(a)})}finally{r.signal.removeEventListener("abort",o)}}}}export{$ as chosenGeneral,F as claudeHarness,R as editGuard,L as systemPromptAppend,K as toolDetail,N as worktreeNote};
@@ -1,10 +1,10 @@
1
- import{existsSync as O}from"node:fs";import{homedir as D}from"node:os";import{join as _}from"node:path";import{Codex as K}from"@openai/codex-sdk";import{log as f}from"../log.js";import{SERVER_NAME as A}from"../tools/server.js";import{ensureWorktree as I,gitCommonDir as P,isolates as M}from"../worktrees.js";import{ignoredKeys as R,layerOptions as j,mergeEnv as L,shadowedKeys as N}from"./layers.js";import{buildPrompt as X,principalInstructions as Y}from"./principal.js";const $=10,U=60;function W(e,r){const t=e??void 0;return r?{...t,[A]:{url:r.url,bearer_token_env_var:r.tokenEnvVar,required:!0,default_tools_approval_mode:"auto",startup_timeout_sec:$,tool_timeout_sec:U}}:t}const B={approvalPolicy:"never",sandboxMode:"workspace-write",networkAccessEnabled:!0,skipGitRepoCheck:!0},x=["workingDirectory","developer_instructions"];function F(e){const r=e?.config,t=r&&typeof r=="object"&&"developer_instructions"in r?["config.developer_instructions"]:[];return[...R(e,x),...t]}const G=new Set(["apiKey","baseUrl","config","configOverrides","env","codexPathOverride"]);function V(e){switch(e.type){case"command_execution":return{type:"tool",name:"Bash"};case"file_change":{const r=e.changes.length;if(r>1)return{type:"tool",name:"Edit",detail:`${r} files`};const t=r===1?e.changes[0]?.path.split("/").pop():void 0;return{type:"tool",name:"Edit",...t?{detail:t}:{}}}case"mcp_tool_call":return{type:"tool",name:`mcp__${e.server}__${e.tool}`};case"web_search":return{type:"tool",name:"WebSearch",...e.query?{detail:e.query}:{}};case"todo_list":return{type:"tool",name:"TodoWrite"};case"reasoning":return{type:"thinking"};default:return}}const H=new Set(["command_execution","mcp_tool_call","web_search","reasoning"]);function z(){const e=new Set;let r="";return t=>{switch(t.type){case"thread.started":return[{type:"session",sessionId:t.thread_id}];case"item.started":case"item.completed":{const i=t.item,p=H.has(i.type)?"item.started":"item.completed";if(t.type!==p||e.has(i.id))return[];if(e.add(i.id),i.type==="agent_message")return r=i.text,i.text.trim()?[{type:"text",text:i.text}]:[];if(i.type==="error")return f.warn({message:i.message},"codex reported a non-fatal error"),[];const c=V(i);return c?[c]:[]}case"turn.completed":return[{type:"done",text:r,usage:{inputTokens:t.usage.input_tokens,cachedInputTokens:t.usage.cached_input_tokens,outputTokens:t.usage.output_tokens}}];case"turn.failed":return[{type:"error",kind:"turn_failed",message:t.error.message}];case"error":return[{type:"error",kind:"stream_error",message:t.message}];default:return[]}}}function J(e){return[`You are working in a git worktree of your own, on the branch "${e}", created for this`,"conversation. Other conversations are working in their own worktrees at the same time.","","Work normally. Commit what you finish on this branch and say which branch it is, since that's","how a person finds your work."].join(`
2
- `)}function Q(e,r){const t=Array.isArray(e)?e:[],i=[...new Set([...t,...r??[]])];return i.length>0?{additionalDirectories:i}:{}}function Z(e){return{model:e.model,modelReasoningEffort:e.effort}}const q={model:"model",modelReasoningEffort:"model_reasoning_effort"};function ee(e,r){const t=e?.config,i=t&&typeof t=="object"?Object.entries(q).filter(([p,c])=>r[p]!==void 0&&c in t).map(([,p])=>`config.${p}`):[];return[...N(e,r),...i]}function fe(e={}){const r=F(e.options);r.length>0&&f.warn({ignored:r},"these agent.options are set by Bishop and cannot be overridden; ignoring them");const t=Z(e),i=ee(e.options,t);i.length>0&&f.warn({shadowed:i},"agent.model and agent.effort override these agent.options");const p=j({defaults:B,...e.options?{configOptions:e.options}:{},general:t,controlledKeys:x}),c={},m={};for(const[o,n]of Object.entries(p))G.has(o)?c[o]=n:m[o]=n;const k=c.env;e.apiKey&&(c.apiKey=e.apiKey);const E=e.worktrees,y=(o,n,a)=>{const d=c.config??{},s=W(d.mcp_servers,a),g=L(k,a?{[a.tokenEnvVar]:a.token}:{}),l={...d,developer_instructions:[n&&Y(n),o].filter(u=>!!u?.trim()).join(`
1
+ import{existsSync as D}from"node:fs";import{homedir as K}from"node:os";import{join as _}from"node:path";import{Codex as A}from"@openai/codex-sdk";import{log as g}from"../log.js";import{SERVER_NAME as I}from"../tools/server.js";import{ensureWorktree as P,gitCommonDir as j,isolates as M}from"../worktrees.js";import{ignoredKeys as R,layerOptions as L,mergeEnv as N,shadowedKeys as X}from"./layers.js";import{buildPrompt as Y,principalInstructions as $}from"./principal.js";const U=10,W=60;function B(e,t){const o=e??void 0;return t?{...o,[I]:{url:t.url,bearer_token_env_var:t.tokenEnvVar,required:!0,default_tools_approval_mode:"auto",startup_timeout_sec:U,tool_timeout_sec:W}}:o}const F={approvalPolicy:"never",sandboxMode:"workspace-write",networkAccessEnabled:!0,skipGitRepoCheck:!0},x=["workingDirectory","developer_instructions"];function G(e){const t=e?.config,o=t&&typeof t=="object"&&"developer_instructions"in t?["config.developer_instructions"]:[];return[...R(e,x),...o]}const V=new Set(["apiKey","baseUrl","config","configOverrides","env","codexPathOverride"]);function H(e){switch(e.type){case"command_execution":return{type:"tool",name:"Bash"};case"file_change":{const t=e.changes.length;if(t>1)return{type:"tool",name:"Edit",detail:`${t} files`};const o=t===1?e.changes[0]?.path.split("/").pop():void 0;return{type:"tool",name:"Edit",...o?{detail:o}:{}}}case"mcp_tool_call":return{type:"tool",name:`mcp__${e.server}__${e.tool}`};case"web_search":return{type:"tool",name:"WebSearch",...e.query?{detail:e.query}:{}};case"todo_list":return{type:"tool",name:"TodoWrite"};case"reasoning":return{type:"thinking"};default:return}}const z=new Set(["command_execution","mcp_tool_call","web_search","reasoning"]);function J(e){const t=new Set;let o="";return i=>{switch(i.type){case"thread.started":return[{type:"session",sessionId:i.thread_id,...e?{model:e}:{}}];case"item.started":case"item.completed":{const s=i.item,l=z.has(s.type)?"item.started":"item.completed";if(i.type!==l||t.has(s.id))return[];if(t.add(s.id),s.type==="agent_message")return o=s.text,s.text.trim()?[{type:"text",text:s.text}]:[];if(s.type==="error")return g.warn({message:s.message},"codex reported a non-fatal error"),[];const u=H(s);return u?[u]:[]}case"turn.completed":return[{type:"done",text:o,usage:{inputTokens:i.usage.input_tokens,cachedInputTokens:i.usage.cached_input_tokens,outputTokens:i.usage.output_tokens}}];case"turn.failed":return[{type:"error",kind:"turn_failed",message:i.error.message}];case"error":return[{type:"error",kind:"stream_error",message:i.message}];default:return[]}}}function Q(e){return[`You are working in a git worktree of your own, on the branch "${e}", created for this`,"conversation. Other conversations are working in their own worktrees at the same time.","","Work normally. Commit what you finish on this branch and say which branch it is, since that's","how a person finds your work."].join(`
2
+ `)}function Z(e,t){const o=Array.isArray(e)?e:[],i=[...new Set([...o,...t??[]])];return i.length>0?{additionalDirectories:i}:{}}function k(e){return{model:e.model,modelReasoningEffort:e.effort}}function q(e){return Object.fromEntries(Object.entries(k(e)).filter(([,t])=>t!==void 0))}const ee={model:"model",modelReasoningEffort:"model_reasoning_effort"};function te(e,t){const o=e?.config,i=o&&typeof o=="object"?Object.entries(ee).filter(([s,l])=>t[s]!==void 0&&l in o).map(([,s])=>`config.${s}`):[];return[...X(e,t),...i]}function pe(e={}){const t=G(e.options);t.length>0&&g.warn({ignored:t},"these agent.options are set by Bishop and cannot be overridden; ignoring them");const o=k(e),i=te(e.options,o);i.length>0&&g.warn({shadowed:i},"agent.model and agent.effort override these agent.options");const s=L({defaults:F,...e.options?{configOptions:e.options}:{},general:o,controlledKeys:x}),l={},u={};for(const[r,n]of Object.entries(s))V.has(r)?l[r]=n:u[r]=n;const E=l.env;e.apiKey&&(l.apiKey=e.apiKey);const b=e.worktrees,w=(r,n,a)=>{const d=l.config??{},c=B(d.mcp_servers,a),m=N(E,a?{[a.tokenEnvVar]:a.token}:{}),f={...d,developer_instructions:[n&&$(n),r].filter(p=>!!p?.trim()).join(`
3
3
 
4
- `)};return s&&(l.mcp_servers=s),new K({...c,...g?{env:g}:{},config:l})};async function b(o){if(!M(E))return{cwd:o.cwd};const n=await I(o.cwd,o.worktree),a=await P(n);return a||f.warn({worktree:n},"couldn't resolve the repository's git directory, so the agent probably can't commit"),{cwd:n,note:J(o.worktree),...a?{writable:[a]}:{}}}return{name:"codex",describeStartupFailure(o,n){return`The Codex agent couldn't start in ${o}:
4
+ `)};return c&&(f.mcp_servers=c),new A({...l,...m?{env:m}:{},config:f})};async function v(r){if(!M(b))return{cwd:r.cwd};const n=await P(r.cwd,r.worktree),a=await j(n);return a||g.warn({worktree:n},"couldn't resolve the repository's git directory, so the agent probably can't commit"),{cwd:n,note:Q(r.worktree),...a?{writable:[a]}:{}}}return{name:"codex",describeStartupFailure(r,n){return`The Codex agent couldn't start in ${r}:
5
5
  ${n}
6
6
 
7
7
  Bishop does not manage OpenAI credentials. The agent authenticates the
8
8
  same way \`codex\` does on this machine, so make sure that works first:
9
9
  CODEX_API_KEY in the environment or .env, or
10
- an existing \`codex login\` on this machine.`},async check(o){const n=new AbortController,a=m.model;try{const s=y(void 0,void 0).startThread({...m,workingDirectory:o}),{events:g}=await s.runStreamed("ping",{signal:n.signal});for await(const l of g){if(l.type==="thread.started"){n.abort();const u=te(e.apiKey);return re(u),{ok:!0,...u?{credentialSource:u}:{},...a?{model:a}:{}}}if(l.type==="error")return{ok:!1,error:l.message}}return{ok:!1,error:"the harness exited without starting a thread"}}catch(d){return n.signal.aborted?{ok:!0}:{ok:!1,error:d instanceof Error?d.message:String(d)}}},async*runTurn(o){const n=new AbortController,a=()=>n.abort();o.signal.addEventListener("abort",a,{once:!0});let d=!1;try{const{cwd:s,note:g,writable:l}=await b(o),u=y(g,o.origin,o.tools),w={...m,workingDirectory:s,...Q(m.additionalDirectories,l)},v=o.sessionId?u.resumeThread(o.sessionId,w):u.startThread(w),C=z(),{events:S}=await v.runStreamed(X(o),{signal:n.signal});for await(const T of S)for(const h of C(T))(h.type==="done"||h.type==="error")&&(d=!0),yield h}catch(s){n.signal.aborted?f.debug({thread:o.threadKey},"codex turn aborted"):d?f.debug({err:s},"codex threw after reporting a result"):(f.error({err:s},"harness turn failed"),yield{type:"error",kind:"harness_failure",message:s instanceof Error?s.message:String(s)})}finally{o.signal.removeEventListener("abort",a)}}}}function te(e){if(e)return"CODEX_API_KEY";const r=process.env.CODEX_HOME??_(D(),".codex");if(O(_(r,"auth.json")))return"existing codex login"}function re(e){e||f.warn("no CODEX_API_KEY and no codex login found, so the first turn will probably fail to authenticate; run `codex login`, or set CODEX_API_KEY")}export{x as CONTROLLED_CODEX_KEYS,B as DEFAULT_CODEX_OPTIONS,Z as codexGeneralOptions,fe as codexHarness,z as codexTranslator,F as ignoredCodexKeys,W as mergeMcpServers,ee as shadowedCodexKeys,Q as withWritable,J as worktreeNote};
10
+ an existing \`codex login\` on this machine.`},async check(r){const n=new AbortController,a=u.model;try{const c=w(void 0,void 0).startThread({...u,workingDirectory:r}),{events:m}=await c.runStreamed("ping",{signal:n.signal});for await(const f of m){if(f.type==="thread.started"){n.abort();const p=re(e.apiKey);return oe(p),{ok:!0,...p?{credentialSource:p}:{},...a?{model:a}:{}}}if(f.type==="error")return{ok:!1,error:f.message}}return{ok:!1,error:"the harness exited without starting a thread"}}catch(d){return n.signal.aborted?{ok:!0}:{ok:!1,error:d instanceof Error?d.message:String(d)}}},async*runTurn(r){const n=new AbortController,a=()=>n.abort();r.signal.addEventListener("abort",a,{once:!0});let d=!1;try{const{cwd:c,note:m,writable:f}=await v(r),p=w(m,r.origin,r.tools),h={...u,...q(r),workingDirectory:c,...Z(u.additionalDirectories,f)},C=r.sessionId?p.resumeThread(r.sessionId,h):p.startThread(h),S=J(h.model),{events:T}=await C.runStreamed(Y(r),{signal:n.signal});for await(const O of T)for(const y of S(O))(y.type==="done"||y.type==="error")&&(d=!0),yield y}catch(c){n.signal.aborted?g.debug({thread:r.threadKey},"codex turn aborted"):d?g.debug({err:c},"codex threw after reporting a result"):(g.error({err:c},"harness turn failed"),yield{type:"error",kind:"harness_failure",message:c instanceof Error?c.message:String(c)})}finally{r.signal.removeEventListener("abort",a)}}}}function re(e){if(e)return"CODEX_API_KEY";const t=process.env.CODEX_HOME??_(K(),".codex");if(D(_(t,"auth.json")))return"existing codex login"}function oe(e){e||g.warn("no CODEX_API_KEY and no codex login found, so the first turn will probably fail to authenticate; run `codex login`, or set CODEX_API_KEY")}export{x as CONTROLLED_CODEX_KEYS,F as DEFAULT_CODEX_OPTIONS,q as chosenCodexOptions,k as codexGeneralOptions,pe as codexHarness,J as codexTranslator,G as ignoredCodexKeys,B as mergeMcpServers,te as shadowedCodexKeys,Z as withWritable,Q as worktreeNote};
@@ -1 +1 @@
1
- import{FETCH_FILE_TOOL as i}from"../harness/principal.js";const a={Read:e=>e?`Reading ${e}`:"Reading a file",Write:e=>e?`Writing ${e}`:"Writing a file",Edit:e=>e?`Editing ${e}`:"Editing a file",NotebookEdit:e=>e?`Editing ${e}`:"Editing a notebook",Bash:e=>e??"Running a command",BashOutput:()=>"Checking on a command",Glob:e=>e?`Looking for ${e}`:"Looking through files",Grep:e=>e?`Searching for ${e}`:"Searching the code",WebFetch:e=>e?`Reading ${e}`:"Fetching a page",WebSearch:e=>e?`Searching for ${e}`:"Searching the web",Task:e=>e?`Working on ${e}`:"Delegating a subtask",TodoWrite:()=>"Planning",ExitPlanMode:()=>"Finishing up a plan",AskUserQuestion:()=>"Considering options"},s={schedule_create:"Setting up a schedule",schedule_list:"Checking what is scheduled",schedule_delete:"Removing a schedule",[i]:"Fetching a file from this conversation"};function l(e){if(!e.startsWith("mcp__"))return;const[,n,...t]=e.split("__");return n==="bishop"?s[t.join("__")]??"Sorting something out":n?`Calling ${n}`:"Calling a tool"}function c(e){const n=e.replaceAll(/[_-]+/g," ").replaceAll(/([a-z\d])([A-Z])/g,"$1 $2").trim();if(!n)return"Working";const t=n.toLowerCase();return t.charAt(0).toUpperCase()+t.slice(1)}const r=100;function u(e){const n=e.trim();return n.length>r?`${n.slice(0,r-3)}...`:n}function m(e){const n=e.summary?.trim();if(n)return u(n);const t=a[e.name];if(t)return t(e.detail);const o=l(e.name);return o||c(e.name)}function g(e){const n=Math.max(0,Math.round(e/1e3)),t=Math.floor(n/60),o=n%60;return t?`${t}m ${o}s`:`${o}s`}function f(e){return e===1?"Used 1 tool":`Used ${e} tools`}function p(e,n){return`${n?"Worked for":"Stopped after"} ${g(e)}`}export{g as elapsed,c as humanizeToolName,u as summaryPhrase,m as toolPhrase,f as toolTally,p as turnTally};
1
+ import{FETCH_FILE_TOOL as i}from"../harness/principal.js";import{SHARE_FILE_TOOL as a}from"../outgoing.js";const s={Read:e=>e?`Reading ${e}`:"Reading a file",Write:e=>e?`Writing ${e}`:"Writing a file",Edit:e=>e?`Editing ${e}`:"Editing a file",NotebookEdit:e=>e?`Editing ${e}`:"Editing a notebook",Bash:e=>e??"Running a command",BashOutput:()=>"Checking on a command",Glob:e=>e?`Looking for ${e}`:"Looking through files",Grep:e=>e?`Searching for ${e}`:"Searching the code",WebFetch:e=>e?`Reading ${e}`:"Fetching a page",WebSearch:e=>e?`Searching for ${e}`:"Searching the web",Task:e=>e?`Working on ${e}`:"Delegating a subtask",TodoWrite:()=>"Planning",ExitPlanMode:()=>"Finishing up a plan",AskUserQuestion:()=>"Considering options"},l={schedule_create:"Setting up a schedule",schedule_list:"Checking what is scheduled",schedule_delete:"Removing a schedule",[i]:"Fetching a file from this conversation",[a]:"Getting a file ready to send"};function c(e){if(!e.startsWith("mcp__"))return;const[,n,...t]=e.split("__");return n==="bishop"?l[t.join("__")]??"Sorting something out":n?`Calling ${n}`:"Calling a tool"}function u(e){const n=e.replaceAll(/[_-]+/g," ").replaceAll(/([a-z\d])([A-Z])/g,"$1 $2").trim();if(!n)return"Working";const t=n.toLowerCase();return t.charAt(0).toUpperCase()+t.slice(1)}const r=100;function g(e){const n=e.trim();return n.length>r?`${n.slice(0,r-3)}...`:n}function d(e){const n=e.summary?.trim();if(n)return g(n);const t=s[e.name];if(t)return t(e.detail);const o=c(e.name);return o||u(e.name)}function h(e){const n=Math.max(0,Math.round(e/1e3)),t=Math.floor(n/60),o=n%60;return t?`${t}m ${o}s`:`${o}s`}function p(e){return e===1?"Used 1 tool":`Used ${e} tools`}function $(e,n){return`${n?"Worked for":"Stopped after"} ${h(e)}`}export{h as elapsed,u as humanizeToolName,g as summaryPhrase,d as toolPhrase,p as toolTally,$ as turnTally};
@@ -0,0 +1,2 @@
1
+ import{randomBytes as b}from"node:crypto";import{stat as T}from"node:fs/promises";import{basename as k,isAbsolute as _}from"node:path";import{log as $}from"./log.js";const C="file_share",z=25*1024*1024,A=10;class m extends Error{constructor(o){super(o),this.name="ShareRefused"}}const j={png:"image/png",jpg:"image/jpeg",jpeg:"image/jpeg",gif:"image/gif",webp:"image/webp",svg:"image/svg+xml",pdf:"application/pdf",csv:"text/csv",tsv:"text/tab-separated-values",txt:"text/plain",log:"text/plain",md:"text/markdown",json:"application/json",yaml:"application/yaml",yml:"application/yaml",html:"text/html",xml:"application/xml",zip:"application/zip",docx:"application/vnd.openxmlformats-officedocument.wordprocessingml.document",xlsx:"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",pptx:"application/vnd.openxmlformats-officedocument.presentationml.presentation"};function M(t){const o=t.lastIndexOf("."),s=o>0?t.slice(o+1).toLowerCase():"";return j[s]??"application/octet-stream"}function u(t){return t>=1024*1024?`${Math.round(t/(1024*1024))}MB`:t>=1024?`${Math.round(t/1024)}KB`:`${t} bytes`}function R(t){return t.replaceAll(/[/\\]/g,"-").replaceAll(/[\u0000-\u001f\u007f]/g,"").trim().replace(/^\.+/,"").trim()||"file"}const I=/<\s*attachment\b([^>]*?)\/?\s*>/gi,B=/\bref\s*=\s*"([^"]*)"|\bref\s*=\s*'([^']*)'/i,L="attachment";function N(t={}){const o=t.budget??z,s=new Map,h=new Set,g=new Set;let d=0;function w(e,n){const a=[],r=[];let i="",c=0;for(const l of e.matchAll(I)){const v=l.index??0,S=e.slice(c,v);c=v+l[0].length;const E=B.exec(l[1]??""),p=(E?.[1]??E?.[2]??"").trim(),f=s.get(p);if(r.push(S),i+=S,!f){g.has(p)||(g.add(p),$.warn({ref:p||"(none)"},"the agent placed a file reference naming nothing"));continue}h.has(f.ref)||(n&&h.add(f.ref),a.push(i,f),i="")}return r.push(e.slice(c)),a.push(i+e.slice(c)),{parts:a.filter(l=>typeof l!="string"||l.trim()),text:r.join("").trim()}}let x=Promise.resolve();async function y(e){const n=e.path.trim();if(!_(n))throw new m(`${n} is not an absolute path. Bishop sends the file from where it sits on disk and has no working directory of yours to resolve a relative one against.`);if(s.size>=A)throw new m(`One answer can carry ${A} files, and that many are already on it.`);const a=await T(n).catch(()=>{});if(!a?.isFile())throw new m(`There is no file at ${n}.`);if(d+a.size>o)throw new m(`${u(a.size)} is more than this conversation can carry: the limit is ${u(o)} across everything one answer sends, and ${u(d)} of it is already spoken for.`);const r=R(e.name?.trim()||k(n)),i={ref:b(6).toString("base64url"),path:n,name:r,size:a.size,mimeType:M(r)};return d+=a.size,s.set(i.ref,i),i}return{offer(e){const n=x.then(()=>y(e),()=>y(e));return x=n.catch(()=>{}),n},offered:()=>[...s.values()],place:e=>w(e,!0),strip:e=>w(e,!1).text,unplaced:()=>[...s.values()].filter(e=>!h.has(e.ref))}}const D=["Sends a file from disk to the conversation you are in, as a real attachment: an upload in","Slack, an attachment on the mail in email.","","Takes an absolute path and hands back a ref. Place it to send it: write",`<${L} ref="the ref" /> in your reply where you want the file to appear, and`,"Bishop puts it there and removes the tag. Say what the file is around it, the way you would","if you were sending it yourself.","","It fails, with the reason, when there is no file at the path or it is too large to send."].join(`
2
+ `);export{A as MAX_SHARED_FILES,z as MAX_SHARE_BYTES,D as SHARE_FILE_DESCRIPTION,C as SHARE_FILE_TOOL,m as ShareRefused,N as fileShelf,M as guessType};
@@ -1,5 +1,5 @@
1
- import{isSilent as i}from"./prompt.js";function c(){const n=[],r=[];let s="",t;return{async handle(e){switch(e.type){case"text":n.push(e.text);return;case"done":s=e.text;return;case"error":t=e.message;return;default:return}},async finish(){},note(e){r.push(e)},outcome(){const e=s.trim()||n.join(`
1
+ import{fileShelf as i,MAX_SHARE_BYTES as f}from"../outgoing.js";import{isSilent as c}from"./prompt.js";function u(){const n=[],s=[];let o="",t;const r=i({budget:f});return{share:e=>r.offer(e),async handle(e){switch(e.type){case"text":n.push(e.text);return;case"done":o=e.text;return;case"error":t=e.message;return;default:return}},async finish(){},note(e){s.push(e)},outcome(){const e=o.trim()||n.join(`
2
2
 
3
- `).trim();return{text:[...r,e].filter(Boolean).join(`
3
+ `).trim();return{text:[...s,r.strip(e)].filter(Boolean).join(`
4
4
 
5
- `),...t?{failure:t}:{},silent:i(e)}}}}export{c as schedulePresence};
5
+ `),files:r.offered(),...t?{failure:t}:{},silent:c(e)}}}}export{u as schedulePresence};
@@ -1,5 +1,5 @@
1
- import{log as a}from"../log.js";import{nextRun as z}from"./cron.js";import{schedulePresence as _}from"./presence.js";import{SCHEDULE_ORIGIN as I,scheduleNote as N}from"./prompt.js";const R=3e4,M=5,A=5e3;function B(r){return r.startsWith("cli:")}function C(r,l){return`schedule:${r}:${l}`}function K(r){const l=r.now??Date.now,E=r.tickMs??R,T=r.authorized??B,s=new Map,m=new Map,d=new Set;let u,S=!1;function g(e,t){try{return r.schedules.setNextRun(e.id,z(e.cron,e.tz,t)),!0}catch(i){const n=i instanceof Error?i.message:String(i);return a.error({schedule:e.id,cron:e.cron,tz:e.tz,err:i},"disabling a schedule whose cron expression cannot be read"),r.schedules.setEnabled(e.id,!1),r.schedules.recordFailure(e.id,t,n),w(e,`The schedule "${e.name}" is off: Bishop can no longer read when it should run. ${n}
1
+ import{log as a}from"../log.js";import{SHARE_FILE_TOOL as $}from"../outgoing.js";import{nextRun as z}from"./cron.js";import{schedulePresence as I}from"./presence.js";import{SCHEDULE_ORIGIN as R,scheduleNote as N}from"./prompt.js";const M=3e4,A=5,F=5e3;function O(r){return r.startsWith("cli:")}function B(r,l){return`schedule:${r}:${l}`}function K(r){const l=r.now??Date.now,T=r.tickMs??M,x=r.authorized??O,s=new Map,g=new Map,u=new Set;let f,S=!1;function w(e,t){try{return r.schedules.setNextRun(e.id,z(e.cron,e.tz,t)),!0}catch(i){const n=i instanceof Error?i.message:String(i);return a.error({schedule:e.id,cron:e.cron,tz:e.tz,err:i},"disabling a schedule whose cron expression cannot be read"),r.schedules.setEnabled(e.id,!1),r.schedules.recordFailure(e.id,t,n),y(e,`The schedule "${e.name}" is off: Bishop can no longer read when it should run. ${n}
2
2
 
3
- Remove it, or add it again with a cron expression that works.`),!1}}async function w(e,t){const i=r.deliver(e.destination,{text:t,title:e.name}).then(()=>{}).catch(n=>{a.error({schedule:e.id,to:e.destination.label,err:n},"could not say that a schedule failed")}).finally(()=>{d.delete(i)});d.add(i),await i}async function y(e,t,i){const n=r.schedules.recordFailure(e.id,t,i);if(a.error({schedule:e.id,name:e.name,failures:n,err:i},"a scheduled run failed"),n>=M){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 w(e,`The schedule "${e.name}" has failed ${n} times in a row and is now off. The last failure was: ${i}
3
+ Remove it, or add it again with a cron expression that works.`),!1}}async function y(e,t){const i=r.deliver(e.destination,{text:t,title:e.name}).then(()=>{}).catch(n=>{a.error({schedule:e.id,to:e.destination.label,err:n},"could not say that a schedule failed")}).finally(()=>{u.delete(i)});u.add(i),await i}async function p(e,t,i){const n=r.schedules.recordFailure(e.id,t,i);if(a.error({schedule:e.id,name:e.name,failures:n,err:i},"a scheduled run failed"),n>=A){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 y(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 w(e,`The schedule "${e.name}" failed: ${i}`)}async function k(e,t,i){const n=_();a.info({schedule:e.id,name:e.name},"a schedule is firing");let c,v;try{v=await r.threads.remember(i)}catch(o){await y(e,l(),o instanceof Error?o.message:String(o));return}const $={thread:v,isNewThread:!0,freshSession:!0,tools:!1,origin:I,note:N(e,t),async messages(){return[{principal:{id:`schedule:${e.id}`,displayName:e.name},text:e.prompt}]},presence:()=>n,async notify(o){n.note(o)},async destination(){return e.destination}};try{await r.run($)}catch(o){c=o instanceof Error?o.message:String(o)}const p=n.outcome(),x=p.failure??c,f=l();if(x){await y(e,f,x);return}if(p.silent){r.schedules.recordSuccess(e.id,f),a.info({schedule:e.id,name:e.name},"a schedule had nothing to report");return}let h;try{h=await r.deliver(e.destination,{text:p.text,title:e.name})}catch(o){await y(e,f,o instanceof Error?o.message:String(o));return}if(h.threadKey&&r.attach)try{await r.attach(h.threadKey,i)}catch(o){a.error({schedule:e.id,thread:h.threadKey,err:o},"a report was delivered but nothing will answer a reply to it")}r.schedules.recordSuccess(e.id,f),a.info({schedule:e.id,name:e.name,to:e.destination.label},"a schedule reported")}async function b(){if(S)return;const e=l();for(const t of r.schedules.due(e)){if(!T(t.createdBy)){a.warn({schedule:t.id,createdBy:t.createdBy},"skipping a schedule whose creator can no longer be authorized"),g(t,e);continue}if(!g(t,e))continue;if(s.has(t.id)){a.warn({schedule:t.id,name:t.name},"skipping a firing while the previous one is still running");continue}const i=C(t.id,e);m.set(t.id,i);const n=k(t,e,i).catch(c=>{a.error({schedule:t.id,err:c},"a schedule firing threw")}).finally(()=>{s.delete(t.id),m.delete(t.id)});s.set(t.id,n)}}return{start(){const e=l(),t=r.schedules.due(e);for(const n of t)a.info({schedule:n.id,name:n.name,due:new Date(n.nextRunAt).toISOString()},"skipping a firing that came due while Bishop was not running"),g(n,e);const i=r.schedules.all().filter(n=>n.enabled);a.info({schedules:i.length,skipped:t.length},"watching the schedules"),u=setInterval(()=>{b().catch(n=>{a.error({err:n},"a scheduler pass failed; trying again on the next tick")})},E),u.unref()},tick:b,async settle(){await Promise.allSettled([...s.values(),...d])},async stop(){if(S=!0,u&&clearInterval(u),s.size>0){a.info({firings:s.size},"stopping scheduled runs still going");for(const n of s.keys()){const c=m.get(n);c&&r.cancel?.(c)}}const e=Promise.allSettled([...s.values(),...d]);let t;const i=new Promise(n=>{t=setTimeout(()=>{a.warn({firings:s.size},"giving up on scheduled runs that would not stop"),n()},A)});await Promise.race([e,i]),t&&clearTimeout(t)}}}export{M as MAX_CONSECUTIVE_FAILURES,B as authorizedToRun,C as firingThread,K as makeScheduler};
5
+ Nothing more will run until \`bishop schedules enable ${e.id}\`.`);return}n===1&&await y(e,`The schedule "${e.name}" failed: ${i}`)}async function k(e,t,i){const n=I();a.info({schedule:e.id,name:e.name},"a schedule is firing");let c,b;try{b=await r.threads.remember(i)}catch(o){await p(e,l(),o instanceof Error?o.message:String(o));return}const _={thread:b,isNewThread:!0,freshSession:!0,tools:[$],origin:R,note:N(e,t),async messages(){return[{principal:{id:`schedule:${e.id}`,displayName:e.name},text:e.prompt}]},presence:()=>n,async notify(o){n.note(o)},async destination(){return e.destination}};try{await r.run(_)}catch(o){c=o instanceof Error?o.message:String(o)}const d=n.outcome(),v=d.failure??c,h=l();if(v){await p(e,h,v);return}if(d.silent){r.schedules.recordSuccess(e.id,h),a.info({schedule:e.id,name:e.name},"a schedule had nothing to report");return}let m;try{m=await r.deliver(e.destination,{text:d.text,title:e.name,...d.files.length>0?{files:d.files}:{}})}catch(o){await p(e,h,o instanceof Error?o.message:String(o));return}if(m.threadKey&&r.attach)try{await r.attach(m.threadKey,i)}catch(o){a.error({schedule:e.id,thread:m.threadKey,err:o},"a report was delivered but nothing will answer a reply to it")}r.schedules.recordSuccess(e.id,h),a.info({schedule:e.id,name:e.name,to:e.destination.label},"a schedule reported")}async function E(){if(S)return;const e=l();for(const t of r.schedules.due(e)){if(!x(t.createdBy)){a.warn({schedule:t.id,createdBy:t.createdBy},"skipping a schedule whose creator can no longer be authorized"),w(t,e);continue}if(!w(t,e))continue;if(s.has(t.id)){a.warn({schedule:t.id,name:t.name},"skipping a firing while the previous one is still running");continue}const i=B(t.id,e);g.set(t.id,i);const n=k(t,e,i).catch(c=>{a.error({schedule:t.id,err:c},"a schedule firing threw")}).finally(()=>{s.delete(t.id),g.delete(t.id)});s.set(t.id,n)}}return{start(){const e=l(),t=r.schedules.due(e);for(const n of t)a.info({schedule:n.id,name:n.name,due:new Date(n.nextRunAt).toISOString()},"skipping a firing that came due while Bishop was not running"),w(n,e);const i=r.schedules.all().filter(n=>n.enabled);a.info({schedules:i.length,skipped:t.length},"watching the schedules"),f=setInterval(()=>{E().catch(n=>{a.error({err:n},"a scheduler pass failed; trying again on the next tick")})},T),f.unref()},tick:E,async settle(){await Promise.allSettled([...s.values(),...u])},async stop(){if(S=!0,f&&clearInterval(f),s.size>0){a.info({firings:s.size},"stopping scheduled runs still going");for(const n of s.keys()){const c=g.get(n);c&&r.cancel?.(c)}}const e=Promise.allSettled([...s.values(),...u]);let t;const i=new Promise(n=>{t=setTimeout(()=>{a.warn({firings:s.size},"giving up on scheduled runs that would not stop"),n()},F)});await Promise.race([e,i]),t&&clearTimeout(t)}}}export{A as MAX_CONSECUTIVE_FAILURES,O as authorizedToRun,B as firingThread,K as makeScheduler};
@@ -1 +1,3 @@
1
- import{Command as l,CommanderError as p,InvalidArgumentError as v}from"commander";import{VERBOSITY as h}from"../../config.js";import{log as g}from"../../log.js";import{parseVerbosity as c}from"../verbosity.js";import{commandOutput as f,tokenize as w}from"./syntax.js";const n="default";function y(t){if(t.trim().toLowerCase()===n)return n;const e=c(t);if(!e)throw new v(`Use one of ${h.join(", ")}, or ${n}.`);return e}function C(t,e,i){const m=[];let a;const s=new l;s.name("!").description("Bishop commands. These are handled here and never reach the agent.").exitOverride().configureOutput({writeOut:r=>m.push(r),writeErr:r=>m.push(r),outputError:(r,o)=>o(r)}),s.command("verbosity").description("How much of a turn this thread shows while it is running.").argument("[level]",`${h.join(", ")}, or ${n} to follow the configured level. Omit it to see the current one.`,y).action(r=>{if(!r){const d=i.verbosity.of(e.thread.id),u=d.fromThread?"":" (the configured default)";a=`Thread verbosity is ${d.level}${u}`;return}if(r===n){i.verbosity.clear(e.thread.id),a=`Thread verbosity follows the configured default, which is ${i.verbosity.of(e.thread.id).level}`;return}const o=c(r);o&&(i.verbosity.set(e.thread.id,o),a=`Thread verbosity set to ${o}`)});try{s.parse(w(t),{from:"user"})}catch(r){if(!(r instanceof p)){const o=r instanceof Error?r.message:String(r);return g.debug({thread:e.thread.threadKey,line:t,err:r},"a command failed"),f(o)}}return f(a??(m.join("").trim()||s.helpInformation()))}export{C as runCommand};
1
+ import{Command as L,CommanderError as N,InvalidArgumentError as g}from"commander";import{VERBOSITY as y}from"../../config.js";import{log as b}from"../../log.js";import{isHarnessName as S,MODEL_SETTINGS as O}from"../../thread-model.js";import{parseVerbosity as E}from"../verbosity.js";import{commandOutput as T,tokenize as j}from"./syntax.js";const a="default";function k(o){if(o.trim().toLowerCase()===a)return a;const e=E(o);if(!e)throw new g(`Use one of ${y.join(", ")}, or ${a}.`);return e}function l(o){const e=o.trim();if(!e)throw new g(`Name one, or ${a} to follow the configured one.`);if(!S(e))throw new g(`"${e}" is not a model name. Letters, digits, and . : / @ - _ only.`);return e}function I(o,e,t){return e?`${o} is ${e}, set in this thread`:t?`${o} is ${t}, the default`:`${o} is whatever the harness runs by default`}function C(o,e){const t=e.model.of(o);return[I("Model",t.model,e.configured.model),I("Effort",t.effort,e.configured.effort)]}function H(o,e,t){const u=[];let h;const f=new L;f.name("!").description("Bishop commands. These are handled here and never reach the agent.").exitOverride().configureOutput({writeOut:r=>u.push(r),writeErr:r=>u.push(r),outputError:(r,n)=>n(r)}),f.command("verbosity").description("How much of a turn this thread shows while it is running.").argument("[level]",`${y.join(", ")}, or ${a} to follow the configured level. Omit it to see the current one.`,k).action(r=>{if(!r){const c=t.verbosity.of(e.thread.id),d=c.fromThread?"":" (the configured default)";h=`Thread verbosity is ${c.level}${d}`;return}if(r===a){t.verbosity.clear(e.thread.id),h=`Thread verbosity follows the configured default, which is ${t.verbosity.of(e.thread.id).level}`;return}const n=E(r);n&&(t.verbosity.set(e.thread.id,n),h=`Thread verbosity set to ${n}`)}),f.command("model").description("Which model this thread runs, and how hard it thinks.").argument("[model]",`a model name in the harness's own naming, or ${a} to follow the configured one. Omit everything to see where the thread stands.`,l).argument("[effort]",`how hard it should think, in the harness's own naming, or ${a}.`,l).option("-m, --model <model>","the model, named as a flag rather than in order",l).option("-e, --effort <effort>","the effort, named as a flag rather than in order",l).action((r,n,c)=>{const d=e.thread.id,s={...c},w=O.filter(i=>s[i]===void 0),v=[r,n].filter(i=>i!==void 0);if(v.length>w.length){h="Name each half once: a model and an effort, in order or as flags.";return}if(v.forEach((i,m)=>{const p=w[m];p&&(s[p]=i)}),!s.model&&!s.effort){h=C(d,t).join(`
2
+ `);return}const $={};for(const i of O){const m=s[i];m&&(m.toLowerCase()===a?t.model.clear(d,i):$[i]=m)}t.model.set(d,$),b.info({thread:e.thread.threadKey,...s},"a thread changed the model it runs"),h=["Set. It takes effect on this thread's next turn.",...C(d,t)].join(`
3
+ `)});try{f.parse(j(o),{from:"user"})}catch(r){if(!(r instanceof N)){const n=r instanceof Error?r.message:String(r);return b.debug({thread:e.thread.threadKey,line:o,err:r},"a command failed"),T(n)}}return T(h??(u.join("").trim()||f.helpInformation()))}export{H as runCommand};