@agentdeploymentco/bishop 0.3.5 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -159,7 +159,7 @@ $ bishop teams setup --endpoint https://your-host.example/api/messages
159
159
 
160
160
  It creates the app through the [Teams Developer CLI](https://microsoft.github.io/teams-sdk/), writes the three `BISHOP_TEAMS_*` credentials to `.env`, and records the app in `.bishop/config.json`. It prints a link to install the app in Teams. You can leave `--endpoint` off and set it later.
161
161
 
162
- It also asks the app for `ChannelMessage.Read.Group` and `ChatMessage.Read.Chat`, which are what let the agent hear a reply that does not @-mention it, and turns on `supportsFiles`, which is what lets anyone attach a file in a chat with it. `teams app create` takes no flags for any of them, so each is a further write, and a Developer Portal that refuses one costs that setting rather than the setup: it says what is missing and what to run. Run setup again on a configured agent and it reports whatever is still missing.
162
+ It also asks the app for `ChannelMessage.Read.Group` and `ChatMessage.Read.Chat`, which are what let the agent hear a reply that does not @-mention it, `ChannelSettings.Read.Group` and `TeamSettings.Read.Group`, which are what tell the agent who can read a channel, and turns on `supportsFiles`, which is what lets anyone attach a file in a chat with it. `teams app create` takes no flags for any of them, so each is a further write, and a Developer Portal that refuses one costs that setting rather than the setup: it says what is missing and what to run. Run setup again on a configured agent and it reports whatever is still missing.
163
163
 
164
164
  One grant is not in the Teams manifest, so setup makes it with the [Azure CLI](https://learn.microsoft.com/cli/azure/install-azure-cli) instead. `Files.ReadWrite.All` on the app registration is what lets the agent read a file attached in a channel and send one there, and consenting it takes an administrator, so setup tries and prints the commands when it cannot: `az` missing, `az` logged out, signed in to another tenant, and a refusal all say which one happened. Install `az` and run `az login --allow-no-subscriptions` before setup if you can make the grant yourself. Nothing else needs `az`, and an agent that only works in direct chats never needs the grant.
165
165
 
@@ -728,10 +728,83 @@ A new worktree holds only tracked files, so `node_modules` and anything else you
728
728
 
729
729
  Bishop sets `baseRef` to `head` so a worktree branches from wherever the agent directory is now. Set it to `fresh` to branch from `origin/<default-branch>` instead. Everything else the harness offers here, including `sparsePaths` for a large monorepo, works the same way. That block is Claude's; under Codex, Bishop always branches from `HEAD` and there is nothing to configure.
730
730
 
731
- Under Claude, Bishop refuses `Edit`, `Write`, and `NotebookEdit` in the shared directory until the agent has entered its worktree, and tells the agent why. Bash is not checked, since a command's target can't be read off its text. So this keeps threads out of each other's way; it isn't a sandbox, and an agent determined to write outside its worktree can. Under Codex the session starts in the worktree, so nothing has to be refused, and `sandboxMode` is what limits where it can write.
731
+ Under Claude, Bishop refuses `Edit`, `Write`, and `NotebookEdit` in the shared directory until the agent has entered its worktree, and tells the agent why. Bash is not checked, since a command's target can't be read off its text. So this keeps threads out of each other's way; it isn't a sandbox, and an agent determined to write outside its worktree can. Under Codex the session starts in the worktree, so nothing has to be refused, and `sandboxMode` is what limits where it can write, or nono when the sandbox is on.
732
732
 
733
733
  **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.
734
734
 
735
+ ### Sandbox
736
+
737
+ The agent has a shell, and without a sandbox it can read anything Bishop can: `bishop.db`, `.bishop/config.json`, `.env`, and files people shared in other threads. Turning the sandbox on runs the agent, and everything it starts, under [nono](https://nono.sh/os-sandbox), which has the kernel refuse all of that. It works the same under either harness. Under Codex, Bishop also switches Codex's own sandbox off (`sandboxMode` becomes `danger-full-access`) and logs that it did, because nono replaces it and Codex's own failed to start inside nono when tested on Linux.
738
+
739
+ Install nono first. On Debian or Ubuntu:
740
+
741
+ ```
742
+ VERSION=$(curl -sIL https://github.com/nolabs-ai/nono/releases/latest | grep -i location | grep -oP 'v\K[0-9a-zA-Z.-]+')
743
+ ARCH=$(dpkg --print-architecture)
744
+ wget https://github.com/nolabs-ai/nono/releases/download/v${VERSION}/nono-cli_${VERSION}_${ARCH}.deb
745
+ sudo dpkg -i nono-cli_${VERSION}_${ARCH}.deb
746
+ ```
747
+
748
+ On macOS, or Linux with Homebrew, `brew install nono`. Other distributions are covered in [nono's install guide](https://nono.sh/docs/cli/getting_started/installation). Then check that this machine can enforce it:
749
+
750
+ ```
751
+ nono setup --check-only
752
+ ```
753
+
754
+ Linux needs kernel 5.13 or later, and macOS 10.15 or later. nono has to be on the `PATH` Bishop runs with, which for a systemd unit is the unit's own `Environment=PATH=…`.
755
+
756
+ Bishop's own directory can't be the agent directory with the sandbox on, because the kernel can't grant a directory while hiding part of it. Run Bishop from a directory of its own and point `--agent` at the agent, either a path or a git URL:
757
+
758
+ ```
759
+ mkdir ~/bishop-home && cd ~/bishop-home
760
+ bishop --agent ~/agents/salesforce-guy
761
+ ```
762
+
763
+ Move `.env` and `.bishop/` from the old directory into the new one first, or run setup there again. Then turn it on in `.bishop/config.json`:
764
+
765
+ ```json
766
+ {
767
+ "agent": {
768
+ "sandbox": true
769
+ }
770
+ }
771
+ ```
772
+
773
+ Bishop checks nono works before it starts, and refuses to start rather than run the agent unconfined. The log says `sandbox is on` when it is.
774
+
775
+ Each turn can read and write its working directory (the thread's worktree in worktree mode), the repository's git directory so it can commit, the temp directory, and Claude Code's own `~/.claude`. It can read the files shared in its own thread, and your git config. Network access is unchanged. Everything else under your home directory is out of reach, and a command that tries gets "Permission denied", which the agent can report like any other error.
776
+
777
+ That includes tools installed under your home directory, like `nvm`, `pyenv` or `~/.cargo`. Grant them with a nono profile, which adds to Bishop's grants rather than replacing them. Save one as `~/.config/nono/profiles/toolchain.json`:
778
+
779
+ ```json
780
+ {
781
+ "meta": { "name": "toolchain" },
782
+ "filesystem": {
783
+ "read": ["$HOME/.nvm", "$HOME/.cargo"]
784
+ }
785
+ }
786
+ ```
787
+
788
+ and name it:
789
+
790
+ ```json
791
+ {
792
+ "agent": {
793
+ "sandbox": { "profile": "toolchain" }
794
+ }
795
+ }
796
+ ```
797
+
798
+ `nono profile guide` explains the format. Don't add a `filesystem.deny` for anything Bishop grants: on Linux nono refuses to start with one, and Bishop will refuse to start with it.
799
+
800
+ The sandbox also keeps the agent out of `~/.ssh` and the `gh` CLI's login, so it can't push. `bishop nono setup` writes a nono profile named `bishop` (or whatever `--name` says) to `~/.config/nono/profiles/` that grants both, plus nono's `node-dev` toolchain and `~/.npm`, and names it in `agent.sandbox`, turning the sandbox on if it was off, so run it from Bishop's own directory, apart from the agent. If a profile already sits at that path it isn't replaced, and if `agent.sandbox` already names another profile the command leaves it and explains how to combine the two. The profile lets the agent read your SSH private keys, so give Bishop's user keys meant for the agent.
801
+
802
+ `file_share` sends only files the agent could read itself: a file somewhere else has to be copied into its working directory first.
803
+
804
+ Leave Claude Code's own `sandbox` setting off under nono. One sandbox is easier to reason about, and Claude's has an escape hatch that retries a blocked command outside it.
805
+
806
+ The container image doesn't include nono, and the sandbox isn't supported there yet. macOS is supported by nono but hasn't been tested with Bishop.
807
+
735
808
  ### Files people share
736
809
 
737
810
  Files go both ways: people attach them for the agent, and the agent sends them back. This half is the first.
@@ -868,6 +941,8 @@ Bishop runs as uid 1000 and needs to write both volumes. On Kubernetes that mean
868
941
 
869
942
  Tags: `latest` and `sha-<commit>` from `main`, plus `<version>` and `<major>.<minor>` from a `v*` git tag. `bishop --version` reads `package.json`, so a release tag has to bump it in the same commit or the image reports a version its tag disagrees with.
870
943
 
944
+ The image doesn't include nono, so `agent.sandbox` isn't supported in it yet.
945
+
871
946
  Setup commands that need a browser or a terminal (`bishop slack setup`, `bishop gmail setup`) are meant to be run on a laptop. Carry the tokens they produce to the container as environment variables, and the `.bishop/config.json` they write into the volume.
872
947
 
873
948
  ## Troubleshooting
@@ -897,6 +972,12 @@ after 12 hours, which stops Bishop from correcting the app name. Run `slack auth
897
972
 
898
973
  **The agent says it can't edit a file.** In worktree mode Bishop refuses writes in the shared directory. The agent is told to call `EnterWorktree` first, and normally does it unprompted. If the agent directory isn't a git repository, that can't work, and Bishop says so in the thread.
899
974
 
975
+ **"agent.sandbox is on, but nono is not installed or not on PATH"** means what it says. Install it as in [Sandbox](#sandbox), and check `which nono` as the user and `PATH` Bishop runs with.
976
+
977
+ **"The sandbox would have to grant …, which contains …/.bishop"** means Bishop's own directory is inside the agent directory. Run Bishop from a directory of its own and point `--agent` at the agent.
978
+
979
+ **The agent says "Permission denied" with the sandbox on.** It reached for something outside its grants. If it needs it, add the path to a nono profile; `nono why --path <path> --op read` says which rule decided.
980
+
900
981
  **Sessions get confused.** Delete `bishop.db` to make every thread start fresh.
901
982
  It holds only the thread-to-session mapping.
902
983
 
@@ -921,7 +1002,7 @@ If Bishop can't take the copy it refuses to start and says nothing was migrated,
921
1002
 
922
1003
  - One agent per directory, one process. No agent registry.
923
1004
  - Slack and Gmail only. No Teams.
924
- - No sandboxing between sessions. [Worktree mode](#worktree-mode) keeps concurrent threads out of each other's files, but it's a convention with a guardrail rather than a sandbox, and it needs a git repository. Without it, two threads can write the same file at once.
1005
+ - No sandboxing between threads on one agent. The [sandbox](#sandbox) keeps the agent out of Bishop's own state and other threads' shared files, but every thread can write the same working directory. [Worktree mode](#worktree-mode) keeps concurrent threads out of each other's files by convention, and it needs a git repository. Without it, two threads can write the same file at once.
925
1006
  - Files come in but don't go out. The agent can read what you attach; it can't attach anything to its answer.
926
1007
  - No approval flow. Permission decisions are the harness's, and Slack has no way to ask a person.
927
1008
  - No way to turn Bishop's own tools off. Bishop listens on `127.0.0.1` on an ephemeral port to serve them, authenticated with a credential minted per turn and destroyed when the turn ends. Nothing is reachable from off the machine, but anything running on it can reach the port and will be refused.
package/dist/cli.js CHANGED
@@ -1,33 +1,35 @@
1
1
  #!/usr/bin/env node
2
- import{stat as ce}from"node:fs/promises";import{join as de,resolve as pe}from"node:path";import{Command as me}from"commander";import{agentSnapshots as he,fixedAgentDir as le}from"./agent-dir.js";import{looksRemote as z,parseRepoSpec as q}from"./agent-source.js";import{everyStore as j,storePath as Y}from"./agent-store.js";import{attachmentStore as ue}from"./attachments.js";import{HARNESSES as fe,loadConfig as $,nonEmptyFlag as W,parseHarnessName as we,positiveIntFlag as ge,resolveAgent as ye}from"./config.js";import{dbPath as J,openDb as Q}from"./db.js";import{describeMissing as ke,loadEnv as S,readCredentials as Se}from"./env.js";import{BishopError as v}from"./errors.js";import{fileStore as ve}from"./files.js";import{DEFAULT_MAX_AGE_DAYS as be,makeCollector as Te,sweep as Ie}from"./gc.js";import{ghAuthLogin as Ee,ghCliIn as Ae,gitAuthenticatesToGithub as _e,withTemporaryGhConfig as Re}from"./github/gh.js";import{AGENT_SCOPES as Ne,checkGithubSetup as Oe}from"./github/setup.js";import{createGmail as Ge}from"./gmail/create.js";import{checkGmailSetup as xe}from"./gmail/setup.js";import{createHarness as $e}from"./harness/create.js";import{sharedFileFetcher as Ce}from"./interface/attachments.js";import{principalCheck as V}from"./interface/authorize.js";import{deliveryRouter as Le}from"./interface/delivery.js";import{conversationMatch as He,destinationResolver as Me}from"./interface/destination.js";import{log as m,requestedLevel as Pe}from"./log.js";import{principalStore as De}from"./principals.js";import{addSchedule as Be,listSchedules as Ue,removeSchedule as Ke,setScheduleEnabled as X}from"./schedule/cli.js";import{authorizedToRun as Fe,makeScheduler as ze}from"./schedule/runner.js";import{scheduleStore as qe}from"./schedule/store.js";import{threadQueue as je}from"./sessions/queue.js";import{interfaceState as Ye,retentionStore as Z,sessionStore as We,threadContexts as Je,threadHandover as Qe,threadMeta as Ve}from"./sessions/store.js";import{ensureGitignore as Xe,openBrowser as Ze,resolveAppDetails as et,SetupError as ee,setupSlack as tt,setupSlackManually as ot,withPrompt as te,writeEnv as oe}from"./setup.js";import{createSlackInterface as nt}from"./slack/interface.js";import{buildManifest as st}from"./slack/manifest.js";import{createTeams as rt}from"./teams/create.js";import{administratorInstructions as at,repointTeamsEndpoint as it,setupTeams as ct,setupTeamsManually as dt}from"./teams/setup.js";import{threadModel as pt}from"./thread-model.js";import{fileTools as mt}from"./tools/files.js";import{scheduleTools as ht}from"./tools/schedules.js";import{startToolServer as lt}from"./tools/server.js";import{makeTurnRunner as ut}from"./turn.js";import{version as ft}from"./version.js";import{hasGitIdentity as wt,isGitRepo as gt,sharedDirNotice as yt,WORKTREE_DIR as kt}from"./worktrees.js";function u(e){process.stderr.write(`${e}
3
- `),process.exit(1)}async function St(e){const t=pe(e??".");let n;try{n=(await ce(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 vt(e){const t=await gt(e),n={enabled:!0,isRepo:t};return t?(m.info({agentDir:e,worktrees:de(e,kt)},"worktree mode is on"),await wt(e)||m.warn({agentDir:e},"git has no user.name or user.email here, so the agent cannot commit what it does in a worktree"),n):(m.warn({agentDir:e},"worktree mode is on but the agent directory is not a git repository; threads share it"),n)}function bt(e){if(e.kind==="options-replaced"){m.warn({harness:e.harness},`agent.harnesses.${e.harness} replaces agent.options, which is ignored for this run`);return}m.warn({harness:e.harness,configured:e.configured},`--harness overrides the configured harness, but agent.options is still written for that one; move it to agent.harnesses.${e.configured} and give ${e.harness} its own block`)}async function Tt(e=[],t={}){e.length>0&&u(`bishop: unknown command "${e[0]}"
2
+ import{stat as de}from"node:fs/promises";import{join as me,resolve as he}from"node:path";import{Command as le}from"commander";import{agentSnapshots as ue,fixedAgentDir as fe}from"./agent-dir.js";import{looksRemote as j,parseRepoSpec as Y}from"./agent-source.js";import{everyStore as W,storePath as J}from"./agent-store.js";import{attachmentStore as we}from"./attachments.js";import{HARNESSES as ge,loadConfig as G,nonEmptyFlag as Q,parseHarnessName as ye,positiveIntFlag as Se,resolveAgent as ke,sandboxConfig as ve,writeConfig as be}from"./config.js";import{dbPath as V,openDb as X}from"./db.js";import{describeMissing as Te,loadEnv as v,readCredentials as Ie}from"./env.js";import{BishopError as b}from"./errors.js";import{fileStore as Ee}from"./files.js";import{DEFAULT_MAX_AGE_DAYS as Ae,makeCollector as _e,sweep as xe}from"./gc.js";import{ghAuthLogin as Ne,ghCliIn as Re,gitAuthenticatesToGithub as Oe,withTemporaryGhConfig as Ge}from"./github/gh.js";import{AGENT_SCOPES as $e,checkGithubSetup as Le}from"./github/setup.js";import{createGmail as Ce}from"./gmail/create.js";import{checkGmailSetup as He}from"./gmail/setup.js";import{createHarness as Pe}from"./harness/create.js";import{sharedFileFetcher as Me}from"./interface/attachments.js";import{principalCheck as Z}from"./interface/authorize.js";import{deliveryRouter as De}from"./interface/delivery.js";import{conversationMatch as Be,destinationResolver as Ue}from"./interface/destination.js";import{log as m,requestedLevel as Fe}from"./log.js";import{DEFAULT_PROFILE as Ke,setupNono as ze}from"./nono-setup.js";import{principalStore as qe}from"./principals.js";import{checkSandbox as je,clearedStaging as Ye,sandboxPolicy as We,writeLauncher as Je}from"./sandbox.js";import{addSchedule as Qe,listSchedules as Ve,removeSchedule as Xe,setScheduleEnabled as ee}from"./schedule/cli.js";import{authorizedToRun as Ze,makeScheduler as et}from"./schedule/runner.js";import{scheduleStore as tt}from"./schedule/store.js";import{threadQueue as ot}from"./sessions/queue.js";import{interfaceState as nt,retentionStore as te,sessionStore as st,threadContexts as it,threadHandover as rt,threadMeta as at}from"./sessions/store.js";import{ensureGitignore as ct,openBrowser as pt,resolveAppDetails as dt,SetupError as oe,setupSlack as mt,setupSlackManually as ht,withPrompt as ne,writeEnv as se}from"./setup.js";import{createSlackInterface as lt}from"./slack/interface.js";import{buildManifest as ut}from"./slack/manifest.js";import{createTeams as ft}from"./teams/create.js";import{administratorInstructions as wt,repointTeamsEndpoint as gt,setupTeams as yt,setupTeamsManually as St}from"./teams/setup.js";import{threadModel as kt}from"./thread-model.js";import{fileTools as vt}from"./tools/files.js";import{scheduleTools as bt}from"./tools/schedules.js";import{startToolServer as Tt}from"./tools/server.js";import{transcriptStore as It}from"./transcript.js";import{makeTurnRunner as Et}from"./turn.js";import{version as At}from"./version.js";import{hasGitIdentity as _t,isGitRepo as xt,sharedDirNotice as Nt,WORKTREE_DIR as Rt}from"./worktrees.js";function u(e){process.stderr.write(`${e}
3
+ `),process.exit(1)}async function Ot(e){const t=he(e??".");let n;try{n=(await de(t)).isDirectory()}catch{throw new b(`--agent directory doesn't exist: ${t}`)}if(!n)throw new b(`--agent is not a directory: ${t}`);return t}async function Gt(e){const t=await xt(e),n={enabled:!0,isRepo:t};return t?(m.info({agentDir:e,worktrees:me(e,Rt)},"worktree mode is on"),await _t(e)||m.warn({agentDir:e},"git has no user.name or user.email here, so the agent cannot commit what it does in a worktree"),n):(m.warn({agentDir:e},"worktree mode is on but the agent directory is not a git repository; threads share it"),n)}function $t(e){if(e.kind==="options-replaced"){m.warn({harness:e.harness},`agent.harnesses.${e.harness} replaces agent.options, which is ignored for this run`);return}m.warn({harness:e.harness,configured:e.configured},`--harness overrides the configured harness, but agent.options is still written for that one; move it to agent.harnesses.${e.configured} and give ${e.harness} its own block`)}async function Lt(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&&z(t.agent)?q(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),a=Se();a.ok||u(ke(a));const r=ye(s,t);for(const o of r.warnings)bt(o);const d=Q(J(n)),c=We(d,r.harness);let p;const l=async o=>await p?.check(o)??{ok:!0},f=i?he({spec:i,store:Y(n,i.slug),dirs:c,autoUpdate:!!t.autoUpdate,recheck:l}):void 0,w=f?await f.start():await St(t.agent),se=f?f.forTurn:le(w,c);m.info({bishopDir:n,agentDir:w,...w===n?{}:{separate:!0},...i?{source:i.cloneUrl,autoUpdate:!!t.autoUpdate}:{},app:s.slack?.app?.name},"bishop starting");const _=s.agent?.worktrees?await vt(w):void 0,b=$e(r.harness,{...r.options?{options:r.options}:{},...r.model?{model:r.model}:{},...r.effort?{effort:r.effort}:{},..._?{worktrees:_}:{},credentials:a.credentials}),R=await b.check(w);R.ok||u(b.describeStartupFailure(w,R.error));const G=R.model??r.model;m.info({harness:b.name,credentials:R.credentialSource??"unreported",model:G,...r.effort?{effort:r.effort}:{},...r.options?{agentOptions:Object.keys(r.options)}:{}},"harness ready"),p=b,m.info({sessions:c.count()},"opened session store");const L=Te({retention:Z(d),stores:()=>j(n),home:n,...s.gc?.maxAgeDays===void 0?{}:{maxAgeDays:s.gc.maxAgeDays}});L.start();let y;const re=o=>y?V(y,T)(o):Fe(o),ae=async(o,h)=>{if(!y)throw new v("no interface is running, so there is nowhere to send");return Me(y,{optedInOnly:!0})(o,h)},ie=(o,h)=>y?He(y)(o,h):!1,H=ve({home:n,...s.files?.maxMb===void 0?{}:{maxMb:s.files.maxMb}}),M=ue(d),T=De(d),P=qe(d),D=await lt({tools:[...ht({schedules:P,authorized:re,resolve:ae,sameConversation:ie,principals:T}),...mt({fetch:Ce({interfaces:()=>y??[],store:M,files:H})})]}),N=Ve(d),x=pt(N),B={...G?{model:G}:{},...r.effort?{effort:r.effort}:{}},U=ut({harness:b,cwd:se,sessions:c,model:x,tools:D,contexts:Je(d,N),..._&&!_.isRepo?{sharedDirNotice:yt(w)}:{}}),K=je(),I={threads:c,events:c,files:H,attachments:M,principals:T,run:o=>K.run(o.thread.threadKey,()=>U.run(o)),stop:o=>U.stop(o)},g=[];if(a.credentials.slack&&g.push(await nt({...I,credentials:a.credentials.slack,meta:N,model:x,configuredModel:B,...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 Ge({...I,credentials:a.credentials.gmail,...s.gmail?{config:s.gmail}:{},state:Ye(d)}))}catch(o){o instanceof v&&u(o.message);const h=a.credentials.gmail,O=h.kind==="oauth"?"the authorized Gmail account":`${h.user}`;u(`Couldn't reach the mailbox for ${O}:
5
+ Run \`bishop --help\` to see what's available.`);const n=process.cwd(),c=t.agent&&j(t.agent)?Y(t.agent):void 0;t.autoUpdate&&!c&&u("--auto-update needs --agent to be a git URL. Bishop never updates a directory it was pointed at; that state is yours to manage."),v(n);const s=await G(n),a=Ie();a.ok||u(Te(a));const i=ke(s,t);for(const o of i.warnings)$t(o);const h=ve(s);if(h){const o=await je(h);m.info({nono:o,...h.profile?{profile:h.profile}:{}},"sandbox is on")}const l=h?(o,d)=>We({home:n,cwd:o,...d?{dirKey:d}:{},...h.profile?{profile:h.profile}:{}}):void 0,r=X(V(n)),p=st(r,i.harness);let w;const $=async o=>await w?.check(o,await l?.(o))??{ok:!0},_=c?ue({spec:c,store:J(n,c.slug),dirs:p,autoUpdate:!!t.autoUpdate,recheck:$}):void 0,g=_?await _.start():await Ot(t.agent),re=_?_.forTurn:fe(g,p);m.info({bishopDir:n,agentDir:g,...g===n?{}:{separate:!0},...c?{source:c.cloneUrl,autoUpdate:!!t.autoUpdate}:{},app:s.slack?.app?.name},"bishop starting");const x=s.agent?.worktrees?await Gt(g):void 0,T=Pe(i.harness,{...i.options?{options:i.options}:{},...i.model?{model:i.model}:{},...i.effort?{effort:i.effort}:{},...x?{worktrees:x}:{},...h?{launcher:await Je(n)}:{},credentials:a.credentials}),N=await T.check(g,await l?.(g));N.ok||u(T.describeStartupFailure(g,N.error));const L=N.model??i.model;m.info({harness:T.name,credentials:N.credentialSource??"unreported",model:L,...i.effort?{effort:i.effort}:{},...i.options?{agentOptions:Object.keys(i.options)}:{}},"harness ready"),w=T,m.info({sessions:p.count()},"opened session store");const P=_e({retention:te(r),stores:()=>W(n),home:n,...s.gc?.maxAgeDays===void 0?{}:{maxAgeDays:s.gc.maxAgeDays}});P.start();let y;const ae=o=>y?Z(y,k)(o):Ze(o),ce=async(o,d)=>{if(!y)throw new b("no interface is running, so there is nowhere to send");return Ue(y,{optedInOnly:!0})(o,d)},pe=(o,d)=>y?Be(y)(o,d):!1,M=Ee({home:n,...s.files?.maxMb===void 0?{}:{maxMb:s.files.maxMb}}),D=we(r),k=qe(r),B=tt(r),U=await Tt({tools:[...bt({schedules:B,authorized:ae,resolve:ce,sameConversation:pe,principals:k}),...vt({fetch:Me({interfaces:()=>y??[],store:D,files:M})})]}),R=at(r),C=kt(R),F={...L?{model:L}:{},...i.effort?{effort:i.effort}:{}},K=Et({harness:T,cwd:re,sessions:p,model:C,tools:U,contexts:it(r,R),transcript:It(r),principals:k,...x&&!x.isRepo?{sharedDirNotice:Nt(g)}:{},...l?{sandbox:{policy:(o,d)=>l(o,d.dirKey),staging:await Ye(n)}}:{}}),z=ot(),I={threads:p,events:p,files:M,attachments:D,principals:k,run:o=>z.run(o.thread.threadKey,()=>K.run(o)),stop:o=>K.stop(o)},f=[];if(a.credentials.slack&&f.push(await lt({...I,credentials:a.credentials.slack,meta:R,model:C,configuredModel:F,...s.slack?.verbosity?{verbosity:s.slack.verbosity}:{},...s.slack?.allow?{allow:s.slack.allow}:{},...s.slack?.maxBotTurns===void 0?{}:{maxBotTurns:s.slack.maxBotTurns}})),a.credentials.gmail)try{f.push(await Ce({...I,credentials:a.credentials.gmail,...s.gmail?{config:s.gmail}:{},state:nt(r)}))}catch(o){o instanceof b&&u(o.message);const d=a.credentials.gmail,O=d.kind==="oauth"?"the authorized Gmail account":`${d.user}`;u(`Couldn't reach the mailbox for ${O}:
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 rt({...I,credentials:a.credentials.teams,meta:N,model:x,configuredModel:B,...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{f.push(await ft({...I,credentials:a.credentials.teams,meta:R,model:C,configuredModel:F,...s.teams?{config:s.teams}:{}}))}catch(o){o instanceof b&&u(o.message),u(`Couldn't start the Teams app for client ${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();m.info({interfaces:g.map(o=>o.name)},"bishop is listening");const F=ze({schedules:P,run:I.run,threads:c,cancel:I.stop,deliver:Le(g),authorized:V(g,T),principals:T,attach:Qe(c)});F.start();for(const o of["SIGINT","SIGTERM"])process.once(o,()=>{m.info({signal:o,inFlight:K.size},"shutting down"),F.stop().catch(h=>m.error({err:h},"the scheduler failed to stop cleanly")).then(()=>Promise.allSettled(g.map(h=>h.stop()))).then(async h=>{const O=h.filter(E=>E.status==="rejected");for(const E of O)m.error({err:E.reason},"an interface failed to stop cleanly");await D.stop().catch(E=>{m.error({err:E},"the tool endpoint failed to stop cleanly")}),await L.stop(),d.close(),process.exit(O.length>0?1:0)})})}function It(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=f;for(const o of f)await o.start();m.info({interfaces:f.map(o=>o.name)},"bishop is listening");const q=et({schedules:B,run:I.run,threads:p,cancel:I.stop,deliver:De(f),authorized:Z(f,k),principals:k,attach:rt(p)});q.start();for(const o of["SIGINT","SIGTERM"])process.once(o,()=>{m.info({signal:o,inFlight:z.size},"shutting down"),q.stop().catch(d=>m.error({err:d},"the scheduler failed to stop cleanly")).then(()=>Promise.allSettled(f.map(d=>d.stop()))).then(async d=>{const O=d.filter(E=>E.status==="rejected");for(const E of O)m.error({err:E.reason},"an interface failed to stop cleanly");await U.stop().catch(E=>{m.error({err:E},"the tool endpoint failed to stop cleanly")}),await P.stop(),r.close(),process.exit(O.length>0?1:0)})})}function Ct(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 Et(e){const t=process.cwd();S(t),e.agent&&!z(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?[Y(t,q(e.agent).slug)]:await j(t),i=await $(t),s=e.maxAgeDays??i.gc?.maxAgeDays;Pe()||(m.level="error");const a=Q(J(t));try{It(await Ie({retention:Z(a),stores:n,home:t,...e.agent?{only:n}:{},...s===void 0?{}:{maxAgeDays:s}}))}finally{a.close()}}function At(){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 _t(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 d=e.browser===!1||!At()?"paste":"browser",c=await te(!0,async p=>xe({...n&&i?{client:{clientId:n,clientSecret:i}}:{},...s?{refreshToken:s}:{},...a?{user:a}:{},...r?{serviceAccount:r}:{},mode:d,openBrowser:Ze,readPaste:async l=>(process.stdout.write(`Open this in a browser and approve it:
17
+ `)}async function Ht(e){const t=process.cwd();v(t),e.agent&&!j(e.agent)&&u("gc --agent takes the git URL Bishop cloned, not a path. It narrows the pass to the files Bishop owns for one agent, and it owns none for a directory agent. Run `bishop gc` with no --agent to collect what its threads left behind.");const n=e.agent?[J(t,Y(e.agent).slug)]:await W(t),c=await G(t),s=e.maxAgeDays??c.gc?.maxAgeDays;Fe()||(m.level="error");const a=X(V(t));try{Ct(await xe({retention:te(a),stores:n,home:t,...e.agent?{only:n}:{},...s===void 0?{}:{maxAgeDays:s}}))}finally{a.close()}}function Pt(){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 Mt(e={}){const t=process.cwd();v(t);const n=process.env.BISHOP_GMAIL_CLIENT_ID?.trim(),c=process.env.BISHOP_GMAIL_CLIENT_SECRET?.trim(),s=process.env.BISHOP_GMAIL_REFRESH_TOKEN?.trim(),a=process.env.BISHOP_GMAIL_USER?.trim(),i=process.env.BISHOP_GMAIL_SERVICE_ACCOUNT?.trim();n&&!c&&u("BISHOP_GMAIL_CLIENT_ID is set but BISHOP_GMAIL_CLIENT_SECRET is not.");const h=e.browser===!1||!Pt()?"paste":"browser",l=await ne(!0,async r=>He({...n&&c?{client:{clientId:n,clientSecret:c}}:{},...s?{refreshToken:s}:{},...a?{user:a}:{},...i?{serviceAccount:i}:{},mode:h,openBrowser:pt,readPaste:async p=>(process.stdout.write(`Open this in a browser and approve it:
18
18
 
19
- ${l}
19
+ ${p}
20
20
 
21
21
  It will finish on a http://127.0.0.1 address that fails to load. That is expected.
22
22
  Copy the whole address out of the browser and paste it here.
23
23
 
24
- `),p("Redirect URL",""))}));if(process.stdout.write(`${c.report}
25
- `),c.refreshToken){const p=await oe(t,{BISHOP_GMAIL_REFRESH_TOKEN:c.refreshToken});process.stdout.write(`
26
- Saved BISHOP_GMAIL_REFRESH_TOKEN to ${p}
27
- `)}c.ok||process.exit(1)}async function Rt(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,d=l=>r.write(l),c=l=>Oe({...i?{token:i,tokenIsSaved:s}:{},...a?{login:()=>Ee(Ne.map(f=>f.scope),l),confirm:async f=>/^y/i.test(await te(!0,w=>w(f,"y"),r))}:{},gitConfigured:_e,gh:Ae(l),...e.temporary?{temporary:!0}:{}}),p=e.temporary&&!i?await Re(c):await c();if(d(`${p.report}
28
- `),e.print)p.token&&process.stdout.write(`${JSON.stringify({GH_TOKEN:p.token})}
29
- `);else if(p.token){if(p.source==="gh"){const f=await oe(t,{GH_TOKEN:p.token});d(`
30
- Saved GH_TOKEN to ${f}
31
- `)}const l=await Xe(t,[".env"]);l.length>0&&d(`Added ${l.join(", ")} to .gitignore
32
- `)}p.ok||process.exit(1)}async function Nt(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 ot(t):await tt(t)}async function Ot(e){if(e.appId!==void 0&&!e.manual)throw new ee("--app-id adopts an app somebody else created, so it needs --manual too.");if(e.endpoint!==void 0&&e.manual)throw new ee("--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 dt(t):await ct(t)}async function Gt(e){const t=process.cwd(),n=await $(t),i=await et({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(st(i),null,2)}
33
- `)}const k=new me().name("bishop").description("An agent gateway. Runs an agent and connects it to Slack and email.").version(ft,"-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 <${fe.join("|")}>`,"which SDK runs the agent",we).option("--model <model>","the model to run, in the harness's own naming",W("--model")).option("--effort <effort>","how hard the model should think, in the harness's own naming",W("--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)=>Tt(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 ${be}`,ge("--max-age-days")).option("--agent <url>","only this agent's snapshots; defaults to every agent Bishop has cloned here").action(Et);const A=k.command("schedules").description("Prompts the agent runs on a clock, and where their answers go");A.command("list").description("Everything scheduled here, in your own time zone").action(()=>Ue(process.cwd())),A.command("add").description("Add a schedule").requiredOption("--name <name>","what a person sees in a list, and the subject of an email").requiredOption("--cron <expression>","five-field cron, or @daily and friends").requiredOption("--prompt <text>","what the agent is asked, including when to stay quiet").requiredOption("--to <destination>","#channel or @person for Slack, an address for Gmail").option("--tz <zone>","IANA zone the expression is read in; defaults to this machine").option("--interface <name>","which interface delivers, when --to is ambiguous").action(e=>Be(process.cwd(),e)),A.command("rm <id>").description("Remove a schedule").action(e=>Ke(process.cwd(),e)),A.command("disable <id>").description("Stop a schedule firing, keeping it").action(e=>X(process.cwd(),e,!1)),A.command("enable <id>").description("Start a disabled schedule firing again, from now").action(e=>X(process.cwd(),e,!0));const xt=k.command("gmail").description("Gmail interface commands");xt.command("setup").description("Authorize a Gmail mailbox, or check the one already configured").option("--no-browser","print a URL to open elsewhere instead of opening one here").action(_t);const $t=k.command("github").description("GitHub credentials for the agent");$t.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(Rt);const ne=k.command("slack").description("Slack interface commands");ne.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(Nt),ne.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(Gt);const C=k.command("teams").description("Microsoft Teams interface commands");C.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(Ot),C.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)=>it(e,t.appId===void 0?{}:{appId:t.appId})),C.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(at(e.endpoint))});try{await k.parseAsync()}catch(e){throw e instanceof v&&u(e.message),e}
24
+ `),r("Redirect URL",""))}));if(process.stdout.write(`${l.report}
25
+ `),l.refreshToken){const r=await se(t,{BISHOP_GMAIL_REFRESH_TOKEN:l.refreshToken});process.stdout.write(`
26
+ Saved BISHOP_GMAIL_REFRESH_TOKEN to ${r}
27
+ `)}l.ok||process.exit(1)}async function Dt(e){const t=process.cwd(),n=process.env.GH_TOKEN?.trim();process.env.GH_TOKEN!==void 0&&!n&&delete process.env.GH_TOKEN,v(t);const c=process.env.GH_TOKEN?.trim(),s=!!c&&!n,a=e.prompt&&process.stdin.isTTY===!0,i=e.print?process.stderr:process.stdout,h=p=>i.write(p),l=p=>Le({...c?{token:c,tokenIsSaved:s}:{},...a?{login:()=>Ne($e.map(w=>w.scope),p),confirm:async w=>/^y/i.test(await ne(!0,$=>$(w,"y"),i))}:{},gitConfigured:Oe,gh:Re(p),...e.temporary?{temporary:!0}:{}}),r=e.temporary&&!c?await Ge(l):await l();if(h(`${r.report}
28
+ `),e.print)r.token&&process.stdout.write(`${JSON.stringify({GH_TOKEN:r.token})}
29
+ `);else if(r.token){if(r.source==="gh"){const w=await se(t,{GH_TOKEN:r.token});h(`
30
+ Saved GH_TOKEN to ${w}
31
+ `)}const p=await ct(t,[".env"]);p.length>0&&h(`Added ${p.join(", ")} to .gitignore
32
+ `)}r.ok||process.exit(1)}async function Bt(e){const t=process.cwd(),n=await ze({config:await G(t),name:e.name});process.stdout.write(`${n.report}
33
+ `),n.config&&process.stdout.write(`Saved ${await be(n.config,t)}
34
+ `),n.ok||process.exit(1)}async function Ut(e){v(process.cwd());const t={...e.name===void 0?{}:{name:e.name},...e.displayName===void 0?{}:{displayName:e.displayName},...e.description===void 0?{}:{description:e.description},prompt:e.prompt};e.manual?await ht(t):await mt(t)}async function Ft(e){if(e.appId!==void 0&&!e.manual)throw new oe("--app-id adopts an app somebody else created, so it needs --manual too.");if(e.endpoint!==void 0&&e.manual)throw new oe("--endpoint sets the URL on an app Bishop is creating, so it cannot be used\nwith --manual. Point an adopted app with `bishop teams endpoint <url>`,\nor ask whoever created it to.");v(process.cwd());const t={...e.name===void 0?{}:{name:e.name},...e.displayName===void 0?{}:{displayName:e.displayName},...e.description===void 0?{}:{description:e.description},...e.endpoint===void 0?{}:{endpoint:e.endpoint},...e.appId===void 0?{}:{appId:e.appId},prompt:e.prompt};e.manual?await St(t):await yt(t)}async function Kt(e){const t=process.cwd(),n=await G(t),c=await dt({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(ut(c),null,2)}
35
+ `)}const S=new le().name("bishop").description("An agent gateway. Runs an agent and connects it to Slack and email.").version(At,"-v, --version").enablePositionalOptions().option("--agent <path-or-url>","the agent's directory, or a git URL to clone; defaults to the current directory").option(`--harness <${ge.join("|")}>`,"which SDK runs the agent",ye).option("--model <model>","the model to run, in the harness's own naming",Q("--model")).option("--effort <effort>","how hard the model should think, in the harness's own naming",Q("--effort")).option("--auto-update","for a git-URL --agent, check for a new commit at the start of every new thread").allowExcessArguments(!0).action((e,t)=>Lt(t.args,e));S.command("gc").description("Collect the snapshots, worktrees, and rows old conversations left behind").option("--max-age-days <days>",`how long a quiet thread's remains are kept; defaults to gc.maxAgeDays, then ${Ae}`,Se("--max-age-days")).option("--agent <url>","only this agent's snapshots; defaults to every agent Bishop has cloned here").action(Ht);const A=S.command("schedules").description("Prompts the agent runs on a clock, and where their answers go");A.command("list").description("Everything scheduled here, in your own time zone").action(()=>Ve(process.cwd())),A.command("add").description("Add a schedule").requiredOption("--name <name>","what a person sees in a list, and the subject of an email").requiredOption("--cron <expression>","five-field cron, or @daily and friends").requiredOption("--prompt <text>","what the agent is asked, including when to stay quiet").requiredOption("--to <destination>","#channel or @person for Slack, an address for Gmail").option("--tz <zone>","IANA zone the expression is read in; defaults to this machine").option("--interface <name>","which interface delivers, when --to is ambiguous").action(e=>Qe(process.cwd(),e)),A.command("rm <id>").description("Remove a schedule").action(e=>Xe(process.cwd(),e)),A.command("disable <id>").description("Stop a schedule firing, keeping it").action(e=>ee(process.cwd(),e,!1)),A.command("enable <id>").description("Start a disabled schedule firing again, from now").action(e=>ee(process.cwd(),e,!0));const zt=S.command("gmail").description("Gmail interface commands");zt.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(Mt);const qt=S.command("github").description("GitHub credentials for the agent");qt.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(Dt);const jt=S.command("nono").description("The nono sandbox the agent runs under");jt.command("setup").description("Write a nono profile that lets a sandboxed agent push").option("--name <name>","the profile name",Ke).action(Bt);const ie=S.command("slack").description("Slack interface commands");ie.command("setup").description("Create and install a Slack app, and save its credentials here").option("--name <name>","app name; defaults to the current directory name").option("--display-name <name>","bot display name in Slack; defaults to --name").option("--description <text>","short description shown in Slack").option("--no-prompt","don't prompt for anything left off").option("--manual","adopt an app you created in Slack yourself, using its tokens").action(Ut),ie.command("manifest").description("Print the app manifest, for creating an app in Slack by hand").option("--name <name>","app name; defaults to config or the directory name").option("--display-name <name>","bot display name; defaults to --name").option("--description <text>","short description shown in Slack").action(Kt);const H=S.command("teams").description("Microsoft Teams interface commands");H.command("setup").description("Create a Teams app and save its credentials here, or check the one configured").option("--name <name>","app name; defaults to the current directory name").option("--display-name <name>","app name shown in Teams; defaults to --name").option("--description <text>","short description shown in Teams").option("--endpoint <url>","the URL Teams POSTs to; can be set later").option("--app-id <id>","with --manual, the Teams app ID you were given").option("--no-prompt","don't prompt for anything left off").option("--manual","adopt an app somebody else created, using its credentials").action(Ft),H.command("endpoint <url>").description("Point the Teams app at a different URL, changing nothing else").option("--app-id <id>","the Teams app to point; defaults to the one in config").action((e,t)=>gt(e,t.appId===void 0?{}:{appId:t.appId})),H.command("instructions").description("Print what to send whoever creates the Teams app, when that is an administrator").option("--endpoint <url>","the URL Teams should POST to, if you already know it").action(e=>{process.stdout.write(wt(e.endpoint))});try{await S.parseAsync()}catch(e){throw e instanceof b&&u(e.message),e}
package/dist/config.js CHANGED
@@ -1,4 +1,4 @@
1
- import{mkdir as h,readFile as w,writeFile as b}from"node:fs/promises";import{dirname as x,join as O}from"node:path";import{z as t}from"zod";import{BishopError as p}from"./errors.js";const y=".bishop",j="config.json",k=t.strictObject({id:t.string().optional(),name:t.string().min(1),displayName:t.string().optional(),description:t.string().optional()}),N=["low","medium","high"],J="high",d=t.string().transform(n=>n.trim().toLowerCase()).pipe(t.enum(N)),$=t.strictObject({workspace:t.string().optional(),app:k.optional(),verbosity:d.optional(),allow:t.array(t.string().min(1)).optional(),maxBotTurns:t.number().int().nonnegative().optional()}),v=t.strictObject({allow:t.array(t.string().min(1)).optional(),pollSeconds:t.number().int().positive().optional(),labelPrefix:t.string().trim().min(1).optional(),markRead:t.boolean().optional()}),E=t.strictObject({cert:t.string().trim().min(1),key:t.string().trim().min(1)}),S=t.strictObject({id:t.string().optional(),name:t.string().min(1),displayName:t.string().optional(),description:t.string().optional()}),C=t.strictObject({app:S.optional(),allow:t.array(t.string().min(1)).optional(),verbosity:d.optional(),port:t.number().int().min(1).max(65535).optional(),host:t.string().trim().min(1).optional(),tls:E.optional()}),a=["claude","codex"],F=t.strictObject({model:t.string().trim().min(1).optional(),effort:t.string().trim().min(1).optional(),options:t.record(t.string(),t.unknown()).optional()}),T=t.strictObject({harness:t.enum(a).optional(),model:t.string().trim().min(1).optional(),effort:t.string().trim().min(1).optional(),harnesses:t.partialRecord(t.enum(a),F,{error:()=>`not a harness Bishop runs; supported: ${a.join(", ")}`}).optional(),worktrees:t.boolean().optional(),options:t.record(t.string(),t.unknown()).optional()}),I=t.strictObject({maxAgeDays:t.number().int().positive().optional()}),A=t.strictObject({maxMb:t.number().positive().optional()}),B=t.strictObject({slack:$.optional(),gmail:v.optional(),teams:C.optional(),agent:T.optional(),files:A.optional(),gc:I.optional()});class l extends p{path;constructor(o,r){super(o,"ConfigError"),this.path=r}}function P(n,o={}){const r=n.agent,s=r?.harness??"claude",e=o.harness??s,i=r?.harnesses?.[e],c=[];r?.options&&(i&&e===s?c.push({kind:"options-replaced",harness:e}):!i&&e!==s&&c.push({kind:"options-foreign",harness:e,configured:s}));const m=o.model??i?.model??r?.model,u=o.effort??i?.effort??r?.effort,g=i?i.options:r?.options;return{harness:e,...m?{model:m}:{},...u?{effort:u}:{},...g?{options:g}:{},warnings:c}}function V(n){return o=>{const r=o.trim();if(!r)throw new p(`${n} needs a value`);return r}}function _(n){return o=>{const r=Number(o.trim());if(!Number.isInteger(r)||r<=0)throw new p(`${n} needs a positive whole number, got "${o}"`);return r}}function Y(n){const o=n.trim();if(a.includes(o))return o;throw new p(`unknown harness "${n}"; supported: ${a.join(", ")}`)}function f(n=process.cwd()){return O(n,y,j)}function R(n){return n.issues.map(o=>`${o.path.length>0?o.path.join("."):"(root)"}: ${o.message}`).join(`
2
- `)}async function z(n=process.cwd()){const o=f(n);let r;try{r=await w(o,"utf8")}catch(i){if(i.code==="ENOENT")return{};throw new l(`Couldn't read ${o}: ${i.message}`,o)}let s;try{s=JSON.parse(r)}catch(i){throw new l(`${o} isn't valid JSON: ${i.message}`,o)}const e=B.safeParse(s);if(!e.success)throw new l(`${o} has invalid values:
3
- ${R(e.error)}`,o);return e.data}async function M(n,o=process.cwd()){const r=f(o);return await h(x(r),{recursive:!0}),await b(r,`${JSON.stringify(n,null,2)}
4
- `,"utf8"),r}function U(n){return n.displayName?.trim()||n.name}export{y as CONFIG_DIR,j as CONFIG_FILE,l as ConfigError,B as ConfigSchema,J as DEFAULT_VERBOSITY,a as HARNESSES,N as VERBOSITY,f as configPath,U as displayNameOf,z as loadConfig,V as nonEmptyFlag,Y as parseHarnessName,_ as positiveIntFlag,P as resolveAgent,M as writeConfig};
1
+ import{mkdir as h,readFile as b,writeFile as w}from"node:fs/promises";import{dirname as x,join as O}from"node:path";import{z as t}from"zod";import{BishopError as p}from"./errors.js";const y=".bishop",j="config.json",k=t.strictObject({id:t.string().optional(),name:t.string().min(1),displayName:t.string().optional(),description:t.string().optional()}),$=["low","medium","high"],J="high",f=t.string().transform(n=>n.trim().toLowerCase()).pipe(t.enum($)),N=t.strictObject({workspace:t.string().optional(),app:k.optional(),verbosity:f.optional(),allow:t.array(t.string().min(1)).optional(),maxBotTurns:t.number().int().nonnegative().optional()}),v=t.strictObject({allow:t.array(t.string().min(1)).optional(),pollSeconds:t.number().int().positive().optional(),labelPrefix:t.string().trim().min(1).optional(),markRead:t.boolean().optional(),internalDomains:t.array(t.string().trim().toLowerCase().regex(/^[^\s@]+\.[^\s@]+$/,"must be a domain like example.com")).optional()}),E=t.strictObject({cert:t.string().trim().min(1),key:t.string().trim().min(1)}),S=t.strictObject({id:t.string().optional(),name:t.string().min(1),displayName:t.string().optional(),description:t.string().optional()}),C=t.strictObject({app:S.optional(),allow:t.array(t.string().min(1)).optional(),verbosity:f.optional(),port:t.number().int().min(1).max(65535).optional(),host:t.string().trim().min(1).optional(),tls:E.optional()}),a=["claude","codex"],F=t.strictObject({model:t.string().trim().min(1).optional(),effort:t.string().trim().min(1).optional(),options:t.record(t.string(),t.unknown()).optional()}),T=t.strictObject({harness:t.enum(a).optional(),model:t.string().trim().min(1).optional(),effort:t.string().trim().min(1).optional(),harnesses:t.partialRecord(t.enum(a),F,{error:()=>`not a harness Bishop runs; supported: ${a.join(", ")}`}).optional(),worktrees:t.boolean().optional(),sandbox:t.union([t.boolean(),t.strictObject({profile:t.string().trim().min(1).optional()})]).optional(),options:t.record(t.string(),t.unknown()).optional()}),I=t.strictObject({maxAgeDays:t.number().int().positive().optional()}),A=t.strictObject({maxMb:t.number().positive().optional()}),B=t.strictObject({slack:N.optional(),gmail:v.optional(),teams:C.optional(),agent:T.optional(),files:A.optional(),gc:I.optional()});class l extends p{path;constructor(o,r){super(o,"ConfigError"),this.path=r}}function P(n,o={}){const r=n.agent,s=r?.harness??"claude",e=o.harness??s,i=r?.harnesses?.[e],c=[];r?.options&&(i&&e===s?c.push({kind:"options-replaced",harness:e}):!i&&e!==s&&c.push({kind:"options-foreign",harness:e,configured:s}));const m=o.model??i?.model??r?.model,u=o.effort??i?.effort??r?.effort,d=i?i.options:r?.options;return{harness:e,...m?{model:m}:{},...u?{effort:u}:{},...d?{options:d}:{},warnings:c}}function V(n){const o=n.agent?.sandbox;if(o)return o===!0||!o.profile?{}:{profile:o.profile}}function _(n){return o=>{const r=o.trim();if(!r)throw new p(`${n} needs a value`);return r}}function Y(n){return o=>{const r=Number(o.trim());if(!Number.isInteger(r)||r<=0)throw new p(`${n} needs a positive whole number, got "${o}"`);return r}}function z(n){const o=n.trim();if(a.includes(o))return o;throw new p(`unknown harness "${n}"; supported: ${a.join(", ")}`)}function g(n=process.cwd()){return O(n,y,j)}function R(n){return n.issues.map(o=>`${o.path.length>0?o.path.join("."):"(root)"}: ${o.message}`).join(`
2
+ `)}async function M(n=process.cwd()){const o=g(n);let r;try{r=await b(o,"utf8")}catch(i){if(i.code==="ENOENT")return{};throw new l(`Couldn't read ${o}: ${i.message}`,o)}let s;try{s=JSON.parse(r)}catch(i){throw new l(`${o} isn't valid JSON: ${i.message}`,o)}const e=B.safeParse(s);if(!e.success)throw new l(`${o} has invalid values:
3
+ ${R(e.error)}`,o);return e.data}async function U(n,o=process.cwd()){const r=g(o);return await h(x(r),{recursive:!0}),await w(r,`${JSON.stringify(n,null,2)}
4
+ `,"utf8"),r}function q(n){return n.displayName?.trim()||n.name}export{y as CONFIG_DIR,j as CONFIG_FILE,l as ConfigError,B as ConfigSchema,J as DEFAULT_VERBOSITY,a as HARNESSES,$ as VERBOSITY,g as configPath,q as displayNameOf,M as loadConfig,_ as nonEmptyFlag,z as parseHarnessName,Y as positiveIntFlag,P as resolveAgent,V as sandboxConfig,U as writeConfig};
package/dist/db.js CHANGED
@@ -1,4 +1,4 @@
1
- import{existsSync as X,mkdirSync as C,renameSync as w,rmSync as D,writeFileSync as S}from"node:fs";import{dirname as l,join as O}from"node:path";import{DatabaseSync as p}from"node:sqlite";import{BishopError as U}from"./errors.js";import{mint as y}from"./ids.js";import{log as A}from"./log.js";const I="bishop.db",g=O(".bishop","backups");function K(r=process.cwd()){return O(r,I)}const L=[`CREATE TABLE sessions (
1
+ import{existsSync as m,mkdirSync as C,renameSync as D,rmSync as w,writeFileSync as S}from"node:fs";import{dirname as l,join as O}from"node:path";import{DatabaseSync as u}from"node:sqlite";import{BishopError as U}from"./errors.js";import{mint as y}from"./ids.js";import{log as A}from"./log.js";const I="bishop.db",g=O(".bishop","backups");function K(r=process.cwd()){return O(r,I)}const L=[`CREATE TABLE sessions (
2
2
  thread_key TEXT PRIMARY KEY,
3
3
  session_id TEXT NOT NULL,
4
4
  created_at INTEGER NOT NULL,
@@ -64,8 +64,8 @@ import{existsSync as X,mkdirSync as C,renameSync as w,rmSync as D,writeFileSync
64
64
  CREATE TABLE thread_ids (
65
65
  thread_key TEXT PRIMARY KEY,
66
66
  id TEXT NOT NULL
67
- );`);const E=r.prepare(`SELECT thread_key, workspace_key FROM sessions
68
- WHERE workspace_key IS NOT NULL ORDER BY updated_at DESC, thread_key`).all(),a=new Map,T=new Map;for(const t of E)a.has(t.workspace_key)||a.set(t.workspace_key,t.thread_key),T.set(t.thread_key,t.workspace_key);const s=(t,i)=>{const c=new Set([i]);let d=i;for(let o=t.get(d);o;o=t.get(d)){if(c.has(o))return i;c.add(o),d=o}return d},n=t=>s(a,t),_=t=>s(T,t),N=r.prepare(`SELECT thread_key FROM sessions
67
+ );`);const s=r.prepare(`SELECT thread_key, workspace_key FROM sessions
68
+ WHERE workspace_key IS NOT NULL ORDER BY updated_at DESC, thread_key`).all(),a=new Map,T=new Map;for(const t of s)a.has(t.workspace_key)||a.set(t.workspace_key,t.thread_key),T.set(t.thread_key,t.workspace_key);const E=(t,i)=>{const c=new Set([i]);let d=i;for(let N=t.get(d);N;N=t.get(d)){if(c.has(N))return i;c.add(N),d=N}return d},n=t=>E(a,t),_=t=>E(T,t),o=r.prepare(`SELECT thread_key FROM sessions
69
69
  UNION SELECT thread_key FROM thread_dirs
70
70
  UNION SELECT thread_key FROM thread_meta
71
71
  UNION SELECT thread_key FROM thread_files`).all().map(t=>t.thread_key),h=r.prepare(`SELECT d.agent_dir AS agent_dir,
@@ -74,7 +74,7 @@ import{existsSync as X,mkdirSync as C,renameSync as w,rmSync as D,writeFileSync
74
74
  FROM (SELECT ? AS key, ? AS dir_key) k
75
75
  LEFT JOIN sessions s ON s.thread_key = k.key
76
76
  LEFT JOIN thread_dirs d ON d.thread_key = k.dir_key`),R=r.prepare(`INSERT INTO threads (id, thread_key, dir_key, agent_dir, created_at, used_at)
77
- VALUES (?, ?, ?, ?, ?, ?)`),k=r.prepare("INSERT INTO thread_ids (thread_key, id) VALUES (?, ?)"),f=new Map;for(const t of N){if(n(t)!==t)continue;const i=_(t),c=n(i)===t?i:t,d=h.get(t,c),o=d.used_at==null?e:Number(d.used_at),m=d.created_at==null?o:Number(d.created_at),u=y("t");R.run(u,t,c,d.agent_dir,m,o),f.set(t,u)}for(const t of N){const i=f.get(n(t));i&&k.run(t,i)}r.exec(`CREATE TABLE sessions_new (
77
+ VALUES (?, ?, ?, ?, ?, ?)`),k=r.prepare("INSERT INTO thread_ids (thread_key, id) VALUES (?, ?)"),f=new Map;for(const t of o){if(n(t)!==t)continue;const i=_(t),c=n(i)===t?i:t,d=h.get(t,c),N=d.used_at==null?e:Number(d.used_at),X=d.created_at==null?N:Number(d.created_at),p=y("t");R.run(p,t,c,d.agent_dir,X,N),f.set(t,p)}for(const t of o){const i=f.get(n(t));i&&k.run(t,i)}r.exec(`CREATE TABLE sessions_new (
78
78
  thread_id TEXT PRIMARY KEY REFERENCES threads (id) ON DELETE CASCADE,
79
79
  session_id TEXT NOT NULL,
80
80
  harness TEXT NOT NULL DEFAULT 'claude',
@@ -141,9 +141,9 @@ import{existsSync as X,mkdirSync as C,renameSync as w,rmSync as D,writeFileSync
141
141
  created_at INTEGER NOT NULL,
142
142
  updated_at INTEGER NOT NULL,
143
143
  UNIQUE (interface, external_id)
144
- );`);const E=r.prepare(`INSERT INTO principals
144
+ );`);const s=r.prepare(`INSERT INTO principals
145
145
  (id, interface, external_id, handle, name, email, kind, affiliation, created_at, updated_at)
146
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`);E.run("p_0","system","0","bishop","Bishop",null,"system","internal",e,e);const a=r.prepare("SELECT DISTINCT created_by FROM schedules WHERE created_by NOT LIKE 'cli:%'").all(),T=r.prepare("UPDATE schedules SET created_by = ? WHERE created_by = ?");for(const{created_by:s}of a){const n=s.indexOf(":");if(n<=0||n===s.length-1)continue;const _=s.slice(0,n),N=s.slice(n+1);if(_==="system")continue;const h=y("p"),R=_==="gmail"?N:null;E.run(h,_,N,N,N,R,"unknown","unknown",e,e),T.run(h,s)}},`CREATE TABLE locations (
146
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`);s.run("p_0","system","0","bishop","Bishop",null,"system","internal",e,e);const a=r.prepare("SELECT DISTINCT created_by FROM schedules WHERE created_by NOT LIKE 'cli:%'").all(),T=r.prepare("UPDATE schedules SET created_by = ? WHERE created_by = ?");for(const{created_by:E}of a){const n=E.indexOf(":");if(n<=0||n===E.length-1)continue;const _=E.slice(0,n),o=E.slice(n+1);if(_==="system")continue;const h=y("p"),R=_==="gmail"?o:null;s.run(h,_,o,o,o,R,"unknown","unknown",e,e),T.run(h,E)}},`CREATE TABLE locations (
147
147
  id TEXT PRIMARY KEY,
148
148
  interface TEXT NOT NULL,
149
149
  external_id TEXT NOT NULL,
@@ -162,13 +162,26 @@ import{existsSync as X,mkdirSync as C,renameSync as w,rmSync as D,writeFileSync
162
162
  WHEN thread_key LIKE 'gmail:%' THEN 'gmail'
163
163
  WHEN thread_key LIKE 'schedule:%' THEN 'schedule'
164
164
  ELSE 'slack'
165
- END;`];function M(r,e){return O(l(r),g,`${I}.v${e}.backup`)}function B(r,e,E){const a=M(e,E);if(X(a)&&!b(a,E))throw new U(`${a} is already there, and it is not a copy of this database at schema v${E}.
165
+ END;`,"ALTER TABLE principals ADD COLUMN self INTEGER NOT NULL DEFAULT 0;",`CREATE TABLE transcript (
166
+ seq INTEGER PRIMARY KEY,
167
+ thread_id TEXT NOT NULL REFERENCES threads (id) ON DELETE CASCADE,
168
+ turn_id TEXT NOT NULL,
169
+ direction TEXT NOT NULL,
170
+ kind TEXT NOT NULL,
171
+ principal_id TEXT,
172
+ external_id TEXT,
173
+ text TEXT NOT NULL,
174
+ files TEXT,
175
+ at INTEGER NOT NULL
176
+ );
177
+ CREATE INDEX transcript_thread ON transcript (thread_id, seq);
178
+ CREATE INDEX transcript_turn ON transcript (turn_id);`];function M(r,e){return O(l(r),g,`${I}.v${e}.backup`)}function B(r,e,s){const a=M(e,s);if(m(a)&&!b(a,s))throw new U(`${a} is already there, and it is not a copy of this database at schema v${s}.
166
179
 
167
180
  Nothing was migrated. Bishop copies the database before every schema
168
181
  change so a bad migration can be undone, and it will not overwrite
169
182
  whatever that file is. Move it aside and start Bishop again.`);const T=`${a}.partial`;try{C(l(a),{recursive:!0}),S(O(l(a),".gitignore"),`*
170
- `,"utf8"),D(T,{force:!0}),r.prepare("VACUUM INTO ?").run(T),w(T,a)}catch(s){const n=s instanceof Error?s.message:String(s);throw new U(`Couldn't copy ${e} to ${a} before migrating it: ${n}
183
+ `,"utf8"),w(T,{force:!0}),r.prepare("VACUUM INTO ?").run(T),D(T,a)}catch(E){const n=E instanceof Error?E.message:String(E);throw new U(`Couldn't copy ${e} to ${a} before migrating it: ${n}
171
184
 
172
185
  Nothing was migrated. Bishop copies the database before every schema
173
186
  change so a bad migration can be undone, and it will not migrate
174
- without one. Free some space, or move that file aside, and start again.`)}return a}function b(r,e){try{const E=new p(r,{readOnly:!0});try{return E.prepare("PRAGMA user_version").get()?.user_version===e}finally{E.close()}}catch{return!1}}function H(r){const e=new p(r);e.exec("PRAGMA journal_mode = WAL"),e.exec("PRAGMA busy_timeout = 5000"),e.exec("PRAGMA foreign_keys = ON");const a=e.prepare("PRAGMA user_version").get()?.user_version??0;if(a>0&&a<L.length){const T=B(e,r,a);A.info({backup:T,from:a},"copied the database before migrating it")}for(let T=a;T<L.length;T+=1){const s=L[T];if(s){e.exec("BEGIN");try{typeof s=="string"?e.exec(s):s(e),e.exec(`PRAGMA user_version = ${T+1}`),e.exec("COMMIT")}catch(n){throw e.exec("ROLLBACK"),n}}}return a===0?A.debug({schema:L.length},"created the session database"):a<L.length&&A.info({from:a,to:L.length},"migrated the session database"),e}export{g as BACKUPS_DIR,I as DB_FILE,L as MIGRATIONS,M as backupPath,K as dbPath,H as openDb};
187
+ without one. Free some space, or move that file aside, and start again.`)}return a}function b(r,e){try{const s=new u(r,{readOnly:!0});try{return s.prepare("PRAGMA user_version").get()?.user_version===e}finally{s.close()}}catch{return!1}}function H(r){const e=new u(r);e.exec("PRAGMA journal_mode = WAL"),e.exec("PRAGMA busy_timeout = 5000"),e.exec("PRAGMA foreign_keys = ON");const a=e.prepare("PRAGMA user_version").get()?.user_version??0;if(a>0&&a<L.length){const T=B(e,r,a);A.info({backup:T,from:a},"copied the database before migrating it")}for(let T=a;T<L.length;T+=1){const E=L[T];if(E){e.exec("BEGIN");try{typeof E=="string"?e.exec(E):E(e),e.exec(`PRAGMA user_version = ${T+1}`),e.exec("COMMIT")}catch(n){throw e.exec("ROLLBACK"),n}}}return a===0?A.debug({schema:L.length},"created the session database"):a<L.length&&A.info({from:a,to:L.length},"migrated the session database"),e}export{g as BACKUPS_DIR,I as DB_FILE,L as MIGRATIONS,M as backupPath,K as dbPath,H as openDb};
package/dist/files.js CHANGED
@@ -1,2 +1,2 @@
1
- import{createHash as g}from"node:crypto";import{createWriteStream as M}from"node:fs";import{mkdir as m,rm as v,stat as h,writeFile as y}from"node:fs/promises";import{dirname as A,join as c}from"node:path";import{pipeline as F}from"node:stream/promises";import{log as f}from"./log.js";const p=c(".bishop","files"),L=100,x=120;function $(e){return{name:e.name,...e.mimeType?{mimeType:e.mimeType}:{},...e.size===void 0?{}:{size:e.size},...e.url?{url:e.url}:{},...e.unavailable?{unavailable:e.unavailable}:{}}}function I(e){return e>=1024*1024?`${Math.round(e/(1024*1024))}MB`:e>=1024?`${Math.round(e/1024)}KB`:`${e} bytes`}function z(e){const r=e.replaceAll(/[^a-zA-Z0-9]+/g,"-").replaceAll(/^-+|-+$/g,"");if(r&&r.length<=80)return r;const i=g("sha256").update(e).digest("hex").slice(0,8);return`${r.slice(0,71)}-${i}`}function T(e){const r=e.replaceAll(/[/\\]/g,"-").replaceAll(/[\u0000-\u001f\u007f]/g,"").trim().replace(/^\.+/,"").trim();if(!r)return"file";if(r.length<=x)return r;const i=r.lastIndexOf("."),o=i>0&&r.length-i<=16?r.slice(i):"";return r.slice(0,x-o.length)+o}async function B(e){const r=c(e,p);await m(r,{recursive:!0}),await y(c(r,".gitignore"),`*
2
- `,"utf8")}function E(e,r,i,o){const t=g("sha256").update(i).digest("hex").slice(0,12);return c(e,p,z(r),t,T(o))}class b extends Error{}async function*_(e,r,i){const o=e instanceof Uint8Array?[e]:e;for await(const t of o){if(i.total+=t.byteLength,i.total>r)throw new b;yield t}}async function k(e,r){const i=await h(e).catch(()=>{});if(i?.isFile()&&!(r!==void 0&&i.size!==r))return{size:i.size}}function H(e){const r=Math.round((e.maxMb??L)*1024*1024),i=`too large to download: the limit is ${I(r)}`,o=new Map;async function t(l,n,s,a){const d=await k(a,n.size);if(d)return f.debug({thread:l,file:n.name},"a shared file is already saved"),{...s,path:a,size:d.size};const u={total:0};try{await B(e.home),await m(A(a),{recursive:!0}),await F(_(await n.fetch(),r,u),M(a))}catch(w){return await v(a,{force:!0}).catch(()=>{}),w instanceof b?(f.info({thread:l,file:n.name},"a shared file is over the limit"),{...s,unavailable:i}):(f.warn({thread:l,file:n.name,err:w},"couldn't download a shared file"),{...s,unavailable:"Bishop could not download it"})}return f.info({thread:l,file:n.name,bytes:u.total},"saved a shared file"),{...s,path:a,size:u.total}}return{async save(l,n){const s=$(n);if(n.unavailable)return f.info({thread:l,file:n.name,reason:n.unavailable},"a shared file cannot be downloaded"),{...s,unavailable:n.unavailable};if(n.size!==void 0&&n.size>r)return{...s,unavailable:i};const a=E(e.home,l,n.id,n.name),d=o.get(a);if(d)return d;const u=t(l,n,s,a).finally(()=>o.delete(a));return o.set(a,u),u}}}async function O(e,r){let i=0;for(const o of r){const t=c(e,p,z(o));if(await h(t).catch(()=>{}))try{await v(t,{recursive:!0,force:!0,maxRetries:1}),i+=1}catch(l){f.warn({dir:t,err:l},"couldn't remove a thread's files")}}return i}export{L as DEFAULT_MAX_FILE_MB,p as FILES_DIR,$ as describedFile,E as filePath,H as fileStore,O as releaseFiles,z as threadDirName};
1
+ import{createHash as v}from"node:crypto";import{createWriteStream as y}from"node:fs";import{mkdir as p,readdir as M,rm as m,stat as A,writeFile as F}from"node:fs/promises";import{dirname as L,join as c}from"node:path";import{pipeline as T}from"node:stream/promises";import{log as f}from"./log.js";const h=c(".bishop","files"),$=100,x=120;function D(e){return{name:e.name,...e.mimeType?{mimeType:e.mimeType}:{},...e.size===void 0?{}:{size:e.size},...e.url?{url:e.url}:{},...e.unavailable?{unavailable:e.unavailable}:{}}}function I(e){return e>=1024*1024?`${Math.round(e/(1024*1024))}MB`:e>=1024?`${Math.round(e/1024)}KB`:`${e} bytes`}function B(e){const r=e.replaceAll(/[^a-zA-Z0-9]+/g,"-").replaceAll(/^-+|-+$/g,"");if(r&&r.length<=80)return r;const i=v("sha256").update(e).digest("hex").slice(0,8);return`${r.slice(0,71)}-${i}`}function E(e){const r=e.replaceAll(/[/\\]/g,"-").replaceAll(/[\u0000-\u001f\u007f]/g,"").trim().replace(/^\.+/,"").trim();if(!r)return"file";if(r.length<=x)return r;const i=r.lastIndexOf("."),o=i>0&&r.length-i<=16?r.slice(i):"";return r.slice(0,x-o.length)+o}async function z(e){const r=c(e,h);await p(r,{recursive:!0}),await F(c(r,".gitignore"),`*
2
+ `,"utf8")}function w(e,r){return c(e,h,B(r))}async function P(e,r){await z(e);const i=w(e,r);return await p(i,{recursive:!0}),i}function _(e,r,i,o){const l=v("sha256").update(i).digest("hex").slice(0,12);return c(w(e,r),l,E(o))}class b extends Error{}async function*k(e,r,i){const o=e instanceof Uint8Array?[e]:e;for await(const l of o){if(i.total+=l.byteLength,i.total>r)throw new b;yield l}}async function S(e,r){const i=await A(e).catch(()=>{});if(i?.isFile()&&!(r!==void 0&&i.size!==r))return{size:i.size}}function W(e){const r=Math.round((e.maxMb??$)*1024*1024),i=`too large to download: the limit is ${I(r)}`,o=new Map;async function l(a,n,s,t){const d=await S(t,n.size);if(d)return f.debug({thread:a,file:n.name},"a shared file is already saved"),{...s,path:t,size:d.size};const u={total:0};try{await z(e.home),await p(L(t),{recursive:!0}),await T(k(await n.fetch(),r,u),y(t))}catch(g){return await m(t,{force:!0}).catch(()=>{}),g instanceof b?(f.info({thread:a,file:n.name},"a shared file is over the limit"),{...s,unavailable:i}):(f.warn({thread:a,file:n.name,err:g},"couldn't download a shared file"),{...s,unavailable:"Bishop could not download it"})}return f.info({thread:a,file:n.name,bytes:u.total},"saved a shared file"),{...s,path:t,size:u.total}}return{async save(a,n){const s=D(n);if(n.unavailable)return f.info({thread:a,file:n.name,reason:n.unavailable},"a shared file cannot be downloaded"),{...s,unavailable:n.unavailable};if(n.size!==void 0&&n.size>r)return{...s,unavailable:i};const t=_(e.home,a,n.id,n.name),d=o.get(t);if(d)return d;const u=l(a,n,s,t).finally(()=>o.delete(t));return o.set(t,u),u}}}async function X(e,r){let i=0;for(const o of r){const l=w(e,o),a=await M(l).catch(()=>{});if(a)try{await m(l,{recursive:!0,force:!0,maxRetries:1}),a.length>0&&(i+=1)}catch(n){f.warn({dir:l,err:n},"couldn't remove a thread's files")}}return i}export{$ as DEFAULT_MAX_FILE_MB,h as FILES_DIR,D as describedFile,P as ensureThreadFilesDir,_ as filePath,W as fileStore,X as releaseFiles,B as threadDirName,w as threadFilesDir};
@@ -0,0 +1 @@
1
+ import{audienceOf as c}from"../interface/context.js";import{isSelf as l}from"./identity.js";import{header as p}from"./message.js";import{domainOf as r,participants as s}from"./route.js";const f=new Set(["gmail.com","googlemail.com"]);function S(t,o){if(o)return new Set(o.map(e=>e.trim().toLowerCase()));const n=r(t);return new Set(n&&!f.has(n)?[n]:[])}function O(t,o,n){const e=s(t,o),a=[...t.to,...t.cc].some(i=>l(o,i.email)),m=p(t.headers,"list-id")!=="";return{visibility:e.length===1&&a&&!m?"one-to-one":"group",audience:c(e.map(i=>n.has(r(i))?"internal":"external"))}}export{S as internalDomains,O as mailObservation};
@@ -1 +1 @@
1
- import{gmailApi as c}from"./api.js";import{accessTokens as s,readServiceAccountKey as n}from"./auth.js";import{gmailAllowList as l}from"./authorize.js";import{createGmailInterface as f}from"./interface.js";import{DEFAULT_LABEL_PREFIX as i,ensureLabels as m}from"./labels.js";import{oauthTokens as h}from"./oauth.js";const k=20;async function d(e,o){if(e.kind==="oauth")return h({client:{clientId:e.clientId,clientSecret:e.clientSecret},refreshToken:e.refreshToken,...o});const r=await n(e.serviceAccount);return s({key:r,subject:e.user,...o})}async function v(e){const o=l(e.config?.allow),r=e.fetch?{fetch:e.fetch}:{},t=c({tokens:await d(e.credentials,r),...r}),a=await m(t,e.config?.labelPrefix??i);return f({api:t,labels:a,allow:o,threads:e.threads,events:e.events,state:e.state,files:e.files,attachments:e.attachments,principals:e.principals,pollIntervalMs:(e.config?.pollSeconds??k)*1e3,...e.config?.markRead===void 0?{}:{markRead:e.config.markRead},run:e.run,stop:e.stop})}export{v as createGmail};
1
+ import{gmailApi as c}from"./api.js";import{accessTokens as n,readServiceAccountKey as s}from"./auth.js";import{gmailAllowList as l}from"./authorize.js";import{createGmailInterface as i}from"./interface.js";import{DEFAULT_LABEL_PREFIX as f,ensureLabels as m}from"./labels.js";import{oauthTokens as h}from"./oauth.js";const k=20;async function g(e,o){if(e.kind==="oauth")return h({client:{clientId:e.clientId,clientSecret:e.clientSecret},refreshToken:e.refreshToken,...o});const a=await s(e.serviceAccount);return n({key:a,subject:e.user,...o})}async function T(e){const o=l(e.config?.allow),a=e.fetch?{fetch:e.fetch}:{},r=c({tokens:await g(e.credentials,a),...a}),t=await m(r,e.config?.labelPrefix??f);return i({api:r,labels:t,allow:o,threads:e.threads,events:e.events,state:e.state,files:e.files,attachments:e.attachments,principals:e.principals,pollIntervalMs:(e.config?.pollSeconds??k)*1e3,...e.config?.markRead===void 0?{}:{markRead:e.config.markRead},...e.config?.internalDomains?{internalDomains:e.config.internalDomains}:{},run:e.run,stop:e.stop})}export{T as createGmail};
@@ -1 +1 @@
1
- import{describedFile as g}from"../files.js";import{trimHistory as b,withinFileBudget as w}from"../interface/history.js";import{log as k}from"../log.js";import{DRAFT as y,SPAM as A,TRASH as F}from"./api.js";import{mailAttachment as I,mailFile as H,mailFileId as v,mailFilePayload as x}from"./files.js";import{isSelf as B}from"./identity.js";import{header as P,mailHeaders as S,parseMessage as f}from"./message.js";import{senderVerified as T}from"./route.js";function u(e){return e.references.length>0||!!P(e.headers,"in-reply-to")}function q(e){return u(e)||e.id!==e.threadId}function z(e,a){return e.map(t=>{const i=g(I(a.api,t));if(!t.fetchable)return i;const m=a.remember({id:v(t),name:t.name,payload:x(t)});return m?{...i,ref:m}:{...i,unavailable:"Bishop could not record a reference for it"}})}async function E(e){let a;try{a=await e.api.thread(e.message.threadId)}catch(n){return k.warn({threadId:e.message.threadId,err:n},"could not read the thread to catch the agent up"),[{kind:"omitted",reason:"unavailable"}]}const t=[];let i=0;const m=()=>{i&&(t.push({kind:"omitted",reason:"unauthorized",count:i}),i=0)};for(const n of a){if(n.id===e.message.id)break;const r=f(n);if(r.labelIds.some(d=>d===y||d===A||d===F))continue;const o=r.from.email.toLowerCase();if(!o||!T(r)){i+=1;continue}const s=B(e.self,o);if(!s&&!e.allow.allows(o)){i+=1;continue}const l=r.attachments.map((d,p)=>H(r.id,p,d));!r.body.trim()&&l.length===0||(m(),t.push({kind:"message",principal:e.principal({email:o,...r.from.name?{name:r.from.name}:{}}),text:r.body,headers:S(r),...s?{self:!0}:{},...l.length>0?{shared:l}:{}}))}m();const c=w(b(t)).map(n=>{if(n.kind!=="message"||!n.shared)return n;const{shared:r,...o}=n,s=z(r,e);return s.length>0?{...o,files:s}:o}),h=a[0]?f(a[0]):e.message;return u(h)&&c.unshift({kind:"omitted",reason:"unavailable"}),c}export{q as continuesAThread,E as mailHistory};
1
+ import{describedFile as g}from"../files.js";import{trimHistory as b,withinFileBudget as w}from"../interface/history.js";import{log as k}from"../log.js";import{DRAFT as y,SPAM as A,TRASH as F}from"./api.js";import{mailAttachment as I,mailFile as H,mailFileId as v,mailFilePayload as x}from"./files.js";import{isSelf as B}from"./identity.js";import{header as P,mailHeaders as S,parseMessage as f}from"./message.js";import{senderVerified as T}from"./route.js";function u(e){return e.references.length>0||!!P(e.headers,"in-reply-to")}function q(e){return u(e)||e.id!==e.threadId}function z(e,a){return e.map(t=>{const i=g(I(a.api,t));if(!t.fetchable)return i;const m=a.remember({id:v(t),name:t.name,payload:x(t)});return m?{...i,ref:m}:{...i,unavailable:"Bishop could not record a reference for it"}})}async function E(e){let a;try{a=await e.api.thread(e.message.threadId)}catch(n){return k.warn({threadId:e.message.threadId,err:n},"could not read the thread to catch the agent up"),[{kind:"omitted",reason:"unavailable"}]}const t=[];let i=0;const m=()=>{i&&(t.push({kind:"omitted",reason:"unauthorized",count:i}),i=0)};for(const n of a){if(n.id===e.message.id)break;const r=f(n);if(r.labelIds.some(s=>s===y||s===A||s===F))continue;const o=r.from.email.toLowerCase();if(!o||!T(r)){i+=1;continue}if(!B(e.self,o)&&!e.allow.allows(o)){i+=1;continue}const c=r.attachments.map((s,p)=>H(r.id,p,s));!r.body.trim()&&c.length===0||(m(),t.push({kind:"message",principal:e.principal({email:o,...r.from.name?{name:r.from.name}:{}}),text:r.body,headers:S(r),...c.length>0?{shared:c}:{}}))}m();const l=w(b(t)).map(n=>{if(n.kind!=="message"||!n.shared)return n;const{shared:r,...o}=n,d=z(r,e);return d.length>0?{...o,files:d}:o}),h=a[0]?f(a[0]):e.message;return u(h)&&l.unshift({kind:"omitted",reason:"unavailable"}),l}export{q as continuesAThread,E as mailHistory};
@@ -1,2 +1,2 @@
1
- import{BishopError as R}from"../errors.js";import{log as r}from"../log.js";import{retry as P}from"../retry.js";import{GmailApiError as $,gmailSendRetryable as q,UNREAD as z}from"./api.js";import{looksLikeAddress as H}from"./authorize.js";import{resolveGmailDestination as U}from"./destination.js";import{mailEtiquetteNote as W}from"./etiquette.js";import{mailAttachment as G,mailFile as J,mailFileId as Q,mailFilePayload as V,parseMailFile as X}from"./files.js";import{continuesAThread as Z,mailHistory as ee}from"./history.js";import{mailIdentity as te}from"./identity.js";import{buildMessage as ae,buildReply as ne,mailHeaders as re,parseMessage as oe,readAttachments as ie}from"./message.js";import{gmailPresence as se}from"./presence.js";import{participants as le,routeMessage as he}from"./route.js";const ce={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",O="gmail:attachedAt",D="in:inbox newer_than:2d",x=50;function T(a){return`gmail:${a}`}async function de(a){try{return await a.sendAs()}catch(w){return r.warn({err:w},"the mailbox would not say who it is; the agent will not know its own name or any alias"),[]}}async function Se(a){const w=await a.api.profile(),c=te(w.emailAddress,await de(a.api)),g=c.address,p={email:g,...c.name?{name:c.name}:{}};function S(e){const t=e.email.trim().toLowerCase();return a.principals.see(f.name,{externalId:t,handle:t,...e.name?.trim()?{name:e.name.trim()}:{},email:t,kind:"unknown",affiliation:"unknown"})}const f={...ce,self:{address:g,...c.name?{aka:[c.name]}:{}}},K=a.now??Date.now,b=a.state.get(O),y=b?Number(b):K();b||a.state.set(O,String(y));const E=new Set;let v,m,I=!1;const u=new Set;async function L(e,t){if(await a.labels.mark(e.threadId,"refused"),!(!t||E.has(e.threadId)))try{await a.api.send({raw:ne({from:p,to:[e.from],subject:e.subject,inReplyTo:e.messageId,references:e.references,body:t}),threadId:e.threadId}),E.add(e.threadId)}catch(s){r.warn({threadId:e.threadId,err:s},"could not send the refusal")}}function N(e,t){if(t.fetchable)return a.attachments.remember(e,f.name,{id:Q(t),name:t.name,payload:V(t)})}async function F(e,t){return t.attachments.length===0?[]:Promise.all(t.attachments.map(async(s,n)=>{const i=J(t.id,n,s),o=N(e.id,i),l=await a.files.save(e.dirKey,G(a.api,i,s.data));return o?{...l,ref:o}:l}))}function Y(e,t,s,n){const i=[],o=le(t,c).map(h=>({email:h})),l=W(t,c);return{thread:e,isNewThread:n,origin:f,...l?{note:l}:{},...Z(t)?{history:()=>ee({api:a.api,message:t,self:c,allow:a.allow,principal:S,remember:h=>a.attachments.remember(e.id,f.name,h)})}:{},async messages(){const h=await F(e,t);return[{principal:S(t.from),text:s,headers:re(t),...h.length>0?{files:h}:{}}]},presence:()=>se({api:a.api,labels:a.labels,self:p,message:t,to:o,threadKey:e.threadKey,notices:i,...l?{mayDecline:!0}:{}}),async notify(h){i.push(h)},async destination(){return{interface:"gmail",handle:t.from.email,label:t.from.email}}}}async function _(e){const t=`gmail:${e}`;if(await a.events.seen(t,{record:!1}))return;let s;try{s=await a.api.message(e)}catch(d){if(d instanceof $&&d.status===404){await a.events.seen(t),r.warn({messageId:e},"a message vanished before it could be read; skipping it");return}throw d}await a.events.seen(t);const n=oe(s);if(n.receivedAt<y){r.warn({messageId:e,threadId:n.threadId,receivedAt:new Date(n.receivedAt).toISOString(),floor:new Date(y).toISOString()},"ignoring an email that predates this agent");return}const i=he(n,{self:c,allow:a.allow});if(i.action==="ignore"){r.debug({messageId:e,threadId:n.threadId,reason:i.reason},"ignoring an email");return}if(i.action==="refuse"){r.warn({messageId:e,threadId:n.threadId,from:n.from.email,reason:i.reason},"refusing an unauthorized email"),await L(n,i.note);return}const o=T(n.threadId),l=await a.threads.has(o),h=await a.threads.remember(o);a.markRead&&await a.api.modifyMessage(e,{removeLabelIds:[z]}).catch(d=>{r.debug({messageId:e,err:d},"could not mark the message read")}),r.info({thread:o,from:n.from.email,newThread:!l},"running a turn from email");const M=a.run(Y(h,n,i.text,!l)).catch(d=>{r.error({thread:o,err:d},"a turn from email failed")}).finally(()=>{u.delete(M)});u.add(M)}async function j(){const e=a.state.get(A);if(!e){const o=await a.api.profile();a.state.set(A,o.historyId),r.info({historyId:o.historyId},"starting from the current state of the mailbox");return}const t=await a.api.history(e);let s=t.messageIds,n=t.historyId;if(t.expired){r.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,x)).reverse(),s.length===x&&r.warn({limit:x,query:D},"the resync search filled its limit, so older mail in the window was not looked at"),n=o.historyId}let i=!0;for(const o of s){if(I)return;try{await _(o)}catch(l){i=!1,r.error({messageId:o,err:l},"could not handle an email")}}if(!i){r.warn({cursor:e},"holding the Gmail cursor so unhandled mail is offered again");return}n&&a.state.set(A,n)}async function k(){if(!(m||I)){m=(async()=>{await j()})();try{await m}catch(e){r.error({err:e},"a Gmail poll failed; retrying on the next interval")}finally{m=void 0}}}async function C(){await Promise.allSettled([...u])}async function B(e){const t=e.handle.trim().toLowerCase();if(!H(t))throw new R(`"${e.handle}" is not an email address, so nothing was sent`);if(!a.allow.allows(t))throw new R(`${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",n=await ie(e.files??[],(o,l)=>{r.error({to:t,file:o.name,err:l},"could not read a file to send with a report")}),i=await P(()=>a.api.send({raw:ae({from:p,to:[{email:t}],subject:s,body:e.text,...n.length>0?{files:n}:{}})}),{what:"gmail messages.send",retryable:q});return r.info({to:t,chars:e.text.length,...n.length>0?{files:n.length}:{}},"delivered by email"),i.threadId?{threadKey:T(i.threadId)}:(r.warn({to:t},"Gmail named no thread for a sent message, so a reply to it starts fresh"),{})}return{name:"gmail",poll:k,settle:C,deliver:B,allows:e=>a.allow.allows(e.email??e.externalId),async resolveDestination(e,t){return U(a.allow,e,{...t?.optedInOnly?{optedInOnly:!0}:{}})},sameConversation:(e,t)=>e.trim().toLowerCase()===t.trim().toLowerCase(),attachment:e=>{const t=X(e);return t?G(a.api,t):void 0},async start(){r.info({address:g,everyMs:a.pollIntervalMs,allow:a.allow.open?"anyone":"a list",answeringMailAfter:new Date(y).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&&(r.info({turns:u.size},"waiting for email turns still running"),await C())}}}export{ce as GMAIL_ORIGIN,Se as createGmailInterface,T as threadKeyFor};
1
+ import{BishopError as O}from"../errors.js";import{log as r}from"../log.js";import{retry as $}from"../retry.js";import{GmailApiError as q,gmailSendRetryable as z,UNREAD as H}from"./api.js";import{looksLikeAddress as U}from"./authorize.js";import{internalDomains as W,mailObservation as J}from"./context.js";import{resolveGmailDestination as Q}from"./destination.js";import{mailEtiquetteNote as V}from"./etiquette.js";import{mailAttachment as R,mailFile as X,mailFileId as Z,mailFilePayload as ee,parseMailFile as te}from"./files.js";import{continuesAThread as ae,mailHistory as ne}from"./history.js";import{isSelf as re,mailIdentity as oe}from"./identity.js";import{buildMessage as ie,buildReply as se,mailHeaders as le,parseMessage as ce,readAttachments as de}from"./message.js";import{gmailPresence as he}from"./presence.js";import{participants as me,routeMessage as ue}from"./route.js";const fe={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
+ `)},x="gmail:historyId",G="gmail:attachedAt",T="in:inbox newer_than:2d",S=50;function K(a){return`gmail:${a}`}async function ye(a){try{return await a.sendAs()}catch(p){return r.warn({err:p},"the mailbox would not say who it is; the agent will not know its own name or any alias"),[]}}async function Oe(a){const p=await a.api.profile(),c=oe(p.emailAddress,await ye(a.api)),f=c.address,y={email:f,...c.name?{name:c.name}:{}},E=W(f,a.internalDomains);function b(e){const t=e.email.trim().toLowerCase();return a.principals.see(w.name,{externalId:t,handle:t,...e.name?.trim()?{name:e.name.trim()}:{},email:t,kind:"unknown",affiliation:"unknown",...re(c,t)?{self:!0}:{}})}const w={...fe,self:{address:f,...c.name?{aka:[c.name]}:{}}};b(y);const L=a.now??Date.now,v=a.state.get(G),g=v?Number(v):L();v||a.state.set(G,String(g));const C=new Set;let I,m,k=!1;const u=new Set;async function N(e,t){if(await a.labels.mark(e.threadId,"refused"),!(!t||C.has(e.threadId)))try{await a.api.send({raw:se({from:y,to:[e.from],subject:e.subject,inReplyTo:e.messageId,references:e.references,body:t}),threadId:e.threadId}),C.add(e.threadId)}catch(s){r.warn({threadId:e.threadId,err:s},"could not send the refusal")}}function F(e,t){if(t.fetchable)return a.attachments.remember(e,w.name,{id:Z(t),name:t.name,payload:ee(t)})}async function Y(e,t){return t.attachments.length===0?[]:Promise.all(t.attachments.map(async(s,n)=>{const i=X(t.id,n,s),o=F(e.id,i),l=await a.files.save(e.dirKey,R(a.api,i,s.data));return o?{...l,ref:o}:l}))}function _(e,t,s,n){const i=[],o=me(t,c).map(d=>({email:d})),l=V(t,c);return{thread:e,isNewThread:n,origin:w,...l?{note:l}:{},...ae(t)?{history:()=>ne({api:a.api,message:t,self:c,allow:a.allow,principal:b,remember:d=>a.attachments.remember(e.id,w.name,d)})}:{},observe:async()=>J(t,c,E),async messages(){const d=await Y(e,t);return[{principal:b(t.from),externalId:t.id,text:s,headers:le(t),...d.length>0?{files:d}:{}}]},presence:()=>he({api:a.api,labels:a.labels,self:y,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 j(e){const t=`gmail:${e}`;if(await a.events.seen(t,{record:!1}))return;let s;try{s=await a.api.message(e)}catch(h){if(h instanceof q&&h.status===404){await a.events.seen(t),r.warn({messageId:e},"a message vanished before it could be read; skipping it");return}throw h}await a.events.seen(t);const n=ce(s);if(n.receivedAt<g){r.warn({messageId:e,threadId:n.threadId,receivedAt:new Date(n.receivedAt).toISOString(),floor:new Date(g).toISOString()},"ignoring an email that predates this agent");return}const i=ue(n,{self:c,allow:a.allow});if(i.action==="ignore"){r.debug({messageId:e,threadId:n.threadId,reason:i.reason},"ignoring an email");return}if(i.action==="refuse"){r.warn({messageId:e,threadId:n.threadId,from:n.from.email,reason:i.reason},"refusing an unauthorized email"),await N(n,i.note);return}const o=K(n.threadId),l=await a.threads.has(o),d=await a.threads.remember(o);a.markRead&&await a.api.modifyMessage(e,{removeLabelIds:[H]}).catch(h=>{r.debug({messageId:e,err:h},"could not mark the message read")}),r.info({thread:o,from:n.from.email,newThread:!l},"running a turn from email");const M=a.run(_(d,n,i.text,!l)).catch(h=>{r.error({thread:o,err:h},"a turn from email failed")}).finally(()=>{u.delete(M)});u.add(M)}async function B(){const e=a.state.get(x);if(!e){const o=await a.api.profile();a.state.set(x,o.historyId),r.info({historyId:o.historyId},"starting from the current state of the mailbox");return}const t=await a.api.history(e);let s=t.messageIds,n=t.historyId;if(t.expired){r.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(T,S)).reverse(),s.length===S&&r.warn({limit:S,query:T},"the resync search filled its limit, so older mail in the window was not looked at"),n=o.historyId}let i=!0;for(const o of s){if(k)return;try{await j(o)}catch(l){i=!1,r.error({messageId:o,err:l},"could not handle an email")}}if(!i){r.warn({cursor:e},"holding the Gmail cursor so unhandled mail is offered again");return}n&&a.state.set(x,n)}async function A(){if(!(m||k)){m=(async()=>{await B()})();try{await m}catch(e){r.error({err:e},"a Gmail poll failed; retrying on the next interval")}finally{m=void 0}}}async function D(){await Promise.allSettled([...u])}async function P(e){const t=e.handle.trim().toLowerCase();if(!U(t))throw new O(`"${e.handle}" is not an email address, so nothing was sent`);if(!a.allow.allows(t))throw new O(`${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",n=await de(e.files??[],(o,l)=>{r.error({to:t,file:o.name,err:l},"could not read a file to send with a report")}),i=await $(()=>a.api.send({raw:ie({from:y,to:[{email:t}],subject:s,body:e.text,...n.length>0?{files:n}:{}})}),{what:"gmail messages.send",retryable:z});return r.info({to:t,chars:e.text.length,...n.length>0?{files:n.length}:{}},"delivered by email"),i.threadId?{threadKey:K(i.threadId)}:(r.warn({to:t},"Gmail named no thread for a sent message, so a reply to it starts fresh"),{})}return{name:"gmail",poll:A,settle:D,deliver:P,allows:e=>a.allow.allows(e.email??e.externalId),async resolveDestination(e,t){return Q(a.allow,e,{...t?.optedInOnly?{optedInOnly:!0}:{}})},sameConversation:(e,t)=>e.trim().toLowerCase()===t.trim().toLowerCase(),attachment:e=>{const t=te(e);return t?R(a.api,t):void 0},async start(){r.info({address:f,everyMs:a.pollIntervalMs,allow:a.allow.open?"anyone":"a list",internalDomains:[...E],answeringMailAfter:new Date(g).toISOString()},"watching the Gmail inbox"),await A(),I=setInterval(()=>{A()},a.pollIntervalMs)},async stop(){k=!0,I&&clearInterval(I),await m?.catch(()=>{}),u.size>0&&(r.info({turns:u.size},"waiting for email turns still running"),await D())}}}export{fe as GMAIL_ORIGIN,Oe as createGmailInterface,K as threadKeyFor};
@@ -1,11 +1,12 @@
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(`
1
+ import{spawn as E}from"node:child_process";import{existsSync as T,mkdirSync as R}from"node:fs";import{mkdir as D}from"node:fs/promises";import{homedir as I}from"node:os";import{join as k,resolve as g}from"node:path";import{query as _}from"@anthropic-ai/claude-agent-sdk";import{log as m}from"../log.js";import{executableDir as A,nonoCommand as C}from"../sandbox.js";import{isolates as w,repoRoot as K,WORKTREE_DIR as L,worktreePath as N}from"../worktrees.js";import{buildAgentOptions as $,CONTROLLED_KEYS as P,SETTINGS_ONLY_KEYS as F}from"./claude-options.js";import{ignoredKeys as v,shadowedKeys as j}from"./layers.js";import{buildPrompt as G,principalInstructions as U}from"./principal.js";function*B(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=W(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:Y(e.subtype)};return;default:return}}function W(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 o=t.url;if(typeof o=="string"&&o)try{return new URL(o).hostname}catch{return o.slice(0,40)}for(const a of["pattern","query","description"]){const c=t[a];if(typeof c=="string"&&c.trim()){const d=c.trim().split(`
2
+ `)[0]??"";return d.length>48?`${d.slice(0,45)}...`:d}}}function H(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 Y(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 M=new Set(["Edit","Write","NotebookEdit"]);function O(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:",`${N(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 z(e){const t=g(e);return({toolName:n,cwd:o})=>{if(M.has(n)&&g(o)===t)return"This is the shared checkout, which other conversations are using. Call EnterWorktree with the name given in your instructions, then make this edit inside the worktree."}}function J(e,t,n,o){const a=w(e)?O(o,n):void 0;return[U(t),a].filter(c=>!!c?.trim()).join(`
4
4
 
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}:
5
+ `)}function Q(e,t){return{model:t?.model??e.model,effort:t?.effort??e.effort}}function V(e){return async t=>{const n=t,o=e({toolName:n.tool_name,cwd:n.cwd});return o?(m.info({tool:n.tool_name,cwd:n.cwd},"refusing an edit in the shared checkout"),{continue:!0,hookSpecificOutput:{hookEventName:"PreToolUse",permissionDecision:"deny",permissionDecisionReason:o}}):{continue:!0}}}function X(e,t=I()){const n=e.CLAUDE_CONFIG_DIR;return n?{allow:[n]}:{allow:[k(t,".claude")],allowFiles:[k(t,".claude.json")].filter(T)}}async function Z(e){const t=await K(e);if(!t||g(t)===g(e))return[];const n=k(t,L);return await D(n,{recursive:!0}),[n]}const q=2048;function x(e,t){const n=t.text.trim();return n?`${e}
6
+ ${n}`:e}function ee(e,t,n){return o=>{const a=X(o.env);for(const f of a.allow??[])R(f,{recursive:!0});const c=A(o.command),d=C(e,{allow:[...a.allow??[],...t],read:c?[c]:[],...a.allowFiles?{allowFiles:a.allowFiles}:{}},o.command,o.args),h=E(d.command,d.args,{...o.cwd?{cwd:o.cwd}:{},env:o.env,stdio:["pipe","pipe","pipe"],signal:o.signal});return h.stderr.on("data",f=>{n.text=(n.text+f.toString()).slice(-q)}),h.on("exit",f=>{const r=n.text.trim();f&&r&&m.warn({code:f,stderr:r},"the sandboxed agent exited with an error")}),h}}function pe(e={}){const t=v(e.options,P);t.length>0&&m.warn({ignored:t},"these agent.options are set by Bishop and cannot be overridden; ignoring them");const n=r=>Q(e,r),o=n(),a=j(e.options,o);a.length>0&&m.warn({shadowed:a},"agent.model and agent.effort override these agent.options");const c=v(e.options,F);c.length>0&&m.warn({misplaced:c},"the harness reads these from agent.options.settings only and ignores them here");const d=e.worktrees;if(typeof e.options?.settings=="string"){const r=["auto memory is left to that file and the agent's own settings"];w(d)&&r.push("new worktrees branch from origin/<default-branch> rather than HEAD"),m.warn({settings:e.options.settings},`agent.options.settings names a file, so Bishop applies no settings defaults: ${r.join("; ")}`)}const h=(r,s,u,p)=>{const l=w(d)?z(r.cwd):void 0,i=l?{PreToolUse:[{hooks:[V(l)]}]}:void 0,y=$({...e.options?{configOptions:e.options}:{},general:n(u),...e.apiKey?{apiKey:e.apiKey}:{},...i?{hooks:i,worktrees:!0}:{},...s?{tools:s}:{},controlled:r});return p?{...y,spawnClaudeCodeProcess:p}:y},f=async(r,s,u)=>ee(s,w(d)?await Z(r):[],u);return{name:"claude",describeStartupFailure(r,s){return`The Claude agent couldn't start in ${r}:
6
7
  ${s}
7
8
 
8
9
  Bishop does not manage Anthropic credentials. The agent authenticates the
9
10
  same way \`claude\` does in this directory, so make sure that works first:
10
11
  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 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};
12
+ an existing \`claude\` login on this machine.`},async check(r,s){const u=new AbortController,p={text:""};try{const l=s?await f(r,s,p):void 0;for await(const i of _({prompt:"ping",options:{...h({cwd:r,systemPrompt:void 0,abortController:u},void 0,void 0,l),maxTurns:1,persistSession:!1}})){if(i.type==="system"&&i.subtype==="init")return u.abort(),{ok:!0,credentialSource:H(i.apiKeySource),...i.model?{model:i.model}:{}};if(i.type==="result")break}return{ok:!1,error:x("the harness exited without starting a session",p)}}catch(l){if(u.signal.aborted)return{ok:!0};const i=l instanceof Error?l.message:String(l);return{ok:!1,error:x(i,p)}}},async*runTurn(r){const s=new AbortController,u=()=>s.abort();r.signal.addEventListener("abort",u,{once:!0});let p=!1;const l={text:""};try{const i=r.sandbox?await f(r.cwd,r.sandbox,l):void 0,y=_({prompt:G(r),options:h({cwd:r.cwd,systemPrompt:{type:"preset",preset:"claude_code",append:J(d,r.origin,r.worktree,r.cwd)},abortController:s,...r.sessionId?{resume:r.sessionId}:{}},r.tools,r,i)});for await(const S of y)for(const b of B(S))(b.type==="done"||b.type==="error")&&(p=!0),yield b}catch(i){s.signal.aborted?m.debug({thread:r.threadKey},"claude turn aborted"):p?m.debug({err:i},"query() threw after reporting a result"):(m.error({err:i},"harness turn failed"),yield{type:"error",kind:"harness_failure",message:x(i instanceof Error?i.message:String(i),l)})}finally{r.signal.removeEventListener("abort",u)}}}}export{Q as chosenGeneral,pe as claudeHarness,X as claudeStateGrants,z as editGuard,J as systemPromptAppend,W as toolDetail,O as worktreeNote};