@agentdeploymentco/bishop 0.1.0 → 0.2.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 +24 -3
- package/dist/agent-dir.js +1 -1
- package/dist/attachments.js +2 -2
- package/dist/cli.js +12 -12
- package/dist/db.js +91 -2
- package/dist/files.js +2 -2
- package/dist/gc.js +2 -2
- package/dist/gmail/interface.js +2 -2
- package/dist/harness/claude.js +5 -5
- package/dist/harness/codex.js +5 -5
- package/dist/ids.js +1 -0
- package/dist/interface/attachments.js +1 -1
- package/dist/interface/state.js +1 -1
- package/dist/schedule/runner.js +3 -3
- package/dist/sessions/store.js +10 -26
- package/dist/setup.js +1 -1
- package/dist/slack/commands/run.js +1 -1
- package/dist/slack/handlers.js +1 -1
- package/dist/slack/interface.js +2 -2
- package/dist/slack/verbosity.js +1 -1
- package/dist/tools/files.js +1 -1
- package/dist/tools/server.js +1 -1
- package/dist/turn.js +1 -1
- package/dist/worktrees.js +2 -2
- package/package.json +1 -1
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
|
|
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 both at once. Bishop handles the connection and the mapping between threads and agent sessions.
|
|
6
6
|
|
|
7
7
|
```
|
|
8
8
|
$ cd path/to/agent
|
|
@@ -12,6 +12,8 @@ $ bishop
|
|
|
12
12
|
|
|
13
13
|
Bishop is an agent gateway: it sits between the places people work and the agents they work with. Today that is Slack and Gmail, one agent, in a single process.
|
|
14
14
|
|
|
15
|
+
[design/glossary.md](design/glossary.md) is what Bishop's own words mean, if a term below is unfamiliar.
|
|
16
|
+
|
|
15
17
|
## Requirements
|
|
16
18
|
|
|
17
19
|
- Node 24 or newer
|
|
@@ -65,7 +67,7 @@ Setup creates the Slack app, installs it, and writes two files:
|
|
|
65
67
|
.env credentials, gitignored for you
|
|
66
68
|
```
|
|
67
69
|
|
|
68
|
-
It also adds `bishop.db
|
|
70
|
+
It also adds `bishop.db*` to `.gitignore`. `bishop` creates the database on its first run to hold the thread-to-session mapping, and that pattern covers its write-ahead log and the copies described under [Upgrading](#upgrading).
|
|
69
71
|
|
|
70
72
|
Then start it:
|
|
71
73
|
|
|
@@ -572,7 +574,7 @@ Bishop sets `workingDirectory` and `developer_instructions` itself and ignores t
|
|
|
572
574
|
|
|
573
575
|
### Worktree mode
|
|
574
576
|
|
|
575
|
-
Threads share one directory, so two people directing
|
|
577
|
+
Threads share one directory, so two people directing an agent in two threads can have it write the same files. Turn that off:
|
|
576
578
|
|
|
577
579
|
```json
|
|
578
580
|
{
|
|
@@ -670,6 +672,8 @@ At startup Bishop checks that the agent can launch, resolves the allow list, and
|
|
|
670
672
|
|
|
671
673
|
### As a systemd service
|
|
672
674
|
|
|
675
|
+
This section assumes Bishop and its dependencies are already installed. [`LINUX_INSTALL.md`](LINUX_INSTALL.md) is the punchlist for running natively on a Linux host instead of the container: Node, the OS packages an agent needs as its shell, `uv`, the npm install, credentials, and a hardened unit.
|
|
676
|
+
|
|
673
677
|
A unit file keeps Bishop running across crashes and reboots on a Linux host:
|
|
674
678
|
|
|
675
679
|
```
|
|
@@ -778,6 +782,23 @@ after 12 hours, which stops Bishop from correcting the app name. Run `slack auth
|
|
|
778
782
|
**Sessions get confused.** Delete `bishop.db` to make every thread start fresh.
|
|
779
783
|
It holds only the thread-to-session mapping.
|
|
780
784
|
|
|
785
|
+
## Upgrading
|
|
786
|
+
|
|
787
|
+
Install the new version and restart. A version that changes the database migrates it on the first start, and says so in the log.
|
|
788
|
+
|
|
789
|
+
**Bishop copies the database before it migrates.** The copy lands in `.bishop/backups/`, named for the schema it holds. To go back, stop Bishop, install the version you were on, and restore it:
|
|
790
|
+
|
|
791
|
+
```
|
|
792
|
+
rm bishop.db bishop.db-wal bishop.db-shm
|
|
793
|
+
cp .bishop/backups/bishop.db.v8.backup bishop.db
|
|
794
|
+
```
|
|
795
|
+
|
|
796
|
+
**Delete `bishop.db-wal` first, as above.** Bishop runs the database in write-ahead logging mode, so recent changes live in that file rather than in `bishop.db`. A Bishop that stopped cleanly leaves nothing there, but one that was killed does, and SQLite replays it into whatever database file it finds beside it without checking that the two belong together. Restore without removing it and you get the migrated database back, looking exactly like a successful rollback. You can't tell which case you're in by looking, so remove them every time.
|
|
797
|
+
|
|
798
|
+
Nothing deletes the copies, and `bishop gc` leaves them alone. One per schema version this database has been migrated from, so a handful at most. Upgrading again from the same version replaces that version's copy: what is worth keeping is the database as it stood just before the migration you are undoing, so if you restore, run the old version for a while and upgrade again, the copy covers that stretch too.
|
|
799
|
+
|
|
800
|
+
If Bishop can't take the copy it refuses to start and says nothing was migrated, which usually means the disk is full or something else is under that name. Nothing has changed at that point, so fix the reason and start again.
|
|
801
|
+
|
|
781
802
|
## What this version doesn't do
|
|
782
803
|
|
|
783
804
|
- One agent per directory, one process. No agent registry.
|
package/dist/agent-dir.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{ensureMirror as
|
|
1
|
+
import{ensureMirror as d,ensureSnapshot as m,readCurrent as w,resolveTip as y,setCurrent as g,snapshotProblem as b}from"./agent-store.js";import{log as s}from"./log.js";function D(t,a){return async c=>(await a.setDir(c.id,t),t)}function x(t){let a,c=!1;async function f(){const r=await d(t.spec,t.store),{branch:n,commit:e}=await y(r,t.spec.branch),i=await w(t.store),o=await m(t.store,e);if(await g(t.store,e),c&&o!==i){s.info({branch:n,commit:e,dir:o},"new threads will start on a new commit");const h=await t.recheck?.(o);h&&!h.ok&&s.error({dir:o,commit:e,err:h.error},"the agent's new commit fails the startup check; turns on it will probably fail")}return o}function l(){return a||(a=f().finally(()=>{a=void 0}),a)}async function u(){try{return await l()}catch(r){const n=await w(t.store);if(!n)throw r;return s.warn({err:r,dir:n},"could not reach the agent repository; starting this thread on the current snapshot"),n}}return{async start(){const r=await u();return c=!0,r},async forTurn(r){const n=await t.dirs.getDir(r.id),e=n?await b(t.store,n):void 0;if(n&&!e)return await t.dirs.touchDir(r.id),n;n&&s.warn({thread:r.threadKey,dir:n,reason:e},"this thread's snapshot can't be used; starting it on the current one");const i=t.autoUpdate?await u():await w(t.store)??await u();return await t.dirs.setDir(r.id,i),s.info({thread:r.threadKey,dir:i},"thread pinned to a snapshot"),i}}}export{x as agentSnapshots,D as fixedAgentDir};
|
package/dist/attachments.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import{
|
|
2
|
-
VALUES (?, ?, ?, ?, ?, ?, ?)`),s=n.prepare("SELECT interface, name, payload FROM thread_files WHERE ref = ? AND
|
|
1
|
+
import{mint as c}from"./ids.js";import{log as p}from"./log.js";function u(n){const i=n.prepare("SELECT ref FROM thread_files WHERE thread_id = ? AND interface = ? AND file_id = ?"),d=n.prepare("UPDATE thread_files SET payload = ?, name = ?, seen_at = ? WHERE ref = ?"),f=n.prepare(`INSERT INTO thread_files (ref, thread_id, interface, file_id, name, payload, seen_at)
|
|
2
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)`),s=n.prepare("SELECT interface, name, payload FROM thread_files WHERE ref = ? AND thread_id = ?");return{remember(r,a,e){try{const t=i.get(r,a,e.id);if(t)return d.run(e.payload,e.name,Date.now(),t.ref),t.ref;const o=c("f");return f.run(o,r,a,e.id,e.name,e.payload,Date.now()),o}catch(t){p.warn({thread:r,file:e.name,err:t},"couldn't record a shared file, so the agent can't fetch it");return}},find(r,a){return s.get(a,r)??void 0}}}export{u as attachmentStore};
|
package/dist/cli.js
CHANGED
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import{stat as te}from"node:fs/promises";import{join as oe,resolve as ne}from"node:path";import{Command as se}from"commander";import{agentSnapshots as re,fixedAgentDir as
|
|
3
|
-
`),process.exit(1)}async function st(e){const t=ne(e??".");let o;try{o=(await te(t)).isDirectory()}catch{throw new b(`--agent directory doesn't exist: ${t}`)}if(!o)throw new b(`--agent is not a directory: ${t}`);return t}async function rt(e){const t=await tt(e),o={enabled:!0,isRepo:t};return t?(p.info({agentDir:e,worktrees:oe(e,nt)},"worktree mode is on"),await et(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"),o):(p.warn({agentDir:e},"worktree mode is on but the agent directory is not a git repository; threads share it"),o)}function
|
|
2
|
+
import{stat as te}from"node:fs/promises";import{join as oe,resolve as ne}from"node:path";import{Command as se}from"commander";import{agentSnapshots as re,fixedAgentDir as ae}from"./agent-dir.js";import{looksRemote as B,parseRepoSpec as M}from"./agent-source.js";import{everyStore as P,storePath as U}from"./agent-store.js";import{attachmentStore as ie}from"./attachments.js";import{HARNESSES as ce,loadConfig as O,nonEmptyFlag as K,parseHarnessName as de,positiveIntFlag as pe,resolveAgent as me}from"./config.js";import{dbPath as F,openDb as z}from"./db.js";import{describeMissing as he,loadEnv as G,readCredentials as le}from"./env.js";import{BishopError as b}from"./errors.js";import{fileStore as ue}from"./files.js";import{DEFAULT_MAX_AGE_DAYS as fe,makeCollector as we,sweep as ge}from"./gc.js";import{ghAuthLogin as ye,ghCliIn as ke,gitAuthenticatesToGithub as Se,withTemporaryGhConfig as ve}from"./github/gh.js";import{AGENT_SCOPES as be,checkGithubSetup as Ee}from"./github/setup.js";import{createGmail as _e}from"./gmail/create.js";import{checkGmailSetup as Ie}from"./gmail/setup.js";import{createHarness as Ae}from"./harness/create.js";import{sharedFileFetcher as Te}from"./interface/attachments.js";import{principalCheck as q}from"./interface/authorize.js";import{deliveryRouter as Ge}from"./interface/delivery.js";import{conversationMatch as Oe,destinationResolver as Ne}from"./interface/destination.js";import{log as p}from"./log.js";import{addSchedule as Re,listSchedules as xe,removeSchedule as $e,setScheduleEnabled as j}from"./schedule/cli.js";import{authorizedToRun as He,makeScheduler as Le}from"./schedule/runner.js";import{scheduleStore as Ce}from"./schedule/store.js";import{threadQueue as De}from"./sessions/queue.js";import{interfaceState as Be,retentionStore as Y,sessionStore as Me,threadHandover as Pe,threadMeta as Ue}from"./sessions/store.js";import{ensureGitignore as Ke,openBrowser as Fe,resolveAppDetails as ze,setupSlack as qe,setupSlackManually as je,withPrompt as W,writeEnv as J}from"./setup.js";import{createSlackInterface as Ye}from"./slack/interface.js";import{buildManifest as We}from"./slack/manifest.js";import{fileTools as Je}from"./tools/files.js";import{scheduleTools as Ve}from"./tools/schedules.js";import{startToolServer as Qe}from"./tools/server.js";import{makeTurnRunner as Xe}from"./turn.js";import{version as Ze}from"./version.js";import{hasGitIdentity as et,isGitRepo as tt,sharedDirNotice as ot,WORKTREE_DIR as nt}from"./worktrees.js";function w(e){process.stderr.write(`${e}
|
|
3
|
+
`),process.exit(1)}async function st(e){const t=ne(e??".");let o;try{o=(await te(t)).isDirectory()}catch{throw new b(`--agent directory doesn't exist: ${t}`)}if(!o)throw new b(`--agent is not a directory: ${t}`);return t}async function rt(e){const t=await tt(e),o={enabled:!0,isRepo:t};return t?(p.info({agentDir:e,worktrees:oe(e,nt)},"worktree mode is on"),await et(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"),o):(p.warn({agentDir:e},"worktree mode is on but the agent directory is not a git repository; threads share it"),o)}function at(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 it(e=[],t={}){e.length>0&&w(`bishop: unknown command "${e[0]}"
|
|
4
4
|
|
|
5
|
-
Run \`bishop --help\` to see what's available.`);const o=process.cwd(),
|
|
5
|
+
Run \`bishop --help\` to see what's available.`);const o=process.cwd(),a=t.agent&&B(t.agent)?M(t.agent):void 0;t.autoUpdate&&!a&&w("--auto-update needs --agent to be a git URL. Bishop never updates a directory it was pointed at; that state is yours to manage."),G(o);const s=await O(o),i=le();i.ok||w(he(i));const r=me(s,t);for(const n of r.warnings)at(n);const m=z(F(o)),c=Me(m,r.harness);let d;const l=async n=>await d?.check(n)??{ok:!0},u=a?re({spec:a,store:U(o,a.slug),dirs:c,autoUpdate:!!t.autoUpdate,recheck:l}):void 0,f=u?await u.start():await st(t.agent),Q=u?u.forTurn:ae(f,c);p.info({bishopDir:o,agentDir:f,...f===o?{}:{separate:!0},...a?{source:a.cloneUrl,autoUpdate:!!t.autoUpdate}:{},app:s.slack?.app?.name},"bishop starting");const _=s.agent?.worktrees?await rt(f):void 0,S=Ae(r.harness,{...r.options?{options:r.options}:{},...r.model?{model:r.model}:{},...r.effort?{effort:r.effort}:{},..._?{worktrees:_}:{},credentials:i.credentials}),I=await S.check(f);I.ok||w(S.describeStartupFailure(f,I.error)),p.info({harness:S.name,credentials:I.credentialSource??"unreported",model:I.model??r.model,...r.effort?{effort:r.effort}:{},...r.options?{agentOptions:Object.keys(r.options)}:{}},"harness ready"),d=S,p.info({sessions:c.count()},"opened session store");const N=we({retention:Y(m),stores:()=>P(o),home:o,...s.gc?.maxAgeDays===void 0?{}:{maxAgeDays:s.gc.maxAgeDays}});N.start();let g;const X=n=>g?q(g)(n):He(n),Z=async(n,h)=>{if(!g)throw new b("no interface is running, so there is nowhere to send");return Ne(g,{optedInOnly:!0})(n,h)},ee=(n,h)=>g?Oe(g)(n,h):!1,R=ue({home:o,...s.files?.maxMb===void 0?{}:{maxMb:s.files.maxMb}}),x=ie(m),$=Ce(m),H=await Qe({tools:[...Ve({schedules:$,authorized:X,resolve:Z,sameConversation:ee}),...Je({fetch:Te({interfaces:()=>g??[],store:x,files:R})})]}),L=Xe({harness:S,cwd:Q,sessions:c,tools:H,..._&&!_.isRepo?{sharedDirNotice:ot(f)}:{}}),C=De(),A={threads:c,events:c,files:R,attachments:x,run:n=>C.run(n.thread.threadKey,()=>L.run(n)),stop:n=>L.stop(n)},y=[];if(i.credentials.slack&&y.push(await Ye({...A,credentials:i.credentials.slack,meta:Ue(m),...s.slack?.verbosity?{verbosity:s.slack.verbosity}:{},...s.slack?.allow?{allow:s.slack.allow}:{},...s.slack?.maxBotTurns===void 0?{}:{maxBotTurns:s.slack.maxBotTurns}})),i.credentials.gmail)try{y.push(await _e({...A,credentials:i.credentials.gmail,...s.gmail?{config:s.gmail}:{},state:Be(m)}))}catch(n){n instanceof b&&w(n.message);const h=i.credentials.gmail,T=h.kind==="oauth"?"the authorized Gmail account":`${h.user}`;w(`Couldn't reach the mailbox for ${T}:
|
|
6
6
|
${n instanceof Error?n.message:String(n)}
|
|
7
7
|
|
|
8
|
-
Run \`bishop gmail setup\` to check the credentials.`)}g=y;for(const n of y)await n.start();p.info({interfaces:y.map(n=>n.name)},"bishop is listening");const D=Le({schedules
|
|
8
|
+
Run \`bishop gmail setup\` to check the credentials.`)}g=y;for(const n of y)await n.start();p.info({interfaces:y.map(n=>n.name)},"bishop is listening");const D=Le({schedules:$,run:A.run,threads:c,cancel:A.stop,deliver:Ge(y),authorized:q(y),attach:Pe(c)});D.start();for(const n of["SIGINT","SIGTERM"])process.once(n,()=>{p.info({signal:n,inFlight:C.size},"shutting down"),D.stop().catch(h=>p.error({err:h},"the scheduler failed to stop cleanly")).then(()=>Promise.allSettled(y.map(h=>h.stop()))).then(async h=>{const T=h.filter(v=>v.status==="rejected");for(const v of T)p.error({err:v.reason},"an interface failed to stop cleanly");await H.stop().catch(v=>{p.error({err:v},"the tool endpoint failed to stop cleanly")}),await N.stop(),m.close(),process.exit(T.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)
|
|
9
9
|
`);for(const t of e.worktrees){const o=t.branch?`, branch ${t.branch}`:"";process.stdout.write(`released ${t.path}${o}
|
|
10
10
|
`)}process.stdout.write(`snapshots: removed ${e.snapshots.length}, kept ${e.keptSnapshots}
|
|
11
11
|
worktrees: released ${e.worktrees.length}
|
|
12
|
-
files: removed
|
|
13
|
-
rows: forgot ${e.threads} threads, ${e.
|
|
14
|
-
`)}async function dt(e){const t=process.cwd();e.agent&&!B(e.agent)&&w("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 o=e.agent?[U(t,M(e.agent).slug)]:await P(t),
|
|
12
|
+
files: removed ${e.files} download directories
|
|
13
|
+
rows: forgot ${e.threads} threads, ${e.events} events
|
|
14
|
+
`)}async function dt(e){const t=process.cwd();e.agent&&!B(e.agent)&&w("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 o=e.agent?[U(t,M(e.agent).slug)]:await P(t),a=await O(t),s=e.maxAgeDays??a.gc?.maxAgeDays;process.env.BISHOP_LOG_LEVEL||(p.level="error");const i=z(F(t));try{ct(await ge({retention:Y(i),stores:o,home:t,...e.agent?{only:o}:{},...s===void 0?{}:{maxAgeDays:s}}))}finally{i.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();G(t);const o=process.env.BISHOP_GMAIL_CLIENT_ID?.trim(),a=process.env.BISHOP_GMAIL_CLIENT_SECRET?.trim(),s=process.env.BISHOP_GMAIL_REFRESH_TOKEN?.trim(),i=process.env.BISHOP_GMAIL_USER?.trim(),r=process.env.BISHOP_GMAIL_SERVICE_ACCOUNT?.trim();o&&!a&&w("BISHOP_GMAIL_CLIENT_ID is set but BISHOP_GMAIL_CLIENT_SECRET is not.");const m=e.browser===!1||!pt()?"paste":"browser",c=await W(!0,async d=>Ie({...o&&a?{client:{clientId:o,clientSecret:a}}:{},...s?{refreshToken:s}:{},...i?{user:i}:{},...r?{serviceAccount:r}:{},mode:m,openBrowser:Fe,readPaste:async l=>(process.stdout.write(`Open this in a browser and approve it:
|
|
15
15
|
|
|
16
16
|
${l}
|
|
17
17
|
|
|
@@ -21,10 +21,10 @@ Copy the whole address out of the browser and paste it here.
|
|
|
21
21
|
`),d("Redirect URL",""))}));if(process.stdout.write(`${c.report}
|
|
22
22
|
`),c.refreshToken){const d=await J(t,{BISHOP_GMAIL_REFRESH_TOKEN:c.refreshToken});process.stdout.write(`
|
|
23
23
|
Saved BISHOP_GMAIL_REFRESH_TOKEN to ${d}
|
|
24
|
-
`)}c.ok||process.exit(1)}async function ht(e){const t=process.cwd(),o=process.env.GH_TOKEN?.trim();process.env.GH_TOKEN!==void 0&&!o&&delete process.env.GH_TOKEN,G(t);const
|
|
24
|
+
`)}c.ok||process.exit(1)}async function ht(e){const t=process.cwd(),o=process.env.GH_TOKEN?.trim();process.env.GH_TOKEN!==void 0&&!o&&delete process.env.GH_TOKEN,G(t);const a=process.env.GH_TOKEN?.trim(),s=!!a&&!o,i=e.prompt&&process.stdin.isTTY===!0,r=e.print?process.stderr:process.stdout,m=l=>r.write(l),c=l=>Ee({...a?{token:a,tokenIsSaved:s}:{},...i?{login:()=>ye(be.map(u=>u.scope),l),confirm:async u=>/^y/i.test(await W(!0,f=>f(u,"y"),r))}:{},gitConfigured:Se,gh:ke(l),...e.temporary?{temporary:!0}:{}}),d=e.temporary&&!a?await ve(c):await c();if(m(`${d.report}
|
|
25
25
|
`),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 ${
|
|
26
|
+
`);else if(d.token){if(d.source==="gh"){const u=await J(t,{GH_TOKEN:d.token});m(`
|
|
27
|
+
Saved GH_TOKEN to ${u}
|
|
28
28
|
`)}const l=await Ke(t,[".env"]);l.length>0&&m(`Added ${l.join(", ")} to .gitignore
|
|
29
|
-
`)}d.ok||process.exit(1)}async function lt(e){G(process.cwd());const t={...e.name===void 0?{}:{name:e.name},...e.displayName===void 0?{}:{displayName:e.displayName},...e.description===void 0?{}:{description:e.description},prompt:e.prompt};e.manual?await je(t):await qe(t)}async function
|
|
30
|
-
`)}const k=new se().name("bishop").description("An agent gateway. Runs an agent and connects it to Slack and email.").version(Ze,"-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 <${ce.join("|")}>`,"which SDK runs the agent",de).option("--model <model>","the model to run, in the harness's own naming",K("--model")).option("--effort <effort>","how hard the model should think, in the harness's own naming",K("--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)=>
|
|
29
|
+
`)}d.ok||process.exit(1)}async function lt(e){G(process.cwd());const t={...e.name===void 0?{}:{name:e.name},...e.displayName===void 0?{}:{displayName:e.displayName},...e.description===void 0?{}:{description:e.description},prompt:e.prompt};e.manual?await je(t):await qe(t)}async function ut(e){const t=process.cwd(),o=await O(t),a=await ze({name:e.name??o.slack?.app?.name,displayName:e.displayName??o.slack?.app?.displayName,description:e.description??o.slack?.app?.description,prompt:!1},t,async(s,i)=>i);process.stdout.write(`${JSON.stringify(We(a),null,2)}
|
|
30
|
+
`)}const k=new se().name("bishop").description("An agent gateway. Runs an agent and connects it to Slack and email.").version(Ze,"-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 <${ce.join("|")}>`,"which SDK runs the agent",de).option("--model <model>","the model to run, in the harness's own naming",K("--model")).option("--effort <effort>","how hard the model should think, in the harness's own naming",K("--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)=>it(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 ${fe}`,pe("--max-age-days")).option("--agent <url>","only this agent's snapshots; defaults to every agent Bishop has cloned here").action(dt);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(()=>xe(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=>Re(process.cwd(),e)),E.command("rm <id>").description("Remove a schedule").action(e=>$e(process.cwd(),e)),E.command("disable <id>").description("Stop a schedule firing, keeping it").action(e=>j(process.cwd(),e,!1)),E.command("enable <id>").description("Start a disabled schedule firing again, from now").action(e=>j(process.cwd(),e,!0));const ft=k.command("gmail").description("Gmail interface commands");ft.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 wt=k.command("github").description("GitHub credentials for the agent");wt.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(ht);const V=k.command("slack").description("Slack interface commands");V.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(lt),V.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(ut);try{await k.parseAsync()}catch(e){throw e instanceof b&&w(e.message),e}
|
package/dist/db.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import{join as
|
|
1
|
+
import{existsSync as m,mkdirSync as X,renameSync as C,rmSync as g,writeFileSync as w}from"node:fs";import{dirname as L,join as h}from"node:path";import{DatabaseSync as A}from"node:sqlite";import{BishopError as l}from"./errors.js";import{mint as S}from"./ids.js";import{log as c}from"./log.js";const u="bishop.db",M=h(".bishop","backups");function K(r=process.cwd()){return h(r,u)}const N=[`CREATE TABLE sessions (
|
|
2
2
|
thread_key TEXT PRIMARY KEY,
|
|
3
3
|
session_id TEXT NOT NULL,
|
|
4
4
|
created_at INTEGER NOT NULL,
|
|
@@ -49,4 +49,93 @@ import{join as a}from"node:path";import{DatabaseSync as n}from"node:sqlite";impo
|
|
|
49
49
|
seen_at INTEGER NOT NULL
|
|
50
50
|
);
|
|
51
51
|
CREATE UNIQUE INDEX thread_files_id ON thread_files (thread_key, interface, file_id);
|
|
52
|
-
CREATE INDEX thread_files_thread ON thread_files (thread_key)
|
|
52
|
+
CREATE INDEX thread_files_thread ON thread_files (thread_key);`,r=>{const a=Date.now();r.exec(`CREATE TABLE threads (
|
|
53
|
+
id TEXT PRIMARY KEY,
|
|
54
|
+
thread_key TEXT NOT NULL UNIQUE,
|
|
55
|
+
dir_key TEXT NOT NULL,
|
|
56
|
+
agent_dir TEXT,
|
|
57
|
+
created_at INTEGER NOT NULL,
|
|
58
|
+
used_at INTEGER NOT NULL
|
|
59
|
+
);
|
|
60
|
+
CREATE INDEX threads_used_at ON threads (used_at);
|
|
61
|
+
-- Every old thread key, against the thread that now holds what it held.
|
|
62
|
+
-- Two keys land on one id wherever a reply took a firing over. Dropped
|
|
63
|
+
-- at the end of this migration.
|
|
64
|
+
CREATE TABLE thread_ids (
|
|
65
|
+
thread_key TEXT PRIMARY KEY,
|
|
66
|
+
id TEXT NOT NULL
|
|
67
|
+
);`);const n=r.prepare(`SELECT thread_key, workspace_key FROM sessions
|
|
68
|
+
WHERE workspace_key IS NOT NULL ORDER BY updated_at DESC, thread_key`).all(),t=new Map,s=new Map;for(const e of n)t.has(e.workspace_key)||t.set(e.workspace_key,e.thread_key),s.set(e.thread_key,e.workspace_key);const d=(e,T)=>{const _=new Set([T]);let E=T;for(let i=e.get(E);i;i=e.get(E)){if(_.has(i))return T;_.add(i),E=i}return E},o=e=>d(t,e),p=e=>d(s,e),O=r.prepare(`SELECT thread_key FROM sessions
|
|
69
|
+
UNION SELECT thread_key FROM thread_dirs
|
|
70
|
+
UNION SELECT thread_key FROM thread_meta
|
|
71
|
+
UNION SELECT thread_key FROM thread_files`).all().map(e=>e.thread_key),y=r.prepare(`SELECT d.agent_dir AS agent_dir,
|
|
72
|
+
d.used_at AS used_at,
|
|
73
|
+
s.created_at AS created_at
|
|
74
|
+
FROM (SELECT ? AS key, ? AS dir_key) k
|
|
75
|
+
LEFT JOIN sessions s ON s.thread_key = k.key
|
|
76
|
+
LEFT JOIN thread_dirs d ON d.thread_key = k.dir_key`),k=r.prepare(`INSERT INTO threads (id, thread_key, dir_key, agent_dir, created_at, used_at)
|
|
77
|
+
VALUES (?, ?, ?, ?, ?, ?)`),U=r.prepare("INSERT INTO thread_ids (thread_key, id) VALUES (?, ?)"),R=new Map;for(const e of O){if(o(e)!==e)continue;const T=p(e),_=o(T)===e?T:e,E=y.get(e,_),i=E.used_at==null?a:Number(E.used_at),I=E.created_at==null?i:Number(E.created_at),f=S("t");k.run(f,e,_,E.agent_dir,I,i),R.set(e,f)}for(const e of O){const T=R.get(o(e));T&&U.run(e,T)}r.exec(`CREATE TABLE sessions_new (
|
|
78
|
+
thread_id TEXT PRIMARY KEY REFERENCES threads (id) ON DELETE CASCADE,
|
|
79
|
+
session_id TEXT NOT NULL,
|
|
80
|
+
harness TEXT NOT NULL DEFAULT 'claude',
|
|
81
|
+
created_at INTEGER NOT NULL,
|
|
82
|
+
updated_at INTEGER NOT NULL
|
|
83
|
+
);
|
|
84
|
+
INSERT INTO sessions_new (thread_id, session_id, harness, created_at, updated_at)
|
|
85
|
+
SELECT t.id, s.session_id, s.harness, s.created_at, s.updated_at
|
|
86
|
+
FROM sessions s JOIN threads t ON t.thread_key = s.thread_key;
|
|
87
|
+
DROP TABLE sessions;
|
|
88
|
+
ALTER TABLE sessions_new RENAME TO sessions;
|
|
89
|
+
|
|
90
|
+
CREATE TABLE thread_meta_new (
|
|
91
|
+
thread_id TEXT NOT NULL REFERENCES threads (id) ON DELETE CASCADE,
|
|
92
|
+
key TEXT NOT NULL,
|
|
93
|
+
value TEXT NOT NULL,
|
|
94
|
+
updated_at INTEGER NOT NULL,
|
|
95
|
+
PRIMARY KEY (thread_id, key)
|
|
96
|
+
);
|
|
97
|
+
INSERT OR IGNORE INTO thread_meta_new (thread_id, key, value, updated_at)
|
|
98
|
+
SELECT i.id, m.key, m.value, m.updated_at
|
|
99
|
+
FROM thread_meta m JOIN thread_ids i ON i.thread_key = m.thread_key
|
|
100
|
+
ORDER BY m.updated_at DESC;
|
|
101
|
+
DROP TABLE thread_meta;
|
|
102
|
+
ALTER TABLE thread_meta_new RENAME TO thread_meta;
|
|
103
|
+
|
|
104
|
+
-- Staged and replaced rather than built beside the old table, because
|
|
105
|
+
-- this one's uniqueness is an index rather than a primary key and two
|
|
106
|
+
-- indexes cannot share a name. The index has to be in place before the
|
|
107
|
+
-- rows, since it is what OR IGNORE reads: a firing and the thread that
|
|
108
|
+
-- took it over could have held the same file, and the newest reference
|
|
109
|
+
-- to it is the one to keep.
|
|
110
|
+
CREATE TABLE thread_files_staged AS
|
|
111
|
+
SELECT f.ref, i.id AS thread_id, f.interface, f.file_id, f.name, f.payload, f.seen_at
|
|
112
|
+
FROM thread_files f JOIN thread_ids i ON i.thread_key = f.thread_key;
|
|
113
|
+
DROP TABLE thread_files;
|
|
114
|
+
CREATE TABLE thread_files (
|
|
115
|
+
ref TEXT PRIMARY KEY,
|
|
116
|
+
thread_id TEXT NOT NULL REFERENCES threads (id) ON DELETE CASCADE,
|
|
117
|
+
interface TEXT NOT NULL,
|
|
118
|
+
file_id TEXT NOT NULL,
|
|
119
|
+
name TEXT NOT NULL,
|
|
120
|
+
payload TEXT NOT NULL,
|
|
121
|
+
seen_at INTEGER NOT NULL
|
|
122
|
+
);
|
|
123
|
+
CREATE UNIQUE INDEX thread_files_id ON thread_files (thread_id, interface, file_id);
|
|
124
|
+
CREATE INDEX thread_files_thread ON thread_files (thread_id);
|
|
125
|
+
INSERT OR IGNORE INTO thread_files
|
|
126
|
+
(ref, thread_id, interface, file_id, name, payload, seen_at)
|
|
127
|
+
SELECT ref, thread_id, interface, file_id, name, payload, seen_at
|
|
128
|
+
FROM thread_files_staged ORDER BY seen_at DESC;
|
|
129
|
+
DROP TABLE thread_files_staged;
|
|
130
|
+
|
|
131
|
+
DROP TABLE thread_ids;
|
|
132
|
+
DROP TABLE thread_dirs;`)}];function D(r,a){return h(L(r),M,`${u}.v${a}.backup`)}function b(r,a,n){const t=D(a,n);if(m(t)&&!B(t,n))throw new l(`${t} is already there, and it is not a copy of this database at schema v${n}.
|
|
133
|
+
|
|
134
|
+
Nothing was migrated. Bishop copies the database before every schema
|
|
135
|
+
change so a bad migration can be undone, and it will not overwrite
|
|
136
|
+
whatever that file is. Move it aside and start Bishop again.`);const s=`${t}.partial`;try{X(L(t),{recursive:!0}),w(h(L(t),".gitignore"),`*
|
|
137
|
+
`,"utf8"),g(s,{force:!0}),r.prepare("VACUUM INTO ?").run(s),C(s,t)}catch(d){const o=d instanceof Error?d.message:String(d);throw new l(`Couldn't copy ${a} to ${t} before migrating it: ${o}
|
|
138
|
+
|
|
139
|
+
Nothing was migrated. Bishop copies the database before every schema
|
|
140
|
+
change so a bad migration can be undone, and it will not migrate
|
|
141
|
+
without one. Free some space, or move that file aside, and start again.`)}return t}function B(r,a){try{const n=new A(r,{readOnly:!0});try{return n.prepare("PRAGMA user_version").get()?.user_version===a}finally{n.close()}}catch{return!1}}function $(r){const a=new A(r);a.exec("PRAGMA journal_mode = WAL"),a.exec("PRAGMA busy_timeout = 5000"),a.exec("PRAGMA foreign_keys = ON");const t=a.prepare("PRAGMA user_version").get()?.user_version??0;if(t>0&&t<N.length){const s=b(a,r,t);c.info({backup:s,from:t},"copied the database before migrating it")}for(let s=t;s<N.length;s+=1){const d=N[s];if(d){a.exec("BEGIN");try{typeof d=="string"?a.exec(d):d(a),a.exec(`PRAGMA user_version = ${s+1}`),a.exec("COMMIT")}catch(o){throw a.exec("ROLLBACK"),o}}}return t===0?c.debug({schema:N.length},"created the session database"):t<N.length&&c.info({from:t,to:N.length},"migrated the session database"),a}export{M as BACKUPS_DIR,u as DB_FILE,N as MIGRATIONS,D as backupPath,K as dbPath,$ 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
|
|
2
|
-
`,"utf8")}function E(e,r,i,o){const
|
|
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};
|
package/dist/gc.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import{readdir as
|
|
2
|
-
`)){if(
|
|
1
|
+
import{readdir as P,realpath as F,rm as x,stat as A}from"node:fs/promises";import{basename as B,join as I,resolve as b,sep as L}from"node:path";import{mirrorPath as M,readCurrent as N,snapshotsPath as R}from"./agent-store.js";import{exec as v}from"./exec.js";import{releaseFiles as U}from"./files.js";import{log as d}from"./log.js";import{worktreeName as $}from"./worktrees.js";const S=30,q=6,T=1440*60*1e3;async function E(e){const t=await v("git",["worktree","list","--porcelain"],{cwd:e});if(t.code===null)return"unavailable";if(t.code!==0)return"refused";const o=[];for(const r of t.stdout.split(`
|
|
2
|
+
`)){if(r.startsWith("worktree ")){o.push({path:r.slice(9).trim()});continue}const n=o[o.length-1];n&&r.startsWith("branch ")&&(n.branch=r.slice(7).trim().replace(/^refs\/heads\//,""))}return o.filter(r=>r.path)}async function C(e,t){const o=await E(e);if(typeof o=="string")return d.warn({dir:t,mirror:e},"couldn't list the mirror's worktrees; leaving this snapshot"),!1;const r=await F(t).catch(()=>t),n=o.filter(s=>s.path===r||s.path.startsWith(`${r}/`));for(const s of n)await v("git",["worktree","unlock",s.path],{cwd:e});await x(t,{recursive:!0,force:!0}),await v("git",["worktree","prune"],{cwd:e});const c=n.map(s=>s.branch).filter(s=>!!s);return await K(e,c),c.length>0&&d.info({dir:t,branches:c},"removed thread branches with the snapshot they're on"),!0}async function K(e,t){for(const o of t)await v("git",["branch","-D",o],{cwd:e})}async function O(e,t){const o=t.now??Date.now(),r=(t.maxAgeDays??S)*24*60*60*1e3,n=R(e),c=M(e),s=await N(e);let l;try{l=(await P(n,{withFileTypes:!0})).filter(w=>w.isDirectory()&&!w.isSymbolicLink()).map(w=>w.name)}catch{return{removed:[],kept:0}}const i=[];let u=0;for(const w of l){const p=I(n,w);if(p===s){u+=1;continue}const k=await A(p).then(g=>g.mtimeMs,()=>{});if(k===void 0)continue;const h=Math.max(k,t.lastActivity.get(p)??0),f=o-h;if(f<=r){u+=1;continue}if(t.stillQuiet&&!t.stillQuiet(p)){d.info({dir:p},"a thread woke up mid-sweep; leaving the snapshot it runs in"),u+=1;continue}if(!await C(c,p)){u+=1;continue}i.push({dir:p,commit:w,ageDays:Math.floor(f/T)}),d.info({dir:p,commit:w},"removed an unused agent snapshot")}return{removed:i,kept:u}}async function Y(e,t={}){const o=new Map;for(const s of e){const l=o.get(s.dir);l?l.push(s):o.set(s.dir,[s])}const r=[],n=[],c=s=>n.push(...s.map(l=>l.id));for(const[s,l]of o){const i=await j(s,t.stores??[]);if(i==="unreadable"){d.warn({dir:s,threads:l.length},"couldn't read this directory; leaving it"),c(l);continue}const u=new Map;let w=!1;for(const h of i){const f=await E(h);if(typeof f=="string"){if(f==="refused"&&!await G(h))continue;d.warn({repo:h,threads:l.length},"couldn't list the worktrees here"),w=!0;continue}for(const g of f){const a=B(g.path);u.has(a)||u.set(a,{repo:h,entry:g})}}const p=new Map;let k=!1;for(const{id:h,threadKey:f,dirKey:g}of l){const a=u.get($(g));if(!a){w&&(n.push(h),k=!0);continue}if(t.stillQuiet&&!t.stillQuiet(h)){d.info({thread:f},"a thread woke up mid-sweep; leaving its worktree"),n.push(h);continue}const{repo:y,entry:m}=a;try{await v("git",["worktree","unlock",m.path],{cwd:y}),await x(m.path,{recursive:!0,force:!0})}catch(_){d.error({path:m.path,thread:f,err:_},"couldn't remove a worktree"),n.push(h);continue}const D=p.get(y)??[];m.branch&&D.push(m.branch),p.set(y,D),r.push({threadKey:f,path:m.path,...m.branch?{branch:m.branch}:{}})}k&&d.warn({dir:s},"leaving rows for threads a repository would not answer about");for(const[h,f]of p)await v("git",["worktree","prune"],{cwd:h}),await K(h,f),d.info({repo:h,branches:f},"released the worktrees of quiet threads")}return{released:r,kept:n}}async function j(e,t){const o=await W(e);if(o==="unreadable")return"unreadable";if(o==="there")return[e];const r=[];for(const n of t){const c=M(n);await W(c)==="there"&&r.push(c)}return r}async function G(e){return(await v("git",["rev-parse","--git-dir"],{cwd:e})).code===0}async function W(e){return A(e).then(()=>"there",t=>t.code==="ENOENT"?"gone":"unreadable")}function Q(e,t){const o=b(e);return t.some(r=>o===b(r)||o.startsWith(`${b(r)}${L}`))}async function H(e){const t=e.now??Date.now(),o=e.maxAgeDays??S,r=t-o*T,n=e.retention,c=e.only,s=n.staleThreads(r).filter(a=>!c||Q(a.dir,c)),{released:l,kept:i}=await Y(s,{stillQuiet:a=>n.quietSince(a,r),stores:e.stores}),u=[];let w=0;const p=n.lastActivityByDir();for(const a of e.stores)try{const y=await O(a,{lastActivity:p,stillQuiet:m=>(n.lastTurnIn(m)??0)<r,maxAgeDays:o,now:t});u.push(...y.removed),w+=y.kept}catch(y){d.error({store:a,err:y},"couldn't collect this agent's snapshots")}const k=n.collectableThreads(r,i).filter(a=>!c||a.dir!==""&&Q(a.dir,c)),h=n.forgetThreads(k,r),f=n.pruneEvents(t),g=await U(e.home,h.flatMap(a=>a.dirKey===a.threadKey?[a.dirKey]:[a.dirKey,a.threadKey]));return{snapshots:u,keptSnapshots:w,worktrees:l,files:g,threads:h.length,events:f}}function X(e){return e.snapshots.length>0||e.worktrees.length>0||e.threads>0||e.events>0}function ne(e){const t=e.now??Date.now,o=e.intervalMs??q*60*60*1e3;let r,n,c=!1;async function s(){const i=await H({retention:e.retention,stores:await e.stores(),home:e.home,...e.maxAgeDays===void 0?{}:{maxAgeDays:e.maxAgeDays},now:t()}),u={snapshots:i.snapshots.length,worktrees:i.worktrees.length,files:i.files,threads:i.threads,events:i.events};return X(i)?d.info(u,"collected what old conversations left behind"):d.debug(u,"nothing to collect"),i}function l(){return c?Promise.resolve(void 0):n||(n=s().finally(()=>{n=void 0}),n)}return{start(){l().catch(i=>d.error({err:i},"the first collection failed")),r=setInterval(()=>{l().catch(i=>{d.error({err:i},"a collection failed; trying again on the next sweep")})},o),r.unref()},sweep:l,async stop(){c=!0,r&&clearInterval(r),await Promise.allSettled([n])}}}export{S as DEFAULT_MAX_AGE_DAYS,q as DEFAULT_SWEEP_HOURS,O as collectSnapshots,X as collected,ne as makeCollector,Y as releaseWorktrees,H as sweep};
|
package/dist/gmail/interface.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import{BishopError as
|
|
2
|
-
`)},b="gmail:historyId",
|
|
1
|
+
import{BishopError as S}from"../errors.js";import{log as n}from"../log.js";import{retry as Y}from"../retry.js";import{GmailApiError as j,gmailSendRetryable as P,UNREAD as $}from"./api.js";import{looksLikeAddress as z}from"./authorize.js";import{resolveGmailDestination as U}from"./destination.js";import{mailAttachment as M,mailFile as H,mailFileId as q,mailFilePayload as B,parseMailFile as Q}from"./files.js";import{continuesAThread as J,mailHistory as V}from"./history.js";import{buildMessage as W,buildReply as X,parseMessage as Z,promptFrom as tt}from"./message.js";import{gmailPresence as et}from"./presence.js";import{participants as rt,routeMessage as at}from"./route.js";const nt={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."].join(`
|
|
2
|
+
`)},b="gmail:historyId",R="gmail:attachedAt",C="in:inbox newer_than:2d",v=50;function E(r){return`gmail:${r}`}async function gt(r){const c=(await r.api.profile()).emailAddress.toLowerCase(),u={email:c},w={...nt,self:{address:c}},G=r.now??Date.now,y=r.state.get(R),h=y?Number(y):G();y||r.state.set(R,String(h));const k=new Set;let g,m,p=!1;const d=new Set;async function D(t,e){if(await r.labels.mark(t.threadId,"refused"),!(!e||k.has(t.threadId)))try{await r.api.send({raw:X({from:u,to:[t.from],subject:t.subject,inReplyTo:t.messageId,references:t.references,body:e}),threadId:t.threadId}),k.add(t.threadId)}catch(l){n.warn({threadId:t.threadId,err:l},"could not send the refusal")}}function K(t,e){if(e.fetchable)return r.attachments.remember(t,w.name,{id:q(e),name:e.name,payload:B(e)})}async function L(t,e){return e.attachments.length===0?[]:Promise.all(e.attachments.map(async(l,a)=>{const s=H(e.id,a,l),i=K(t.id,s),o=await r.files.save(t.dirKey,M(r.api,s,l.data));return i?{...o,ref:i}:o}))}function N(t,e,l,a){const s=[],i=rt(e,c).map(o=>({email:o}));return{thread:t,isNewThread:a,origin:w,...J(e)?{history:()=>V({api:r.api,message:e,self:c,allow:r.allow,remember:o=>r.attachments.remember(t.id,w.name,o)})}:{},async messages(){const o=await L(t,e);return[{principal:{id:e.from.email,email:e.from.email,...e.from.name?{realName:e.from.name}:{}},text:tt({...e,body:l},c),...o.length>0?{files:o}:{}}]},presence:()=>et({api:r.api,labels:r.labels,self:u,message:e,to:i,threadKey:t.threadKey,notices:s}),async notify(o){s.push(o)},async destination(){return{interface:"gmail",handle:e.from.email,label:e.from.email}}}}async function O(t){const e=`gmail:${t}`;if(await r.events.seen(e,{record:!1}))return;let l;try{l=await r.api.message(t)}catch(f){if(f instanceof j&&f.status===404){await r.events.seen(e),n.warn({messageId:t},"a message vanished before it could be read; skipping it");return}throw f}await r.events.seen(e);const a=Z(l);if(a.receivedAt<h){n.warn({messageId:t,threadId:a.threadId,receivedAt:new Date(a.receivedAt).toISOString(),floor:new Date(h).toISOString()},"ignoring an email that predates this agent");return}const s=at(a,{self:c,allow:r.allow});if(s.action==="ignore"){n.debug({messageId:t,threadId:a.threadId,reason:s.reason},"ignoring an email");return}if(s.action==="refuse"){n.warn({messageId:t,threadId:a.threadId,from:a.from.email,reason:s.reason},"refusing an unauthorized email"),await D(a,s.note);return}const i=E(a.threadId),o=await r.threads.has(i),_=await r.threads.remember(i);r.markRead&&await r.api.modifyMessage(t,{removeLabelIds:[$]}).catch(f=>{n.debug({messageId:t,err:f},"could not mark the message read")}),n.info({thread:i,from:a.from.email,newThread:!o},"running a turn from email");const x=r.run(N(_,a,s.text,!o)).catch(f=>{n.error({thread:i,err:f},"a turn from email failed")}).finally(()=>{d.delete(x)});d.add(x)}async function F(){const t=r.state.get(b);if(!t){const i=await r.api.profile();r.state.set(b,i.historyId),n.info({historyId:i.historyId},"starting from the current state of the mailbox");return}const e=await r.api.history(t);let l=e.messageIds,a=e.historyId;if(e.expired){n.warn({cursor:t},"the Gmail history cursor is too old; falling back to a search of recent mail");const i=await r.api.profile();l=(await r.api.search(C,v)).reverse(),l.length===v&&n.warn({limit:v,query:C},"the resync search filled its limit, so older mail in the window was not looked at"),a=i.historyId}let s=!0;for(const i of l){if(p)return;try{await O(i)}catch(o){s=!1,n.error({messageId:i,err:o},"could not handle an email")}}if(!s){n.warn({cursor:t},"holding the Gmail cursor so unhandled mail is offered again");return}a&&r.state.set(b,a)}async function I(){if(!(m||p)){m=(async()=>{await F()})();try{await m}catch(t){n.error({err:t},"a Gmail poll failed; retrying on the next interval")}finally{m=void 0}}}async function A(){await Promise.allSettled([...d])}async function T(t){const e=t.handle.trim().toLowerCase();if(!z(e))throw new S(`"${t.handle}" is not an email address, so nothing was sent`);if(!r.allow.allows(e))throw new S(`${e} is not on this agent's allow list, so nothing was sent to it`);const l=t.title?.trim()||"A message from your agent",a=await Y(()=>r.api.send({raw:W({from:u,to:[{email:e}],subject:l,body:t.text})}),{what:"gmail messages.send",retryable:P});return n.info({to:e,chars:t.text.length},"delivered by email"),a.threadId?{threadKey:E(a.threadId)}:(n.warn({to:e},"Gmail named no thread for a sent message, so a reply to it starts fresh"),{})}return{name:"gmail",poll:I,settle:A,deliver:T,allows:t=>r.allow.allows(t),async resolveDestination(t,e){return U(r.allow,t,{...e?.optedInOnly?{optedInOnly:!0}:{}})},sameConversation:(t,e)=>t.trim().toLowerCase()===e.trim().toLowerCase(),attachment:t=>{const e=Q(t);return e?M(r.api,e):void 0},async start(){n.info({address:c,everyMs:r.pollIntervalMs,allow:r.allow.open?"anyone":"a list",answeringMailAfter:new Date(h).toISOString()},"watching the Gmail inbox"),await I(),g=setInterval(()=>{I()},r.pollIntervalMs)},async stop(){p=!0,g&&clearInterval(g),await m?.catch(()=>{}),d.size>0&&(n.info({turns:d.size},"waiting for email turns still running"),await A())}}}export{nt as GMAIL_ORIGIN,gt as createGmailInterface,E as threadKeyFor};
|
package/dist/harness/claude.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import{resolve as y}from"node:path";import{query as m}from"@anthropic-ai/claude-agent-sdk";import{log as c}from"../log.js";import{isolates as p,
|
|
2
|
-
`)[0]??"";return
|
|
3
|
-
`)}function
|
|
1
|
+
import{resolve as y}from"node:path";import{query as m}from"@anthropic-ai/claude-agent-sdk";import{log as c}from"../log.js";import{isolates as p,worktreePath as b}from"../worktrees.js";import{buildAgentOptions as k,CONTROLLED_KEYS as _,SETTINGS_ONLY_KEYS as x}from"./claude-options.js";import{ignoredKeys as g,shadowedKeys as v}from"./layers.js";import{buildPrompt as E,principalInstructions as S}from"./principal.js";function*T(e){switch(e.type){case"system":e.subtype==="init"&&(yield{type:"session",sessionId:e.session_id});return;case"assistant":for(const t of e.message.content)if(t.type==="text"&&t.text.trim())yield{type:"text",text:t.text};else if(t.type==="tool_use"){const n=C(t.input);yield{type:"tool",name:t.name,...n?{detail:n}:{}}}else t.type==="thinking"&&(yield{type:"thinking"});return;case"tool_use_summary":yield{type:"tool_summary",summary:e.summary};return;case"result":e.subtype==="success"?yield{type:"done",text:e.result,usage:{turns:e.num_turns,costUsd:e.total_cost_usd}}:yield{type:"error",kind:e.subtype,message:A(e.subtype)};return;default:return}}function C(e){if(!e||typeof e!="object")return;const t=e,n=t.file_path??t.notebook_path??t.path;if(typeof n=="string"&&n)return n.split("/").pop()??n;const i=t.url;if(typeof i=="string"&&i)try{return new URL(i).hostname}catch{return i.slice(0,40)}for(const u of["pattern","query","description"]){const a=t[u];if(typeof a=="string"&&a.trim()){const l=a.trim().split(`
|
|
2
|
+
`)[0]??"";return l.length>48?`${l.slice(0,45)}...`:l}}}function K(e){switch(e){case"none":return"existing claude login or cloud provider";case"/login managed key":return"key managed by claude login";default:return e}}function A(e){switch(e){case"error_max_turns":return"the agent hit its turn limit before finishing";case"error_max_budget_usd":return"the agent hit its cost limit before finishing";case"error_during_execution":return"the agent stopped on an error";default:return`the agent stopped: ${e}`}}const P=new Set(["Edit","Write","NotebookEdit"]);function I(e,t){return["This directory is shared with other conversations that may be running at the same time.",`Before you edit, create, or delete any file here, call EnterWorktree with name "${t}"`,"so your changes land in a git worktree of your own. Load the tool with ToolSearch first if","it isn't available. If a worktree by that name already exists, enter it by path instead:",`${b(e,t)}.`,"","Once you are in the worktree, work normally. Relative paths resolve inside it, and you","stay in it for every later message in this thread. Commit what you finish on the","worktree's branch and say which branch it is, since that's how a person finds your work.","Don't call ExitWorktree."].join(`
|
|
3
|
+
`)}function N(e){const t=y(e);return({toolName:n,cwd:i})=>{if(P.has(n)&&y(i)===t)return"This is the shared checkout, which other conversations are using. Call EnterWorktree with the name given in your instructions, then make this edit inside the worktree."}}function R(e,t,n,i){const u=p(e)?I(i,n):void 0;return[S(t),u].filter(a=>!!a?.trim()).join(`
|
|
4
4
|
|
|
5
|
-
`)}function
|
|
5
|
+
`)}function L(e){return async t=>{const n=t,i=e({toolName:n.tool_name,cwd:n.cwd});return i?(c.info({tool:n.tool_name,cwd:n.cwd},"refusing an edit in the shared checkout"),{continue:!0,hookSpecificOutput:{hookEventName:"PreToolUse",permissionDecision:"deny",permissionDecisionReason:i}}):{continue:!0}}}function Y(e={}){const t=g(e.options,_);t.length>0&&c.warn({ignored:t},"these agent.options are set by Bishop and cannot be overridden; ignoring them");const n={model:e.model,effort:e.effort},i=v(e.options,n);i.length>0&&c.warn({shadowed:i},"agent.model and agent.effort override these agent.options");const u=g(e.options,x);u.length>0&&c.warn({misplaced:u},"the harness reads these from agent.options.settings only and ignores them here");const a=e.worktrees;if(typeof e.options?.settings=="string"){const r=["auto memory is left to that file and the agent's own settings"];p(a)&&r.push("new worktrees branch from origin/<default-branch> rather than HEAD"),c.warn({settings:e.options.settings},`agent.options.settings names a file, so Bishop applies no settings defaults: ${r.join("; ")}`)}const l=(r,s)=>{const o=p(a)?N(r.cwd):void 0,f=o?{PreToolUse:[{hooks:[L(o)]}]}:void 0;return k({...e.options?{configOptions:e.options}:{},general:n,...e.apiKey?{apiKey:e.apiKey}:{},...f?{hooks:f,worktrees:!0}:{},...s?{tools:s}:{},controlled:r})};return{name:"claude",describeStartupFailure(r,s){return`The Claude agent couldn't start in ${r}:
|
|
6
6
|
${s}
|
|
7
7
|
|
|
8
8
|
Bishop does not manage Anthropic credentials. The agent authenticates the
|
|
9
9
|
same way \`claude\` does in this directory, so make sure that works first:
|
|
10
10
|
ANTHROPIC_API_KEY in the environment or .env, or
|
|
11
|
-
an existing \`claude\` login on this machine.`},async check(r){const s=new AbortController;try{for await(const o of m({prompt:"ping",options:{...
|
|
11
|
+
an existing \`claude\` login on this machine.`},async check(r){const s=new AbortController;try{for await(const o of m({prompt:"ping",options:{...l({cwd:r,systemPrompt:void 0,abortController:s}),maxTurns:1,persistSession:!1}})){if(o.type==="system"&&o.subtype==="init")return s.abort(),{ok:!0,credentialSource:K(o.apiKeySource),...o.model?{model:o.model}:{}};if(o.type==="result")break}return{ok:!1,error:"the harness exited without starting a session"}}catch(o){return s.signal.aborted?{ok:!0}:{ok:!1,error:o instanceof Error?o.message:String(o)}}},async*runTurn(r){const s=new AbortController,o=()=>s.abort();r.signal.addEventListener("abort",o,{once:!0});let f=!1;try{const d=m({prompt:E(r),options:l({cwd:r.cwd,systemPrompt:{type:"preset",preset:"claude_code",append:R(a,r.origin,r.worktree,r.cwd)},abortController:s,...r.sessionId?{resume:r.sessionId}:{}},r.tools)});for await(const w of d)for(const h of T(w))(h.type==="done"||h.type==="error")&&(f=!0),yield h}catch(d){s.signal.aborted?c.debug({thread:r.threadKey},"claude turn aborted"):f?c.debug({err:d},"query() threw after reporting a result"):(c.error({err:d},"harness turn failed"),yield{type:"error",kind:"harness_failure",message:d instanceof Error?d.message:String(d)})}finally{r.signal.removeEventListener("abort",o)}}}}export{Y as claudeHarness,N as editGuard,R as systemPromptAppend,C as toolDetail,I as worktreeNote};
|
package/dist/harness/codex.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import{existsSync as
|
|
2
|
-
`)}function
|
|
1
|
+
import{existsSync as O}from"node:fs";import{homedir as D}from"node:os";import{join as _}from"node:path";import{Codex as K}from"@openai/codex-sdk";import{log as f}from"../log.js";import{SERVER_NAME as A}from"../tools/server.js";import{ensureWorktree as I,gitCommonDir as P,isolates as M}from"../worktrees.js";import{ignoredKeys as R,layerOptions as j,mergeEnv as L,shadowedKeys as N}from"./layers.js";import{buildPrompt as X,principalInstructions as Y}from"./principal.js";const $=10,U=60;function W(e,r){const t=e??void 0;return r?{...t,[A]:{url:r.url,bearer_token_env_var:r.tokenEnvVar,required:!0,default_tools_approval_mode:"auto",startup_timeout_sec:$,tool_timeout_sec:U}}:t}const B={approvalPolicy:"never",sandboxMode:"workspace-write",networkAccessEnabled:!0,skipGitRepoCheck:!0},x=["workingDirectory","developer_instructions"];function F(e){const r=e?.config,t=r&&typeof r=="object"&&"developer_instructions"in r?["config.developer_instructions"]:[];return[...R(e,x),...t]}const G=new Set(["apiKey","baseUrl","config","configOverrides","env","codexPathOverride"]);function V(e){switch(e.type){case"command_execution":return{type:"tool",name:"Bash"};case"file_change":{const r=e.changes.length;if(r>1)return{type:"tool",name:"Edit",detail:`${r} files`};const t=r===1?e.changes[0]?.path.split("/").pop():void 0;return{type:"tool",name:"Edit",...t?{detail:t}:{}}}case"mcp_tool_call":return{type:"tool",name:`mcp__${e.server}__${e.tool}`};case"web_search":return{type:"tool",name:"WebSearch",...e.query?{detail:e.query}:{}};case"todo_list":return{type:"tool",name:"TodoWrite"};case"reasoning":return{type:"thinking"};default:return}}const H=new Set(["command_execution","mcp_tool_call","web_search","reasoning"]);function z(){const e=new Set;let r="";return t=>{switch(t.type){case"thread.started":return[{type:"session",sessionId:t.thread_id}];case"item.started":case"item.completed":{const i=t.item,p=H.has(i.type)?"item.started":"item.completed";if(t.type!==p||e.has(i.id))return[];if(e.add(i.id),i.type==="agent_message")return r=i.text,i.text.trim()?[{type:"text",text:i.text}]:[];if(i.type==="error")return f.warn({message:i.message},"codex reported a non-fatal error"),[];const c=V(i);return c?[c]:[]}case"turn.completed":return[{type:"done",text:r,usage:{inputTokens:t.usage.input_tokens,cachedInputTokens:t.usage.cached_input_tokens,outputTokens:t.usage.output_tokens}}];case"turn.failed":return[{type:"error",kind:"turn_failed",message:t.error.message}];case"error":return[{type:"error",kind:"stream_error",message:t.message}];default:return[]}}}function J(e){return[`You are working in a git worktree of your own, on the branch "${e}", created for this`,"conversation. Other conversations are working in their own worktrees at the same time.","","Work normally. Commit what you finish on this branch and say which branch it is, since that's","how a person finds your work."].join(`
|
|
2
|
+
`)}function Q(e,r){const t=Array.isArray(e)?e:[],i=[...new Set([...t,...r??[]])];return i.length>0?{additionalDirectories:i}:{}}function Z(e){return{model:e.model,modelReasoningEffort:e.effort}}const q={model:"model",modelReasoningEffort:"model_reasoning_effort"};function ee(e,r){const t=e?.config,i=t&&typeof t=="object"?Object.entries(q).filter(([p,c])=>r[p]!==void 0&&c in t).map(([,p])=>`config.${p}`):[];return[...N(e,r),...i]}function fe(e={}){const r=F(e.options);r.length>0&&f.warn({ignored:r},"these agent.options are set by Bishop and cannot be overridden; ignoring them");const t=Z(e),i=ee(e.options,t);i.length>0&&f.warn({shadowed:i},"agent.model and agent.effort override these agent.options");const p=j({defaults:B,...e.options?{configOptions:e.options}:{},general:t,controlledKeys:x}),c={},m={};for(const[o,n]of Object.entries(p))G.has(o)?c[o]=n:m[o]=n;const k=c.env;e.apiKey&&(c.apiKey=e.apiKey);const E=e.worktrees,y=(o,n,a)=>{const d=c.config??{},s=W(d.mcp_servers,a),g=L(k,a?{[a.tokenEnvVar]:a.token}:{}),l={...d,developer_instructions:[n&&Y(n),o].filter(u=>!!u?.trim()).join(`
|
|
3
3
|
|
|
4
|
-
`)};return
|
|
5
|
-
${
|
|
4
|
+
`)};return s&&(l.mcp_servers=s),new K({...c,...g?{env:g}:{},config:l})};async function b(o){if(!M(E))return{cwd:o.cwd};const n=await I(o.cwd,o.worktree),a=await P(n);return a||f.warn({worktree:n},"couldn't resolve the repository's git directory, so the agent probably can't commit"),{cwd:n,note:J(o.worktree),...a?{writable:[a]}:{}}}return{name:"codex",describeStartupFailure(o,n){return`The Codex agent couldn't start in ${o}:
|
|
5
|
+
${n}
|
|
6
6
|
|
|
7
7
|
Bishop does not manage OpenAI credentials. The agent authenticates the
|
|
8
8
|
same way \`codex\` does on this machine, so make sure that works first:
|
|
9
9
|
CODEX_API_KEY in the environment or .env, or
|
|
10
|
-
an existing \`codex login\` on this machine.`},async check(o){const
|
|
10
|
+
an existing \`codex login\` on this machine.`},async check(o){const n=new AbortController,a=m.model;try{const s=y(void 0,void 0).startThread({...m,workingDirectory:o}),{events:g}=await s.runStreamed("ping",{signal:n.signal});for await(const l of g){if(l.type==="thread.started"){n.abort();const u=te(e.apiKey);return re(u),{ok:!0,...u?{credentialSource:u}:{},...a?{model:a}:{}}}if(l.type==="error")return{ok:!1,error:l.message}}return{ok:!1,error:"the harness exited without starting a thread"}}catch(d){return n.signal.aborted?{ok:!0}:{ok:!1,error:d instanceof Error?d.message:String(d)}}},async*runTurn(o){const n=new AbortController,a=()=>n.abort();o.signal.addEventListener("abort",a,{once:!0});let d=!1;try{const{cwd:s,note:g,writable:l}=await b(o),u=y(g,o.origin,o.tools),w={...m,workingDirectory:s,...Q(m.additionalDirectories,l)},v=o.sessionId?u.resumeThread(o.sessionId,w):u.startThread(w),C=z(),{events:S}=await v.runStreamed(X(o),{signal:n.signal});for await(const T of S)for(const h of C(T))(h.type==="done"||h.type==="error")&&(d=!0),yield h}catch(s){n.signal.aborted?f.debug({thread:o.threadKey},"codex turn aborted"):d?f.debug({err:s},"codex threw after reporting a result"):(f.error({err:s},"harness turn failed"),yield{type:"error",kind:"harness_failure",message:s instanceof Error?s.message:String(s)})}finally{o.signal.removeEventListener("abort",a)}}}}function te(e){if(e)return"CODEX_API_KEY";const r=process.env.CODEX_HOME??_(D(),".codex");if(O(_(r,"auth.json")))return"existing codex login"}function re(e){e||f.warn("no CODEX_API_KEY and no codex login found, so the first turn will probably fail to authenticate; run `codex login`, or set CODEX_API_KEY")}export{x as CONTROLLED_CODEX_KEYS,B as DEFAULT_CODEX_OPTIONS,Z as codexGeneralOptions,fe as codexHarness,z as codexTranslator,F as ignoredCodexKeys,W as mergeMcpServers,ee as shadowedCodexKeys,Q as withWritable,J as worktreeNote};
|
package/dist/ids.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{randomBytes as t}from"node:crypto";function o(r){return`${r}_${t(8).toString("base64url")}`}export{o as mint};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{log as
|
|
1
|
+
import{log as i}from"../log.js";function s(a){return async(n,o)=>{const e=a.store.find(n.id,o);if(!e)return;const t=a.interfaces().find(f=>f.name===e.interface);if(!t)return i.warn({thread:n.threadKey,interface:e.interface},"a shared file was recorded by an interface that is not running"),{name:e.name,unavailable:`Bishop is not connected to ${e.interface}, which holds this file`};const r=t.attachment(e.payload);return r?a.files.save(n.dirKey,r):(i.warn({thread:n.threadKey,interface:e.interface,file:e.name},"an interface could not make sense of a shared file it recorded"),{name:e.name,unavailable:"Bishop no longer knows how to fetch this file"})}}export{s as sharedFileFetcher};
|
package/dist/interface/state.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
function
|
|
1
|
+
function a(){const n=new Map;return{async has(e){return n.has(e)},async remember(e){const r=n.get(e);if(r)return r;const t={id:e,threadKey:e,dirKey:e};return n.set(e,t),t}}}function d(n=5e3){const e=[],r=new Set;return{async seen(t,o){if(r.has(t))return!0;if(o?.record===!1)return!1;if(r.add(t),e.push(t),e.length>n){const s=e.shift();s!==void 0&&r.delete(s)}return!1}}}export{d as memoryEventLog,a as memoryThreadRegistry};
|
package/dist/schedule/runner.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import{log as a}from"../log.js";import{nextRun as
|
|
1
|
+
import{log as a}from"../log.js";import{nextRun as z}from"./cron.js";import{schedulePresence as _}from"./presence.js";import{SCHEDULE_ORIGIN as I,scheduleNote as N}from"./prompt.js";const R=3e4,M=5,A=5e3;function B(r){return r.startsWith("cli:")}function C(r,l){return`schedule:${r}:${l}`}function K(r){const l=r.now??Date.now,E=r.tickMs??R,T=r.authorized??B,s=new Map,m=new Map,d=new Set;let u,S=!1;function g(e,t){try{return r.schedules.setNextRun(e.id,z(e.cron,e.tz,t)),!0}catch(i){const n=i instanceof Error?i.message:String(i);return a.error({schedule:e.id,cron:e.cron,tz:e.tz,err:i},"disabling a schedule whose cron expression cannot be read"),r.schedules.setEnabled(e.id,!1),r.schedules.recordFailure(e.id,t,n),w(e,`The schedule "${e.name}" is off: Bishop can no longer read when it should run. ${n}
|
|
2
2
|
|
|
3
|
-
Remove it, or add it again with a cron expression that works.`),!1}}async function w(e,
|
|
3
|
+
Remove it, or add it again with a cron expression that works.`),!1}}async function w(e,t){const i=r.deliver(e.destination,{text:t,title:e.name}).then(()=>{}).catch(n=>{a.error({schedule:e.id,to:e.destination.label,err:n},"could not say that a schedule failed")}).finally(()=>{d.delete(i)});d.add(i),await i}async function y(e,t,i){const n=r.schedules.recordFailure(e.id,t,i);if(a.error({schedule:e.id,name:e.name,failures:n,err:i},"a scheduled run failed"),n>=M){r.schedules.setEnabled(e.id,!1),a.error({schedule:e.id,name:e.name,failures:n},"disabling a schedule after too many failures in a row"),await w(e,`The schedule "${e.name}" has failed ${n} times in a row and is now off. The last failure was: ${i}
|
|
4
4
|
|
|
5
|
-
Nothing more will run until \`bishop schedules enable ${e.id}\`.`);return}
|
|
5
|
+
Nothing more will run until \`bishop schedules enable ${e.id}\`.`);return}n===1&&await w(e,`The schedule "${e.name}" failed: ${i}`)}async function k(e,t,i){const n=_();a.info({schedule:e.id,name:e.name},"a schedule is firing");let c,v;try{v=await r.threads.remember(i)}catch(o){await y(e,l(),o instanceof Error?o.message:String(o));return}const $={thread:v,isNewThread:!0,freshSession:!0,tools:!1,origin:I,note:N(e,t),async messages(){return[{principal:{id:`schedule:${e.id}`,displayName:e.name},text:e.prompt}]},presence:()=>n,async notify(o){n.note(o)},async destination(){return e.destination}};try{await r.run($)}catch(o){c=o instanceof Error?o.message:String(o)}const p=n.outcome(),x=p.failure??c,f=l();if(x){await y(e,f,x);return}if(p.silent){r.schedules.recordSuccess(e.id,f),a.info({schedule:e.id,name:e.name},"a schedule had nothing to report");return}let h;try{h=await r.deliver(e.destination,{text:p.text,title:e.name})}catch(o){await y(e,f,o instanceof Error?o.message:String(o));return}if(h.threadKey&&r.attach)try{await r.attach(h.threadKey,i)}catch(o){a.error({schedule:e.id,thread:h.threadKey,err:o},"a report was delivered but nothing will answer a reply to it")}r.schedules.recordSuccess(e.id,f),a.info({schedule:e.id,name:e.name,to:e.destination.label},"a schedule reported")}async function b(){if(S)return;const e=l();for(const t of r.schedules.due(e)){if(!T(t.createdBy)){a.warn({schedule:t.id,createdBy:t.createdBy},"skipping a schedule whose creator can no longer be authorized"),g(t,e);continue}if(!g(t,e))continue;if(s.has(t.id)){a.warn({schedule:t.id,name:t.name},"skipping a firing while the previous one is still running");continue}const i=C(t.id,e);m.set(t.id,i);const n=k(t,e,i).catch(c=>{a.error({schedule:t.id,err:c},"a schedule firing threw")}).finally(()=>{s.delete(t.id),m.delete(t.id)});s.set(t.id,n)}}return{start(){const e=l(),t=r.schedules.due(e);for(const n of t)a.info({schedule:n.id,name:n.name,due:new Date(n.nextRunAt).toISOString()},"skipping a firing that came due while Bishop was not running"),g(n,e);const i=r.schedules.all().filter(n=>n.enabled);a.info({schedules:i.length,skipped:t.length},"watching the schedules"),u=setInterval(()=>{b().catch(n=>{a.error({err:n},"a scheduler pass failed; trying again on the next tick")})},E),u.unref()},tick:b,async settle(){await Promise.allSettled([...s.values(),...d])},async stop(){if(S=!0,u&&clearInterval(u),s.size>0){a.info({firings:s.size},"stopping scheduled runs still going");for(const n of s.keys()){const c=m.get(n);c&&r.cancel?.(c)}}const e=Promise.allSettled([...s.values(),...d]);let t;const i=new Promise(n=>{t=setTimeout(()=>{a.warn({firings:s.size},"giving up on scheduled runs that would not stop"),n()},A)});await Promise.race([e,i]),t&&clearTimeout(t)}}}export{M as MAX_CONSECUTIVE_FAILURES,B as authorizedToRun,C as firingThread,K as makeScheduler};
|
package/dist/sessions/store.js
CHANGED
|
@@ -1,27 +1,11 @@
|
|
|
1
|
-
import{log as
|
|
1
|
+
import{mint as g}from"../ids.js";import{log as h}from"../log.js";const D=10080*60*1e3,O="id, thread_key, dir_key";function R(e){return{id:e.id,threadKey:e.thread_key,dirKey:e.dir_key}}function L(e,d){const s=e.prepare("SELECT session_id, harness FROM sessions WHERE thread_id = ?"),n=e.prepare(`INSERT INTO sessions (thread_id, session_id, harness, created_at, updated_at)
|
|
2
2
|
VALUES (?, ?, ?, ?, ?)
|
|
3
|
-
ON CONFLICT (
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
ON CONFLICT (
|
|
12
|
-
used_at = excluded.used_at`),T=e.prepare("UPDATE thread_dirs SET used_at = ? WHERE thread_key = ?");return{async get(n){const u=s.get(n);if(u?.session_id){if(u.harness!==d){k.warn({thread:n,recorded:u.harness,running:d},"this thread belongs to a different harness; starting a new session");return}return u.session_id}},async set(n,u){const _=Date.now();a.run(n,u,d,_,_)},async has(n){return s.get(n)!==void 0},async remember(){},async seen(n,u){return u?.record===!1?c.get(n)!==void 0:o.run(n,Date.now()).changes===0},count(){return Number(i.get().n)},async getDir(n){return y.get(n)?.agent_dir},async setDir(n,u){l.run(n,u,Date.now())},async touchDir(n){T.run(Date.now(),n)},async getWorkspace(n){return h.get(n)?.workspace_key??void 0},async continueFrom(n,u,_){const t=Date.now();O.run(n,u,d,t,t,_)}}}function L(e){return async(d,s)=>{const a=await e.get(s);if(!a){k.warn({thread:d,continues:s},"nothing to continue: that thread has no session, so a reply here starts fresh");return}const o=await e.getWorkspace(s)??s;await e.continueFrom(d,a,o),k.info({thread:d,continues:s,session:a,workspace:o},"a new thread continues another")}}function f(e){const d=e.prepare("SELECT agent_dir AS dir, MAX(used_at) AS last_used FROM thread_dirs GROUP BY agent_dir"),s=e.prepare("SELECT MAX(used_at) AS last_used FROM thread_dirs WHERE agent_dir = ?"),a=e.prepare("SELECT thread_key, agent_dir FROM thread_dirs WHERE used_at < ? ORDER BY used_at"),o=e.prepare("SELECT 1 FROM thread_dirs WHERE thread_key = ? AND used_at < ?"),c=e.prepare("DELETE FROM thread_dirs WHERE thread_key = ? AND used_at < ?"),i=t=>`DELETE FROM sessions WHERE thread_key IN (
|
|
13
|
-
SELECT s.thread_key FROM sessions s
|
|
14
|
-
JOIN thread_dirs d ON d.thread_key = COALESCE(s.workspace_key, s.thread_key)
|
|
15
|
-
WHERE d.used_at < ?${t}
|
|
16
|
-
)`,y=e.prepare(i("")),h=t=>`DELETE FROM thread_meta WHERE thread_key IN (
|
|
17
|
-
SELECT s.thread_key FROM sessions s
|
|
18
|
-
JOIN thread_dirs d ON d.thread_key = COALESCE(s.workspace_key, s.thread_key)
|
|
19
|
-
WHERE d.used_at < ?${t}
|
|
20
|
-
)`,O=e.prepare(h("")),l=t=>`SELECT thread_key FROM thread_dirs WHERE used_at < ?${t.replaceAll("d.","")}
|
|
21
|
-
UNION
|
|
22
|
-
SELECT s.thread_key FROM sessions s
|
|
23
|
-
JOIN thread_dirs d ON d.thread_key = COALESCE(s.workspace_key, s.thread_key)
|
|
24
|
-
WHERE d.used_at < ?${t}`,T=e.prepare(l("")),n=e.prepare("DELETE FROM thread_meta WHERE updated_at < ? AND thread_key NOT IN (SELECT thread_key FROM sessions)"),u=e.prepare("DELETE FROM processed_events WHERE seen_at < ?"),_=e.prepare("DELETE FROM thread_files WHERE thread_key = ?");return{lastActivityByDir(){const t=d.all();return new Map(t.map(r=>[r.dir,Number(r.last_used)]))},lastTurnIn(t){const r=s.get(t);return r?.last_used==null?void 0:Number(r.last_used)},staleThreads(t){return a.all(t).map(E=>({threadKey:E.thread_key,dir:E.agent_dir}))},quietSince(t,r){return o.get(t,r)!==void 0},forgetThreads(t,r){let E=0;for(const p of t)E+=Number(c.run(p,r).changes);return E},pruneSessions(t,r=[]){if(r.length===0)return Number(y.run(t).changes);const E=` AND d.thread_key NOT IN (${r.map(()=>"?").join(", ")})`;return Number(e.prepare(i(E)).run(t,...r).changes)},pruneThreadMeta(t,r=[]){let E=Number(n.run(t).changes);if(r.length===0)return E+Number(O.run(t).changes);const p=` AND d.thread_key NOT IN (${r.map(()=>"?").join(", ")})`;return E+=Number(e.prepare(h(p)).run(t,...r).changes),E},collectableThreads(t,r=[]){const E=r.length===0?"":` AND d.thread_key NOT IN (${r.map(()=>"?").join(", ")})`,p=r.length===0?T:e.prepare(l(E)),N=r.length===0?[t,t]:[t,...r,t,...r];return p.all(...N).map(g=>g.thread_key)},forgetFiles(t){let r=0;for(const E of t)r+=Number(_.run(E).changes);return r},pruneEvents(t=Date.now()){return Number(u.run(t-R).changes)}}}function w(e){const d=e.prepare("SELECT value FROM thread_meta WHERE thread_key = ? AND key = ?"),s=e.prepare(`INSERT INTO thread_meta (thread_key, key, value, updated_at) VALUES (?, ?, ?, ?)
|
|
25
|
-
ON CONFLICT (thread_key, key) DO UPDATE SET value = excluded.value,
|
|
26
|
-
updated_at = excluded.updated_at`),a=e.prepare("DELETE FROM thread_meta WHERE thread_key = ? AND key = ?");return{get(o,c){return d.get(o,c)?.value},set(o,c,i){s.run(o,c,i,Date.now())},clear(o,c){a.run(o,c)}}}function C(){const e=new Map,d=(s,a)=>`${s}\0${a}`;return{get:(s,a)=>e.get(d(s,a)),set:(s,a,o)=>{e.set(d(s,a),o)},clear:(s,a)=>{e.delete(d(s,a))}}}function m(e){const d=e.prepare("SELECT value FROM interface_state WHERE key = ?"),s=e.prepare(`INSERT INTO interface_state (key, value, updated_at) VALUES (?, ?, ?)
|
|
27
|
-
ON CONFLICT (key) DO UPDATE SET value = excluded.value, updated_at = excluded.updated_at`);return{get(a){return d.get(a)?.value},set(a,o){s.run(a,o,Date.now())}}}export{R as EVENT_TTL_MS,m as interfaceState,C as memoryThreadMeta,f as retentionStore,D as sessionStore,L as threadContinuation,w as threadMeta};
|
|
3
|
+
ON CONFLICT (thread_id) DO UPDATE SET session_id = excluded.session_id,
|
|
4
|
+
harness = excluded.harness,
|
|
5
|
+
updated_at = excluded.updated_at`),u=e.prepare("INSERT OR IGNORE INTO processed_events (event_id, seen_at) VALUES (?, ?)"),o=e.prepare("SELECT 1 FROM processed_events WHERE event_id = ?"),E=e.prepare("SELECT COUNT(*) AS n FROM sessions"),T=e.prepare("SELECT agent_dir FROM threads WHERE id = ?"),p=e.prepare(`SELECT ${O} FROM threads WHERE thread_key = ?`),_=e.prepare(`INSERT INTO threads (id, thread_key, dir_key, created_at, used_at) VALUES (?, ?, ?, ?, ?)
|
|
6
|
+
ON CONFLICT (thread_key) DO NOTHING`),t=e.prepare("SELECT 1 FROM sessions s JOIN threads t ON t.id = s.thread_id WHERE t.thread_key = ?"),i=e.prepare("UPDATE threads SET thread_key = ? WHERE id = ?"),l=e.prepare("UPDATE threads SET agent_dir = ?, used_at = ? WHERE id = ?"),S=e.prepare("UPDATE threads SET used_at = ? WHERE id = ?");return{async get(r){const a=s.get(r);if(a?.session_id){if(a.harness!==d){h.warn({thread:r,recorded:a.harness,running:d},"this thread belongs to a different harness; starting a new session");return}return a.session_id}},async set(r,a){const c=Date.now();n.run(r,a,d,c,c)},async has(r){return t.get(r)!==void 0},async remember(r){const a=Date.now();return _.run(g("t"),r,r,a,a),R(p.get(r))},async seen(r,a){return a?.record===!1?o.get(r)!==void 0:u.run(r,Date.now()).changes===0},count(){return Number(E.get().n)},async getDir(r){return T.get(r)?.agent_dir??void 0},async setDir(r,a){l.run(a,Date.now(),r)},async touchDir(r){S.run(Date.now(),r)},async readdress(r,a){const c=p.get(r);if(!c)return;const N=p.get(a);if(N?.id===c.id)return R(c);if(!N)return i.run(a,c.id),{id:c.id,threadKey:a,dirKey:c.dir_key}}}}function A(e){return async(d,s)=>{const n=await e.readdress(s,d);if(!n){h.warn({thread:d,firing:s},"nothing to attach: a reply here starts a fresh conversation");return}h.info({thread:d,firing:s,id:n.id},"a report thread continues its firing")}}function m(e){const d=e.prepare(`SELECT agent_dir AS dir, MAX(used_at) AS last_used FROM threads
|
|
7
|
+
WHERE agent_dir IS NOT NULL GROUP BY agent_dir`),s=e.prepare("SELECT MAX(used_at) AS last_used FROM threads WHERE agent_dir = ?"),n=e.prepare(`SELECT ${O}, agent_dir FROM threads
|
|
8
|
+
WHERE used_at < ? AND agent_dir IS NOT NULL ORDER BY used_at`),u=e.prepare("SELECT 1 FROM threads WHERE id = ? AND used_at < ?"),o=t=>`SELECT ${O}, agent_dir FROM threads WHERE used_at < ?${t} ORDER BY used_at`,E=e.prepare(o("")),T=e.prepare("DELETE FROM threads WHERE id = ? AND used_at < ?"),p=e.prepare("DELETE FROM processed_events WHERE seen_at < ?"),_=t=>({...R(t),dir:t.agent_dir??""});return{lastActivityByDir(){const t=d.all();return new Map(t.map(i=>[i.dir,Number(i.last_used)]))},lastTurnIn(t){const i=s.get(t);return i?.last_used==null?void 0:Number(i.last_used)},staleThreads(t){return n.all(t).map(_)},quietSince(t,i){return u.get(t,i)!==void 0},collectableThreads(t,i=[]){if(i.length===0)return E.all(t).map(_);const l=` AND id NOT IN (${i.map(()=>"?").join(", ")})`;return e.prepare(o(l)).all(t,...i).map(_)},forgetThreads(t,i){return t.filter(l=>Number(T.run(l.id,i).changes)>0)},pruneEvents(t=Date.now()){return Number(p.run(t-D).changes)}}}function M(e){const d=e.prepare("SELECT value FROM thread_meta WHERE thread_id = ? AND key = ?"),s=e.prepare(`INSERT INTO thread_meta (thread_id, key, value, updated_at) VALUES (?, ?, ?, ?)
|
|
9
|
+
ON CONFLICT (thread_id, key) DO UPDATE SET value = excluded.value,
|
|
10
|
+
updated_at = excluded.updated_at`),n=e.prepare("DELETE FROM thread_meta WHERE thread_id = ? AND key = ?");return{get(u,o){return d.get(u,o)?.value},set(u,o,E){s.run(u,o,E,Date.now())},clear(u,o){n.run(u,o)}}}function v(){const e=new Map,d=(s,n)=>`${s} ${n}`;return{get:(s,n)=>e.get(d(s,n)),set:(s,n,u)=>{e.set(d(s,n),u)},clear:(s,n)=>{e.delete(d(s,n))}}}function C(e){const d=e.prepare("SELECT value FROM interface_state WHERE key = ?"),s=e.prepare(`INSERT INTO interface_state (key, value, updated_at) VALUES (?, ?, ?)
|
|
11
|
+
ON CONFLICT (key) DO UPDATE SET value = excluded.value, updated_at = excluded.updated_at`);return{get(n){return d.get(n)?.value},set(n,u){s.run(n,u,Date.now())}}}export{D as EVENT_TTL_MS,C as interfaceState,v as memoryThreadMeta,m as retentionStore,L as sessionStore,A as threadHandover,M as threadMeta};
|
package/dist/setup.js
CHANGED
|
@@ -14,7 +14,7 @@ Its credentials are in .env and its configuration in .bishop/config.json.
|
|
|
14
14
|
|
|
15
15
|
To set it up again, remove slack.app.id from .bishop/config.json and the
|
|
16
16
|
BISHOP_SLACK_* lines from .env first.
|
|
17
|
-
`),!0):!1}async function T(n,t,e){await B({...t,slack:{...t.slack,...e.workspace?{workspace:e.workspace}:{},app:e.app}},n);const a=await F(n,{BISHOP_SLACK_BOT_TOKEN:e.botToken,BISHOP_SLACK_APP_TOKEN:e.appToken}),o=await R(n,[".env","bishop.db"]);return` .bishop/config.json app configuration
|
|
17
|
+
`),!0):!1}async function T(n,t,e){await B({...t,slack:{...t.slack,...e.workspace?{workspace:e.workspace}:{},app:e.app}},n);const a=await F(n,{BISHOP_SLACK_BOT_TOKEN:e.botToken,BISHOP_SLACK_APP_TOKEN:e.appToken}),o=await R(n,[".env","bishop.db*"]);return` .bishop/config.json app configuration
|
|
18
18
|
${a} credentials
|
|
19
19
|
`+(o.length?` .gitignore added ${o.join(", ")}
|
|
20
20
|
`:"")}async function X(n){const t=n.cwd??process.cwd(),e=n.prompt&&process.stdin.isTTY===!0,a=r=>process.stdout.write(r),o=await g(t);if(I(o,a))return;const p=await K();if(!p)throw new d(E);u.debug({slackCli:p},"found the slack cli");let s=await w();if(s.length===0){if(!e)throw new d("The Slack CLI is not logged in to a workspace.\nRun `slack auth login`, then run this again.");if(a("The Slack CLI is not logged in to a workspace. Starting `slack auth login`.\n\n"),s=await P()?await w():[],s.length===0)throw new d("The Slack CLI is still not logged in, so the app cannot be created.\nRun `slack auth login` on its own to see what went wrong.");a(`
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{Command as l,CommanderError as p,InvalidArgumentError as v}from"commander";import{VERBOSITY as h}from"../../config.js";import{log as
|
|
1
|
+
import{Command as l,CommanderError as p,InvalidArgumentError as v}from"commander";import{VERBOSITY as h}from"../../config.js";import{log as g}from"../../log.js";import{parseVerbosity as c}from"../verbosity.js";import{commandOutput as f,tokenize as w}from"./syntax.js";const n="default";function y(t){if(t.trim().toLowerCase()===n)return n;const e=c(t);if(!e)throw new v(`Use one of ${h.join(", ")}, or ${n}.`);return e}function C(t,e,i){const m=[];let a;const s=new l;s.name("!").description("Bishop commands. These are handled here and never reach the agent.").exitOverride().configureOutput({writeOut:r=>m.push(r),writeErr:r=>m.push(r),outputError:(r,o)=>o(r)}),s.command("verbosity").description("How much of a turn this thread shows while it is running.").argument("[level]",`${h.join(", ")}, or ${n} to follow the configured level. Omit it to see the current one.`,y).action(r=>{if(!r){const d=i.verbosity.of(e.thread.id),u=d.fromThread?"":" (the configured default)";a=`Thread verbosity is ${d.level}${u}`;return}if(r===n){i.verbosity.clear(e.thread.id),a=`Thread verbosity follows the configured default, which is ${i.verbosity.of(e.thread.id).level}`;return}const o=c(r);o&&(i.verbosity.set(e.thread.id,o),a=`Thread verbosity set to ${o}`)});try{s.parse(w(t),{from:"user"})}catch(r){if(!(r instanceof p)){const o=r instanceof Error?r.message:String(r);return g.debug({thread:e.thread.threadKey,line:t,err:r},"a command failed"),f(o)}}return f(a??(m.join("").trim()||s.helpInformation()))}export{C as runCommand};
|
package/dist/slack/handlers.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{log as a}from"../log.js";import{markHeld as
|
|
1
|
+
import{log as a}from"../log.js";import{markHeld as L,releaseHeld as O}from"./pending.js";import{FILE_SHARE as P,isBotEvent as A,isSelfEvent as R,routeAppMention as W,routeMessage as Y}from"./route.js";import{say as $}from"./say.js";import{mentionsBot as j,messageKeyString as C,spokenAt as G,threadKeyOf as H,threadKeyString as S}from"./thread.js";function ee(w,q){const{identity:d,botName:m,threads:x,events:z,run:D,stop:N,allows:I,bots:p,participants:T,command:B}=q,l=new Map,y=new Map;async function U(t,s,r){const n=l.get(s);if(!n?.length)return[];l.set(s,[]),await y.get(s);const e=await O(r.client,t,n,{botUserId:d.botUserId,...m?{botName:m}:{}}),c=[];for(const h of e)I(h.userId)?c.push(h):a.warn({thread:s,user:h.userId},"dropping a held message");return c}async function F(t,s,r){const n=H(t,d.teamId);if(!n){a.debug({source:r},"dropping event with no usable thread key");return}const e=S(n),c=await x.has(e),h={botUserId:d.botUserId,botId:d.botId,...m?{botName:m}:{},threadKnown:c},u=r==="app_mention"?W(t,h):Y(t,h);if((c||u.action==="run")&&t.user&&!R(t,h)&&(!t.subtype||t.subtype===P)&&T.saw(e,t.user,G(t.ts)),u.action==="ignore"){t.user&&t.user!==d.botUserId&&!A(t)&&p.clear(e),a.debug({source:r,thread:e,reason:u.reason},"ignoring event");return}const f=A(t);if(f&&!p.answersBots){a.debug({source:r,thread:e},"ignoring a bot: this agent answers none");return}const v=C(n,t);if(v&&await z.seen(`slack:${v}`)){a.info({source:r,thread:e,message:v,eventId:t.event_id},"duplicate message, skipping");return}const o=t.user??"";if(!I(o)){if(f){a.debug({source:r,thread:e,user:o},"ignoring a bot off the allow list");return}a.warn({source:r,thread:e,user:o},"refusing an unauthorized request");try{await s.client.chat.postEphemeral({channel:n.channelId,user:o,thread_ts:n.threadTs,text:"You're not on this agent's allow list."})}catch(i){a.debug({err:i},"could not post the refusal")}return}const K=await x.remember(e);if(B&&await B({key:n,keyString:e,thread:K,text:u.text},s)){f||p.clear(e),a.info({source:r,thread:e,user:o},"ran a command");return}const b=p.admit(e,f);if(!b.run){const i={source:r,thread:e,user:o},g="holding off: too many bot turns in a row in this thread";if(b.say?a.warn(i,g):a.debug(i,g),b.say)try{await $(s.client,{channel:n.channelId,threadTs:n.threadTs},b.say)}catch(k){a.debug({err:k},"could not say why Bishop is holding off")}return}const _={text:u.text,userId:o,ts:t.ts??n.threadTs,isBot:f,mentionsAgent:j(t.text??"",d.botUserId),files:u.files},M=l.get(e);if(M){M.push(_),a.info({source:r,thread:e,user:o},"holding a message until the turn ends");const i=(y.get(e)??Promise.resolve()).then(()=>L(s.client,n.channelId,_.ts));y.set(e,i),await i;return}l.set(e,[]);try{let i=[_],g=!c;for(;i.length||l.get(e)?.length;){if(!i.length){i=await U(n,e,s);continue}a.info({thread:e,messages:i.length,newThread:g},"running a turn");try{await D({key:n,keyString:e,thread:K,channelId:n.channelId,threadTs:n.threadTs,messages:i,isNewThread:g,participants:T.count(e),isDirectMessage:t.channel_type==="im"||n.channelId.startsWith("D")},s)}catch(k){a.error({thread:e,err:k},"turn failed")}g=!1,i=await U(n,e,s)}}finally{l.delete(e),y.delete(e)}}async function E(t,s,r){try{await F(t,s,r)}catch(n){a.error({source:r,err:n},"failed to handle a Slack event")}}w.event("app_mention",async({event:t,body:s,client:r})=>{await E({...t,event_id:s.event_id},{client:r},"app_mention")}),w.event("message",async({event:t,body:s,client:r})=>{await E({...t,event_id:s.event_id},{client:r},"message")}),w.event("agent_session_stopped",async({event:t})=>{const s=t,r=H(s,d.teamId);if(!r)return;if(!I(s.user??"")){a.warn({user:s.user},"ignoring a stop from an unauthorized user");return}const n=S(r),e=N(n);a.info({thread:n,user:s.user,aborted:e},"stop requested")})}export{ee as registerHandlers};
|
package/dist/slack/interface.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import{log as
|
|
2
|
-
`)},Z=3e3;async function ee(t,s,i=Z){const l=await ae(t.botUserId,s,i),r=S(l)??t.username;if(!r)return;const h=[t.appName,l.realName].filter(w=>!!w?.trim()&&w?.toLowerCase()!==r.toLowerCase());return{address:`@${r}`,...h.length>0?{aka:[...new Set(h)]}:{}}}async function ae(t,s,i){const l={userId:t};let r;const h=new Promise(w=>{r=setTimeout(()=>w(l),i)});try{return await Promise.race([s.lookup(t).catch(()=>l),h])}finally{clearTimeout(r)}}async function ge(t){const{app:s,identity:i}=await C(t.credentials),l=await D(s.client,t.allow),r=J(s.client,{self:i.botUserId}),h=L(t.maxBotTurns),w=z(),
|
|
1
|
+
import{log as f}from"../log.js";import{createSlackApp as C}from"./app.js";import{createAuthorizer as D}from"./authorizer.js";import{botTurnCap as L}from"./bots.js";import{runCommand as O}from"./commands/run.js";import{commandLine as E}from"./commands/syntax.js";import{deliverToSlack as H,describeSlackHandle as F,parseSlackHandle as b,resolveSlackDestination as K,slackHandle as _}from"./destination.js";import{etiquetteNote as U,threadParticipants as z}from"./etiquette.js";import{describeSlackFile as R,parseSlackFile as W,slackAttachment as v,slackFilePayload as j}from"./files.js";import{registerHandlers as q}from"./handlers.js";import{landedMidThread as G,threadHistory as V}from"./history.js";import{linkMentions as I,mentionableName as S,renderMentions as T}from"./mentions.js";import{createPresence as Y}from"./presence.js";import{say as N}from"./say.js";import{threadKeyString as $}from"./thread.js";import{userDirectory as J}from"./users.js";import{threadVerbosity as Q}from"./verbosity.js";const X={name:"slack",guidance:["This conversation is a Slack thread. Each thing you write is posted as its own message while","you work, and the tools you call show as a card between them, so someone is watching the","whole turn. Say what you are doing as you go, and keep it to a line or two. Your last message","is the answer to what was asked. Slack renders standard markdown, tables included, so write","normally and keep it short.","People and other agents are named to you as @name. Write @name to mention one and Bishop","makes it a real Slack mention; a name it cannot match to one account stays plain text, so","use the name you were given. @here, @channel, and @everyone are never mentions, whoever","writes them.","A thread can hold a conversation between other people that has nothing to do with you.",'Bishop marks a message you may leave alone with a <thread> tag carrying reply="optional":',"other people are talking here and this one did not name you, so answer it only if you have","something worth adding. To stay out of it, say <no-output> and nothing else, and Bishop","posts nothing at all. Every message without that tag is yours to answer."].join(`
|
|
2
|
+
`)},Z=3e3;async function ee(t,s,i=Z){const l=await ae(t.botUserId,s,i),r=S(l)??t.username;if(!r)return;const h=[t.appName,l.realName].filter(w=>!!w?.trim()&&w?.toLowerCase()!==r.toLowerCase());return{address:`@${r}`,...h.length>0?{aka:[...new Set(h)]}:{}}}async function ae(t,s,i){const l={userId:t};let r;const h=new Promise(w=>{r=setTimeout(()=>w(l),i)});try{return await Promise.race([s.lookup(t).catch(()=>l),h])}finally{clearTimeout(r)}}async function ge(t){const{app:s,identity:i}=await C(t.credentials),l=await D(s.client,t.allow),r=J(s.client,{self:i.botUserId}),h=L(t.maxBotTurns),w=z(),g=Q(t.meta,t.verbosity),c=await ee(i,r),y={...X,...c?{self:c}:{}};c?f.info({address:c.address,aka:c.aka},"the agent's name in Slack"):f.warn("could not read the bot's own name; the agent will not know what it is called");const k=new Map,x=async({key:e,keyString:a,thread:o,text:p},m)=>{const n=E(p);if(!n)return!1;const d=O(n,{thread:o},{verbosity:g});try{await N(m.client,{channel:e.channelId,threadTs:e.threadTs},d),k.get(a)?.interrupted()}catch(u){f.warn({thread:a,err:u},"couldn't answer a command")}return!0};async function B(e){const a=await r.lookup(e.userId),o=S(a);return{id:e.userId,...o?{displayName:o}:{},...a.realName?{realName:a.realName}:{},...a.email?{email:a.email}:{},...e.isBot?{isBot:!0}:{}}}function M(e,a){const o=R(a);if(!o.unavailable)return t.attachments.remember(e,y.name,{id:o.id,name:o.name,payload:j(a)})}async function P(e,a){return a.files.length===0?[]:Promise.all(a.files.map(async o=>{const p=M(e.id,o),m=await t.files.save(e.dirKey,v(o,t.credentials.botToken));return p?{...m,ref:p}:m}))}function A(e,a){const o=T(e.messages[0]?.text??"",r),p=G(e),m=U({participants:e.participants,mentioned:e.messages.some(n=>n.mentionsAgent)});return{thread:e.thread,isNewThread:e.isNewThread,origin:y,...m?{note:m}:{},...p?{history:()=>V({client:a.client,key:e.key,before:p,allows:n=>l.allows(n),answersBots:h.answersBots,self:{botUserId:i.botUserId,...i.botId?{botId:i.botId}:{},...c?{botName:c.address}:{}},users:r,remember:n=>t.attachments.remember(e.thread.id,y.name,n),saw:(n,d)=>w.saw(e.keyString,n,d)})}:{},async messages(){return Promise.all(e.messages.map(async(n,d)=>{const u=await P(e.thread,n);return{principal:await B(n),text:d===0?await o:await T(n.text,r),...u.length>0?{files:u}:{}}}))},presence:()=>{const n=Y(e,a,{outgoing:d=>I(d,r),title:()=>o,verbosity:()=>g.of(e.thread.id).level,...m?{mayDecline:!0}:{},...t.taskIntervalMs===void 0?{}:{taskIntervalMs:t.taskIntervalMs}});return k.set(e.keyString,n),{handle:d=>n.handle(d),finish:async()=>{try{await n.finish()}finally{k.delete(e.keyString)}}}},async notify(n){await N(a.client,{channel:e.channelId,threadTs:e.threadTs},n)},async destination(){const n=_({channel:e.channelId,threadTs:e.threadTs});return{interface:"slack",handle:n,label:await F(s.client,n)}}}}return q(s,{identity:i,...c?{botName:c.address}:{},threads:t.threads,events:t.events,run:(e,a)=>t.run(A(e,a)),stop:t.stop,allows:e=>l.allows(e),bots:h,participants:w,command:x}),{name:"slack",async start(){await s.start(),i.scopes.includes("files:read")||f.warn("this Slack app has no files:read scope, so it cannot open files people share; reinstall it to grant one"),r.prime(),f.info("listening on Slack via Socket Mode")},async stop(){l.stop(),await s.stop()},allows:e=>l.allows(e),resolveDestination:(e,a)=>K(s.client,e,{...a?.optedInOnly?{allows:o=>l.allows(o)}:{}}),sameConversation:(e,a)=>b(e).channel===b(a).channel,deliver:async e=>{const a=await H(s.client,{...e,text:await I(e.text,r)});return a?{threadKey:$({teamId:i.teamId,channelId:a.channel,threadTs:a.threadTs})}:{}},attachment:e=>{const a=W(e);return a?v(a,t.credentials.botToken):void 0}}}export{X as SLACK_ORIGIN,ge as createSlackInterface,ee as slackIdentity};
|
package/dist/slack/verbosity.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{DEFAULT_VERBOSITY as l,VERBOSITY as f}from"../config.js";import{log as u}from"../log.js";const n="slack.verbosity";function a(e){const t=e.trim().toLowerCase();return f.includes(t)?t:void 0}function
|
|
1
|
+
import{DEFAULT_VERBOSITY as l,VERBOSITY as f}from"../config.js";import{log as u}from"../log.js";const n="slack.verbosity";function a(e){const t=e.trim().toLowerCase();return f.includes(t)?t:void 0}function d(e,t=l){const s=new Set;return{of(r){const o=e.get(r,n);if(o===void 0)return{level:t,fromThread:!1};const i=a(o);return i?{level:i,fromThread:!0}:(s.has(`${r} ${o}`)||(s.add(`${r} ${o}`),u.warn({thread:r,stored:o},"unknown verbosity on this thread; using the default")),{level:t,fromThread:!1})},set(r,o){e.set(r,n,o)},clear(r){e.clear(r,n)}}}function p(e){return e!=="low"}function m(e){return e==="high"}export{a as parseVerbosity,p as showsProgress,m as showsTasks,d as threadVerbosity};
|
package/dist/tools/files.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
import{z as a}from"zod";import{FETCH_FILE_TOOL as h,fileTag as f}from"../harness/principal.js";import{defineTool as s,ToolRefusal as o}from"./types.js";function c(i){return[s({name:h,description:["Downloads a file somebody shared in this conversation and gives you the path to it.","","Takes the ref from an <attachment> tag. Ask for a file when you need to open it, rather","than for everything in the conversation. Asking for one already downloaded hands back","the same path without fetching it again, so ask rather than reusing a path from earlier","in the conversation. It fails, with the reason, when there is no file to be had."].join(`
|
|
2
|
-
`),input:{ref:a.string().trim().min(1).describe("The ref attribute of the file tag naming the file you want")},async run(t,n){const e=await i.fetch(n.
|
|
2
|
+
`),input:{ref:a.string().trim().min(1).describe("The ref attribute of the file tag naming the file you want")},async run(t,n){const e=await i.fetch(n.thread,t.ref);if(!e)throw new o(`No file with the ref ${t.ref} was shared in this conversation. Refs come from the file tags Bishop writes, and one only works in the conversation it was written in.`);if(!e.path){const r=e.unavailable??"the download produced no file";throw new o(`Bishop could not fetch the file with ref ${t.ref}: ${r}.`+(e.url?` The original is at ${e.url}`:""))}return f({...e,ref:t.ref})}})]}export{c as fileTools};
|
package/dist/tools/server.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{randomBytes as v,randomUUID as S}from"node:crypto";import{createServer as E}from"node:http";import{McpServer as T}from"@modelcontextprotocol/sdk/server/mcp.js";import{StreamableHTTPServerTransport as b}from"@modelcontextprotocol/sdk/server/streamableHttp.js";import{isInitializeRequest as B}from"@modelcontextprotocol/sdk/types.js";import{log as
|
|
1
|
+
import{randomBytes as v,randomUUID as S}from"node:crypto";import{createServer as E}from"node:http";import{McpServer as T}from"@modelcontextprotocol/sdk/server/mcp.js";import{StreamableHTTPServerTransport as b}from"@modelcontextprotocol/sdk/server/streamableHttp.js";import{isInitializeRequest as B}from"@modelcontextprotocol/sdk/types.js";import{log as p}from"../log.js";import{ToolRefusal as g}from"./types.js";const O="bishop",y="/mcp",_="BISHOP_TOOL_TOKEN",x=1048576;function k(i){const r=i.headers.authorization;return r&&/^Bearer\s+(.+)$/i.exec(r.trim())?.[1]?.trim()||void 0}function u(i,r,s){i.writeHead(r,{"content-type":"application/json"}),i.end(JSON.stringify({jsonrpc:"2.0",id:null,error:{code:-32001,message:s}}))}async function R(i){const r=[];let s=0;for await(const n of i){if(s+=n.length,s>x)throw new Error("request body is too large");r.push(n)}if(r.length!==0)return JSON.parse(Buffer.concat(r).toString("utf8"))}const A="Bishop's tools answer to a person, and this message came from another bot. Ask someone in the conversation to do it.";function N(i,r){const s=new T({name:O,version:"1"});for(const n of i)s.registerTool(n.name,{description:n.description,inputSchema:n.input},async m=>{try{if(r.principal.isBot)throw new g(A);return{content:[{type:"text",text:await n.run(m,r)}]}}catch(a){const d=a instanceof Error?a.message:String(a);return a instanceof g?p.info({tool:n.name,thread:r.thread.threadKey,reason:d},"a tool refused"):p.error({tool:n.name,thread:r.thread.threadKey,err:a},"a tool failed"),{content:[{type:"text",text:d}],isError:!0}}});return s}async function L(i){const r=i.host??"127.0.0.1",s=new Map,n=new Map;async function m(e,t){const o=new b({sessionIdGenerator:()=>S(),onsessioninitialized:c=>{if(s.get(e)!==t){o.close().catch(()=>{});return}n.set(c,{token:e,transport:o}),t.sessions.add(c)}});return o.onclose=()=>{const c=o.sessionId;c&&(n.delete(c),t.sessions.delete(c))},await N(i.tools,t.context).connect(o),o}const a=E((e,t)=>{(async()=>{try{if(!e.url?.startsWith(y))return u(t,404,"no such endpoint");const o=k(e),c=o?s.get(o):void 0;if(!c||!o)return u(t,401,"this call carries no valid turn credential");const l=e.method==="POST"?await R(e):void 0,f=e.headers["mcp-session-id"],h=typeof f=="string"?n.get(f):void 0;if(h){if(h.token!==o)return u(t,404,"no such session");await h.transport.handleRequest(e,t,l);return}if(typeof f=="string")return u(t,404,"no such session");if(!B(l))return u(t,400,"the first call on a connection must be initialize");await(await m(o,c)).handleRequest(e,t,l)}catch(o){p.error({err:o},"a tool call could not be handled"),t.headersSent?t.end():u(t,400,"that request could not be handled")}})()});await new Promise((e,t)=>{a.once("error",t),a.listen(0,r,()=>{a.removeListener("error",t),e()})});const d=a.address();if(!d||typeof d=="string")throw new Error("the tool endpoint started without a port");const w=`http://${r}:${d.port}${y}`;return p.info({url:w,tools:i.tools.map(e=>e.name)},"serving the agent its own tools"),{url:w,toolNames:i.tools.map(e=>e.name),grant(e){const t=v(32).toString("base64url"),o={context:e,sessions:new Set};return s.set(t,o),{access:{url:w,tokenEnvVar:_,token:t},revoke:()=>{if(s.delete(t)){for(const l of o.sessions){const f=n.get(l);n.delete(l),f?.transport.close().catch(h=>{p.debug({err:h},"a tool connection did not close cleanly")})}o.sessions.clear()}}}},async stop(){for(const{transport:e}of n.values())await e.close().catch(()=>{});n.clear(),s.clear(),a.closeAllConnections(),await new Promise(e=>a.close(()=>e()))}}}export{y as PATH,O as SERVER_NAME,_ as TOKEN_ENV_VAR,L as startToolServer};
|
package/dist/turn.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{log as
|
|
1
|
+
import{log as o}from"./log.js";import{worktreeName as b}from"./worktrees.js";function v(i){const s=new Map,l=new Set;async function y(e){const r=i.sharedDirNotice;if(!(!r||l.has(e.thread.threadKey))){l.add(e.thread.threadKey);try{await e.notify(r)}catch(n){o.debug({thread:e.thread.threadKey,err:n},"could not deliver the shared-directory notice")}}}function w(e,r){if(!(!i.tools||e.tools===!1))return i.tools.grant({thread:e.thread,principal:r,origin:e.origin,destination:()=>e.destination()})}async function m(e){await y(e);const r=new AbortController,n=e.presence();let d;try{s.set(e.thread.threadKey,r);const a=e.freshSession?void 0:await i.sessions.get(e.thread.id),h=await i.cwd(e.thread),[p,f]=await Promise.all([e.messages(),a?void 0:e.history?.().catch(t=>{o.warn({thread:e.thread.threadKey,err:t},"could not read the history")})]),[c,...g]=p;if(!c)throw new Error("a turn with no messages");const K=g.at(-1)?.principal??c.principal;if(r.signal.aborted){o.info({thread:e.thread.threadKey},"turn stopped before the agent started");return}d=w(e,K);for await(const t of i.harness.runTurn({messages:[c,...g],cwd:h,threadKey:e.thread.threadKey,worktree:b(e.thread.dirKey),...a?{sessionId:a}:{},...e.note?{note:e.note}:{},...f?.length?{history:f}:{},origin:e.origin,...d?{tools:d.access}:{},signal:r.signal}))t.type==="session"&&t.sessionId!==a&&await i.sessions.set(e.thread.id,t.sessionId),t.type==="text"&&o.debug({thread:e.thread.threadKey,chars:t.text.length},"agent said something"),t.type==="tool"&&o.debug({thread:e.thread.threadKey,tool:t.name,detail:t.summary??t.detail},"tool activity"),t.type==="done"&&o.info({thread:e.thread.threadKey,...t.usage},"turn complete"),t.type==="error"&&o.error({thread:e.thread.threadKey,kind:t.kind,err:t.message},"turn errored"),await n.handle(t)}catch(a){const h=a instanceof Error?a.message:String(a);throw o.error({thread:e.thread.threadKey,err:a},"turn failed before the agent answered"),await n.handle({type:"error",kind:"turn_failed",message:h}),a}finally{d?.revoke(),s.get(e.thread.threadKey)===r&&s.delete(e.thread.threadKey),await n.finish()}}return{run:m,stop(e){const r=s.get(e);return r?(r.abort(),!0):!1}}}export{v as makeTurnRunner};
|
package/dist/worktrees.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import{createHash as w}from"node:crypto";import{existsSync as u,mkdirSync as
|
|
2
|
-
and, retrying with the existing branch: ${o}`}function f(t,r){if(!r)return t;const e=s(t,r);return
|
|
1
|
+
import{createHash as w}from"node:crypto";import{existsSync as u,mkdirSync as h}from"node:fs";import{join as p,resolve as s}from"node:path";import{BishopError as d}from"./errors.js";import{exec as n}from"./exec.js";const l=64,c="bishop-",m=".claude/worktrees";function b(t){const r=t.replaceAll(/[^a-zA-Z0-9]+/g,"-").replaceAll(/-+/g,"-").replaceAll(/^-+|-+$/g,""),e=`${c}${r}`;if(e.length<=l)return e;const o=w("sha256").update(t).digest("hex").slice(0,8),i=l-c.length-o.length-1;return`${c}${r.slice(0,i)}-${o}`}function g(t,r){return s(t,m,r)}function I(t){return`\`${t}\` isn't a git repository, so I'm working in it directly. Another thread changing the same files at the same time can conflict.`}function N(t){return!!(t?.enabled&&t.isRepo)}function x(t){return u(p(t,".git"))}async function y(t){const r=await n("git",["rev-parse","--show-prefix"],{cwd:t});return r.code===0?r.stdout.trim():""}async function M(t,r){const e=g(t,r),o=await y(t);if(x(e))return f(e,o);if(u(e))throw new d(`${e} exists but is not a git worktree, which usually means an earlier attempt failed partway. Remove the directory and this thread will get a fresh one.`);const i=await n("git",["worktree","add","-b",r,e,"HEAD"],{cwd:t});if(i.code!==0){await n("git",["worktree","prune"],{cwd:t});const a=await n("git",["worktree","add",e,r],{cwd:t});if(a.code!==0)throw new d(`Couldn't create a git worktree for this thread at ${e}: ${k(i,a)}`)}return f(e,o)}function k(t,r){const e=t.stderr.trim(),o=r.stderr.trim();return!o||o===e?e:`${e}
|
|
2
|
+
and, retrying with the existing branch: ${o}`}function f(t,r){if(!r)return t;const e=s(t,r);return h(e,{recursive:!0}),e}async function P(t){const r=await n("git",["rev-parse","--git-common-dir"],{cwd:t}),e=r.stdout.trim();if(!(r.code!==0||!e))return s(t,e)}async function W(t){return(await n("git",["rev-parse","--show-toplevel"],{cwd:t})).code===0}async function _(t){const r=await n("git",["config","--get","user.email"],{cwd:t}),e=await n("git",["config","--get","user.name"],{cwd:t});return r.code===0&&r.stdout.trim()!==""&&e.code===0&&e.stdout.trim()!==""}export{m as WORKTREE_DIR,M as ensureWorktree,P as gitCommonDir,_ as hasGitIdentity,W as isGitRepo,N as isolates,I as sharedDirNotice,b as worktreeName,g as worktreePath};
|