@agentdeploymentco/bishop 0.2.0 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +128 -9
- package/dist/cli.js +17 -14
- package/dist/config.js +4 -4
- package/dist/env.js +4 -4
- package/dist/gmail/api.js +1 -1
- package/dist/gmail/etiquette.js +1 -0
- package/dist/gmail/fake.js +6 -4
- package/dist/gmail/history.js +1 -1
- package/dist/gmail/identity.js +1 -0
- package/dist/gmail/interface.js +2 -2
- package/dist/gmail/labels.js +1 -1
- package/dist/gmail/message.js +10 -12
- package/dist/gmail/presence.js +3 -3
- package/dist/gmail/route.js +1 -1
- package/dist/harness/claude.js +5 -5
- package/dist/harness/codex.js +4 -4
- package/dist/harness/principal.js +9 -8
- package/dist/interface/format.js +4 -0
- package/dist/interface/phrases.js +1 -0
- package/dist/interface/verbosity.js +1 -0
- package/dist/log.js +1 -1
- package/dist/outgoing.js +2 -0
- package/dist/schedule/presence.js +3 -3
- package/dist/schedule/runner.js +3 -3
- package/dist/slack/commands/run.js +3 -1
- package/dist/slack/destination.js +1 -1
- package/dist/slack/format.js +1 -4
- package/dist/slack/interface.js +2 -2
- package/dist/slack/manifest.js +1 -1
- package/dist/slack/presence.js +3 -3
- package/dist/slack/tasks.js +1 -1
- package/dist/slack/upload.js +1 -0
- package/dist/slack/verbosity.js +1 -1
- package/dist/teams/api.js +1 -0
- package/dist/teams/authorize.js +1 -0
- package/dist/teams/create.js +1 -0
- package/dist/teams/entra.js +3 -0
- package/dist/teams/fake.js +1 -0
- package/dist/teams/files.js +1 -0
- package/dist/teams/graph.js +1 -0
- package/dist/teams/interface.js +4 -0
- package/dist/teams/logger.js +1 -0
- package/dist/teams/presence.js +7 -0
- package/dist/teams/progress.js +1 -0
- package/dist/teams/provision.js +10 -0
- package/dist/teams/route.js +1 -0
- package/dist/teams/server.js +1 -0
- package/dist/teams/setup.js +190 -0
- package/dist/teams/thread.js +1 -0
- package/dist/teams/verify.js +2 -0
- package/dist/thread-model.js +1 -0
- package/dist/tools/files.js +2 -2
- package/dist/tools/server.js +1 -1
- package/dist/turn.js +1 -1
- package/package.json +8 -1
- package/dist/slack/phrases.js +0 -1
- /package/dist/{slack → interface}/etiquette.js +0 -0
- /package/dist/{slack → interface}/status.js +0 -0
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
Bishop puts an agent where your team already works.
|
|
4
4
|
|
|
5
|
-
Point it at a directory an agent already works in, and the agent becomes something your team can talk to: a Slack thread, an email thread, or
|
|
5
|
+
Point it at a directory an agent already works in, and the agent becomes something your team can talk to: a Slack thread, an email thread, a Teams conversation, or all of them at once. Bishop handles the connection and the mapping between threads and agent sessions.
|
|
6
6
|
|
|
7
7
|
```
|
|
8
8
|
$ cd path/to/agent
|
|
@@ -10,7 +10,7 @@ $ bishop slack setup
|
|
|
10
10
|
$ bishop
|
|
11
11
|
```
|
|
12
12
|
|
|
13
|
-
Bishop is an agent gateway: it sits between the places people work and the agents they work with. Today that is Slack and
|
|
13
|
+
Bishop is an agent gateway: it sits between the places people work and the agents they work with. Today that is Slack, Gmail and Microsoft Teams, one agent, in a single process.
|
|
14
14
|
|
|
15
15
|
[design/glossary.md](design/glossary.md) is what Bishop's own words mean, if a term below is unfamiliar.
|
|
16
16
|
|
|
@@ -18,7 +18,7 @@ Bishop is an agent gateway: it sits between the places people work and the agent
|
|
|
18
18
|
|
|
19
19
|
- Node 24 or newer
|
|
20
20
|
- A directory the agent works in, meaning `claude` or `codex` runs there and does something useful
|
|
21
|
-
- At least one interface. For Slack: the [`slack` CLI](https://docs.slack.dev/tools/slack-cli/), logged in with `slack auth login`, unless you set the app up by hand (see [Without the Slack CLI](#without-the-slack-cli)), plus permission to install an app in your workspace. For email: a Google Workspace account, a Google Cloud project, and a Workspace user for the agent.
|
|
21
|
+
- At least one interface. For Slack: the [`slack` CLI](https://docs.slack.dev/tools/slack-cli/), logged in with `slack auth login`, unless you set the app up by hand (see [Without the Slack CLI](#without-the-slack-cli)), plus permission to install an app in your workspace. For email: a Google Workspace account, a Google Cloud project, and a Workspace user for the agent. For Teams: the [Teams Developer CLI](https://microsoft.github.io/teams-sdk/), logged in with `teams login`, unless an administrator creates the app for you, plus a way for Teams to reach this host over HTTPS (see [Microsoft Teams](#microsoft-teams)). Reading a file attached in a Teams channel also wants the [Azure CLI](https://learn.microsoft.com/cli/azure/install-azure-cli), which setup uses for the one grant that is not in the Teams manifest.
|
|
22
22
|
|
|
23
23
|
## Install
|
|
24
24
|
|
|
@@ -147,7 +147,82 @@ Google revokes Gmail refresh tokens when the mailbox's password is reset. The ag
|
|
|
147
147
|
|
|
148
148
|
If that isn't acceptable, use domain-wide delegation instead. Nothing about it expires and no person is ever needed again, but it costs more to set up: a service account key, which most organizations now block by default, plus a Workspace super admin to grant the delegation. Set `BISHOP_GMAIL_USER` and `BISHOP_GMAIL_SERVICE_ACCOUNT` instead of the three OAuth variables, and run `bishop gmail setup` to check it. It prints the exact client ID and scope to paste into the admin console when the grant is missing.
|
|
149
149
|
|
|
150
|
-
Bishop runs whichever interfaces have credentials. Set up
|
|
150
|
+
Bishop runs whichever interfaces have credentials. Set up more than one and a single process answers all of them at once, with separate threads and separate sessions.
|
|
151
|
+
|
|
152
|
+
### Microsoft Teams
|
|
153
|
+
|
|
154
|
+
Teams needs a Teams app, and the app needs a URL to deliver to. Slack holds a socket open; Teams POSTs every message to an HTTPS endpoint Bishop serves. Nothing binds a port unless the Teams credentials are set.
|
|
155
|
+
|
|
156
|
+
```
|
|
157
|
+
$ bishop teams setup --endpoint https://your-host.example/api/messages
|
|
158
|
+
```
|
|
159
|
+
|
|
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
|
+
|
|
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.
|
|
163
|
+
|
|
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.Read.All` on the app registration is what lets the agent read a file attached in a channel, 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
|
+
|
|
166
|
+
**Adding or changing those permissions on an app that is already installed means removing it and installing it again.** Teams asks for them at install time and at no other time, so an app updated in place keeps the consent it was installed with, whatever its manifest now says. Remove it from every team and chat it is in, then add it back. A new app is unaffected, since installing it is the first thing that happens to it.
|
|
167
|
+
|
|
168
|
+
Install the CLI with the flags, then log in:
|
|
169
|
+
|
|
170
|
+
```
|
|
171
|
+
$ npm install -g --allow-scripts=@azure/msal-node-extensions,keytar @microsoft/teams.cli
|
|
172
|
+
$ teams login
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
The CLI keeps its login in a native module that npm refuses to build by default, and falls back to holding the token in memory rather than reporting it. Installed without the flags it logs in, says it worked, and starts every later command logged out. Bishop says so when it sees that happen.
|
|
176
|
+
|
|
177
|
+
**Run this on a workstation.** Over SSH the CLI has no browser to open and falls back to the device code flow, which Microsoft blocks by default in every tenant, so no login can succeed there. Set up on a laptop and copy the `BISHOP_TEAMS_*` lines to the server, or use the administrator path below, which needs no CLI at all. `bishop teams setup` and `bishop teams endpoint` both say this rather than let a login fail with an Entra error code.
|
|
178
|
+
|
|
179
|
+
Run it again and it reports what's configured, including where the app currently points, and changes nothing. If only half of setup is on disk, an app with no credentials or credentials with no app, it says which half and stops rather than create a second app: a client secret is shown once, and an orphaned app is left behind in your tenant.
|
|
180
|
+
|
|
181
|
+
#### The endpoint
|
|
182
|
+
|
|
183
|
+
Bishop serves `/api/messages` on port 3978 by default, on loopback, so something has to carry the public HTTPS to it. A tunnel does that without opening a port on the host:
|
|
184
|
+
|
|
185
|
+
```
|
|
186
|
+
$ cloudflared tunnel --url http://localhost:3978
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
That gives you the URL to hand `bishop teams setup --endpoint`. An ephemeral tunnel hostname changes on every restart, and a Teams app pointed at the old one goes quiet with nothing in the log to say why, so use a stable hostname for anything but a trial.
|
|
190
|
+
|
|
191
|
+
To terminate TLS in Bishop itself instead, give it a certificate:
|
|
192
|
+
|
|
193
|
+
```json
|
|
194
|
+
{
|
|
195
|
+
"teams": {
|
|
196
|
+
"port": 443,
|
|
197
|
+
"host": "0.0.0.0",
|
|
198
|
+
"tls": { "cert": "/etc/bishop/fullchain.pem", "key": "/etc/bishop/privkey.pem" }
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
**When the URL changes**, which is every time an ephemeral tunnel restarts:
|
|
204
|
+
|
|
205
|
+
```
|
|
206
|
+
$ bishop teams endpoint https://new-host.example/api/messages
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
That touches the app and nothing else. When the change rewrites the app's manifest, which moving to a different domain does, it says so: the app has to be reinstalled in Teams before it receives anything again.
|
|
210
|
+
|
|
211
|
+
#### When somebody else owns the app
|
|
212
|
+
|
|
213
|
+
In many tenants only an administrator can create it, and installing custom apps can be turned off tenant-wide. `bishop teams instructions` prints what to send them. When they send back a client ID, a client secret, and a tenant ID:
|
|
214
|
+
|
|
215
|
+
```
|
|
216
|
+
$ bishop teams setup --manual
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
It checks the three against Microsoft before saving them, using the same request Bishop makes on every turn, so a bad secret fails here instead of silently on the first message.
|
|
220
|
+
|
|
221
|
+
#### Installing the app
|
|
222
|
+
|
|
223
|
+
Last, install the app in Teams by opening the link setup printed. When this is blocked the link does nothing at all: no activity is delivered, so Bishop's log stays silent and a working endpoint looks broken.
|
|
224
|
+
|
|
225
|
+
Sideloading has to be allowed twice over, and either setting blocks it alone. `bishop teams setup` names whichever one is in the way. Both live in the Teams admin center at `admin.teams.microsoft.com`, which is its own portal rather than a page of the Microsoft 365 admin center or Entra: "Let users interact with custom apps" under Teams apps, Manage apps, Org-wide app settings, and "Upload custom apps" in the app setup policy assigned to the account, under Teams apps, Setup policies. An administrator can skip the per-user policy by uploading the app package under Teams apps, Manage apps, which publishes it to the whole org and takes a few hours to appear.
|
|
151
226
|
|
|
152
227
|
### GitHub
|
|
153
228
|
|
|
@@ -292,6 +367,31 @@ Two rules are worth stating plainly, because both are about mail the agent must
|
|
|
292
367
|
|
|
293
368
|
**Bishop never answers mail that arrived before you pointed it at the mailbox.** It records that moment the first time it connects and refuses anything older, whatever else happens to its polling state. Startup logs the cutoff as `answeringMailAfter`. Restarts still catch up on whatever arrived while it was down, because the cutoff is when it first attached, not when the process started. There is no setting that moves it earlier.
|
|
294
369
|
|
|
370
|
+
## Using it in Teams
|
|
371
|
+
|
|
372
|
+
Install the app in Teams and message it directly, or @-mention it in a channel. A channel reply starts a thread and the agent follows that thread; a chat has no threads, so the whole chat is one session.
|
|
373
|
+
|
|
374
|
+
Teams behaves much the way Slack does:
|
|
375
|
+
|
|
376
|
+
- **A turn shows itself while it runs.** What the agent writes lands as it writes it, and a run of tool calls shares one message that is rewritten as the work moves and replaced by a count of them when the agent speaks again. A turn ends with a line saying what it cost, edited onto the last message. Set `teams.verbosity` in `.bishop/config.json` to `medium` to drop the tool calls, or `low` to leave the answer alone with no closing line, which is how Teams behaved before. There is still no typing indicator: Teams hides it while a stream is open and outside one it lapses in seconds.
|
|
377
|
+
- **Tables do not render.** Teams supports a narrower markdown than Slack. Bold, italics, links, code and lists work, and the agent is told to write a list where it would otherwise reach for a table.
|
|
378
|
+
- **@-mention it once per thread, not once per message.** Teams delivers a channel or group-chat message to an app only when the app is @-mentioned. `bishop teams setup` asks for two resource-specific consent permissions that lift that, so once the agent has answered in a thread it hears the replies without being tagged again. Without them the agent still works and still has to be tagged every time.
|
|
379
|
+
- **Changing those permissions means removing the app and installing it again.** Teams asks for them when an app is installed and at no other time. Updating an app that is already in a team or a chat does not ask, however the manifest changed, so it goes on hearing nothing but mentions until somebody removes it from every team and chat it is in and adds it back. This catches out anyone upgrading an agent that was already running in Teams.
|
|
380
|
+
- **It can stay out of a conversation.** In a thread with several people talking, a message that did not name the agent is one it may leave alone, and it says nothing at all rather than answering. A message that names it is always answered, as is anything in a one-to-one chat.
|
|
381
|
+
- **There is no catch-up.** The permissions change what arrives from now on, not what was said before the agent was pulled in.
|
|
382
|
+
- **Files come through in a direct chat, and images anywhere.** Attach a file in a chat with the agent and it gets the path, the same as in Slack; an image pasted into a message reaches it in any scope. Teams shows no attach button at all until the app's manifest asks for files, so `bishop teams setup` turns that on when it creates the app, and reports it when re-run against an app that predates this.
|
|
383
|
+
- **A file attached in a channel needs one more grant.** Teams keeps that file off the message it delivers, so Bishop reads it back through Microsoft Graph, which takes `Files.Read.All` on the app registration. `bishop teams setup` makes that grant with `az` when it can, and prints the commands for an administrator when it cannot. Re-run it and it says whether the grant is there, which it reads from the app's own token rather than from `az`. No Azure subscription is involved and the bot stays Teams-managed. Without the grant the agent is still told a file was attached and says it could not read it, rather than answering as though the message were empty.
|
|
384
|
+
- **A file attached in a group chat may still not come through.** The channel path covers it if Teams describes it the same way, which is untested. A direct chat and a channel are the two that are known to work.
|
|
385
|
+
- **There is no stop button.**
|
|
386
|
+
|
|
387
|
+
The allow list takes user principal names (`hayes@example.com`), `@example.com` for everyone at a domain, or an Entra object ID:
|
|
388
|
+
|
|
389
|
+
```json
|
|
390
|
+
{ "teams": { "allow": ["hayes@example.com", "@example.com"] } }
|
|
391
|
+
```
|
|
392
|
+
|
|
393
|
+
Leave it out and anyone in the tenant who can reach the app can use the agent. Somebody not on the list is told so, rather than left with silence that looks like the agent being broken.
|
|
394
|
+
|
|
295
395
|
## Using it in Slack
|
|
296
396
|
|
|
297
397
|
**DM the bot** and every message reaches the agent.
|
|
@@ -305,6 +405,8 @@ Each thread is its own agent session with its own history. Two threads are two i
|
|
|
305
405
|
|
|
306
406
|
**Turn it down.** A thread showing every tool call is a thread that's hard to read when you already trust the agent. Send `` `! verbosity medium` `` to drop the tool card, or `` `! verbosity low` `` to leave only the answer. Send `` `! verbosity` `` to see where a thread stands, and `` `! verbosity default` `` to put it back. It applies to the turn that's running, so you can quiet one mid-flight. `slack.verbosity` in the config sets where every thread starts, and Slack always marks the thread as working whatever the level.
|
|
307
407
|
|
|
408
|
+
**Run one thread on another model.** Send `` `! model opus` `` to move a thread onto a different model, `` `! model terra high` `` to set the model and the effort together, or `` `! model --effort high` `` to change how hard it thinks and leave the model alone. Send `` `! model` `` to see where a thread stands, and `default` in either position to put it back: `` `! model default default` ``. Names go to the harness as you type them, in its own naming, so `opus` or `gpt-5.1-codex-max` means whatever it means there; Bishop only checks that it looks like a model name. A name the harness doesn't know does what a bad `--model` does, which differs by harness and is described under `agent.model` below: Codex fails the turn, Claude says so in prose about a model and silently ignores an effort it doesn't recognize. `` `! model` `` is how you see what a thread is being run with, which is the only sign of a typo in the silent case. Unlike verbosity this takes effect on the thread's next turn, not the one running, and it lasts for the life of the thread.
|
|
409
|
+
|
|
308
410
|
**Commands go to Bishop, not the agent.** Anything whose whole message is code opening with `! ` is a command: Bishop runs it, answers in the thread, and the agent is never told about either. `` `! help` `` lists them, and any of them takes `--help`. What follows the `!` has to look like a command name, so code and pasted output reach the agent as they always did.
|
|
309
411
|
|
|
310
412
|
**It doesn't answer every message in a busy thread.** In a thread where more than one other person is talking, the agent can leave a message alone when it wasn't for the agent, the way anyone else in the thread would. Slack still shows it thinking for a moment, the way somebody starts typing and then doesn't send. It always answers a message that @-mentions it, and always answers in a thread where you're the only other person talking, so a DM and a thread between you and the agent are unchanged.
|
|
@@ -409,6 +511,8 @@ Every block is optional, and an interface runs when its credentials are set rath
|
|
|
409
511
|
|
|
410
512
|
`slack` takes `allow`, `maxBotTurns`, and `verbosity`, plus the `workspace` and `app` fields setup writes.
|
|
411
513
|
|
|
514
|
+
`teams` takes `allow`, `verbosity`, `port`, `host`, and `tls`, plus the `app` fields setup writes. Its `verbosity` is Slack's three levels and works almost the same way, except that `low` also drops the line saying what a turn cost. It is also the only place the level is set: Teams has no command messages, so there is no way to turn one conversation down without turning them all down.
|
|
515
|
+
|
|
412
516
|
`agent` takes `harness`, `model`, `effort`, `worktrees`, `options`, and `harnesses`. The last two are alternatives: `options` for a config that runs one harness, `harnesses` for one that switches.
|
|
413
517
|
|
|
414
518
|
Every key is checked at startup, so a typo fails with the path to it rather than being ignored. The one exception is `agent.options`, which is passed through to the harness unvalidated.
|
|
@@ -627,7 +731,9 @@ Under Claude, Bishop refuses `Edit`, `Write`, and `NotebookEdit` in the shared d
|
|
|
627
731
|
|
|
628
732
|
**When the agent directory isn't a git repository**, there's nothing to isolate. Bishop warns at startup and says so once in each thread, then edits the directory directly.
|
|
629
733
|
|
|
630
|
-
###
|
|
734
|
+
### Files people share
|
|
735
|
+
|
|
736
|
+
Files go both ways: people attach them for the agent, and the agent sends them back. This half is the first.
|
|
631
737
|
|
|
632
738
|
Bishop downloads what people attach and saves it under `.bishop/files`, one directory per thread, collected with everything else that thread left behind. The agent reads it from disk, so any file type works.
|
|
633
739
|
|
|
@@ -643,15 +749,26 @@ A file shared earlier in the conversation is named to the agent rather than down
|
|
|
643
749
|
|
|
644
750
|
The default is 100. A file over the limit isn't refused: the agent is told its name and that it was too large, so it can say so. There is no restriction on type and won't be.
|
|
645
751
|
|
|
646
|
-
**Slack needs the `files:read`
|
|
752
|
+
**Slack needs the `files:read` and `files:write` scopes**, which are new. An app set up before these has to be reinstalled to grant them, and until then Bishop warns at startup about each one it is missing, the agent sees a note in place of each file, and a file it sends fails to upload. Run `bishop slack manifest` for the current manifest, or reinstall from App Settings. Gmail needs nothing new. **Teams needs `supportsFiles`** on the app's bot, without which it offers no attach button in a chat with the agent and only pasted images arrive. `bishop teams setup` sets it on a new app and tells you when an existing one is missing it, which needs the Teams CLI logged in.
|
|
753
|
+
|
|
754
|
+
### Files the agent sends
|
|
755
|
+
|
|
756
|
+
The other half. The agent can send a file back: an upload in the Slack thread, an attachment on the mail in email. It registers the file with a tool, which checks it and hands back a reference, and writes that reference into its reply where the file should appear. In Slack the file arrives on the message carrying the words around it, the way a person sends one. A file it registers and never places is still sent, after the answer, so nothing is silently dropped.
|
|
757
|
+
|
|
758
|
+
Teams cannot carry one yet. An agent that tries in a Teams conversation is told so and can say what it made instead, and a scheduled report bound for Teams arrives with a line naming the file it could not attach.
|
|
759
|
+
|
|
760
|
+
The limit is 25MB per answer in Slack and 4MB by email, across everything one answer sends. Neither is configurable, and the email one is lower because of how Bishop talks to Gmail rather than what Gmail accepts. Anything past it is dropped with a line in the log and the message still goes. A scheduled report can carry files too, and an agent that reports nothing sends nothing, attachment included.
|
|
647
761
|
|
|
648
762
|
### Credentials
|
|
649
763
|
|
|
650
|
-
`.env` holds
|
|
764
|
+
`.env` holds each interface's credentials, written by its setup command:
|
|
651
765
|
|
|
652
766
|
```
|
|
653
767
|
BISHOP_SLACK_BOT_TOKEN=xoxb-...
|
|
654
768
|
BISHOP_SLACK_APP_TOKEN=xapp-...
|
|
769
|
+
BISHOP_TEAMS_CLIENT_ID=...
|
|
770
|
+
BISHOP_TEAMS_CLIENT_SECRET=...
|
|
771
|
+
BISHOP_TEAMS_TENANT_ID=...
|
|
655
772
|
```
|
|
656
773
|
|
|
657
774
|
Add `ANTHROPIC_API_KEY` if the agent should use a specific key. Without one, the agent authenticates however `claude` does on that machine, usually an existing login. Bishop logs which of the two it used at startup.
|
|
@@ -660,13 +777,13 @@ Anything else in `.env` reaches the agent's environment too, which is how `GH_TO
|
|
|
660
777
|
|
|
661
778
|
## Running it for real
|
|
662
779
|
|
|
663
|
-
Bishop is one process that logs JSON to stdout and errors to stderr, so any process supervisor works.
|
|
780
|
+
Bishop is one process that logs JSON to stdout and errors to stderr, so any process supervisor works. Slack and Gmail need outbound access only, since one holds a socket open and the other polls. Teams is the exception: it POSTs activities to an endpoint rather than holding a socket open, so Bishop serves one when Teams credentials are present. That endpoint binds loopback like every other port Bishop opens, and a tunnel gives it a public hostname and a certificate without an inbound firewall rule.
|
|
664
781
|
|
|
665
782
|
```
|
|
666
783
|
$ BISHOP_LOG_LEVEL=info bishop
|
|
667
784
|
```
|
|
668
785
|
|
|
669
|
-
`BISHOP_LOG_LEVEL=debug` adds tool activity and every routing decision. Pipe through `npx pino-pretty` to read it by eye.
|
|
786
|
+
`BISHOP_LOG_LEVEL=debug` adds tool activity and every routing decision. Pipe through `npx pino-pretty` to read it by eye. It works in `.env` as well as exported, and an exported one wins. It is the only setting of Bishop's own that `.env` carries; everything else lives in `config.json`. A value that isn't a level (`trace`, `debug`, `info`, `warn`, `error`, `fatal`, `silent`) logs a warning and leaves the level at `info`.
|
|
670
787
|
|
|
671
788
|
At startup Bishop checks that the agent can launch, resolves the allow list, and connects to Slack, failing with a specific message if any of those doesn't work.
|
|
672
789
|
|
|
@@ -816,6 +933,8 @@ Worth knowing if you're wondering why the `slack` CLI is a requirement.
|
|
|
816
933
|
|
|
817
934
|
Socket Mode needs an app-level (`xapp-`) token, and no Slack API will mint one. The `slack` CLI does, and hands it to a project it runs. So setup writes a throwaway Slack project, runs it once to collect both tokens, and stops it. The CLI creates that app as a dev app and appends ` (local)` to its name, so setup pushes the intended manifest over it afterward. That changes no permissions and needs no reinstall.
|
|
818
935
|
|
|
936
|
+
Teams needs none of that. `teams app create` talks to the Developer Portal directly and reports the app and its credentials as JSON, which setup reads and writes to `.env` under its own variable names.
|
|
937
|
+
|
|
819
938
|
## License
|
|
820
939
|
|
|
821
940
|
Not yet chosen, and the published package says so: `license` is `UNLICENSED`, which grants nothing. Nobody outside the company should build on it until there are real terms.
|
package/dist/cli.js
CHANGED
|
@@ -1,17 +1,20 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import{stat as
|
|
3
|
-
`),process.exit(1)}async function
|
|
2
|
+
import{stat as ie}from"node:fs/promises";import{join as ce,resolve as de}from"node:path";import{Command as pe}from"commander";import{agentSnapshots as me,fixedAgentDir as he}from"./agent-dir.js";import{looksRemote as K,parseRepoSpec as F}from"./agent-source.js";import{everyStore as z,storePath as q}from"./agent-store.js";import{attachmentStore as le}from"./attachments.js";import{HARNESSES as ue,loadConfig as O,nonEmptyFlag as j,parseHarnessName as fe,positiveIntFlag as we,resolveAgent as ge}from"./config.js";import{dbPath as Y,openDb as W}from"./db.js";import{describeMissing as ye,loadEnv as S,readCredentials as ke}from"./env.js";import{BishopError as v}from"./errors.js";import{fileStore as Se}from"./files.js";import{DEFAULT_MAX_AGE_DAYS as ve,makeCollector as be,sweep as Te}from"./gc.js";import{ghAuthLogin as Ie,ghCliIn as Ee,gitAuthenticatesToGithub as Ae,withTemporaryGhConfig as _e}from"./github/gh.js";import{AGENT_SCOPES as Re,checkGithubSetup as Ne}from"./github/setup.js";import{createGmail as Oe}from"./gmail/create.js";import{checkGmailSetup as Ge}from"./gmail/setup.js";import{createHarness as $e}from"./harness/create.js";import{sharedFileFetcher as xe}from"./interface/attachments.js";import{principalCheck as J}from"./interface/authorize.js";import{deliveryRouter as Le}from"./interface/delivery.js";import{conversationMatch as Ce,destinationResolver as He}from"./interface/destination.js";import{log as p,requestedLevel as Me}from"./log.js";import{addSchedule as Pe,listSchedules as De,removeSchedule as Be,setScheduleEnabled as Q}from"./schedule/cli.js";import{authorizedToRun as Ue,makeScheduler as Ke}from"./schedule/runner.js";import{scheduleStore as Fe}from"./schedule/store.js";import{threadQueue as ze}from"./sessions/queue.js";import{interfaceState as qe,retentionStore as V,sessionStore as je,threadHandover as Ye,threadMeta as We}from"./sessions/store.js";import{ensureGitignore as Je,openBrowser as Qe,resolveAppDetails as Ve,SetupError as X,setupSlack as Xe,setupSlackManually as Ze,withPrompt as Z,writeEnv as ee}from"./setup.js";import{createSlackInterface as et}from"./slack/interface.js";import{buildManifest as tt}from"./slack/manifest.js";import{createTeams as ot}from"./teams/create.js";import{administratorInstructions as nt,repointTeamsEndpoint as st,setupTeams as rt,setupTeamsManually as at}from"./teams/setup.js";import{threadModel as it}from"./thread-model.js";import{fileTools as ct}from"./tools/files.js";import{scheduleTools as dt}from"./tools/schedules.js";import{startToolServer as pt}from"./tools/server.js";import{makeTurnRunner as mt}from"./turn.js";import{version as ht}from"./version.js";import{hasGitIdentity as lt,isGitRepo as ut,sharedDirNotice as ft,WORKTREE_DIR as wt}from"./worktrees.js";function u(e){process.stderr.write(`${e}
|
|
3
|
+
`),process.exit(1)}async function gt(e){const t=de(e??".");let n;try{n=(await ie(t)).isDirectory()}catch{throw new v(`--agent directory doesn't exist: ${t}`)}if(!n)throw new v(`--agent is not a directory: ${t}`);return t}async function yt(e){const t=await ut(e),n={enabled:!0,isRepo:t};return t?(p.info({agentDir:e,worktrees:ce(e,wt)},"worktree mode is on"),await lt(e)||p.warn({agentDir:e},"git has no user.name or user.email here, so the agent cannot commit what it does in a worktree"),n):(p.warn({agentDir:e},"worktree mode is on but the agent directory is not a git repository; threads share it"),n)}function kt(e){if(e.kind==="options-replaced"){p.warn({harness:e.harness},`agent.harnesses.${e.harness} replaces agent.options, which is ignored for this run`);return}p.warn({harness:e.harness,configured:e.configured},`--harness overrides the configured harness, but agent.options is still written for that one; move it to agent.harnesses.${e.configured} and give ${e.harness} its own block`)}async function St(e=[],t={}){e.length>0&&u(`bishop: unknown command "${e[0]}"
|
|
4
4
|
|
|
5
|
-
Run \`bishop --help\` to see what's available.`);const
|
|
6
|
-
${
|
|
5
|
+
Run \`bishop --help\` to see what's available.`);const n=process.cwd(),i=t.agent&&K(t.agent)?F(t.agent):void 0;t.autoUpdate&&!i&&u("--auto-update needs --agent to be a git URL. Bishop never updates a directory it was pointed at; that state is yours to manage."),S(n);const s=await O(n),a=ke();a.ok||u(ye(a));const r=ge(s,t);for(const o of r.warnings)kt(o);const m=W(Y(n)),c=je(m,r.harness);let d;const l=async o=>await d?.check(o)??{ok:!0},f=i?me({spec:i,store:q(n,i.slug),dirs:c,autoUpdate:!!t.autoUpdate,recheck:l}):void 0,w=f?await f.start():await gt(t.agent),oe=f?f.forTurn:he(w,c);p.info({bishopDir:n,agentDir:w,...w===n?{}:{separate:!0},...i?{source:i.cloneUrl,autoUpdate:!!t.autoUpdate}:{},app:s.slack?.app?.name},"bishop starting");const A=s.agent?.worktrees?await yt(w):void 0,b=$e(r.harness,{...r.options?{options:r.options}:{},...r.model?{model:r.model}:{},...r.effort?{effort:r.effort}:{},...A?{worktrees:A}:{},credentials:a.credentials}),_=await b.check(w);_.ok||u(b.describeStartupFailure(w,_.error));const N=_.model??r.model;p.info({harness:b.name,credentials:_.credentialSource??"unreported",model:N,...r.effort?{effort:r.effort}:{},...r.options?{agentOptions:Object.keys(r.options)}:{}},"harness ready"),d=b,p.info({sessions:c.count()},"opened session store");const $=be({retention:V(m),stores:()=>z(n),home:n,...s.gc?.maxAgeDays===void 0?{}:{maxAgeDays:s.gc.maxAgeDays}});$.start();let y;const ne=o=>y?J(y)(o):Ue(o),se=async(o,h)=>{if(!y)throw new v("no interface is running, so there is nowhere to send");return He(y,{optedInOnly:!0})(o,h)},re=(o,h)=>y?Ce(y)(o,h):!1,x=Se({home:n,...s.files?.maxMb===void 0?{}:{maxMb:s.files.maxMb}}),L=le(m),C=Fe(m),H=await pt({tools:[...dt({schedules:C,authorized:ne,resolve:se,sameConversation:re}),...ct({fetch:xe({interfaces:()=>y??[],store:L,files:x})})]}),M=We(m),P=it(M),ae={...N?{model:N}:{},...r.effort?{effort:r.effort}:{}},D=mt({harness:b,cwd:oe,sessions:c,model:P,tools:H,...A&&!A.isRepo?{sharedDirNotice:ft(w)}:{}}),B=ze(),T={threads:c,events:c,files:x,attachments:L,run:o=>B.run(o.thread.threadKey,()=>D.run(o)),stop:o=>D.stop(o)},g=[];if(a.credentials.slack&&g.push(await et({...T,credentials:a.credentials.slack,meta:M,model:P,configuredModel:ae,...s.slack?.verbosity?{verbosity:s.slack.verbosity}:{},...s.slack?.allow?{allow:s.slack.allow}:{},...s.slack?.maxBotTurns===void 0?{}:{maxBotTurns:s.slack.maxBotTurns}})),a.credentials.gmail)try{g.push(await Oe({...T,credentials:a.credentials.gmail,...s.gmail?{config:s.gmail}:{},state:qe(m)}))}catch(o){o instanceof v&&u(o.message);const h=a.credentials.gmail,R=h.kind==="oauth"?"the authorized Gmail account":`${h.user}`;u(`Couldn't reach the mailbox for ${R}:
|
|
6
|
+
${o instanceof Error?o.message:String(o)}
|
|
7
7
|
|
|
8
|
-
Run \`bishop gmail setup\` to check the credentials.`)}
|
|
9
|
-
|
|
8
|
+
Run \`bishop gmail setup\` to check the credentials.`)}if(a.credentials.teams)try{g.push(await ot({...T,credentials:a.credentials.teams,...s.teams?{config:s.teams}:{}}))}catch(o){o instanceof v&&u(o.message),u(`Couldn't start the Teams app for client ${a.credentials.teams.clientId}:
|
|
9
|
+
${o instanceof Error?o.message:String(o)}
|
|
10
|
+
|
|
11
|
+
Check the credentials in .env and the \`teams\` block in .bishop/config.json.`)}y=g;for(const o of g)await o.start();p.info({interfaces:g.map(o=>o.name)},"bishop is listening");const U=Ke({schedules:C,run:T.run,threads:c,cancel:T.stop,deliver:Le(g),authorized:J(g),attach:Ye(c)});U.start();for(const o of["SIGINT","SIGTERM"])process.once(o,()=>{p.info({signal:o,inFlight:B.size},"shutting down"),U.stop().catch(h=>p.error({err:h},"the scheduler failed to stop cleanly")).then(()=>Promise.allSettled(g.map(h=>h.stop()))).then(async h=>{const R=h.filter(I=>I.status==="rejected");for(const I of R)p.error({err:I.reason},"an interface failed to stop cleanly");await H.stop().catch(I=>{p.error({err:I},"the tool endpoint failed to stop cleanly")}),await $.stop(),m.close(),process.exit(R.length>0?1:0)})})}function vt(e){for(const t of e.snapshots)process.stdout.write(`removed snapshot ${t.commit} (unused for ${t.ageDays} days)
|
|
12
|
+
`);for(const t of e.worktrees){const n=t.branch?`, branch ${t.branch}`:"";process.stdout.write(`released ${t.path}${n}
|
|
10
13
|
`)}process.stdout.write(`snapshots: removed ${e.snapshots.length}, kept ${e.keptSnapshots}
|
|
11
14
|
worktrees: released ${e.worktrees.length}
|
|
12
15
|
files: removed ${e.files} download directories
|
|
13
16
|
rows: forgot ${e.threads} threads, ${e.events} events
|
|
14
|
-
`)}async function
|
|
17
|
+
`)}async function bt(e){const t=process.cwd();S(t),e.agent&&!K(e.agent)&&u("gc --agent takes the git URL Bishop cloned, not a path. It narrows the pass to the files Bishop owns for one agent, and it owns none for a directory agent. Run `bishop gc` with no --agent to collect what its threads left behind.");const n=e.agent?[q(t,F(e.agent).slug)]:await z(t),i=await O(t),s=e.maxAgeDays??i.gc?.maxAgeDays;Me()||(p.level="error");const a=W(Y(t));try{vt(await Te({retention:V(a),stores:n,home:t,...e.agent?{only:n}:{},...s===void 0?{}:{maxAgeDays:s}}))}finally{a.close()}}function Tt(){return!(process.env.SSH_CONNECTION||process.env.SSH_TTY||process.env.SSH_CLIENT||process.platform==="linux"&&!process.env.DISPLAY&&!process.env.WAYLAND_DISPLAY)}async function It(e={}){const t=process.cwd();S(t);const n=process.env.BISHOP_GMAIL_CLIENT_ID?.trim(),i=process.env.BISHOP_GMAIL_CLIENT_SECRET?.trim(),s=process.env.BISHOP_GMAIL_REFRESH_TOKEN?.trim(),a=process.env.BISHOP_GMAIL_USER?.trim(),r=process.env.BISHOP_GMAIL_SERVICE_ACCOUNT?.trim();n&&!i&&u("BISHOP_GMAIL_CLIENT_ID is set but BISHOP_GMAIL_CLIENT_SECRET is not.");const m=e.browser===!1||!Tt()?"paste":"browser",c=await Z(!0,async d=>Ge({...n&&i?{client:{clientId:n,clientSecret:i}}:{},...s?{refreshToken:s}:{},...a?{user:a}:{},...r?{serviceAccount:r}:{},mode:m,openBrowser:Qe,readPaste:async l=>(process.stdout.write(`Open this in a browser and approve it:
|
|
15
18
|
|
|
16
19
|
${l}
|
|
17
20
|
|
|
@@ -19,12 +22,12 @@ It will finish on a http://127.0.0.1 address that fails to load. That is expecte
|
|
|
19
22
|
Copy the whole address out of the browser and paste it here.
|
|
20
23
|
|
|
21
24
|
`),d("Redirect URL",""))}));if(process.stdout.write(`${c.report}
|
|
22
|
-
`),c.refreshToken){const d=await
|
|
25
|
+
`),c.refreshToken){const d=await ee(t,{BISHOP_GMAIL_REFRESH_TOKEN:c.refreshToken});process.stdout.write(`
|
|
23
26
|
Saved BISHOP_GMAIL_REFRESH_TOKEN to ${d}
|
|
24
|
-
`)}c.ok||process.exit(1)}async function
|
|
27
|
+
`)}c.ok||process.exit(1)}async function Et(e){const t=process.cwd(),n=process.env.GH_TOKEN?.trim();process.env.GH_TOKEN!==void 0&&!n&&delete process.env.GH_TOKEN,S(t);const i=process.env.GH_TOKEN?.trim(),s=!!i&&!n,a=e.prompt&&process.stdin.isTTY===!0,r=e.print?process.stderr:process.stdout,m=l=>r.write(l),c=l=>Ne({...i?{token:i,tokenIsSaved:s}:{},...a?{login:()=>Ie(Re.map(f=>f.scope),l),confirm:async f=>/^y/i.test(await Z(!0,w=>w(f,"y"),r))}:{},gitConfigured:Ae,gh:Ee(l),...e.temporary?{temporary:!0}:{}}),d=e.temporary&&!i?await _e(c):await c();if(m(`${d.report}
|
|
25
28
|
`),e.print)d.token&&process.stdout.write(`${JSON.stringify({GH_TOKEN:d.token})}
|
|
26
|
-
`);else if(d.token){if(d.source==="gh"){const
|
|
27
|
-
Saved GH_TOKEN to ${
|
|
28
|
-
`)}const l=await
|
|
29
|
-
`)}d.ok||process.exit(1)}async function
|
|
30
|
-
`)}const k=new
|
|
29
|
+
`);else if(d.token){if(d.source==="gh"){const f=await ee(t,{GH_TOKEN:d.token});m(`
|
|
30
|
+
Saved GH_TOKEN to ${f}
|
|
31
|
+
`)}const l=await Je(t,[".env"]);l.length>0&&m(`Added ${l.join(", ")} to .gitignore
|
|
32
|
+
`)}d.ok||process.exit(1)}async function At(e){S(process.cwd());const t={...e.name===void 0?{}:{name:e.name},...e.displayName===void 0?{}:{displayName:e.displayName},...e.description===void 0?{}:{description:e.description},prompt:e.prompt};e.manual?await Ze(t):await Xe(t)}async function _t(e){if(e.appId!==void 0&&!e.manual)throw new X("--app-id adopts an app somebody else created, so it needs --manual too.");if(e.endpoint!==void 0&&e.manual)throw new X("--endpoint sets the URL on an app Bishop is creating, so it cannot be used\nwith --manual. Point an adopted app with `bishop teams endpoint <url>`,\nor ask whoever created it to.");S(process.cwd());const t={...e.name===void 0?{}:{name:e.name},...e.displayName===void 0?{}:{displayName:e.displayName},...e.description===void 0?{}:{description:e.description},...e.endpoint===void 0?{}:{endpoint:e.endpoint},...e.appId===void 0?{}:{appId:e.appId},prompt:e.prompt};e.manual?await at(t):await rt(t)}async function Rt(e){const t=process.cwd(),n=await O(t),i=await Ve({name:e.name??n.slack?.app?.name,displayName:e.displayName??n.slack?.app?.displayName,description:e.description??n.slack?.app?.description,prompt:!1},t,async(s,a)=>a);process.stdout.write(`${JSON.stringify(tt(i),null,2)}
|
|
33
|
+
`)}const k=new pe().name("bishop").description("An agent gateway. Runs an agent and connects it to Slack and email.").version(ht,"-v, --version").enablePositionalOptions().option("--agent <path-or-url>","the agent's directory, or a git URL to clone; defaults to the current directory").option(`--harness <${ue.join("|")}>`,"which SDK runs the agent",fe).option("--model <model>","the model to run, in the harness's own naming",j("--model")).option("--effort <effort>","how hard the model should think, in the harness's own naming",j("--effort")).option("--auto-update","for a git-URL --agent, check for a new commit at the start of every new thread").allowExcessArguments(!0).action((e,t)=>St(t.args,e));k.command("gc").description("Collect the snapshots, worktrees, and rows old conversations left behind").option("--max-age-days <days>",`how long a quiet thread's remains are kept; defaults to gc.maxAgeDays, then ${ve}`,we("--max-age-days")).option("--agent <url>","only this agent's snapshots; defaults to every agent Bishop has cloned here").action(bt);const E=k.command("schedules").description("Prompts the agent runs on a clock, and where their answers go");E.command("list").description("Everything scheduled here, in your own time zone").action(()=>De(process.cwd())),E.command("add").description("Add a schedule").requiredOption("--name <name>","what a person sees in a list, and the subject of an email").requiredOption("--cron <expression>","five-field cron, or @daily and friends").requiredOption("--prompt <text>","what the agent is asked, including when to stay quiet").requiredOption("--to <destination>","#channel or @person for Slack, an address for Gmail").option("--tz <zone>","IANA zone the expression is read in; defaults to this machine").option("--interface <name>","which interface delivers, when --to is ambiguous").action(e=>Pe(process.cwd(),e)),E.command("rm <id>").description("Remove a schedule").action(e=>Be(process.cwd(),e)),E.command("disable <id>").description("Stop a schedule firing, keeping it").action(e=>Q(process.cwd(),e,!1)),E.command("enable <id>").description("Start a disabled schedule firing again, from now").action(e=>Q(process.cwd(),e,!0));const Nt=k.command("gmail").description("Gmail interface commands");Nt.command("setup").description("Authorize a Gmail mailbox, or check the one already configured").option("--no-browser","print a URL to open elsewhere instead of opening one here").action(It);const Ot=k.command("github").description("GitHub credentials for the agent");Ot.command("setup").description("Give the agent a GitHub token, or check the one it's using").option("--no-prompt","don't offer to run `gh auth login`").option("--print","write the credential to stdout as JSON instead of to .env").option("--temporary","log in without touching this machine's own gh login").action(Et);const te=k.command("slack").description("Slack interface commands");te.command("setup").description("Create and install a Slack app, and save its credentials here").option("--name <name>","app name; defaults to the current directory name").option("--display-name <name>","bot display name in Slack; defaults to --name").option("--description <text>","short description shown in Slack").option("--no-prompt","don't prompt for anything left off").option("--manual","adopt an app you created in Slack yourself, using its tokens").action(At),te.command("manifest").description("Print the app manifest, for creating an app in Slack by hand").option("--name <name>","app name; defaults to config or the directory name").option("--display-name <name>","bot display name; defaults to --name").option("--description <text>","short description shown in Slack").action(Rt);const G=k.command("teams").description("Microsoft Teams interface commands");G.command("setup").description("Create a Teams app and save its credentials here, or check the one configured").option("--name <name>","app name; defaults to the current directory name").option("--display-name <name>","app name shown in Teams; defaults to --name").option("--description <text>","short description shown in Teams").option("--endpoint <url>","the URL Teams POSTs to; can be set later").option("--app-id <id>","with --manual, the Teams app ID you were given").option("--no-prompt","don't prompt for anything left off").option("--manual","adopt an app somebody else created, using its credentials").action(_t),G.command("endpoint <url>").description("Point the Teams app at a different URL, changing nothing else").option("--app-id <id>","the Teams app to point; defaults to the one in config").action((e,t)=>st(e,t.appId===void 0?{}:{appId:t.appId})),G.command("instructions").description("Print what to send whoever creates the Teams app, when that is an administrator").option("--endpoint <url>","the URL Teams should POST to, if you already know it").action(e=>{process.stdout.write(nt(e.endpoint))});try{await k.parseAsync()}catch(e){throw e instanceof v&&u(e.message),e}
|
package/dist/config.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import{mkdir as
|
|
2
|
-
`)}async function
|
|
3
|
-
${
|
|
4
|
-
`,"utf8"),r}function
|
|
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};
|
package/dist/env.js
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
import{config as
|
|
1
|
+
import{config as S}from"dotenv";import{applyLogLevel as E}from"./log.js";const I=[{name:"BISHOP_GMAIL_CLIENT_ID",hint:"OAuth client ID for a Desktop app"},{name:"BISHOP_GMAIL_CLIENT_SECRET",hint:"the matching OAuth client secret"},{name:"BISHOP_GMAIL_REFRESH_TOKEN",hint:"issued by `bishop gmail setup`; copy it from the laptop you authorized on"}],u=[{name:"BISHOP_GMAIL_USER",hint:"the Workspace address the agent sends and receives as"},{name:"BISHOP_GMAIL_SERVICE_ACCOUNT",hint:"path to the service account JSON key, or the JSON itself"}],m=[{name:"slack",sets:[{label:"Slack",vars:[{name:"BISHOP_SLACK_BOT_TOKEN",hint:"the xoxb- token issued when the Slack app was installed"},{name:"BISHOP_SLACK_APP_TOKEN",hint:"the xapp- app-level token from the app\u2019s Basic Information page"}]}],setup:"run `bishop slack setup` for Slack"},{name:"gmail",sets:[{label:"Gmail over OAuth",vars:I},{label:"Gmail over domain-wide delegation",vars:u}],setup:"run `bishop gmail setup` for Gmail"},{name:"teams",sets:[{label:"Microsoft Teams",vars:[{name:"BISHOP_TEAMS_CLIENT_ID",hint:"the Teams app\u2019s client ID"},{name:"BISHOP_TEAMS_CLIENT_SECRET",hint:"the matching client secret"},{name:"BISHOP_TEAMS_TENANT_ID",hint:"the Entra tenant the app is registered in"}]}],setup:"run `bishop teams setup` for Teams"}];function d(e=process.cwd()){S({path:`${e}/.env`,override:!1,quiet:!0}),E()}function t(e,s){return e[s]?.trim()||void 0}function f(e,s){return s.sets.find(a=>a.vars.some(o=>t(e,o.name)))}function O(e=process.env){const s=new Map,a=[];let o;for(const p of m){const c=f(e,p);if(!c)continue;s.set(p.name,c);const h=c.vars.filter(l=>!t(e,l.name));h.length!==0&&(o??=c.label,a.push(...h.map(l=>({name:l.name,hint:l.hint,set:c.label}))))}if(a.length>0)return{ok:!1,missing:a,...o?{attempting:o}:{}};if(s.size===0)return{ok:!1,noInterface:!0,missing:m.flatMap(p=>p.sets[0]?.vars??[])};const n=t(e,"ANTHROPIC_API_KEY"),i=t(e,"CODEX_API_KEY"),_=s.get("gmail"),r=_?_.vars===I?{kind:"oauth",clientId:t(e,"BISHOP_GMAIL_CLIENT_ID"),clientSecret:t(e,"BISHOP_GMAIL_CLIENT_SECRET"),refreshToken:t(e,"BISHOP_GMAIL_REFRESH_TOKEN")}:{kind:"service-account",user:t(e,"BISHOP_GMAIL_USER"),serviceAccount:t(e,"BISHOP_GMAIL_SERVICE_ACCOUNT")}:void 0;return{ok:!0,credentials:{...s.has("slack")?{slack:{botToken:t(e,"BISHOP_SLACK_BOT_TOKEN"),appToken:t(e,"BISHOP_SLACK_APP_TOKEN")}}:{},...r?{gmail:r}:{},...s.has("teams")?{teams:{clientId:t(e,"BISHOP_TEAMS_CLIENT_ID"),clientSecret:t(e,"BISHOP_TEAMS_CLIENT_SECRET"),tenantId:t(e,"BISHOP_TEAMS_TENANT_ID")}}:{},...n?{anthropicApiKey:n}:{},...i?{codexApiKey:i}:{}}}}function g(e){const s=e.missing.map(n=>` ${n.name}: ${n.hint}`).join(`
|
|
2
2
|
`);if(e.noInterface)return`Bishop has no interface configured, so nothing could reach the agent.
|
|
3
3
|
|
|
4
4
|
${s}
|
|
5
5
|
|
|
6
|
-
Set one interface's variables: ${m.map(n=>n.setup).join(", ")}.`;const a=new Map;for(const n of e.missing){const i=n.set??e.attempting??"Missing credentials";a.set(i,[...a.get(i)??[],n])}return`${[...a].map(([n,i])=>{const
|
|
6
|
+
Set one interface's variables: ${m.map(n=>n.setup).join(", ")}.`;const a=new Map;for(const n of e.missing){const i=n.set??e.attempting??"Missing credentials";a.set(i,[...a.get(i)??[],n])}return`${[...a].map(([n,i])=>{const _=i.map(r=>` ${r.name}: ${r.hint}`).join(`
|
|
7
7
|
`);return`${n} is missing:
|
|
8
|
-
${
|
|
8
|
+
${_}`}).join(`
|
|
9
9
|
|
|
10
10
|
`)}
|
|
11
11
|
|
|
12
|
-
Add them to .env, or re-run the setup for that interface.`}export{
|
|
12
|
+
Add them to .env, or re-run the setup for that interface.`}export{g as describeMissing,d as loadEnv,O as readCredentials};
|
package/dist/gmail/api.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{log as u}from"../log.js";const h="https://gmail.googleapis.com/gmail/v1/users/me",
|
|
1
|
+
import{log as u}from"../log.js";const h="https://gmail.googleapis.com/gmail/v1/users/me",y=300*1e3,f="INBOX",p="UNREAD",w="SPAM",b="TRASH",A="DRAFT";class d extends Error{status;constructor(c,s){super(c),this.status=s,this.name="GmailApiError"}}function I(i){return i instanceof d?i.status===429?!0:i.status===403&&/ratelimitexceeded/i.test(i.message):!1}function S(i){const c=i.fetch??globalThis.fetch;async function s(t,e={}){const a=new URL(`${h}${t}`);for(const[l,m]of Object.entries(e.query??{}))m!==void 0&&a.searchParams.set(l,m);const r=await i.tokens.get(),o=await c(a.toString(),{method:e.method??"GET",headers:{authorization:`Bearer ${r}`,...e.body===void 0?{}:{"content-type":"application/json"}},...e.body===void 0?{}:{body:JSON.stringify(e.body)},signal:AbortSignal.timeout(y)}),n=await o.text();if(!o.ok)throw new d(`${e.method??"GET"} ${t} failed (${o.status}): ${n}`,o.status);return n?JSON.parse(n):{}}return{async profile(){const t=await s("/profile");if(!t.emailAddress||!t.historyId)throw new d("users.getProfile returned no address or history id",200);return{emailAddress:t.emailAddress,historyId:t.historyId}},async sendAs(){return((await s("/settings/sendAs")).sendAs??[]).flatMap(e=>{const a=e.sendAsEmail?.trim().toLowerCase();if(!a)return[];const r=e.displayName?.trim();return[{email:a,...r?{displayName:r}:{},...e.isPrimary?{isPrimary:!0}:{}}]})},async history(t){const e=new Set;let a,r;do{let o;try{o=await s("/history",{query:{startHistoryId:t,historyTypes:"messageAdded",labelId:f,...a?{pageToken:a}:{}}})}catch(n){if(n instanceof d&&n.status===404)return{messageIds:[],expired:!0};throw n}for(const n of o.history??[])for(const l of n.messagesAdded??[])l.message?.id&&e.add(l.message.id);r=o.historyId??r,a=o.nextPageToken}while(a);return{messageIds:[...e],...r?{historyId:r}:{},expired:!1}},async search(t,e){return((await s("/messages",{query:{q:t,maxResults:String(e)}})).messages??[]).map(r=>r.id).filter(r=>!!r)},async message(t){return await s(`/messages/${encodeURIComponent(t)}`,{query:{format:"full"}})},async thread(t){return(await s(`/threads/${encodeURIComponent(t)}`,{query:{format:"full"}})).messages??[]},async attachment(t,e){const a=await s(`/messages/${encodeURIComponent(t)}/attachments/${encodeURIComponent(e)}`);if(a.data===void 0)throw new d("the attachment came back with no data",200);return Buffer.from(a.data,"base64url")},async send(t){const e=await s("/messages/send",{method:"POST",body:{raw:t.raw,...t.threadId?{threadId:t.threadId}:{}}});return{id:e.id??"",threadId:e.threadId??t.threadId??""}},async modifyMessage(t,e){await s(`/messages/${encodeURIComponent(t)}/modify`,{method:"POST",body:e})},async modifyThread(t,e){await s(`/threads/${encodeURIComponent(t)}/modify`,{method:"POST",body:e})},async labels(){return((await s("/labels")).labels??[]).filter(e=>!!(e.id&&e.name)).map(e=>({id:e.id,name:e.name}))},async createLabel(t){const e=await s("/labels",{method:"POST",body:{name:t,labelListVisibility:"labelShow",messageListVisibility:"show"}});if(!e.id)throw new d(`could not create the label ${t}`,200);return u.info({label:t,id:e.id},"created a Gmail label"),{id:e.id,name:e.name??t}}}}export{A as DRAFT,d as GmailApiError,f as INBOX,w as SPAM,b as TRASH,p as UNREAD,S as gmailApi,I as gmailSendRetryable};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{renderTag as i}from"../harness/principal.js";import{isSelf as r}from"./identity.js";function a(n,t){const o=n.to.map(e=>e.email),f=n.cc.map(e=>e.email),c=o.length===1?o[0]:void 0;if(c&&r(t,c)||[...o,...f].filter(e=>e&&e!==n.from.email&&!r(t,e)).length===0)return;const d=[...o,...f].some(e=>r(t,e));return i("thread",{reply:"optional",...d?{}:{addressed:"no"}})}export{a as mailEtiquetteNote};
|
package/dist/gmail/fake.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
|
-
import{INBOX as
|
|
1
|
+
import{INBOX as $}from"./api.js";function y(c){const r={};for(const o of c.replaceAll(/\r\n[ \t]+/g," ").split(`\r
|
|
2
|
+
`)){const n=o.indexOf(":");n>0&&(r[o.slice(0,n).toLowerCase()]=o.slice(n+1).trim())}return r}function b(c,r){return c["content-transfer-encoding"]==="base64"?Buffer.from(r.replaceAll(`\r
|
|
3
|
+
`,""),"base64").toString("utf8"):r}const x=/filename="([^"]*)"/;function w(c){const r=Buffer.from(c,"base64url").toString("utf8"),o=r.indexOf(`\r
|
|
2
4
|
\r
|
|
3
|
-
`),
|
|
4
|
-
|
|
5
|
-
|
|
5
|
+
`),n=y(r.slice(0,o)),u=r.slice(o+4),f=/boundary="([^"]+)"/.exec(n["content-type"]??"")?.[1];if(!f)return{text:b(n,u),headers:n,files:[]};const[t,...i]=u.split(`--${f}`).slice(1,-1).map(l=>{const h=l.indexOf(`\r
|
|
6
|
+
\r
|
|
7
|
+
`),p=y(l.slice(0,h).replace(/^\r\n/,""));return{headers:p,body:b(p,l.slice(h+4))}});return{text:(t?.body??"").replace(/\r\n$/,""),headers:n,files:i.map(l=>({name:x.exec(l.headers["content-disposition"]??"")?.[1]??"",mimeType:(l.headers["content-type"]??"").split(";")[0]?.trim()??"",contents:l.body.replace(/\r\n$/,"")}))}}function S(c="agent@acme.com",r={}){const o=new Map,n=new Map,u=new Map,f=[],t=[],i=[],l=new Map;let h=!1,p=1;function g(e,s){const a=n.get(e)??new Set;for(const d of s.removeLabelIds??[])a.delete(d);for(const d of s.addLabelIds??[])a.add(d);n.set(e,a)}return{calls:t,sent:f,deliver(e){const s=e.id??`m${p++}`,a=e.threadId??s,d=Object.entries(e.headers??{}).map(([m,I])=>({name:m,value:I}));return o.set(s,{id:s,threadId:a,internalDate:e.internalDate??String(Date.now()),labelIds:e.labelIds??[$],payload:e.payload??{mimeType:"text/plain",headers:d,body:{data:Buffer.from(e.body??"","utf8").toString("base64url")}}}),i.push(s),s},attach(e,s){l.set(e,s)},labelsOn(e){return[...n.get(e)??[]]},expireHistory(){h=!0},async profile(){return t.push("profile"),{emailAddress:c,historyId:String(i.length)}},async sendAs(){return t.push("sendAs"),[{email:c,...r.displayName?{displayName:r.displayName}:{},isPrimary:!0},...(r.alsoSendsAs??[]).map(e=>({email:e}))]},async history(e){if(t.push(`history:${e}`),h)return h=!1,{messageIds:[],expired:!0};const s=Number(e);return{messageIds:i.slice(s),historyId:String(i.length),expired:!1}},async search(e,s){return t.push(`search:${e}`),i.slice(-s).reverse()},async message(e){t.push(`message:${e}`);const s=o.get(e);if(!s)throw new Error(`no such message ${e}`);return s},async thread(e){return t.push(`thread:${e}`),i.map(s=>o.get(s)).filter(s=>s?.threadId===e)},async attachment(e,s){t.push(`attachment:${s}`);const a=l.get(s);if(a===void 0)throw new Error(`no such attachment ${s} on ${e}`);return Buffer.from(a)},async send(e){return t.push("send"),f.push({raw:e.raw,...e.threadId?{threadId:e.threadId}:{},...w(e.raw)}),{id:`sent-${f.length}`,threadId:e.threadId??`sent-thread-${f.length}`}},async modifyMessage(e,s){t.push(`modifyMessage:${e}`);const a=o.get(e);if(!a)return;const d=new Set(a.labelIds??[]);for(const m of s.removeLabelIds??[])d.delete(m);for(const m of s.addLabelIds??[])d.add(m);a.labelIds=[...d]},async modifyThread(e,s){t.push(`modifyThread:${e}`),g(e,s)},async labels(){return t.push("labels"),[...u.values()]},async createLabel(e){t.push(`createLabel:${e}`);const s={id:`label-${u.size+1}`,name:e};return u.set(s.id,s),s}}}export{S as fakeMailbox};
|
package/dist/gmail/history.js
CHANGED
|
@@ -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
|
|
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 j(e){return u(e)||e.id!==e.threadId}function z(e,o){return e.map(t=>{const i=g(I(o.api,t));if(!t.fetchable)return i;const m=o.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 q(e){let o;try{o=await e.api.thread(e.message.threadId)}catch(a){return k.warn({threadId:e.message.threadId,err:a},"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 a of o){if(a.id===e.message.id)break;const r=f(a);if(r.labelIds.some(d=>d===y||d===A||d===F))continue;const n=r.from.email.toLowerCase();if(!n||!T(r)){i+=1;continue}const s=B(e.self,n);if(!s&&!e.allow.allows(n)){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:{id:n,email:n,...r.from.name?{realName:r.from.name}:{}},text:r.body,headers:S(r),...s?{self:!0}:{},...l.length>0?{shared:l}:{}}))}m();const c=w(b(t)).map(a=>{if(a.kind!=="message"||!a.shared)return a;const{shared:r,...n}=a,s=z(r,e);return s.length>0?{...n,files:s}:n}),h=o[0]?f(o[0]):e.message;return u(h)&&c.unshift({kind:"omitted",reason:"unavailable"}),c}export{j as continuesAThread,q as mailHistory};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
function m(e,r){const i=e.trim().toLowerCase(),n=(r.find(t=>t.email===i)??r.find(t=>t.isPrimary))?.displayName?.trim();return{address:i,...n?{name:n}:{}}}function o(e,r){return r.trim().toLowerCase()===e.address}export{o as isSelf,m as mailIdentity};
|
package/dist/gmail/interface.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import{BishopError as
|
|
2
|
-
`)},
|
|
1
|
+
import{BishopError as C}from"../errors.js";import{log as n}from"../log.js";import{retry as B}from"../retry.js";import{GmailApiError as P,gmailSendRetryable as $,UNREAD as q}from"./api.js";import{looksLikeAddress as z}from"./authorize.js";import{resolveGmailDestination as H}from"./destination.js";import{mailEtiquetteNote as U}from"./etiquette.js";import{mailAttachment as R,mailFile as W,mailFileId as J,mailFilePayload as Q,parseMailFile as V}from"./files.js";import{continuesAThread as X,mailHistory as Z}from"./history.js";import{mailIdentity as ee}from"./identity.js";import{buildMessage as te,buildReply as ae,mailHeaders as re,parseMessage as ne,readAttachments as oe}from"./message.js";import{gmailPresence as ie}from"./presence.js";import{participants as se,routeMessage as le}from"./route.js";const de={name:"gmail",guidance:["This conversation is an email thread. Everything you write during a turn is collected and","sent as one plain-text reply to everyone on the thread, so give the whole answer in a","single message and never say you will follow up separately: there is no way for you to","send anything until someone writes to you again.","","Nobody is watching while you work. None of your tool calls are visible and nothing you say","arrives until the turn is over, so a turn that takes minutes costs nothing. Markdown is not","rendered; write plain prose, and use blank lines rather than formatting for structure.","","A <headers> tag above each message says who it was addressed to. Everyone on the To and Cc","lines receives your reply and there is no way to answer one of them privately. Being on Cc","usually means you are being kept informed rather than asked for something.","","A thread can be a conversation between other people that you were only copied on. Bishop",'marks a message you may leave alone with a <thread> tag carrying reply="optional": you were',"not the only person it was written to. Stay out of those unless you are sure somebody is","asking you to do something. To stay out, say <no-output> and nothing else, and Bishop sends","no mail at all. Every message without that tag is yours to answer.","","Sure means the sender named you, or asked for something you can act on. Work of yours coming","up in conversation is not, and neither is a question somebody else is better placed to","answer, and neither is having something you think worth adding. When you are sure, answer,","however many people are on the thread. A message replying to something you said is a","conversation you are already part of, so lean towards answering that one.","",'When the tag also carries addressed="no", your address is on neither line. You reached this',"through a group, a forwarding alias, or a blind copy, so nobody wrote to you and a single","address can stand for a thousand people. Answer only if the message asks you for something","by name.","","One address can be a thousand people wherever it appears. Judge how many you would be","interrupting by who an address reaches rather than by how many you can count, and treat one","you cannot place as a crowd."].join(`
|
|
2
|
+
`)},A="gmail:historyId",G="gmail:attachedAt",D="in:inbox newer_than:2d",S=50;function N(a){return`gmail:${a}`}async function he(a){try{return await a.sendAs()}catch(y){return n.warn({err:y},"the mailbox would not say who it is; the agent will not know its own name or any alias"),[]}}async function Se(a){const y=await a.api.profile(),h=ee(y.emailAddress,await he(a.api)),w=h.address,g={email:w,...h.name?{name:h.name}:{}},p={...de,self:{address:w,...h.name?{aka:[h.name]}:{}}},O=a.now??Date.now,b=a.state.get(G),f=b?Number(b):O();b||a.state.set(G,String(f));const x=new Set;let v,m,I=!1;const u=new Set;async function T(e,t){if(await a.labels.mark(e.threadId,"refused"),!(!t||x.has(e.threadId)))try{await a.api.send({raw:ae({from:g,to:[e.from],subject:e.subject,inReplyTo:e.messageId,references:e.references,body:t}),threadId:e.threadId}),x.add(e.threadId)}catch(s){n.warn({threadId:e.threadId,err:s},"could not send the refusal")}}function K(e,t){if(t.fetchable)return a.attachments.remember(e,p.name,{id:J(t),name:t.name,payload:Q(t)})}async function L(e,t){return t.attachments.length===0?[]:Promise.all(t.attachments.map(async(s,r)=>{const i=W(t.id,r,s),o=K(e.id,i),l=await a.files.save(e.dirKey,R(a.api,i,s.data));return o?{...l,ref:o}:l}))}function F(e,t,s,r){const i=[],o=se(t,h).map(d=>({email:d})),l=U(t,h);return{thread:e,isNewThread:r,origin:p,...l?{note:l}:{},...X(t)?{history:()=>Z({api:a.api,message:t,self:h,allow:a.allow,remember:d=>a.attachments.remember(e.id,p.name,d)})}:{},async messages(){const d=await L(e,t);return[{principal:{id:t.from.email,email:t.from.email,...t.from.name?{realName:t.from.name}:{}},text:s,headers:re(t),...d.length>0?{files:d}:{}}]},presence:()=>ie({api:a.api,labels:a.labels,self:g,message:t,to:o,threadKey:e.threadKey,notices:i,...l?{mayDecline:!0}:{}}),async notify(d){i.push(d)},async destination(){return{interface:"gmail",handle:t.from.email,label:t.from.email}}}}async function Y(e){const t=`gmail:${e}`;if(await a.events.seen(t,{record:!1}))return;let s;try{s=await a.api.message(e)}catch(c){if(c instanceof P&&c.status===404){await a.events.seen(t),n.warn({messageId:e},"a message vanished before it could be read; skipping it");return}throw c}await a.events.seen(t);const r=ne(s);if(r.receivedAt<f){n.warn({messageId:e,threadId:r.threadId,receivedAt:new Date(r.receivedAt).toISOString(),floor:new Date(f).toISOString()},"ignoring an email that predates this agent");return}const i=le(r,{self:h,allow:a.allow});if(i.action==="ignore"){n.debug({messageId:e,threadId:r.threadId,reason:i.reason},"ignoring an email");return}if(i.action==="refuse"){n.warn({messageId:e,threadId:r.threadId,from:r.from.email,reason:i.reason},"refusing an unauthorized email"),await T(r,i.note);return}const o=N(r.threadId),l=await a.threads.has(o),d=await a.threads.remember(o);a.markRead&&await a.api.modifyMessage(e,{removeLabelIds:[q]}).catch(c=>{n.debug({messageId:e,err:c},"could not mark the message read")}),n.info({thread:o,from:r.from.email,newThread:!l},"running a turn from email");const M=a.run(F(d,r,i.text,!l)).catch(c=>{n.error({thread:o,err:c},"a turn from email failed")}).finally(()=>{u.delete(M)});u.add(M)}async function _(){const e=a.state.get(A);if(!e){const o=await a.api.profile();a.state.set(A,o.historyId),n.info({historyId:o.historyId},"starting from the current state of the mailbox");return}const t=await a.api.history(e);let s=t.messageIds,r=t.historyId;if(t.expired){n.warn({cursor:e},"the Gmail history cursor is too old; falling back to a search of recent mail");const o=await a.api.profile();s=(await a.api.search(D,S)).reverse(),s.length===S&&n.warn({limit:S,query:D},"the resync search filled its limit, so older mail in the window was not looked at"),r=o.historyId}let i=!0;for(const o of s){if(I)return;try{await Y(o)}catch(l){i=!1,n.error({messageId:o,err:l},"could not handle an email")}}if(!i){n.warn({cursor:e},"holding the Gmail cursor so unhandled mail is offered again");return}r&&a.state.set(A,r)}async function k(){if(!(m||I)){m=(async()=>{await _()})();try{await m}catch(e){n.error({err:e},"a Gmail poll failed; retrying on the next interval")}finally{m=void 0}}}async function E(){await Promise.allSettled([...u])}async function j(e){const t=e.handle.trim().toLowerCase();if(!z(t))throw new C(`"${e.handle}" is not an email address, so nothing was sent`);if(!a.allow.allows(t))throw new C(`${t} is not on this agent's allow list, so nothing was sent to it`);const s=e.title?.trim()||"A message from your agent",r=await oe(e.files??[],(o,l)=>{n.error({to:t,file:o.name,err:l},"could not read a file to send with a report")}),i=await B(()=>a.api.send({raw:te({from:g,to:[{email:t}],subject:s,body:e.text,...r.length>0?{files:r}:{}})}),{what:"gmail messages.send",retryable:$});return n.info({to:t,chars:e.text.length,...r.length>0?{files:r.length}:{}},"delivered by email"),i.threadId?{threadKey:N(i.threadId)}:(n.warn({to:t},"Gmail named no thread for a sent message, so a reply to it starts fresh"),{})}return{name:"gmail",poll:k,settle:E,deliver:j,allows:e=>a.allow.allows(e),async resolveDestination(e,t){return H(a.allow,e,{...t?.optedInOnly?{optedInOnly:!0}:{}})},sameConversation:(e,t)=>e.trim().toLowerCase()===t.trim().toLowerCase(),attachment:e=>{const t=V(e);return t?R(a.api,t):void 0},async start(){n.info({address:w,everyMs:a.pollIntervalMs,allow:a.allow.open?"anyone":"a list",answeringMailAfter:new Date(f).toISOString()},"watching the Gmail inbox"),await k(),v=setInterval(()=>{k()},a.pollIntervalMs)},async stop(){I=!0,v&&clearInterval(v),await m?.catch(()=>{}),u.size>0&&(n.info({turns:u.size},"waiting for email turns still running"),await E())}}}export{de as GMAIL_ORIGIN,Se as createGmailInterface,N as threadKeyFor};
|
package/dist/gmail/labels.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{log as
|
|
1
|
+
import{log as d}from"../log.js";const u="Bishop",l={working:"Working",done:"Done",failed:"Failed",refused:"Refused",quiet:"Quiet"};async function m(a,i){const o=new Map;try{const r=new Map(a?(await a.labels()).map(e=>[e.name,e.id]):[]);for(const[e,n]of Object.entries(l)){const s=`${i}/${n}`,t=r.get(s)??(await a.createLabel(s)).id;o.set(e,t)}d.info({prefix:i,labels:o.size},"Gmail labels ready")}catch(r){d.warn({prefix:i,err:r},"could not set up Gmail labels; threads will not be marked")}return{async mark(r,e){const n=o.get(e);if(!n)return;const s=[...o.entries()].filter(([t])=>t!==e).map(([,t])=>t);try{await a.modifyThread(r,{addLabelIds:[n],removeLabelIds:s})}catch(t){d.debug({threadId:r,label:e,err:t},"could not label the thread")}}}}export{u as DEFAULT_LABEL_PREFIX,m as ensureLabels};
|