@agentdeploymentco/bishop 0.3.2 → 0.3.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -373,16 +373,17 @@ Install the app in Teams and message it directly, or @-mention it in a channel.
373
373
 
374
374
  Teams behaves much the way Slack does:
375
375
 
376
- - **A turn shows itself while it runs.** What the agent writes lands as it writes it, and a run of tool calls shares one message that is rewritten as the work moves and replaced by a count of them when the agent speaks again. A turn ends with a line saying what it cost, edited onto the last message. Set `teams.verbosity` in `.bishop/config.json` to `medium` to drop the tool calls, or `low` to leave the answer alone with no closing line, which is how Teams behaved before. There is still no typing indicator: Teams hides it while a stream is open and outside one it lapses in seconds.
376
+ - **A turn shows itself while it runs.** What the agent writes lands as it writes it, and a run of tool calls shares one message that is rewritten as the work moves and replaced by a count of them when the agent speaks again. A turn ends with a line saying what it cost, edited onto the last message. Send `` `! verbosity medium` `` to drop the tool calls in one conversation, or `` `! verbosity low` `` to leave the answer alone with no closing line, which is how Teams behaved before. `teams.verbosity` in `.bishop/config.json` sets where every conversation starts. There is still no typing indicator: Teams hides it while a stream is open and outside one it lapses in seconds.
377
+ - **Commands work as they do in Slack.** Type the command in backticks, like `` `! model opus` ``, and Teams formats it as code. The whole message has to be the code: a command inside a sentence goes to the agent.
377
378
  - **Tables do not render.** Teams supports a narrower markdown than Slack. Bold, italics, links, code and lists work, and the agent is told to write a list where it would otherwise reach for a table.
378
379
  - **@-mention it once per thread, not once per message.** Teams delivers a channel or group-chat message to an app only when the app is @-mentioned. `bishop teams setup` asks for two resource-specific consent permissions that lift that, so once the agent has answered in a thread it hears the replies without being tagged again. Without them the agent still works and still has to be tagged every time.
379
380
  - **Changing those permissions means removing the app and installing it again.** Teams asks for them when an app is installed and at no other time. Updating an app that is already in a team or a chat does not ask, however the manifest changed, so it goes on hearing nothing but mentions until somebody removes it from every team and chat it is in and adds it back. This catches out anyone upgrading an agent that was already running in Teams.
380
381
  - **It can stay out of a conversation.** In a thread with several people talking, a message that did not name the agent is one it may leave alone, and it says nothing at all rather than answering. A message that names it is always answered, as is anything in a one-to-one chat.
381
- - **There is no catch-up.** The permissions change what arrives from now on, not what was said before the agent was pulled in.
382
+ - **It catches up when it is pulled in partway.** @-mention it halfway down a channel thread or in a group chat that has been going a while, and its first answer there has read what was said before, the way it does in Slack. Only messages from people on the allow list are quoted to it; anyone else is counted and not quoted. Reading them back goes through Microsoft Graph on the same two permissions, so an app installed without them answers from the mention alone.
382
383
  - **Files come through in a direct chat, and images anywhere.** Attach a file in a chat with the agent and it gets the path, the same as in Slack; an image pasted into a message reaches it in any scope. Teams shows no attach button at all until the app's manifest asks for files, so `bishop teams setup` turns that on when it creates the app, and reports it when re-run against an app that predates this.
383
384
  - **A file attached in a channel needs one more grant.** Teams keeps that file off the message it delivers, so Bishop reads it back through Microsoft Graph, which takes `Files.Read.All` on the app registration. `bishop teams setup` makes that grant with `az` when it can, and prints the commands for an administrator when it cannot. Re-run it and it says whether the grant is there, which it reads from the app's own token rather than from `az`. No Azure subscription is involved and the bot stays Teams-managed. Without the grant the agent is still told a file was attached and says it could not read it, rather than answering as though the message were empty.
384
385
  - **A file attached in a group chat may still not come through.** The channel path covers it if Teams describes it the same way, which is untested. A direct chat and a channel are the two that are known to work.
385
- - **There is no stop button.**
386
+ - **Stop a turn with a command.** Teams has no stop button for Bishop to draw, so send `` `! stop` `` in the conversation instead. It stops the turn running there, which closes with a line saying how long it ran, and anything you sent while it was running is answered next.
386
387
 
387
388
  The allow list takes user principal names (`hayes@example.com`), `@example.com` for everyone at a domain, or an Entra object ID:
388
389
 
@@ -421,7 +422,7 @@ Each thread is its own agent session with its own history. Two threads are two i
421
422
 
422
423
  **It can talk to another agent, in a thread you started.** Pull two agents into the same thread and they can address each other, and the one you are talking to is told which messages came from another agent rather than a person. Neither can start the exchange: a bot's message is answered only in a thread that already has a session, so a bot can't DM the agent or open a thread with it. Apps that post under a name rather than an account, like an incoming webhook, are never answered at all. And an exchange between them is capped at three turns in a row by default, after which Bishop says it's holding off and waits for someone to say something. A turn another agent drew can't use Bishop's own tools either, so it can't leave a schedule behind. Set `slack.maxBotTurns` to change the cap, or to `0` to have the agent never answer another bot.
423
424
 
424
- In a DM, Slack shows a stop button for as long as the turn runs. Pressing it cancels the agent mid-turn.
425
+ In a DM, Slack shows a stop button for as long as the turn runs. Pressing it cancels the agent mid-turn. `` `! stop` `` does the same in any thread, and anything sent while the turn was running is answered next.
425
426
 
426
427
  Slack caps messages near 4000 characters, so Bishop splits longer answers. Slack also doesn't render tables and handles nested lists badly, so agent output written for a terminal will sometimes look wrong in a thread.
427
428
 
@@ -511,7 +512,7 @@ Every block is optional, and an interface runs when its credentials are set rath
511
512
 
512
513
  `slack` takes `allow`, `maxBotTurns`, and `verbosity`, plus the `workspace` and `app` fields setup writes.
513
514
 
514
- `teams` takes `allow`, `verbosity`, `port`, `host`, and `tls`, plus the `app` fields setup writes. Its `verbosity` is Slack's three levels and works almost the same way, except that `low` also drops the line saying what a turn cost. It is also the only place the level is set: Teams has no command messages, so there is no way to turn one conversation down without turning them all down.
515
+ `teams` takes `allow`, `verbosity`, `port`, `host`, and `tls`, plus the `app` fields setup writes. Its `verbosity` is Slack's three levels and works the same way, including `! verbosity` for one conversation, except that `low` also drops the line saying what a turn cost.
515
516
 
516
517
  `agent` takes `harness`, `model`, `effort`, `worktrees`, `options`, and `harnesses`. The last two are alternatives: `options` for a config that runs one harness, `harnesses` for one that switches.
517
518
 
package/dist/cli.js CHANGED
@@ -1,20 +1,20 @@
1
1
  #!/usr/bin/env node
2
- import{stat as ie}from"node:fs/promises";import{join as ce,resolve as de}from"node:path";import{Command as pe}from"commander";import{agentSnapshots as me,fixedAgentDir as he}from"./agent-dir.js";import{looksRemote as K,parseRepoSpec as F}from"./agent-source.js";import{everyStore as z,storePath as q}from"./agent-store.js";import{attachmentStore as le}from"./attachments.js";import{HARNESSES as ue,loadConfig as O,nonEmptyFlag as j,parseHarnessName as fe,positiveIntFlag as we,resolveAgent as ge}from"./config.js";import{dbPath as Y,openDb as W}from"./db.js";import{describeMissing as ye,loadEnv as S,readCredentials as ke}from"./env.js";import{BishopError as v}from"./errors.js";import{fileStore as Se}from"./files.js";import{DEFAULT_MAX_AGE_DAYS as ve,makeCollector as be,sweep as Te}from"./gc.js";import{ghAuthLogin as Ie,ghCliIn as Ee,gitAuthenticatesToGithub as Ae,withTemporaryGhConfig as _e}from"./github/gh.js";import{AGENT_SCOPES as Re,checkGithubSetup as Ne}from"./github/setup.js";import{createGmail as Oe}from"./gmail/create.js";import{checkGmailSetup as Ge}from"./gmail/setup.js";import{createHarness as $e}from"./harness/create.js";import{sharedFileFetcher as xe}from"./interface/attachments.js";import{principalCheck as J}from"./interface/authorize.js";import{deliveryRouter as Le}from"./interface/delivery.js";import{conversationMatch as Ce,destinationResolver as He}from"./interface/destination.js";import{log as p,requestedLevel as Me}from"./log.js";import{addSchedule as Pe,listSchedules as De,removeSchedule as Be,setScheduleEnabled as Q}from"./schedule/cli.js";import{authorizedToRun as Ue,makeScheduler as Ke}from"./schedule/runner.js";import{scheduleStore as Fe}from"./schedule/store.js";import{threadQueue as ze}from"./sessions/queue.js";import{interfaceState as qe,retentionStore as V,sessionStore as je,threadHandover as Ye,threadMeta as We}from"./sessions/store.js";import{ensureGitignore as Je,openBrowser as Qe,resolveAppDetails as Ve,SetupError as X,setupSlack as Xe,setupSlackManually as Ze,withPrompt as Z,writeEnv as ee}from"./setup.js";import{createSlackInterface as et}from"./slack/interface.js";import{buildManifest as tt}from"./slack/manifest.js";import{createTeams as ot}from"./teams/create.js";import{administratorInstructions as nt,repointTeamsEndpoint as st,setupTeams as rt,setupTeamsManually as at}from"./teams/setup.js";import{threadModel as it}from"./thread-model.js";import{fileTools as ct}from"./tools/files.js";import{scheduleTools as dt}from"./tools/schedules.js";import{startToolServer as pt}from"./tools/server.js";import{makeTurnRunner as mt}from"./turn.js";import{version as ht}from"./version.js";import{hasGitIdentity as lt,isGitRepo as ut,sharedDirNotice as ft,WORKTREE_DIR as wt}from"./worktrees.js";function u(e){process.stderr.write(`${e}
2
+ import{stat as ie}from"node:fs/promises";import{join as ce,resolve as de}from"node:path";import{Command as pe}from"commander";import{agentSnapshots as me,fixedAgentDir as he}from"./agent-dir.js";import{looksRemote as F,parseRepoSpec as z}from"./agent-source.js";import{everyStore as q,storePath as j}from"./agent-store.js";import{attachmentStore as le}from"./attachments.js";import{HARNESSES as ue,loadConfig as $,nonEmptyFlag as Y,parseHarnessName as fe,positiveIntFlag as we,resolveAgent as ge}from"./config.js";import{dbPath as W,openDb as J}from"./db.js";import{describeMissing as ye,loadEnv as S,readCredentials as ke}from"./env.js";import{BishopError as v}from"./errors.js";import{fileStore as Se}from"./files.js";import{DEFAULT_MAX_AGE_DAYS as ve,makeCollector as be,sweep as Te}from"./gc.js";import{ghAuthLogin as Ie,ghCliIn as Ee,gitAuthenticatesToGithub as Ae,withTemporaryGhConfig as _e}from"./github/gh.js";import{AGENT_SCOPES as Re,checkGithubSetup as Ne}from"./github/setup.js";import{createGmail as Oe}from"./gmail/create.js";import{checkGmailSetup as Ge}from"./gmail/setup.js";import{createHarness as $e}from"./harness/create.js";import{sharedFileFetcher as xe}from"./interface/attachments.js";import{principalCheck as Q}from"./interface/authorize.js";import{deliveryRouter as Le}from"./interface/delivery.js";import{conversationMatch as Ce,destinationResolver as He}from"./interface/destination.js";import{log as p,requestedLevel as Me}from"./log.js";import{addSchedule as Pe,listSchedules as De,removeSchedule as Be,setScheduleEnabled as V}from"./schedule/cli.js";import{authorizedToRun as Ue,makeScheduler as Ke}from"./schedule/runner.js";import{scheduleStore as Fe}from"./schedule/store.js";import{threadQueue as ze}from"./sessions/queue.js";import{interfaceState as qe,retentionStore as X,sessionStore as je,threadHandover as Ye,threadMeta as We}from"./sessions/store.js";import{ensureGitignore as Je,openBrowser as Qe,resolveAppDetails as Ve,SetupError as Z,setupSlack as Xe,setupSlackManually as Ze,withPrompt as ee,writeEnv as te}from"./setup.js";import{createSlackInterface as et}from"./slack/interface.js";import{buildManifest as tt}from"./slack/manifest.js";import{createTeams as ot}from"./teams/create.js";import{administratorInstructions as nt,repointTeamsEndpoint as st,setupTeams as rt,setupTeamsManually as at}from"./teams/setup.js";import{threadModel as it}from"./thread-model.js";import{fileTools as ct}from"./tools/files.js";import{scheduleTools as dt}from"./tools/schedules.js";import{startToolServer as pt}from"./tools/server.js";import{makeTurnRunner as mt}from"./turn.js";import{version as ht}from"./version.js";import{hasGitIdentity as lt,isGitRepo as ut,sharedDirNotice as ft,WORKTREE_DIR as wt}from"./worktrees.js";function u(e){process.stderr.write(`${e}
3
3
  `),process.exit(1)}async function gt(e){const t=de(e??".");let n;try{n=(await ie(t)).isDirectory()}catch{throw new v(`--agent directory doesn't exist: ${t}`)}if(!n)throw new v(`--agent is not a directory: ${t}`);return t}async function yt(e){const t=await ut(e),n={enabled:!0,isRepo:t};return t?(p.info({agentDir:e,worktrees:ce(e,wt)},"worktree mode is on"),await lt(e)||p.warn({agentDir:e},"git has no user.name or user.email here, so the agent cannot commit what it does in a worktree"),n):(p.warn({agentDir:e},"worktree mode is on but the agent directory is not a git repository; threads share it"),n)}function kt(e){if(e.kind==="options-replaced"){p.warn({harness:e.harness},`agent.harnesses.${e.harness} replaces agent.options, which is ignored for this run`);return}p.warn({harness:e.harness,configured:e.configured},`--harness overrides the configured harness, but agent.options is still written for that one; move it to agent.harnesses.${e.configured} and give ${e.harness} its own block`)}async function St(e=[],t={}){e.length>0&&u(`bishop: unknown command "${e[0]}"
4
4
 
5
- Run \`bishop --help\` to see what's available.`);const n=process.cwd(),i=t.agent&&K(t.agent)?F(t.agent):void 0;t.autoUpdate&&!i&&u("--auto-update needs --agent to be a git URL. Bishop never updates a directory it was pointed at; that state is yours to manage."),S(n);const s=await O(n),a=ke();a.ok||u(ye(a));const r=ge(s,t);for(const o of r.warnings)kt(o);const m=W(Y(n)),c=je(m,r.harness);let d;const l=async o=>await d?.check(o)??{ok:!0},f=i?me({spec:i,store:q(n,i.slug),dirs:c,autoUpdate:!!t.autoUpdate,recheck:l}):void 0,w=f?await f.start():await gt(t.agent),oe=f?f.forTurn:he(w,c);p.info({bishopDir:n,agentDir:w,...w===n?{}:{separate:!0},...i?{source:i.cloneUrl,autoUpdate:!!t.autoUpdate}:{},app:s.slack?.app?.name},"bishop starting");const A=s.agent?.worktrees?await yt(w):void 0,b=$e(r.harness,{...r.options?{options:r.options}:{},...r.model?{model:r.model}:{},...r.effort?{effort:r.effort}:{},...A?{worktrees:A}:{},credentials:a.credentials}),_=await b.check(w);_.ok||u(b.describeStartupFailure(w,_.error));const N=_.model??r.model;p.info({harness:b.name,credentials:_.credentialSource??"unreported",model:N,...r.effort?{effort:r.effort}:{},...r.options?{agentOptions:Object.keys(r.options)}:{}},"harness ready"),d=b,p.info({sessions:c.count()},"opened session store");const $=be({retention:V(m),stores:()=>z(n),home:n,...s.gc?.maxAgeDays===void 0?{}:{maxAgeDays:s.gc.maxAgeDays}});$.start();let y;const ne=o=>y?J(y)(o):Ue(o),se=async(o,h)=>{if(!y)throw new v("no interface is running, so there is nowhere to send");return He(y,{optedInOnly:!0})(o,h)},re=(o,h)=>y?Ce(y)(o,h):!1,x=Se({home:n,...s.files?.maxMb===void 0?{}:{maxMb:s.files.maxMb}}),L=le(m),C=Fe(m),H=await pt({tools:[...dt({schedules:C,authorized:ne,resolve:se,sameConversation:re}),...ct({fetch:xe({interfaces:()=>y??[],store:L,files:x})})]}),M=We(m),P=it(M),ae={...N?{model:N}:{},...r.effort?{effort:r.effort}:{}},D=mt({harness:b,cwd:oe,sessions:c,model:P,tools:H,...A&&!A.isRepo?{sharedDirNotice:ft(w)}:{}}),B=ze(),T={threads:c,events:c,files:x,attachments:L,run:o=>B.run(o.thread.threadKey,()=>D.run(o)),stop:o=>D.stop(o)},g=[];if(a.credentials.slack&&g.push(await et({...T,credentials:a.credentials.slack,meta:M,model:P,configuredModel:ae,...s.slack?.verbosity?{verbosity:s.slack.verbosity}:{},...s.slack?.allow?{allow:s.slack.allow}:{},...s.slack?.maxBotTurns===void 0?{}:{maxBotTurns:s.slack.maxBotTurns}})),a.credentials.gmail)try{g.push(await Oe({...T,credentials:a.credentials.gmail,...s.gmail?{config:s.gmail}:{},state:qe(m)}))}catch(o){o instanceof v&&u(o.message);const h=a.credentials.gmail,R=h.kind==="oauth"?"the authorized Gmail account":`${h.user}`;u(`Couldn't reach the mailbox for ${R}:
5
+ Run \`bishop --help\` to see what's available.`);const n=process.cwd(),i=t.agent&&F(t.agent)?z(t.agent):void 0;t.autoUpdate&&!i&&u("--auto-update needs --agent to be a git URL. Bishop never updates a directory it was pointed at; that state is yours to manage."),S(n);const s=await $(n),a=ke();a.ok||u(ye(a));const r=ge(s,t);for(const o of r.warnings)kt(o);const m=J(W(n)),c=je(m,r.harness);let d;const l=async o=>await d?.check(o)??{ok:!0},f=i?me({spec:i,store:j(n,i.slug),dirs:c,autoUpdate:!!t.autoUpdate,recheck:l}):void 0,w=f?await f.start():await gt(t.agent),ne=f?f.forTurn:he(w,c);p.info({bishopDir:n,agentDir:w,...w===n?{}:{separate:!0},...i?{source:i.cloneUrl,autoUpdate:!!t.autoUpdate}:{},app:s.slack?.app?.name},"bishop starting");const A=s.agent?.worktrees?await yt(w):void 0,b=$e(r.harness,{...r.options?{options:r.options}:{},...r.model?{model:r.model}:{},...r.effort?{effort:r.effort}:{},...A?{worktrees:A}:{},credentials:a.credentials}),_=await b.check(w);_.ok||u(b.describeStartupFailure(w,_.error));const N=_.model??r.model;p.info({harness:b.name,credentials:_.credentialSource??"unreported",model:N,...r.effort?{effort:r.effort}:{},...r.options?{agentOptions:Object.keys(r.options)}:{}},"harness ready"),d=b,p.info({sessions:c.count()},"opened session store");const L=be({retention:X(m),stores:()=>q(n),home:n,...s.gc?.maxAgeDays===void 0?{}:{maxAgeDays:s.gc.maxAgeDays}});L.start();let y;const se=o=>y?Q(y)(o):Ue(o),re=async(o,h)=>{if(!y)throw new v("no interface is running, so there is nowhere to send");return He(y,{optedInOnly:!0})(o,h)},ae=(o,h)=>y?Ce(y)(o,h):!1,C=Se({home:n,...s.files?.maxMb===void 0?{}:{maxMb:s.files.maxMb}}),H=le(m),M=Fe(m),P=await pt({tools:[...dt({schedules:M,authorized:se,resolve:re,sameConversation:ae}),...ct({fetch:xe({interfaces:()=>y??[],store:H,files:C})})]}),O=We(m),G=it(O),D={...N?{model:N}:{},...r.effort?{effort:r.effort}:{}},B=mt({harness:b,cwd:ne,sessions:c,model:G,tools:P,...A&&!A.isRepo?{sharedDirNotice:ft(w)}:{}}),U=ze(),T={threads:c,events:c,files:C,attachments:H,run:o=>U.run(o.thread.threadKey,()=>B.run(o)),stop:o=>B.stop(o)},g=[];if(a.credentials.slack&&g.push(await et({...T,credentials:a.credentials.slack,meta:O,model:G,configuredModel:D,...s.slack?.verbosity?{verbosity:s.slack.verbosity}:{},...s.slack?.allow?{allow:s.slack.allow}:{},...s.slack?.maxBotTurns===void 0?{}:{maxBotTurns:s.slack.maxBotTurns}})),a.credentials.gmail)try{g.push(await Oe({...T,credentials:a.credentials.gmail,...s.gmail?{config:s.gmail}:{},state:qe(m)}))}catch(o){o instanceof v&&u(o.message);const h=a.credentials.gmail,R=h.kind==="oauth"?"the authorized Gmail account":`${h.user}`;u(`Couldn't reach the mailbox for ${R}:
6
6
  ${o instanceof Error?o.message:String(o)}
7
7
 
8
- Run \`bishop gmail setup\` to check the credentials.`)}if(a.credentials.teams)try{g.push(await ot({...T,credentials:a.credentials.teams,...s.teams?{config:s.teams}:{}}))}catch(o){o instanceof v&&u(o.message),u(`Couldn't start the Teams app for client ${a.credentials.teams.clientId}:
8
+ Run \`bishop gmail setup\` to check the credentials.`)}if(a.credentials.teams)try{g.push(await ot({...T,credentials:a.credentials.teams,meta:O,model:G,configuredModel:D,...s.teams?{config:s.teams}:{}}))}catch(o){o instanceof v&&u(o.message),u(`Couldn't start the Teams app for client ${a.credentials.teams.clientId}:
9
9
  ${o instanceof Error?o.message:String(o)}
10
10
 
11
- Check the credentials in .env and the \`teams\` block in .bishop/config.json.`)}y=g;for(const o of g)await o.start();p.info({interfaces:g.map(o=>o.name)},"bishop is listening");const U=Ke({schedules:C,run:T.run,threads:c,cancel:T.stop,deliver:Le(g),authorized:J(g),attach:Ye(c)});U.start();for(const o of["SIGINT","SIGTERM"])process.once(o,()=>{p.info({signal:o,inFlight:B.size},"shutting down"),U.stop().catch(h=>p.error({err:h},"the scheduler failed to stop cleanly")).then(()=>Promise.allSettled(g.map(h=>h.stop()))).then(async h=>{const R=h.filter(I=>I.status==="rejected");for(const I of R)p.error({err:I.reason},"an interface failed to stop cleanly");await H.stop().catch(I=>{p.error({err:I},"the tool endpoint failed to stop cleanly")}),await $.stop(),m.close(),process.exit(R.length>0?1:0)})})}function vt(e){for(const t of e.snapshots)process.stdout.write(`removed snapshot ${t.commit} (unused for ${t.ageDays} days)
11
+ Check the credentials in .env and the \`teams\` block in .bishop/config.json.`)}y=g;for(const o of g)await o.start();p.info({interfaces:g.map(o=>o.name)},"bishop is listening");const K=Ke({schedules:M,run:T.run,threads:c,cancel:T.stop,deliver:Le(g),authorized:Q(g),attach:Ye(c)});K.start();for(const o of["SIGINT","SIGTERM"])process.once(o,()=>{p.info({signal:o,inFlight:U.size},"shutting down"),K.stop().catch(h=>p.error({err:h},"the scheduler failed to stop cleanly")).then(()=>Promise.allSettled(g.map(h=>h.stop()))).then(async h=>{const R=h.filter(I=>I.status==="rejected");for(const I of R)p.error({err:I.reason},"an interface failed to stop cleanly");await P.stop().catch(I=>{p.error({err:I},"the tool endpoint failed to stop cleanly")}),await L.stop(),m.close(),process.exit(R.length>0?1:0)})})}function vt(e){for(const t of e.snapshots)process.stdout.write(`removed snapshot ${t.commit} (unused for ${t.ageDays} days)
12
12
  `);for(const t of e.worktrees){const n=t.branch?`, branch ${t.branch}`:"";process.stdout.write(`released ${t.path}${n}
13
13
  `)}process.stdout.write(`snapshots: removed ${e.snapshots.length}, kept ${e.keptSnapshots}
14
14
  worktrees: released ${e.worktrees.length}
15
15
  files: removed ${e.files} download directories
16
16
  rows: forgot ${e.threads} threads, ${e.events} events
17
- `)}async function bt(e){const t=process.cwd();S(t),e.agent&&!K(e.agent)&&u("gc --agent takes the git URL Bishop cloned, not a path. It narrows the pass to the files Bishop owns for one agent, and it owns none for a directory agent. Run `bishop gc` with no --agent to collect what its threads left behind.");const n=e.agent?[q(t,F(e.agent).slug)]:await z(t),i=await O(t),s=e.maxAgeDays??i.gc?.maxAgeDays;Me()||(p.level="error");const a=W(Y(t));try{vt(await Te({retention:V(a),stores:n,home:t,...e.agent?{only:n}:{},...s===void 0?{}:{maxAgeDays:s}}))}finally{a.close()}}function Tt(){return!(process.env.SSH_CONNECTION||process.env.SSH_TTY||process.env.SSH_CLIENT||process.platform==="linux"&&!process.env.DISPLAY&&!process.env.WAYLAND_DISPLAY)}async function It(e={}){const t=process.cwd();S(t);const n=process.env.BISHOP_GMAIL_CLIENT_ID?.trim(),i=process.env.BISHOP_GMAIL_CLIENT_SECRET?.trim(),s=process.env.BISHOP_GMAIL_REFRESH_TOKEN?.trim(),a=process.env.BISHOP_GMAIL_USER?.trim(),r=process.env.BISHOP_GMAIL_SERVICE_ACCOUNT?.trim();n&&!i&&u("BISHOP_GMAIL_CLIENT_ID is set but BISHOP_GMAIL_CLIENT_SECRET is not.");const m=e.browser===!1||!Tt()?"paste":"browser",c=await Z(!0,async d=>Ge({...n&&i?{client:{clientId:n,clientSecret:i}}:{},...s?{refreshToken:s}:{},...a?{user:a}:{},...r?{serviceAccount:r}:{},mode:m,openBrowser:Qe,readPaste:async l=>(process.stdout.write(`Open this in a browser and approve it:
17
+ `)}async function bt(e){const t=process.cwd();S(t),e.agent&&!F(e.agent)&&u("gc --agent takes the git URL Bishop cloned, not a path. It narrows the pass to the files Bishop owns for one agent, and it owns none for a directory agent. Run `bishop gc` with no --agent to collect what its threads left behind.");const n=e.agent?[j(t,z(e.agent).slug)]:await q(t),i=await $(t),s=e.maxAgeDays??i.gc?.maxAgeDays;Me()||(p.level="error");const a=J(W(t));try{vt(await Te({retention:X(a),stores:n,home:t,...e.agent?{only:n}:{},...s===void 0?{}:{maxAgeDays:s}}))}finally{a.close()}}function Tt(){return!(process.env.SSH_CONNECTION||process.env.SSH_TTY||process.env.SSH_CLIENT||process.platform==="linux"&&!process.env.DISPLAY&&!process.env.WAYLAND_DISPLAY)}async function It(e={}){const t=process.cwd();S(t);const n=process.env.BISHOP_GMAIL_CLIENT_ID?.trim(),i=process.env.BISHOP_GMAIL_CLIENT_SECRET?.trim(),s=process.env.BISHOP_GMAIL_REFRESH_TOKEN?.trim(),a=process.env.BISHOP_GMAIL_USER?.trim(),r=process.env.BISHOP_GMAIL_SERVICE_ACCOUNT?.trim();n&&!i&&u("BISHOP_GMAIL_CLIENT_ID is set but BISHOP_GMAIL_CLIENT_SECRET is not.");const m=e.browser===!1||!Tt()?"paste":"browser",c=await ee(!0,async d=>Ge({...n&&i?{client:{clientId:n,clientSecret:i}}:{},...s?{refreshToken:s}:{},...a?{user:a}:{},...r?{serviceAccount:r}:{},mode:m,openBrowser:Qe,readPaste:async l=>(process.stdout.write(`Open this in a browser and approve it:
18
18
 
19
19
  ${l}
20
20
 
@@ -22,12 +22,12 @@ It will finish on a http://127.0.0.1 address that fails to load. That is expecte
22
22
  Copy the whole address out of the browser and paste it here.
23
23
 
24
24
  `),d("Redirect URL",""))}));if(process.stdout.write(`${c.report}
25
- `),c.refreshToken){const d=await ee(t,{BISHOP_GMAIL_REFRESH_TOKEN:c.refreshToken});process.stdout.write(`
25
+ `),c.refreshToken){const d=await te(t,{BISHOP_GMAIL_REFRESH_TOKEN:c.refreshToken});process.stdout.write(`
26
26
  Saved BISHOP_GMAIL_REFRESH_TOKEN to ${d}
27
- `)}c.ok||process.exit(1)}async function Et(e){const t=process.cwd(),n=process.env.GH_TOKEN?.trim();process.env.GH_TOKEN!==void 0&&!n&&delete process.env.GH_TOKEN,S(t);const i=process.env.GH_TOKEN?.trim(),s=!!i&&!n,a=e.prompt&&process.stdin.isTTY===!0,r=e.print?process.stderr:process.stdout,m=l=>r.write(l),c=l=>Ne({...i?{token:i,tokenIsSaved:s}:{},...a?{login:()=>Ie(Re.map(f=>f.scope),l),confirm:async f=>/^y/i.test(await Z(!0,w=>w(f,"y"),r))}:{},gitConfigured:Ae,gh:Ee(l),...e.temporary?{temporary:!0}:{}}),d=e.temporary&&!i?await _e(c):await c();if(m(`${d.report}
27
+ `)}c.ok||process.exit(1)}async function Et(e){const t=process.cwd(),n=process.env.GH_TOKEN?.trim();process.env.GH_TOKEN!==void 0&&!n&&delete process.env.GH_TOKEN,S(t);const i=process.env.GH_TOKEN?.trim(),s=!!i&&!n,a=e.prompt&&process.stdin.isTTY===!0,r=e.print?process.stderr:process.stdout,m=l=>r.write(l),c=l=>Ne({...i?{token:i,tokenIsSaved:s}:{},...a?{login:()=>Ie(Re.map(f=>f.scope),l),confirm:async f=>/^y/i.test(await ee(!0,w=>w(f,"y"),r))}:{},gitConfigured:Ae,gh:Ee(l),...e.temporary?{temporary:!0}:{}}),d=e.temporary&&!i?await _e(c):await c();if(m(`${d.report}
28
28
  `),e.print)d.token&&process.stdout.write(`${JSON.stringify({GH_TOKEN:d.token})}
29
- `);else if(d.token){if(d.source==="gh"){const f=await ee(t,{GH_TOKEN:d.token});m(`
29
+ `);else if(d.token){if(d.source==="gh"){const f=await te(t,{GH_TOKEN:d.token});m(`
30
30
  Saved GH_TOKEN to ${f}
31
31
  `)}const l=await Je(t,[".env"]);l.length>0&&m(`Added ${l.join(", ")} to .gitignore
32
- `)}d.ok||process.exit(1)}async function At(e){S(process.cwd());const t={...e.name===void 0?{}:{name:e.name},...e.displayName===void 0?{}:{displayName:e.displayName},...e.description===void 0?{}:{description:e.description},prompt:e.prompt};e.manual?await Ze(t):await Xe(t)}async function _t(e){if(e.appId!==void 0&&!e.manual)throw new X("--app-id adopts an app somebody else created, so it needs --manual too.");if(e.endpoint!==void 0&&e.manual)throw new X("--endpoint sets the URL on an app Bishop is creating, so it cannot be used\nwith --manual. Point an adopted app with `bishop teams endpoint <url>`,\nor ask whoever created it to.");S(process.cwd());const t={...e.name===void 0?{}:{name:e.name},...e.displayName===void 0?{}:{displayName:e.displayName},...e.description===void 0?{}:{description:e.description},...e.endpoint===void 0?{}:{endpoint:e.endpoint},...e.appId===void 0?{}:{appId:e.appId},prompt:e.prompt};e.manual?await at(t):await rt(t)}async function Rt(e){const t=process.cwd(),n=await O(t),i=await Ve({name:e.name??n.slack?.app?.name,displayName:e.displayName??n.slack?.app?.displayName,description:e.description??n.slack?.app?.description,prompt:!1},t,async(s,a)=>a);process.stdout.write(`${JSON.stringify(tt(i),null,2)}
33
- `)}const k=new pe().name("bishop").description("An agent gateway. Runs an agent and connects it to Slack and email.").version(ht,"-v, --version").enablePositionalOptions().option("--agent <path-or-url>","the agent's directory, or a git URL to clone; defaults to the current directory").option(`--harness <${ue.join("|")}>`,"which SDK runs the agent",fe).option("--model <model>","the model to run, in the harness's own naming",j("--model")).option("--effort <effort>","how hard the model should think, in the harness's own naming",j("--effort")).option("--auto-update","for a git-URL --agent, check for a new commit at the start of every new thread").allowExcessArguments(!0).action((e,t)=>St(t.args,e));k.command("gc").description("Collect the snapshots, worktrees, and rows old conversations left behind").option("--max-age-days <days>",`how long a quiet thread's remains are kept; defaults to gc.maxAgeDays, then ${ve}`,we("--max-age-days")).option("--agent <url>","only this agent's snapshots; defaults to every agent Bishop has cloned here").action(bt);const E=k.command("schedules").description("Prompts the agent runs on a clock, and where their answers go");E.command("list").description("Everything scheduled here, in your own time zone").action(()=>De(process.cwd())),E.command("add").description("Add a schedule").requiredOption("--name <name>","what a person sees in a list, and the subject of an email").requiredOption("--cron <expression>","five-field cron, or @daily and friends").requiredOption("--prompt <text>","what the agent is asked, including when to stay quiet").requiredOption("--to <destination>","#channel or @person for Slack, an address for Gmail").option("--tz <zone>","IANA zone the expression is read in; defaults to this machine").option("--interface <name>","which interface delivers, when --to is ambiguous").action(e=>Pe(process.cwd(),e)),E.command("rm <id>").description("Remove a schedule").action(e=>Be(process.cwd(),e)),E.command("disable <id>").description("Stop a schedule firing, keeping it").action(e=>Q(process.cwd(),e,!1)),E.command("enable <id>").description("Start a disabled schedule firing again, from now").action(e=>Q(process.cwd(),e,!0));const Nt=k.command("gmail").description("Gmail interface commands");Nt.command("setup").description("Authorize a Gmail mailbox, or check the one already configured").option("--no-browser","print a URL to open elsewhere instead of opening one here").action(It);const Ot=k.command("github").description("GitHub credentials for the agent");Ot.command("setup").description("Give the agent a GitHub token, or check the one it's using").option("--no-prompt","don't offer to run `gh auth login`").option("--print","write the credential to stdout as JSON instead of to .env").option("--temporary","log in without touching this machine's own gh login").action(Et);const te=k.command("slack").description("Slack interface commands");te.command("setup").description("Create and install a Slack app, and save its credentials here").option("--name <name>","app name; defaults to the current directory name").option("--display-name <name>","bot display name in Slack; defaults to --name").option("--description <text>","short description shown in Slack").option("--no-prompt","don't prompt for anything left off").option("--manual","adopt an app you created in Slack yourself, using its tokens").action(At),te.command("manifest").description("Print the app manifest, for creating an app in Slack by hand").option("--name <name>","app name; defaults to config or the directory name").option("--display-name <name>","bot display name; defaults to --name").option("--description <text>","short description shown in Slack").action(Rt);const G=k.command("teams").description("Microsoft Teams interface commands");G.command("setup").description("Create a Teams app and save its credentials here, or check the one configured").option("--name <name>","app name; defaults to the current directory name").option("--display-name <name>","app name shown in Teams; defaults to --name").option("--description <text>","short description shown in Teams").option("--endpoint <url>","the URL Teams POSTs to; can be set later").option("--app-id <id>","with --manual, the Teams app ID you were given").option("--no-prompt","don't prompt for anything left off").option("--manual","adopt an app somebody else created, using its credentials").action(_t),G.command("endpoint <url>").description("Point the Teams app at a different URL, changing nothing else").option("--app-id <id>","the Teams app to point; defaults to the one in config").action((e,t)=>st(e,t.appId===void 0?{}:{appId:t.appId})),G.command("instructions").description("Print what to send whoever creates the Teams app, when that is an administrator").option("--endpoint <url>","the URL Teams should POST to, if you already know it").action(e=>{process.stdout.write(nt(e.endpoint))});try{await k.parseAsync()}catch(e){throw e instanceof v&&u(e.message),e}
32
+ `)}d.ok||process.exit(1)}async function At(e){S(process.cwd());const t={...e.name===void 0?{}:{name:e.name},...e.displayName===void 0?{}:{displayName:e.displayName},...e.description===void 0?{}:{description:e.description},prompt:e.prompt};e.manual?await Ze(t):await Xe(t)}async function _t(e){if(e.appId!==void 0&&!e.manual)throw new Z("--app-id adopts an app somebody else created, so it needs --manual too.");if(e.endpoint!==void 0&&e.manual)throw new Z("--endpoint sets the URL on an app Bishop is creating, so it cannot be used\nwith --manual. Point an adopted app with `bishop teams endpoint <url>`,\nor ask whoever created it to.");S(process.cwd());const t={...e.name===void 0?{}:{name:e.name},...e.displayName===void 0?{}:{displayName:e.displayName},...e.description===void 0?{}:{description:e.description},...e.endpoint===void 0?{}:{endpoint:e.endpoint},...e.appId===void 0?{}:{appId:e.appId},prompt:e.prompt};e.manual?await at(t):await rt(t)}async function Rt(e){const t=process.cwd(),n=await $(t),i=await Ve({name:e.name??n.slack?.app?.name,displayName:e.displayName??n.slack?.app?.displayName,description:e.description??n.slack?.app?.description,prompt:!1},t,async(s,a)=>a);process.stdout.write(`${JSON.stringify(tt(i),null,2)}
33
+ `)}const k=new pe().name("bishop").description("An agent gateway. Runs an agent and connects it to Slack and email.").version(ht,"-v, --version").enablePositionalOptions().option("--agent <path-or-url>","the agent's directory, or a git URL to clone; defaults to the current directory").option(`--harness <${ue.join("|")}>`,"which SDK runs the agent",fe).option("--model <model>","the model to run, in the harness's own naming",Y("--model")).option("--effort <effort>","how hard the model should think, in the harness's own naming",Y("--effort")).option("--auto-update","for a git-URL --agent, check for a new commit at the start of every new thread").allowExcessArguments(!0).action((e,t)=>St(t.args,e));k.command("gc").description("Collect the snapshots, worktrees, and rows old conversations left behind").option("--max-age-days <days>",`how long a quiet thread's remains are kept; defaults to gc.maxAgeDays, then ${ve}`,we("--max-age-days")).option("--agent <url>","only this agent's snapshots; defaults to every agent Bishop has cloned here").action(bt);const E=k.command("schedules").description("Prompts the agent runs on a clock, and where their answers go");E.command("list").description("Everything scheduled here, in your own time zone").action(()=>De(process.cwd())),E.command("add").description("Add a schedule").requiredOption("--name <name>","what a person sees in a list, and the subject of an email").requiredOption("--cron <expression>","five-field cron, or @daily and friends").requiredOption("--prompt <text>","what the agent is asked, including when to stay quiet").requiredOption("--to <destination>","#channel or @person for Slack, an address for Gmail").option("--tz <zone>","IANA zone the expression is read in; defaults to this machine").option("--interface <name>","which interface delivers, when --to is ambiguous").action(e=>Pe(process.cwd(),e)),E.command("rm <id>").description("Remove a schedule").action(e=>Be(process.cwd(),e)),E.command("disable <id>").description("Stop a schedule firing, keeping it").action(e=>V(process.cwd(),e,!1)),E.command("enable <id>").description("Start a disabled schedule firing again, from now").action(e=>V(process.cwd(),e,!0));const Nt=k.command("gmail").description("Gmail interface commands");Nt.command("setup").description("Authorize a Gmail mailbox, or check the one already configured").option("--no-browser","print a URL to open elsewhere instead of opening one here").action(It);const Ot=k.command("github").description("GitHub credentials for the agent");Ot.command("setup").description("Give the agent a GitHub token, or check the one it's using").option("--no-prompt","don't offer to run `gh auth login`").option("--print","write the credential to stdout as JSON instead of to .env").option("--temporary","log in without touching this machine's own gh login").action(Et);const oe=k.command("slack").description("Slack interface commands");oe.command("setup").description("Create and install a Slack app, and save its credentials here").option("--name <name>","app name; defaults to the current directory name").option("--display-name <name>","bot display name in Slack; defaults to --name").option("--description <text>","short description shown in Slack").option("--no-prompt","don't prompt for anything left off").option("--manual","adopt an app you created in Slack yourself, using its tokens").action(At),oe.command("manifest").description("Print the app manifest, for creating an app in Slack by hand").option("--name <name>","app name; defaults to config or the directory name").option("--display-name <name>","bot display name; defaults to --name").option("--description <text>","short description shown in Slack").action(Rt);const x=k.command("teams").description("Microsoft Teams interface commands");x.command("setup").description("Create a Teams app and save its credentials here, or check the one configured").option("--name <name>","app name; defaults to the current directory name").option("--display-name <name>","app name shown in Teams; defaults to --name").option("--description <text>","short description shown in Teams").option("--endpoint <url>","the URL Teams POSTs to; can be set later").option("--app-id <id>","with --manual, the Teams app ID you were given").option("--no-prompt","don't prompt for anything left off").option("--manual","adopt an app somebody else created, using its credentials").action(_t),x.command("endpoint <url>").description("Point the Teams app at a different URL, changing nothing else").option("--app-id <id>","the Teams app to point; defaults to the one in config").action((e,t)=>st(e,t.appId===void 0?{}:{appId:t.appId})),x.command("instructions").description("Print what to send whoever creates the Teams app, when that is an administrator").option("--endpoint <url>","the URL Teams should POST to, if you already know it").action(e=>{process.stdout.write(nt(e.endpoint))});try{await k.parseAsync()}catch(e){throw e instanceof v&&u(e.message),e}
@@ -0,0 +1,3 @@
1
+ import{Command as N,CommanderError as C,InvalidArgumentError as g}from"commander";import{VERBOSITY as b}from"../config.js";import{parseVerbosity as O}from"../interface/verbosity.js";import{log as w}from"../log.js";import{isHarnessName as L,MODEL_SETTINGS as E}from"../thread-model.js";import{commandOutput as S,tokenize as j}from"./syntax.js";const a="default";function k(o){if(o.trim().toLowerCase()===a)return a;const e=O(o);if(!e)throw new g(`Use one of ${b.join(", ")}, or ${a}.`);return e}function u(o){const e=o.trim();if(!e)throw new g(`Name one, or ${a} to follow the configured one.`);if(!L(e))throw new g(`"${e}" is not a model name. Letters, digits, and . : / @ - _ only.`);return e}function T(o,e,r){return e?`${o} is ${e}, set in this thread`:r?`${o} is ${r}, the default`:`${o} is whatever the harness runs by default`}function I(o,e){const r=e.model.of(o);return[T("Model",r.model,e.configured.model),T("Effort",r.effort,e.configured.effort)]}function G(o,e,r){const l=[];let h;const d=new N;d.name("!").description("Bishop commands. These are handled here and never reach the agent.").exitOverride().configureOutput({writeOut:t=>l.push(t),writeErr:t=>l.push(t),outputError:(t,n)=>n(t)}),d.command("verbosity").description("How much of a turn this thread shows while it is running.").argument("[level]",`${b.join(", ")}, or ${a} to follow the configured level. Omit it to see the current one.`,k).action(t=>{if(!t){const c=r.verbosity.of(e.thread.id),s=c.fromThread?"":" (the configured default)";h=`Thread verbosity is ${c.level}${s}`;return}if(t===a){r.verbosity.clear(e.thread.id),h=`Thread verbosity follows the configured default, which is ${r.verbosity.of(e.thread.id).level}`;return}const n=O(t);n&&(r.verbosity.set(e.thread.id,n),h=`Thread verbosity set to ${n}`)}),d.command("stop").description("Stops the turn running in this thread. Messages waiting behind it still run.").action(()=>{const t=r.stop(e.thread.threadKey);w.info({thread:e.thread.threadKey,stopped:t},"stop requested by command"),h=t?"Stopping the turn running in this thread":"Nothing is running in this thread"}),d.command("model").description("Which model this thread runs, and how hard it thinks.").argument("[model]",`a model name in the harness's own naming, or ${a} to follow the configured one. Omit everything to see where the thread stands.`,u).argument("[effort]",`how hard it should think, in the harness's own naming, or ${a}.`,u).option("-m, --model <model>","the model, named as a flag rather than in order",u).option("-e, --effort <effort>","the effort, named as a flag rather than in order",u).action((t,n,c)=>{const s=e.thread.id,f={...c},p=E.filter(i=>f[i]===void 0),v=[t,n].filter(i=>i!==void 0);if(v.length>p.length){h="Name each half once: a model and an effort, in order or as flags.";return}if(v.forEach((i,m)=>{const $=p[m];$&&(f[$]=i)}),!f.model&&!f.effort){h=I(s,r).join(`
2
+ `);return}const y={};for(const i of E){const m=f[i];m&&(m.toLowerCase()===a?r.model.clear(s,i):y[i]=m)}r.model.set(s,y),w.info({thread:e.thread.threadKey,...f},"a thread changed the model it runs"),h=["Set. It takes effect on this thread's next turn.",...I(s,r)].join(`
3
+ `)});try{d.parse(j(o),{from:"user"})}catch(t){if(!(t instanceof C)){const n=t instanceof Error?t.message:String(t);return w.debug({thread:e.thread.threadKey,line:o,err:t},"a command failed"),S(n)}}return S(h??(l.join("").trim()||d.helpInformation()))}export{G as runCommand};
@@ -0,0 +1,3 @@
1
+ const d="!>";function o(i){const n=i.trim();if(n.startsWith("```")&&n.endsWith("```")&&n.length>=6){const t=n.slice(3,-3);return t.includes("```")?void 0:t}if(n.startsWith("`")&&n.endsWith("`")&&n.length>=2){const t=n.slice(1,-1);return t.includes("`")?void 0:t}}const c=/^-{0,2}[a-z][a-z0-9-]*$/i;function a(i){const n=o(i);return n===void 0?void 0:f(n)}function f(i){const n=i.trim();if(!n)return;const t=/^!\s+(\S+)(?:\s+([\s\S]*))?$/.exec(n),e=t?.[1];if(!e||!c.test(e))return;const r=t?.[2]?.trim();return r?`${e} ${r}`:e}function l(i){const n=[];let t="",e=!1,r;for(let u=0;u<i.length;u+=1){const s=i[u];if(s==="\\"&&r!=="'"&&u+1<i.length){t+=i[++u],e=!0;continue}if(r){s===r?r=void 0:t+=s;continue}if(s==='"'||s==="'"){r=s,e=!0;continue}if(/\s/.test(s)){e&&n.push(t),t="",e=!1;continue}t+=s,e=!0}if(r)throw new Error(`unterminated ${r==='"'?"double":"single"} quote`);return e&&n.push(t),n}function h(i){const n=i.trim().split(`
2
+ `),[t="",...e]=n;return["```",`!> ${t}`,...e,"```"].join(`
3
+ `)}export{f as commandIn,a as commandLine,h as commandOutput,l as tokenize};
@@ -1,10 +1,10 @@
1
- import{existsSync as D}from"node:fs";import{homedir as K}from"node:os";import{join as _}from"node:path";import{Codex as A}from"@openai/codex-sdk";import{log as g}from"../log.js";import{SERVER_NAME as I}from"../tools/server.js";import{ensureWorktree as P,gitCommonDir as j,isolates as M}from"../worktrees.js";import{ignoredKeys as R,layerOptions as L,mergeEnv as N,shadowedKeys as X}from"./layers.js";import{buildPrompt as Y,principalInstructions as $}from"./principal.js";const U=10,W=60;function B(e,t){const o=e??void 0;return t?{...o,[I]:{url:t.url,bearer_token_env_var:t.tokenEnvVar,required:!0,default_tools_approval_mode:"auto",startup_timeout_sec:U,tool_timeout_sec:W}}:o}const F={approvalPolicy:"never",sandboxMode:"workspace-write",networkAccessEnabled:!0,skipGitRepoCheck:!0},x=["workingDirectory","developer_instructions"];function G(e){const t=e?.config,o=t&&typeof t=="object"&&"developer_instructions"in t?["config.developer_instructions"]:[];return[...R(e,x),...o]}const V=new Set(["apiKey","baseUrl","config","configOverrides","env","codexPathOverride"]);function H(e){switch(e.type){case"command_execution":return{type:"tool",name:"Bash"};case"file_change":{const t=e.changes.length;if(t>1)return{type:"tool",name:"Edit",detail:`${t} files`};const o=t===1?e.changes[0]?.path.split("/").pop():void 0;return{type:"tool",name:"Edit",...o?{detail:o}:{}}}case"mcp_tool_call":return{type:"tool",name:`mcp__${e.server}__${e.tool}`};case"web_search":return{type:"tool",name:"WebSearch",...e.query?{detail:e.query}:{}};case"todo_list":return{type:"tool",name:"TodoWrite"};case"reasoning":return{type:"thinking"};default:return}}const z=new Set(["command_execution","mcp_tool_call","web_search","reasoning"]);function J(e){const t=new Set;let o="";return i=>{switch(i.type){case"thread.started":return[{type:"session",sessionId:i.thread_id,...e?{model:e}:{}}];case"item.started":case"item.completed":{const s=i.item,l=z.has(s.type)?"item.started":"item.completed";if(i.type!==l||t.has(s.id))return[];if(t.add(s.id),s.type==="agent_message")return o=s.text,s.text.trim()?[{type:"text",text:s.text}]:[];if(s.type==="error")return g.warn({message:s.message},"codex reported a non-fatal error"),[];const u=H(s);return u?[u]:[]}case"turn.completed":return[{type:"done",text:o,usage:{inputTokens:i.usage.input_tokens,cachedInputTokens:i.usage.cached_input_tokens,outputTokens:i.usage.output_tokens}}];case"turn.failed":return[{type:"error",kind:"turn_failed",message:i.error.message}];case"error":return[{type:"error",kind:"stream_error",message:i.message}];default:return[]}}}function Q(e){return[`You are working in a git worktree of your own, on the branch "${e}", created for this`,"conversation. Other conversations are working in their own worktrees at the same time.","","Work normally. Commit what you finish on this branch and say which branch it is, since that's","how a person finds your work."].join(`
2
- `)}function Z(e,t){const o=Array.isArray(e)?e:[],i=[...new Set([...o,...t??[]])];return i.length>0?{additionalDirectories:i}:{}}function k(e){return{model:e.model,modelReasoningEffort:e.effort}}function q(e){return Object.fromEntries(Object.entries(k(e)).filter(([,t])=>t!==void 0))}const ee={model:"model",modelReasoningEffort:"model_reasoning_effort"};function te(e,t){const o=e?.config,i=o&&typeof o=="object"?Object.entries(ee).filter(([s,l])=>t[s]!==void 0&&l in o).map(([,s])=>`config.${s}`):[];return[...X(e,t),...i]}function pe(e={}){const t=G(e.options);t.length>0&&g.warn({ignored:t},"these agent.options are set by Bishop and cannot be overridden; ignoring them");const o=k(e),i=te(e.options,o);i.length>0&&g.warn({shadowed:i},"agent.model and agent.effort override these agent.options");const s=L({defaults:F,...e.options?{configOptions:e.options}:{},general:o,controlledKeys:x}),l={},u={};for(const[r,n]of Object.entries(s))V.has(r)?l[r]=n:u[r]=n;const E=l.env;e.apiKey&&(l.apiKey=e.apiKey);const b=e.worktrees,w=(r,n,a)=>{const d=l.config??{},c=B(d.mcp_servers,a),m=N(E,a?{[a.tokenEnvVar]:a.token}:{}),f={...d,developer_instructions:[n&&$(n),r].filter(p=>!!p?.trim()).join(`
1
+ import{existsSync as K}from"node:fs";import{homedir as D}from"node:os";import{join as _}from"node:path";import{Codex as A}from"@openai/codex-sdk";import{log as g}from"../log.js";import{SERVER_NAME as I}from"../tools/server.js";import{ensureWorktree as P,gitDirs as j,isolates as M}from"../worktrees.js";import{ignoredKeys as R,layerOptions as L,mergeEnv as N,shadowedKeys as X}from"./layers.js";import{buildPrompt as Y,principalInstructions as $}from"./principal.js";const U=10,W=60;function B(e,t){const o=e??void 0;return t?{...o,[I]:{url:t.url,bearer_token_env_var:t.tokenEnvVar,required:!0,default_tools_approval_mode:"auto",startup_timeout_sec:U,tool_timeout_sec:W}}:o}const F={approvalPolicy:"never",sandboxMode:"workspace-write",networkAccessEnabled:!0,skipGitRepoCheck:!0},x=["workingDirectory","developer_instructions"];function G(e){const t=e?.config,o=t&&typeof t=="object"&&"developer_instructions"in t?["config.developer_instructions"]:[];return[...R(e,x),...o]}const V=new Set(["apiKey","baseUrl","config","configOverrides","env","codexPathOverride"]);function H(e){switch(e.type){case"command_execution":return{type:"tool",name:"Bash"};case"file_change":{const t=e.changes.length;if(t>1)return{type:"tool",name:"Edit",detail:`${t} files`};const o=t===1?e.changes[0]?.path.split("/").pop():void 0;return{type:"tool",name:"Edit",...o?{detail:o}:{}}}case"mcp_tool_call":return{type:"tool",name:`mcp__${e.server}__${e.tool}`};case"web_search":return{type:"tool",name:"WebSearch",...e.query?{detail:e.query}:{}};case"todo_list":return{type:"tool",name:"TodoWrite"};case"reasoning":return{type:"thinking"};default:return}}const z=new Set(["command_execution","mcp_tool_call","web_search","reasoning"]);function J(e){const t=new Set;let o="";return i=>{switch(i.type){case"thread.started":return[{type:"session",sessionId:i.thread_id,...e?{model:e}:{}}];case"item.started":case"item.completed":{const s=i.item,l=z.has(s.type)?"item.started":"item.completed";if(i.type!==l||t.has(s.id))return[];if(t.add(s.id),s.type==="agent_message")return o=s.text,s.text.trim()?[{type:"text",text:s.text}]:[];if(s.type==="error")return g.warn({message:s.message},"codex reported a non-fatal error"),[];const u=H(s);return u?[u]:[]}case"turn.completed":return[{type:"done",text:o,usage:{inputTokens:i.usage.input_tokens,cachedInputTokens:i.usage.cached_input_tokens,outputTokens:i.usage.output_tokens}}];case"turn.failed":return[{type:"error",kind:"turn_failed",message:i.error.message}];case"error":return[{type:"error",kind:"stream_error",message:i.message}];default:return[]}}}function Q(e){return[`You are working in a git worktree of your own, on the branch "${e}", created for this`,"conversation. Other conversations are working in their own worktrees at the same time.","","Work normally. Commit what you finish on this branch and say which branch it is, since that's","how a person finds your work."].join(`
2
+ `)}function Z(e,t){const o=Array.isArray(e)?e:[],i=[...new Set([...o,...t??[]])];return i.length>0?{additionalDirectories:i}:{}}function k(e){return{model:e.model,modelReasoningEffort:e.effort}}function q(e){return Object.fromEntries(Object.entries(k(e)).filter(([,t])=>t!==void 0))}const ee={model:"model",modelReasoningEffort:"model_reasoning_effort"};function te(e,t){const o=e?.config,i=o&&typeof o=="object"?Object.entries(ee).filter(([s,l])=>t[s]!==void 0&&l in o).map(([,s])=>`config.${s}`):[];return[...X(e,t),...i]}function pe(e={}){const t=G(e.options);t.length>0&&g.warn({ignored:t},"these agent.options are set by Bishop and cannot be overridden; ignoring them");const o=k(e),i=te(e.options,o);i.length>0&&g.warn({shadowed:i},"agent.model and agent.effort override these agent.options");const s=L({defaults:F,...e.options?{configOptions:e.options}:{},general:o,controlledKeys:x}),l={},u={};for(const[r,n]of Object.entries(s))V.has(r)?l[r]=n:u[r]=n;const E=l.env;e.apiKey&&(l.apiKey=e.apiKey);const b=e.worktrees,y=(r,n,a)=>{const d=l.config??{},c=B(d.mcp_servers,a),m=N(E,a?{[a.tokenEnvVar]:a.token}:{}),f={...d,developer_instructions:[n&&$(n),r].filter(p=>!!p?.trim()).join(`
3
3
 
4
- `)};return c&&(f.mcp_servers=c),new A({...l,...m?{env:m}:{},config:f})};async function v(r){if(!M(b))return{cwd:r.cwd};const n=await P(r.cwd,r.worktree),a=await j(n);return a||g.warn({worktree:n},"couldn't resolve the repository's git directory, so the agent probably can't commit"),{cwd:n,note:Q(r.worktree),...a?{writable:[a]}:{}}}return{name:"codex",describeStartupFailure(r,n){return`The Codex agent couldn't start in ${r}:
4
+ `)};return c&&(f.mcp_servers=c),new A({...l,...m?{env:m}:{},config:f})};async function v(r){if(!M(b))return{cwd:r.cwd};const n=await P(r.cwd,r.worktree),a=await j(n);return a.length===0&&g.warn({worktree:n},"couldn't resolve the worktree's git directories, so the agent probably can't commit"),{cwd:n,note:Q(r.worktree),...a.length>0?{writable:a}:{}}}return{name:"codex",describeStartupFailure(r,n){return`The Codex agent couldn't start in ${r}:
5
5
  ${n}
6
6
 
7
7
  Bishop does not manage OpenAI credentials. The agent authenticates the
8
8
  same way \`codex\` does on this machine, so make sure that works first:
9
9
  CODEX_API_KEY in the environment or .env, or
10
- an existing \`codex login\` on this machine.`},async check(r){const n=new AbortController,a=u.model;try{const c=w(void 0,void 0).startThread({...u,workingDirectory:r}),{events:m}=await c.runStreamed("ping",{signal:n.signal});for await(const f of m){if(f.type==="thread.started"){n.abort();const p=re(e.apiKey);return oe(p),{ok:!0,...p?{credentialSource:p}:{},...a?{model:a}:{}}}if(f.type==="error")return{ok:!1,error:f.message}}return{ok:!1,error:"the harness exited without starting a thread"}}catch(d){return n.signal.aborted?{ok:!0}:{ok:!1,error:d instanceof Error?d.message:String(d)}}},async*runTurn(r){const n=new AbortController,a=()=>n.abort();r.signal.addEventListener("abort",a,{once:!0});let d=!1;try{const{cwd:c,note:m,writable:f}=await v(r),p=w(m,r.origin,r.tools),h={...u,...q(r),workingDirectory:c,...Z(u.additionalDirectories,f)},C=r.sessionId?p.resumeThread(r.sessionId,h):p.startThread(h),S=J(h.model),{events:T}=await C.runStreamed(Y(r),{signal:n.signal});for await(const O of T)for(const y of S(O))(y.type==="done"||y.type==="error")&&(d=!0),yield y}catch(c){n.signal.aborted?g.debug({thread:r.threadKey},"codex turn aborted"):d?g.debug({err:c},"codex threw after reporting a result"):(g.error({err:c},"harness turn failed"),yield{type:"error",kind:"harness_failure",message:c instanceof Error?c.message:String(c)})}finally{r.signal.removeEventListener("abort",a)}}}}function re(e){if(e)return"CODEX_API_KEY";const t=process.env.CODEX_HOME??_(K(),".codex");if(D(_(t,"auth.json")))return"existing codex login"}function oe(e){e||g.warn("no CODEX_API_KEY and no codex login found, so the first turn will probably fail to authenticate; run `codex login`, or set CODEX_API_KEY")}export{x as CONTROLLED_CODEX_KEYS,F as DEFAULT_CODEX_OPTIONS,q as chosenCodexOptions,k as codexGeneralOptions,pe as codexHarness,J as codexTranslator,G as ignoredCodexKeys,B as mergeMcpServers,te as shadowedCodexKeys,Z as withWritable,Q as worktreeNote};
10
+ an existing \`codex login\` on this machine.`},async check(r){const n=new AbortController,a=u.model;try{const c=y(void 0,void 0).startThread({...u,workingDirectory:r}),{events:m}=await c.runStreamed("ping",{signal:n.signal});for await(const f of m){if(f.type==="thread.started"){n.abort();const p=re(e.apiKey);return oe(p),{ok:!0,...p?{credentialSource:p}:{},...a?{model:a}:{}}}if(f.type==="error")return{ok:!1,error:f.message}}return{ok:!1,error:"the harness exited without starting a thread"}}catch(d){return n.signal.aborted?{ok:!0}:{ok:!1,error:d instanceof Error?d.message:String(d)}}},async*runTurn(r){const n=new AbortController,a=()=>n.abort();r.signal.addEventListener("abort",a,{once:!0});let d=!1;try{const{cwd:c,note:m,writable:f}=await v(r),p=y(m,r.origin,r.tools),h={...u,...q(r),workingDirectory:c,...Z(u.additionalDirectories,f)},C=r.sessionId?p.resumeThread(r.sessionId,h):p.startThread(h),S=J(h.model),{events:T}=await C.runStreamed(Y(r),{signal:n.signal});for await(const O of T)for(const w of S(O))(w.type==="done"||w.type==="error")&&(d=!0),yield w}catch(c){n.signal.aborted?g.debug({thread:r.threadKey},"codex turn aborted"):d?g.debug({err:c},"codex threw after reporting a result"):(g.error({err:c},"harness turn failed"),yield{type:"error",kind:"harness_failure",message:c instanceof Error?c.message:String(c)})}finally{r.signal.removeEventListener("abort",a)}}}}function re(e){if(e)return"CODEX_API_KEY";const t=process.env.CODEX_HOME??_(D(),".codex");if(K(_(t,"auth.json")))return"existing codex login"}function oe(e){e||g.warn("no CODEX_API_KEY and no codex login found, so the first turn will probably fail to authenticate; run `codex login`, or set CODEX_API_KEY")}export{x as CONTROLLED_CODEX_KEYS,F as DEFAULT_CODEX_OPTIONS,q as chosenCodexOptions,k as codexGeneralOptions,pe as codexHarness,J as codexTranslator,G as ignoredCodexKeys,B as mergeMcpServers,te as shadowedCodexKeys,Z as withWritable,Q as worktreeNote};
@@ -1 +1 @@
1
- const S=60,u=12e3,d=2e3,a=20;function h(t){let c=20;const n=[...t];for(let o=n.length-1;o>=0;o-=1){const i=n[o],e=i?.shared;if(!(!i||!e?.length)){if(e.length<=c){c-=e.length;continue}n[o]={...i,shared:e.slice(e.length-c)},c=0}}return n}function l(t){return t.kind!=="message"||t.text.length<=2e3?t:{...t,text:`${t.text.slice(0,2e3)}\u2026 (message truncated)`}}function f(t){const c=t[0];if(!c)return[];const n=t.slice(1).map(l);let o=0,i=0;for(const s of n)s.kind==="message"&&(o+=1,i+=s.text.length);let e=0,r=0;for(;r<n.length&&(o>60||i>12e3);){const s=n[r];r+=1,s&&(s.kind==="message"?(e+=1,o-=1,i-=s.text.length):e+=s.count??0)}const A=e?[{kind:"omitted",reason:"trimmed",count:e}]:[];return[l(c),...A,...n.slice(r)]}export{u as MAX_CHARS,a as MAX_FILES,S as MAX_MESSAGES,d as MAX_MESSAGE_CHARS,f as trimHistory,h as withinFileBudget};
1
+ const a=60,u=12e3,m=2e3,A=20;function S(t){let i=20;const e=[...t];for(let o=e.length-1;o>=0;o-=1){const c=e[o],n=c?.shared;if(!(!c||!n?.length)){if(n.length<=i){i-=n.length;continue}e[o]={...c,shared:n.slice(n.length-i)},i=0}}return e}function l(t){return t.kind!=="message"||t.text.length<=2e3?t:{...t,text:`${t.text.slice(0,2e3)}\u2026 (message truncated)`}}function f(t){const i=t[0];if(!i)return[];const e=t.slice(1).map(l);let o=0,c=0;for(const r of e)r.kind==="message"&&(o+=1,c+=r.text.length);let n=0,s=0;for(;s<e.length&&(o>60||c>12e3);){const r=e[s];s+=1,r&&(r.kind==="message"?(n+=1,o-=1,c-=r.text.length):n+=r.count??0)}const d=n?[{kind:"omitted",reason:"trimmed",count:n}]:[];return[l(i),...d,...e.slice(s)]}function h(t,i){const e=i?1:0,o=t[e];return o?.kind==="omitted"&&o.reason==="trimmed"?t.map((c,n)=>n===e?{kind:"omitted",reason:"trimmed"}:c):[...t.slice(0,e),{kind:"omitted",reason:"unavailable"},...t.slice(e)]}export{u as MAX_CHARS,A as MAX_FILES,a as MAX_MESSAGES,m as MAX_MESSAGE_CHARS,h as declareUnread,f as trimHistory,S as withinFileBudget};
@@ -1 +1 @@
1
- function r(o){return o!=="low"}function s(o){return o==="high"}function t(o){return o!=="low"}export{t as showsCost,r as showsProgress,s as showsTasks};
1
+ import{DEFAULT_VERBOSITY as f,VERBOSITY as l}from"../config.js";import{log as a}from"../log.js";function p(e){return e!=="low"}function w(e){return e==="high"}function m(e){return e!=="low"}function c(e){const t=e.trim().toLowerCase();return l.includes(t)?t:void 0}function T(e,t,s=f){const n=`${t}.verbosity`,i=new Set;return{of(r){const o=e.get(r,n);if(o===void 0)return{level:s,fromThread:!1};const u=c(o);return u?{level:u,fromThread:!0}:(i.has(`${r} ${o}`)||(i.add(`${r} ${o}`),a.warn({thread:r,stored:o},"unknown verbosity on this thread; using the default")),{level:s,fromThread:!1})},set(r,o){e.set(r,n,o)},clear(r){e.clear(r,n)}}}export{c as parseVerbosity,m as showsCost,p as showsProgress,w as showsTasks,T as threadVerbosity};
@@ -1 +1 @@
1
- import{describedFile as h}from"../files.js";import{trimHistory as b,withinFileBudget as I}from"../interface/history.js";import{log as c}from"../log.js";import{describeSlackFile as g,slackFilePayload as k}from"./files.js";import{mentionableName as p,renderMentions as y}from"./mentions.js";import{FILE_SHARE as w}from"./route.js";import{nameBotMentions as d,spokenAt as x}from"./thread.js";const T=200,B=3;function O(e){const t=e.messages[0]?.ts;if(!(!t||t===e.threadTs))return t}async function W(e){const t=await v(e);if(!t)return c.warn({thread:e.key.threadTs},"could not read the thread to catch the agent up"),[{kind:"omitted",reason:"unavailable"}];for(const s of t.messages)s.userId&&!f(s,e.self)&&e.saw?.(s.userId,x(s.ts));const r=A(t.messages,e),n=I(b(r.entries));return F(t.complete?n:_(n,r.opensWithParent),e)}function _(e,t){const r=t?1:0,n=e[r];return n?.kind==="omitted"&&n.reason==="trimmed"?e.map((s,a)=>a===r?{kind:"omitted",reason:"trimmed"}:s):[...e.slice(0,r),{kind:"omitted",reason:"unavailable"},...e.slice(r)]}async function v(e){const t=[],r=new Set;let n;try{for(let s=0;s<B;s+=1){const a=await e.client.conversations.replies({channel:e.key.channelId,ts:e.key.threadTs,latest:e.before,inclusive:!1,limit:T,...n?{cursor:n}:{}});for(const o of a.messages??[]){if(!o.ts||o.ts>=e.before||r.has(o.ts))continue;r.add(o.ts);const{subtype:i,files:u}=o;t.push({ts:o.ts,...o.user?{userId:o.user}:{},...o.bot_id?{botId:o.bot_id}:{},...i?{subtype:i}:{},text:o.text??"",files:u??[]})}if(n=a.response_metadata?.next_cursor,!n)return l(t,!0)}return l(t,!1)}catch(s){return c.warn({thread:e.key.threadTs,read:t.length,err:s},"could not read the whole thread"),t.length?l(t,!1):void 0}}function l(e,t){return{messages:[...e].sort((n,s)=>n.ts<s.ts?-1:n.ts>s.ts?1:0),complete:t}}function N(e,t){return e.botId&&!t.answersBots?!1:!!(e.userId&&t.allows(e.userId))}function f(e,t){return e.userId&&e.userId===t.botUserId?!0:!!t.botId&&e.botId===t.botId}function P(e,t){return e.map(r=>{const n=g(r),s=h(n);if(n.unavailable)return s;const a=t.remember({id:n.id,name:n.name,payload:k(r)});return a?{...s,ref:a}:{...s,unavailable:"Bishop could not record a reference for it"}})}function A(e,t){const r=[];let n=!1,s=0;const a=()=>{s&&(r.push({kind:"omitted",reason:"unauthorized",count:s}),s=0)};for(const o of e){const i=f(o,t.self),u=o.ts===t.key.threadTs;if(o.subtype&&o.subtype!==w&&!i)continue;if(!i&&!N(o,t)){u&&(n=!0),s+=1;continue}if(!d(o.text,t.self.botUserId)&&o.files.length===0)continue;const m=d(o.text,t.self.botUserId,t.self.botName);u&&(n=!0),a(),r.push({kind:"message",principal:{id:o.userId??"",...o.botId&&!i?{isBot:!0}:{}},text:m,...i?{self:!0}:{},...o.files.length>0?{shared:o.files}:{}})}return a(),{entries:r,opensWithParent:n}}async function F(e,t){return Promise.all(e.map(async r=>{if(r.kind!=="message")return r;const{shared:n,...s}=r,a=n?P(n,t):[];return{...s,principal:await M(r.principal,t),text:await y(r.text,t.users),...a.length>0?{files:a}:{}}}))}async function M(e,t){if(!e.id)return e;const r=await t.users.lookup(e.id),n=p(r);return{...e,...n?{displayName:n}:{},...r.realName?{realName:r.realName}:{},...r.email?{email:r.email}:{}}}export{O as landedMidThread,W as threadHistory};
1
+ import{describedFile as h}from"../files.js";import{declareUnread as b,trimHistory as I,withinFileBudget as g}from"../interface/history.js";import{log as c}from"../log.js";import{describeSlackFile as p,slackFilePayload as y}from"./files.js";import{mentionableName as k,renderMentions as w}from"./mentions.js";import{FILE_SHARE as x}from"./route.js";import{nameBotMentions as f,spokenAt as T}from"./thread.js";const B=200,_=3;function O(t){const e=t.messages[0]?.ts;if(!(!e||e===t.threadTs))return e}async function W(t){const e=await N(t);if(!e)return c.warn({thread:t.key.threadTs},"could not read the thread to catch the agent up"),[{kind:"omitted",reason:"unavailable"}];for(const o of e.messages)o.userId&&!d(o,t.self)&&t.saw?.(o.userId,T(o.ts));const r=A(e.messages,t),n=g(I(r.entries));return F(e.complete?n:b(n,r.opensWithParent),t)}async function N(t){const e=[],r=new Set;let n;try{for(let o=0;o<_;o+=1){const a=await t.client.conversations.replies({channel:t.key.channelId,ts:t.key.threadTs,latest:t.before,inclusive:!1,limit:B,...n?{cursor:n}:{}});for(const s of a.messages??[]){if(!s.ts||s.ts>=t.before||r.has(s.ts))continue;r.add(s.ts);const{subtype:i,files:u}=s;e.push({ts:s.ts,...s.user?{userId:s.user}:{},...s.bot_id?{botId:s.bot_id}:{},...i?{subtype:i}:{},text:s.text??"",files:u??[]})}if(n=a.response_metadata?.next_cursor,!n)return l(e,!0)}return l(e,!1)}catch(o){return c.warn({thread:t.key.threadTs,read:e.length,err:o},"could not read the whole thread"),e.length?l(e,!1):void 0}}function l(t,e){return{messages:[...t].sort((n,o)=>n.ts<o.ts?-1:n.ts>o.ts?1:0),complete:e}}function P(t,e){return t.botId&&!e.answersBots?!1:!!(t.userId&&e.allows(t.userId))}function d(t,e){return t.userId&&t.userId===e.botUserId?!0:!!e.botId&&t.botId===e.botId}function v(t,e){return t.map(r=>{const n=p(r),o=h(n);if(n.unavailable)return o;const a=e.remember({id:n.id,name:n.name,payload:y(r)});return a?{...o,ref:a}:{...o,unavailable:"Bishop could not record a reference for it"}})}function A(t,e){const r=[];let n=!1,o=0;const a=()=>{o&&(r.push({kind:"omitted",reason:"unauthorized",count:o}),o=0)};for(const s of t){const i=d(s,e.self),u=s.ts===e.key.threadTs;if(s.subtype&&s.subtype!==x&&!i)continue;if(!i&&!P(s,e)){u&&(n=!0),o+=1;continue}if(!f(s.text,e.self.botUserId)&&s.files.length===0)continue;const m=f(s.text,e.self.botUserId,e.self.botName);u&&(n=!0),a(),r.push({kind:"message",principal:{id:s.userId??"",...s.botId&&!i?{isBot:!0}:{}},text:m,...i?{self:!0}:{},...s.files.length>0?{shared:s.files}:{}})}return a(),{entries:r,opensWithParent:n}}async function F(t,e){return Promise.all(t.map(async r=>{if(r.kind!=="message")return r;const{shared:n,...o}=r,a=n?v(n,e):[];return{...o,principal:await M(r.principal,e),text:await w(r.text,e.users),...a.length>0?{files:a}:{}}}))}async function M(t,e){if(!t.id)return t;const r=await e.users.lookup(t.id),n=k(r);return{...t,...n?{displayName:n}:{},...r.realName?{realName:r.realName}:{},...r.email?{email:r.email}:{}}}export{O as landedMidThread,W as threadHistory};
@@ -1,2 +1,2 @@
1
- import{etiquetteNote as C,threadParticipants as L}from"../interface/etiquette.js";import{log as p}from"../log.js";import{createSlackApp as O}from"./app.js";import{createAuthorizer as E}from"./authorizer.js";import{botTurnCap as H}from"./bots.js";import{runCommand as F}from"./commands/run.js";import{commandLine as K}from"./commands/syntax.js";import{deliverToSlack as _,describeSlackHandle as U,parseSlackHandle as b,resolveSlackDestination as z,slackHandle as R}from"./destination.js";import{describeSlackFile as W,parseSlackFile as j,slackAttachment as v,slackFilePayload as q}from"./files.js";import{registerHandlers as G}from"./handlers.js";import{landedMidThread as V,threadHistory as Y}from"./history.js";import{linkMentions as I,mentionableName as S,renderMentions as T}from"./mentions.js";import{createPresence as $}from"./presence.js";import{say as N}from"./say.js";import{threadKeyString as J}from"./thread.js";import{userDirectory as Q}from"./users.js";import{threadVerbosity as X}from"./verbosity.js";const Z={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
- `)},ee=3e3;async function ae(t,i,s=ee){const l=await te(t.botUserId,i,s),r=S(l)??t.username;if(!r)return;const m=[t.appName,l.realName].filter(h=>!!h?.trim()&&h?.toLowerCase()!==r.toLowerCase());return{address:`@${r}`,...m.length>0?{aka:[...new Set(m)]}:{}}}async function te(t,i,s){const l={userId:t};let r;const m=new Promise(h=>{r=setTimeout(()=>h(l),s)});try{return await Promise.race([i.lookup(t).catch(()=>l),m])}finally{clearTimeout(r)}}async function be(t){const{app:i,identity:s}=await O(t.credentials),l=await E(i.client,t.allow),r=Q(i.client,{self:s.botUserId}),m=H(t.maxBotTurns),h=L(),g=X(t.meta,t.verbosity),x={verbosity:g,model:t.model,configured:t.configuredModel??{}},c=await ae(s,r),y={...Z,...c?{self:c}:{}};c?p.info({address:c.address,aka:c.aka},"the agent's name in Slack"):p.warn("could not read the bot's own name; the agent will not know what it is called");const k=new Map,M=async({key:e,keyString:a,thread:o,text:w},d)=>{const n=K(w);if(!n)return!1;const f=F(n,{thread:o},x);try{await N(d.client,{channel:e.channelId,threadTs:e.threadTs},f),k.get(a)?.interrupted()}catch(u){p.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 P(e,a){const o=W(a);if(!o.unavailable)return t.attachments.remember(e,y.name,{id:o.id,name:o.name,payload:q(a)})}async function A(e,a){return a.files.length===0?[]:Promise.all(a.files.map(async o=>{const w=P(e.id,o),d=await t.files.save(e.dirKey,v(o,t.credentials.botToken));return w?{...d,ref:w}:d}))}function D(e,a){const o=T(e.messages[0]?.text??"",r),w=V(e),d=C({participants:e.participants,mentioned:e.messages.some(n=>n.mentionsAgent)});return{thread:e.thread,isNewThread:e.isNewThread,origin:y,...d?{note:d}:{},...w?{history:()=>Y({client:a.client,key:e.key,before:w,allows:n=>l.allows(n),answersBots:m.answersBots,self:{botUserId:s.botUserId,...s.botId?{botId:s.botId}:{},...c?{botName:c.address}:{}},users:r,remember:n=>t.attachments.remember(e.thread.id,y.name,n),saw:(n,f)=>h.saw(e.keyString,n,f)})}:{},async messages(){return Promise.all(e.messages.map(async(n,f)=>{const u=await A(e.thread,n);return{principal:await B(n),text:f===0?await o:await T(n.text,r),...u.length>0?{files:u}:{}}}))},presence:()=>{const n=$(e,a,{outgoing:f=>I(f,r),title:()=>o,verbosity:()=>g.of(e.thread.id).level,...d?{mayDecline:!0}:{},...t.taskIntervalMs===void 0?{}:{taskIntervalMs:t.taskIntervalMs}});return k.set(e.keyString,n),{...n,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=R({channel:e.channelId,threadTs:e.threadTs});return{interface:"slack",handle:n,label:await U(i.client,n)}}}}return G(i,{identity:s,...c?{botName:c.address}:{},threads:t.threads,events:t.events,run:(e,a)=>t.run(D(e,a)),stop:t.stop,allows:e=>l.allows(e),bots:m,participants:h,command:M}),{name:"slack",async start(){await i.start(),s.scopes.includes("files:read")||p.warn("this Slack app has no files:read scope, so it cannot open files people share; reinstall it to grant one"),s.scopes.includes("files:write")||p.warn("this Slack app has no files:write scope, so it cannot send files back; reinstall it to grant one"),r.prime(),p.info("listening on Slack via Socket Mode")},async stop(){l.stop(),await i.stop()},allows:e=>l.allows(e),resolveDestination:(e,a)=>z(i.client,e,{...a?.optedInOnly?{allows:o=>l.allows(o)}:{}}),sameConversation:(e,a)=>b(e).channel===b(a).channel,deliver:async e=>{const a=await _(i.client,{...e,text:await I(e.text,r)});return a?{threadKey:J({teamId:s.teamId,channelId:a.channel,threadTs:a.threadTs})}:{}},attachment:e=>{const a=j(e);return a?v(a,t.credentials.botToken):void 0}}}export{Z as SLACK_ORIGIN,be as createSlackInterface,ae as slackIdentity};
1
+ import{runCommand as C}from"../commands/run.js";import{commandLine as L}from"../commands/syntax.js";import{etiquetteNote as O,threadParticipants as E}from"../interface/etiquette.js";import{threadVerbosity as H}from"../interface/verbosity.js";import{log as f}from"../log.js";import{createSlackApp as F}from"./app.js";import{createAuthorizer as K}from"./authorizer.js";import{botTurnCap as _}from"./bots.js";import{deliverToSlack as U,describeSlackHandle as z,parseSlackHandle as b,resolveSlackDestination as R,slackHandle as W}from"./destination.js";import{describeSlackFile as j,parseSlackFile as q,slackAttachment as v,slackFilePayload as G}from"./files.js";import{registerHandlers as V}from"./handlers.js";import{landedMidThread as Y,threadHistory as $}from"./history.js";import{linkMentions as I,mentionableName as S,renderMentions as T}from"./mentions.js";import{createPresence as J}from"./presence.js";import{say as N}from"./say.js";import{threadKeyString as Q}from"./thread.js";import{userDirectory as X}from"./users.js";const Z={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
+ `)},ee=3e3;async function te(a,i,s=ee){const l=await ae(a.botUserId,i,s),r=S(l)??a.username;if(!r)return;const m=[a.appName,l.realName].filter(h=>!!h?.trim()&&h?.toLowerCase()!==r.toLowerCase());return{address:`@${r}`,...m.length>0?{aka:[...new Set(m)]}:{}}}async function ae(a,i,s){const l={userId:a};let r;const m=new Promise(h=>{r=setTimeout(()=>h(l),s)});try{return await Promise.race([i.lookup(a).catch(()=>l),m])}finally{clearTimeout(r)}}async function be(a){const{app:i,identity:s}=await F(a.credentials),l=await K(i.client,a.allow),r=X(i.client,{self:s.botUserId}),m=_(a.maxBotTurns),h=E(),g=H(a.meta,"slack",a.verbosity),x={verbosity:g,model:a.model,configured:a.configuredModel??{},stop:a.stop},c=await te(s,r),y={...Z,...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,M=async({key:e,keyString:t,thread:o,text:w},d)=>{const n=L(w);if(!n)return!1;const p=C(n,{thread:o},x);try{await N(d.client,{channel:e.channelId,threadTs:e.threadTs},p),k.get(t)?.interrupted()}catch(u){f.warn({thread:t,err:u},"couldn't answer a command")}return!0};async function B(e){const t=await r.lookup(e.userId),o=S(t);return{id:e.userId,...o?{displayName:o}:{},...t.realName?{realName:t.realName}:{},...t.email?{email:t.email}:{},...e.isBot?{isBot:!0}:{}}}function P(e,t){const o=j(t);if(!o.unavailable)return a.attachments.remember(e,y.name,{id:o.id,name:o.name,payload:G(t)})}async function A(e,t){return t.files.length===0?[]:Promise.all(t.files.map(async o=>{const w=P(e.id,o),d=await a.files.save(e.dirKey,v(o,a.credentials.botToken));return w?{...d,ref:w}:d}))}function D(e,t){const o=T(e.messages[0]?.text??"",r),w=Y(e),d=O({participants:e.participants,mentioned:e.messages.some(n=>n.mentionsAgent)});return{thread:e.thread,isNewThread:e.isNewThread,origin:y,...d?{note:d}:{},...w?{history:()=>$({client:t.client,key:e.key,before:w,allows:n=>l.allows(n),answersBots:m.answersBots,self:{botUserId:s.botUserId,...s.botId?{botId:s.botId}:{},...c?{botName:c.address}:{}},users:r,remember:n=>a.attachments.remember(e.thread.id,y.name,n),saw:(n,p)=>h.saw(e.keyString,n,p)})}:{},async messages(){return Promise.all(e.messages.map(async(n,p)=>{const u=await A(e.thread,n);return{principal:await B(n),text:p===0?await o:await T(n.text,r),...u.length>0?{files:u}:{}}}))},presence:()=>{const n=J(e,t,{outgoing:p=>I(p,r),title:()=>o,verbosity:()=>g.of(e.thread.id).level,...d?{mayDecline:!0}:{},...a.taskIntervalMs===void 0?{}:{taskIntervalMs:a.taskIntervalMs}});return k.set(e.keyString,n),{...n,finish:async()=>{try{await n.finish()}finally{k.delete(e.keyString)}}}},async notify(n){await N(t.client,{channel:e.channelId,threadTs:e.threadTs},n)},async destination(){const n=W({channel:e.channelId,threadTs:e.threadTs});return{interface:"slack",handle:n,label:await z(i.client,n)}}}}return V(i,{identity:s,...c?{botName:c.address}:{},threads:a.threads,events:a.events,run:(e,t)=>a.run(D(e,t)),stop:a.stop,allows:e=>l.allows(e),bots:m,participants:h,command:M}),{name:"slack",async start(){await i.start(),s.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"),s.scopes.includes("files:write")||f.warn("this Slack app has no files:write scope, so it cannot send files back; reinstall it to grant one"),r.prime(),f.info("listening on Slack via Socket Mode")},async stop(){l.stop(),await i.stop()},allows:e=>l.allows(e),resolveDestination:(e,t)=>R(i.client,e,{...t?.optedInOnly?{allows:o=>l.allows(o)}:{}}),sameConversation:(e,t)=>b(e).channel===b(t).channel,deliver:async e=>{const t=await U(i.client,{...e,text:await I(e.text,r)});return t?{threadKey:Q({teamId:s.teamId,channelId:t.channel,threadTs:t.threadTs})}:{}},attachment:e=>{const t=q(e);return t?v(t,a.credentials.botToken):void 0}}}export{Z as SLACK_ORIGIN,be as createSlackInterface,te as slackIdentity};
@@ -0,0 +1 @@
1
+ import{commandIn as d,commandLine as u}from"../commands/syntax.js";import{htmlText as f}from"./html.js";const r=/<pre\b[^>]*>([\s\S]*?)<\/pre>/gi,l=/<code\b[^>]*>([\s\S]*?)<\/code>/gi;function i(n){return n.replace(/\s+/g," ").trim()}function s(n){const e=[...n.matchAll(r)];if(e.length>1)return;const[t]=e;if(t){const c=n.replace(r,"");return/<code\b/i.test(c)?void 0:t[1]}const o=[...n.matchAll(l)];return o.length===1?o[0]?.[1]:void 0}function a(n){const e=u(n.text);if(e)return e;if(!n.html)return;const t=s(n.html);if(t===void 0)return;const o=f(t);if(i(o)===i(n.text))return d(o)}export{a as teamsCommandLine};
@@ -1,2 +1,2 @@
1
- import{randomUUID as L}from"node:crypto";import{readFile as I}from"node:fs/promises";import{App as $}from"@microsoft/teams.apps";import{BishopError as x}from"../errors.js";import{log as h}from"../log.js";import{retry as g}from"../retry.js";import{teamsRetryable as y}from"./api.js";import{teamsAllowList as S}from"./authorize.js";import{DOWNLOAD_TIMEOUT_MS as U,teamsFilesOf as C}from"./files.js";import{teamsGraph as F}from"./graph.js";import{createTeamsInterface as k}from"./interface.js";import{teamsLogger as v}from"./logger.js";import{createListener as M}from"./server.js";import{graphAddressOf as R}from"./thread.js";const q=3978;async function B(e){const t=async(c,s)=>{try{return await I(c,"utf8")}catch(d){throw new x(`Couldn't read the TLS ${s} at ${c}: ${d.message}`,"ConfigError")}};return{cert:await t(e.cert,"certificate"),key:await t(e.key,"key")}}function p(e,t=[]){return{type:"message",text:e,...t.length>0?{attachments:t}:{}}}const j="application/vnd.microsoft.teams.card.file.consent",A="application/vnd.microsoft.teams.card.file.info";function E(e){if(!e||typeof e!="object")return;const t=e.ref;return typeof t=="string"?t:void 0}function N(e){const t={description:`Bishop wants to send ${e.name}.`,sizeInBytes:e.size,acceptContext:{ref:e.ref},declineContext:{ref:e.ref}};return{contentType:j,name:e.name,content:t}}function _(e){return{id:L(),contentType:A,contentUrl:e.webUrl,name:e.name,content:{uniqueId:e.uniqueId,fileType:e.fileType}}}async function D(e,t){const c=t.uploadInfo?.uploadUrl;if(!c)throw new Error("Teams returned no upload URL for the accepted file");const s=T(c),d=new Uint8Array(await I(e.path)),m=Math.max(0,d.byteLength-1),w=await fetch(s,{method:"PUT",headers:{"content-type":"application/octet-stream","content-length":String(d.byteLength),"content-range":`bytes 0-${m}/${d.byteLength}`},body:d});if(!w.ok)throw new Error(`Teams returned ${w.status} while receiving ${e.name}`)}async function se(e){const t=S(e.config?.allow),c=M({port:e.config?.port??q,...e.config?.host?{host:e.config.host}:{},...e.config?.tls?{tls:await B(e.config.tls)}:{}}),s=new $({clientId:e.credentials.clientId,clientSecret:e.credentials.clientSecret,tenantId:e.credentials.tenantId,httpServerAdapter:c,logger:v()}),d=F(e.credentials),m=new Map,b=k({api:{async send(r,n,o){const a=o?.files??[];let i=n,u=[];o?.channel&&a.length>0?(u=(await Promise.all(a.map(l=>d.uploadChannelFile(o.channel,l)))).map(_),i=[n,...u.map(l=>`<attachment id="${l.id}"></attachment>`)].filter(Boolean).join(`
2
- `)):a.length>0&&(u=a.map(f=>(m.set(f.ref,f),N(f))));try{const f=await g(()=>s.send(r,p(i,u)),{what:"teams send",retryable:y});return f?.id?{id:f.id}:void 0}catch(f){for(const l of a)m.delete(l.ref);throw f}},async update(r,n,o){await g(()=>s.api.conversations.updateActivity(r,n,p(o)),{what:"teams update",retryable:y})},async member(r,n){return await g(()=>s.api.conversations.getMemberById(r,n),{what:"teams roster",retryable:y})},async sharedFiles(r){const n=R(r);return n?d.files(n):[]},async fetchFile(r){const n=await fetch(T(r),{signal:AbortSignal.timeout(U)});if(!n.ok)throw new Error(`the download returned ${n.status}`);if(!n.body)throw new Error("the download sent no body");return n.body},async fetchAttachment(r){const n=T(r),o=new URL(s.api.serviceUrl);if(n.host!==o.host)throw h.warn({host:n.host,service:o.host},"refusing to send the app credential to a host that is not the Teams service"),new Error("the image is not hosted on the Teams service");const a=await s.api.http.get(n.toString(),{responseType:"arraybuffer",timeout:U});return P(a.data)},async fetchShared(r){return d.fetchShared(r)}},allow:t,...e.config?.verbosity?{verbosity:e.config.verbosity}:{},listener:c,threads:e.threads,events:e.events,files:e.files,attachments:e.attachments,run:e.run,stop:e.stop});s.on("message",async({activity:r})=>{await b.handle(G(r))}),s.on("file.consent.accept",async({activity:r,send:n})=>(await O(r.value,n),{status:200})),s.on("file.consent.decline",async({activity:r,send:n})=>{const o=E(r.value.context),a=o?m.get(o):void 0;return o&&m.delete(o),a?(await n(p(`${a.name} was not sent because the file-consent request was declined.`)),{status:200}):(h.warn("received a Teams file-consent decline for an unknown file"),{status:200})});async function O(r,n){const o=E(r.context),a=o?m.get(o):void 0;if(o&&m.delete(o),!a){h.warn("received a Teams file-consent acceptance for an unknown file");return}try{await D(a,r);const i=r.uploadInfo;if(!i?.contentUrl||!i.uniqueId)throw new Error("Teams returned incomplete file information after the upload");await n(p(`${a.name} was sent.`,[{contentType:A,contentUrl:i.contentUrl,name:i.name??a.name,content:{uniqueId:i.uniqueId,fileType:i.fileType??a.mimeType}}]))}catch(i){h.warn({err:i,file:a.name},"could not finish a Teams file upload"),await n(p(`Could not send ${a.name}: ${i.message}`))}}return await s.initialize(),h.info({endpoint:s.server.messagingEndpoint},"the Teams app is mounted"),b}function T(e){const t=new URL(e);if(t.protocol!=="https:")throw new Error("the download URL is not HTTPS");return t}function P(e){if(e instanceof Uint8Array)return e;if(e instanceof ArrayBuffer)return new Uint8Array(e);throw new Error("the download came back as something other than bytes")}function z(e){const t=e.recipient?.id,c=t?e.stripMentionsText?.({accountId:t}):e;return(c?.stripMentionsText?.({tagOnly:!0})??c)?.text??e.text??""}function G(e){const t=H(e.channelData);return{id:e.id,conversationId:e.conversation.id,conversationType:typeof e.conversation.conversationType=="string"?e.conversation.conversationType:"channel",text:z(e),mentioned:e.isRecipientMentioned?.()===!0,from:{id:e.from.id,...e.from.aadObjectId?{aadObjectId:e.from.aadObjectId}:{},...e.from.name?{name:e.from.name}:{}},...e.recipient?.id?{recipientId:e.recipient.id}:{},...e.recipient?.name?{recipientName:e.recipient.name}:{},files:C(e.attachments),...t?{channel:t}:{}}}function H(e){const t=e?.team?.aadGroupId,c=e?.channel?.id;if(!(typeof t!="string"||typeof c!="string"))return{teamId:t,channelId:c}}export{se as createTeams,G as inboundOf};
1
+ import{randomUUID as x}from"node:crypto";import{readFile as I}from"node:fs/promises";import{App as $}from"@microsoft/teams.apps";import{BishopError as S}from"../errors.js";import{log as h}from"../log.js";import{retry as g}from"../retry.js";import{teamsRetryable as y}from"./api.js";import{teamsAllowList as M}from"./authorize.js";import{DOWNLOAD_TIMEOUT_MS as U,teamsFilesOf as C}from"./files.js";import{teamsGraph as F}from"./graph.js";import{createTeamsInterface as k}from"./interface.js";import{teamsLogger as v}from"./logger.js";import{createListener as R}from"./server.js";import{graphAddressOf as O}from"./thread.js";const q=3978;async function B(e){const t=async(s,c)=>{try{return await I(s,"utf8")}catch(d){throw new S(`Couldn't read the TLS ${c} at ${s}: ${d.message}`,"ConfigError")}};return{cert:await t(e.cert,"certificate"),key:await t(e.key,"key")}}function p(e,t=[]){return{type:"message",text:e,...t.length>0?{attachments:t}:{}}}const j="application/vnd.microsoft.teams.card.file.consent",A="application/vnd.microsoft.teams.card.file.info";function E(e){if(!e||typeof e!="object")return;const t=e.ref;return typeof t=="string"?t:void 0}function N(e){const t={description:`Bishop wants to send ${e.name}.`,sizeInBytes:e.size,acceptContext:{ref:e.ref},declineContext:{ref:e.ref}};return{contentType:j,name:e.name,content:t}}function _(e){return{id:x(),contentType:A,contentUrl:e.webUrl,name:e.name,content:{uniqueId:e.uniqueId,fileType:e.fileType}}}async function D(e,t){const s=t.uploadInfo?.uploadUrl;if(!s)throw new Error("Teams returned no upload URL for the accepted file");const c=T(s),d=new Uint8Array(await I(e.path)),l=Math.max(0,d.byteLength-1),w=await fetch(c,{method:"PUT",headers:{"content-type":"application/octet-stream","content-length":String(d.byteLength),"content-range":`bytes 0-${l}/${d.byteLength}`},body:d});if(!w.ok)throw new Error(`Teams returned ${w.status} while receiving ${e.name}`)}async function ce(e){const t=M(e.config?.allow),s=R({port:e.config?.port??q,...e.config?.host?{host:e.config.host}:{},...e.config?.tls?{tls:await B(e.config.tls)}:{}}),c=new $({clientId:e.credentials.clientId,clientSecret:e.credentials.clientSecret,tenantId:e.credentials.tenantId,httpServerAdapter:s,logger:v()}),d=F(e.credentials),l=new Map,b=k({api:{async send(r,n,o){const a=o?.files??[];let i=n,u=[];o?.channel&&a.length>0?(u=(await Promise.all(a.map(m=>d.uploadChannelFile(o.channel,m)))).map(_),i=[n,...u.map(m=>`<attachment id="${m.id}"></attachment>`)].filter(Boolean).join(`
2
+ `)):a.length>0&&(u=a.map(f=>(l.set(f.ref,f),N(f))));try{const f=await g(()=>c.send(r,p(i,u)),{what:"teams send",retryable:y});return f?.id?{id:f.id}:void 0}catch(f){for(const m of a)l.delete(m.ref);throw f}},async update(r,n,o){await g(()=>c.api.conversations.updateActivity(r,n,p(o)),{what:"teams update",retryable:y})},async member(r,n){return await g(()=>c.api.conversations.getMemberById(r,n),{what:"teams roster",retryable:y})},async sharedFiles(r){const n=O(r);return n?d.files(n):[]},async history(r){const n=O(r);if(n)return d.conversation(n)},async fetchFile(r){const n=await fetch(T(r),{signal:AbortSignal.timeout(U)});if(!n.ok)throw new Error(`the download returned ${n.status}`);if(!n.body)throw new Error("the download sent no body");return n.body},async fetchAttachment(r){const n=T(r),o=new URL(c.api.serviceUrl);if(n.host!==o.host)throw h.warn({host:n.host,service:o.host},"refusing to send the app credential to a host that is not the Teams service"),new Error("the image is not hosted on the Teams service");const a=await c.api.http.get(n.toString(),{responseType:"arraybuffer",timeout:U});return P(a.data)},async fetchShared(r){return d.fetchShared(r)}},allow:t,...e.config?.verbosity?{verbosity:e.config.verbosity}:{},listener:s,meta:e.meta,model:e.model,...e.configuredModel?{configuredModel:e.configuredModel}:{},threads:e.threads,events:e.events,files:e.files,attachments:e.attachments,run:e.run,stop:e.stop});c.on("message",async({activity:r})=>{await b.handle(G(r))}),c.on("file.consent.accept",async({activity:r,send:n})=>(await L(r.value,n),{status:200})),c.on("file.consent.decline",async({activity:r,send:n})=>{const o=E(r.value.context),a=o?l.get(o):void 0;return o&&l.delete(o),a?(await n(p(`${a.name} was not sent because the file-consent request was declined.`)),{status:200}):(h.warn("received a Teams file-consent decline for an unknown file"),{status:200})});async function L(r,n){const o=E(r.context),a=o?l.get(o):void 0;if(o&&l.delete(o),!a){h.warn("received a Teams file-consent acceptance for an unknown file");return}try{await D(a,r);const i=r.uploadInfo;if(!i?.contentUrl||!i.uniqueId)throw new Error("Teams returned incomplete file information after the upload");await n(p(`${a.name} was sent.`,[{contentType:A,contentUrl:i.contentUrl,name:i.name??a.name,content:{uniqueId:i.uniqueId,fileType:i.fileType??a.mimeType}}]))}catch(i){h.warn({err:i,file:a.name},"could not finish a Teams file upload"),await n(p(`Could not send ${a.name}: ${i.message}`))}}return await c.initialize(),h.info({endpoint:c.server.messagingEndpoint},"the Teams app is mounted"),b}function T(e){const t=new URL(e);if(t.protocol!=="https:")throw new Error("the download URL is not HTTPS");return t}function P(e){if(e instanceof Uint8Array)return e;if(e instanceof ArrayBuffer)return new Uint8Array(e);throw new Error("the download came back as something other than bytes")}function z(e){const t=e.recipient?.id,s=t?e.stripMentionsText?.({accountId:t}):e;return(s?.stripMentionsText?.({tagOnly:!0})??s)?.text??e.text??""}function G(e){const t=W(e.channelData);return{id:e.id,conversationId:e.conversation.id,conversationType:typeof e.conversation.conversationType=="string"?e.conversation.conversationType:"channel",text:z(e),...H(e.attachments),mentioned:e.isRecipientMentioned?.()===!0,from:{id:e.from.id,...e.from.aadObjectId?{aadObjectId:e.from.aadObjectId}:{},...e.from.name?{name:e.from.name}:{}},...e.recipient?.id?{recipientId:e.recipient.id}:{},...e.recipient?.name?{recipientName:e.recipient.name}:{},files:C(e.attachments),...t?{channel:t}:{}}}function H(e){const t=e?.find(s=>s.contentType?.toLowerCase()==="text/html"&&typeof s.content=="string")?.content;return typeof t=="string"?{html:t}:{}}function W(e){const t=e?.team?.aadGroupId,s=e?.channel?.id;if(!(typeof t!="string"||typeof s!="string"))return{teamId:t,channelId:s}}export{ce as createTeams,G as inboundOf};
@@ -1 +1 @@
1
- import{FILE_DOWNLOAD_INFO as x,GRAPH_REFERENCE as F}from"./files.js";function b(){const n=[],l=[],p=[],u=[],h=[],f=new Map,m=new Map,y=new Map;let s,i,c,w=!1,g=1;const d=(e,t)=>{u.push({url:e,credential:t});const o=y.get(e);if(o===void 0)throw new Error(`nothing hosted at ${e}`);return new TextEncoder().encode(o)};return{sent:n,edits:l,rosterCalls:p,fetched:u,sharedCalls:h,addMember(e,t){m.set(`${e}:${t.id}`,t)},addBytes(e,t){y.set(e,t)},addShared(e,t){f.set(e,t)},failNextSend(e=new Error("Teams is down")){s=e},failNextUpdate(e=new Error("Teams is down")){i=e},sendAnonymously(){w=!0},failMember(e=new Error("no roster access")){c=e},async send(e,t,o){if(s){const a=s;throw s=void 0,a}const r=`activity-${g++}`;return n.push({conversationId:e,text:t,id:r,...o?.files?.length?{files:o.files}:{}}),w?void 0:{id:r}},async update(e,t,o){if(i){const a=i;throw i=void 0,a}l.push({conversationId:e,activityId:t,text:o});const r=n.find(a=>a.id===t);if(!r)throw new Error(`no activity ${t} to update`);r.text=o},async member(e,t){if(p.push(e),c)throw c;return m.get(`${e}:${t}`)},async sharedFiles(e){return e.conversationType==="personal"?[]:(h.push(e.id),f.get(e.id)??[])},async fetchFile(e){return d(e,"none")},async fetchAttachment(e){return d(e,"bot")},async fetchShared(e){return d(e,"graph")}}}function T(n={}){return{contentType:x,name:"doc.txt",uniqueId:"1150D938-8870-4044-9F2C-5BBDEBA70C9D",contentUrl:"https://contoso.sharepoint.com/personal/jerry_contoso_com/Documents/doc.txt",etag:"123",downloadUrl:"https://download.contoso.com/doc.txt?tempauth=abc",...n}}function D(n={}){return{contentType:F,name:"deploy.log",uniqueId:"fe1a6fcf-0000-4000-8000-00000000000b",etag:'"{FE1A6FCF-0000-4000-8000-00000000000B},2"',contentUrl:"https://contoso.sharepoint.com/sites/ops/Shared Documents/General/deploy.log",size:24,mimeType:"text/plain",...n}}function $(n={}){return{contentType:"image/png",contentUrl:"https://smba.trafficmanager.net/amer/v3/attachments/img-1/views/original",...n}}function A(n={}){return{id:"1700000000001",conversationId:"19:chat@thread.v2",conversationType:"personal",text:"what is the deploy status",mentioned:!1,recipientId:"28:the-agent",recipientName:"bishop",files:[],...n,from:{id:"29:bot-scoped-id",aadObjectId:"00000000-0000-0000-0000-00000000000a",name:"Hayes",...n.from}}}export{T as aFile,A as aMessage,D as aSharedFile,$ as anImage,b as fakeTeams};
1
+ import{FILE_DOWNLOAD_INFO as F,GRAPH_REFERENCE as E}from"./files.js";function M(){const n=[],p=[],u=[],l=[],h=[],f=[],m=new Map,y=new Map,w=new Map,g=new Map;let a,i,d,b=!1,x=1;const c=(e,t)=>{l.push({url:e,credential:t});const o=g.get(e);if(o===void 0)throw new Error(`nothing hosted at ${e}`);return new TextEncoder().encode(o)};return{sent:n,edits:p,rosterCalls:u,fetched:l,sharedCalls:h,historyCalls:f,addMember(e,t){w.set(`${e}:${t.id}`,t)},addBytes(e,t){g.set(e,t)},addShared(e,t){y.set(e,t)},addHistory(e,t){m.set(e,t)},failNextSend(e=new Error("Teams is down")){a=e},failNextUpdate(e=new Error("Teams is down")){i=e},sendAnonymously(){b=!0},failMember(e=new Error("no roster access")){d=e},async send(e,t,o){if(a){const s=a;throw a=void 0,s}const r=`activity-${x++}`;return n.push({conversationId:e,text:t,id:r,...o?.files?.length?{files:o.files}:{}}),b?void 0:{id:r}},async update(e,t,o){if(i){const s=i;throw i=void 0,s}p.push({conversationId:e,activityId:t,text:o});const r=n.find(s=>s.id===t);if(!r)throw new Error(`no activity ${t} to update`);r.text=o},async member(e,t){if(u.push(e),d)throw d;return w.get(`${e}:${t}`)},async sharedFiles(e){return e.conversationType==="personal"?[]:(h.push(e.id),y.get(e.id)??[])},async history(e){if(e.conversationType==="personal")return;f.push(e.id);const t=m.get(e.conversationId);if(!t)return;const o=r=>Number(r.id)<Number(e.id);return{...t,messages:t.messages.filter(o)}},async fetchFile(e){return c(e,"none")},async fetchAttachment(e){return c(e,"bot")},async fetchShared(e){return c(e,"graph")}}}function _(n={}){return{contentType:F,name:"doc.txt",uniqueId:"1150D938-8870-4044-9F2C-5BBDEBA70C9D",contentUrl:"https://contoso.sharepoint.com/personal/jerry_contoso_com/Documents/doc.txt",etag:"123",downloadUrl:"https://download.contoso.com/doc.txt?tempauth=abc",...n}}function N(n={}){return{contentType:E,name:"deploy.log",uniqueId:"fe1a6fcf-0000-4000-8000-00000000000b",etag:'"{FE1A6FCF-0000-4000-8000-00000000000B},2"',contentUrl:"https://contoso.sharepoint.com/sites/ops/Shared Documents/General/deploy.log",size:24,mimeType:"text/plain",...n}}function v(n={}){return{contentType:"image/png",contentUrl:"https://smba.trafficmanager.net/amer/v3/attachments/img-1/views/original",...n}}function C(n={}){return{id:"1700000000001",conversationId:"19:chat@thread.v2",conversationType:"personal",text:"what is the deploy status",mentioned:!1,recipientId:"28:the-agent",recipientName:"bishop",files:[],...n,from:{id:"29:bot-scoped-id",aadObjectId:"00000000-0000-0000-0000-00000000000a",name:"Hayes",...n.from}}}function D(n={}){return{id:"1700000000000",at:17e11,user:{id:"00000000-0000-0000-0000-00000000000b",name:"Kim"},html:"<p>the deploy is stuck on step three</p>",appMentions:[],files:[],...n}}export{_ as aFile,D as aGraphMessage,C as aMessage,N as aSharedFile,v as anImage,M as fakeTeams};
@@ -1 +1 @@
1
- import{readFile as E}from"node:fs/promises";import{log as f}from"../log.js";import{retry as w}from"../retry.js";import{DOWNLOAD_TIMEOUT_MS as y,GRAPH_REFERENCE as $}from"./files.js";const m="https://graph.microsoft.com/v1.0",U="https://login.microsoftonline.com",S=1e4,R=2,G=1e3,M=6e4,D=2,k=1e3,P=6e4;class u extends Error{status;retryAfterMs;constructor(s,c,h){super(s),this.status=c,this.retryAfterMs=h,this.name="GraphError"}}function I(o){const{status:s,retryAfterMs:c}=o??{};return typeof s!="number"?!1:s===0?!0:s===429?c===void 0?!0:{retryAfterMs:c}:s>=500}function p(o){const s=o.headers.get("retry-after");if(s===null)return;const c=Number(s);return Number.isFinite(c)&&c>=0?c*1e3:void 0}function g(o){return`u!${Buffer.from(o,"utf8").toString("base64url")}`}function v(o){if(o.kind==="chat")return`/chats/${o.chatId}/messages/${o.messageId}`;const s=`/teams/${o.teamId}/channels/${o.channelId}/messages`;return o.replyToId&&o.replyToId!==o.messageId?`${s}/${o.replyToId}/replies/${o.messageId}`:`${s}/${o.messageId}`}function B(o){let s;async function c(a){if(s&&s.expiresAt>Date.now())return s.token;const n=await fetch(`${U}/${o.tenantId}/oauth2/v2.0/token`,{method:"POST",headers:{"content-type":"application/x-www-form-urlencoded"},body:new URLSearchParams({client_id:o.clientId,client_secret:o.clientSecret,grant_type:"client_credentials",scope:"https://graph.microsoft.com/.default"}),signal:a}).catch(t=>{throw new u(`could not reach Entra: ${t.message}`,0)});if(!n.ok)throw new u(`Entra refused the app credentials with ${n.status}`,n.status,p(n));const e=await n.json();if(!e.access_token)throw new u("Entra returned no access token",0);const r=(e.expires_in??3600)*1e3;return s={token:e.access_token,expiresAt:Date.now()+r-P},s.token}async function h(a,n,e){return w(async()=>{const r=await fetch(`${m}${a}`,{headers:{authorization:`Bearer ${await c(e)}`},signal:e}).catch(t=>{throw new u(`could not reach Graph: ${t.message}`,0)});if(!r.ok)throw new u(`Graph answered ${n} with ${r.status}`,r.status,p(r));return await r.json()},{what:`graph ${n}`,retryable:I,attempts:R,maxDelayMs:G})}async function T(a,n,e,r){return w(async()=>{const t=await fetch(`${m}${a}`,{method:"PUT",headers:{authorization:`Bearer ${await c(r)}`,"content-type":"application/octet-stream","content-length":String(e.byteLength)},body:e,signal:r}).catch(i=>{throw new u(`could not reach Graph: ${i.message}`,0)});if(!t.ok)throw new u(`Graph answered ${n} with ${t.status}`,t.status,p(t));return await t.json()},{what:`graph ${n}`,retryable:I,attempts:D,maxDelayMs:k})}async function A(a,n,e,r){try{const i=(await h(`/drives/${encodeURIComponent(a)}/items/${encodeURIComponent(n)}?$select=sharepointIds`,"the SharePoint identifiers of an uploaded file",r)).sharepointIds?.listItemUniqueId;if(i)return i}catch(t){f.debug({err:t,itemId:n},"could not read the SharePoint ID of an uploaded file")}return e?.match(/\{([0-9a-fA-F-]{36})\}/)?.[1]}async function b(a,n){const e=await h(`/teams/${encodeURIComponent(a.teamId)}/channels/${encodeURIComponent(a.channelId)}/filesFolder`,"a channel files folder",n);if(!e.id||!e.parentReference?.driveId)throw new u("Graph returned an incomplete channel files folder",0);return{driveId:e.parentReference.driveId,folderId:e.id}}async function _(a,n){try{return await h(`/shares/${g(a)}/driveItem`,"a shared file",n)}catch(e){const r=e instanceof u?e.status:0;return f.warn({err:e,status:r},"could not read a file shared in a Teams conversation"),r===401||r===403?{refused:"Bishop is not allowed to read files in this Microsoft 365 tenant"}:void 0}}return{async files(a){const n=AbortSignal.timeout(S);let e;try{e=await h(v(a),"a message",n)}catch(t){return f.warn({err:t,address:a.kind},"could not read a Teams message from Graph"),[]}const r=(e.attachments??[]).filter(t=>t.contentType===$&&t.contentUrl);return Promise.all(r.map(async t=>{const i=t.contentUrl,d=await _(i,n),l={contentType:$,contentUrl:i,...t.id?{uniqueId:t.id}:{},...t.name?{name:t.name}:{}};return d?"refused"in d?{...l,unavailable:d.refused}:{...l,name:t.name??d.name,...d.eTag?{etag:d.eTag}:{},...d.size===void 0?{}:{size:d.size},...d.file?.mimeType?{mimeType:d.file.mimeType}:{}}:l}))},async fetchShared(a){const n=AbortSignal.timeout(y),e=await fetch(`${m}/shares/${g(a)}/driveItem/content`,{headers:{authorization:`Bearer ${await c(n)}`},redirect:"manual",signal:n});if(e.status>=300&&e.status<400){const r=e.headers.get("location");if(!r)throw new Error("Graph redirected the download to nowhere");const t=await fetch(r,{signal:AbortSignal.timeout(y)});if(!t.ok)throw new Error(`the download returned ${t.status}`);if(!t.body)throw new Error("the download sent no body");return t.body}if(!e.ok)throw new Error(`Graph answered the download with ${e.status}`);if(!e.body)throw new Error("the download sent no body");return e.body},async uploadChannelFile(a,n){const e=AbortSignal.timeout(M),r=new Uint8Array(await E(n.path)),t=await b(a,e),i=await T(`/drives/${encodeURIComponent(t.driveId)}/items/${encodeURIComponent(t.folderId)}:/${encodeURIComponent(n.name)}:/content`,`the file ${n.name} in a Teams channel`,r,e);if(!i.id||!i.webUrl)throw new u(`Graph returned an incomplete upload for ${n.name}`,0);return{id:i.id,uniqueId:await A(t.driveId,i.id,i.eTag,e),name:i.name??n.name,webUrl:i.webUrl,fileType:i.file?.mimeType??n.mimeType}}}}export{I as graphRetryable,B as teamsGraph};
1
+ import{readFile as P}from"node:fs/promises";import{log as p}from"../log.js";import{retry as b}from"../retry.js";import{DOWNLOAD_TIMEOUT_MS as A,GRAPH_REFERENCE as _}from"./files.js";const g="https://graph.microsoft.com/v1.0",v="https://login.microsoftonline.com",N=1e4,O=2,x=1e3,L=2e4,E=50,C=3,B=10,F=6e4,z=2,q=1e3,H=6e4;class l extends Error{status;retryAfterMs;constructor(i,c,u){super(i),this.status=c,this.retryAfterMs=u,this.name="GraphError"}}function S(e){const{status:i,retryAfterMs:c}=e??{};return typeof i!="number"?!1:i===0?!0:i===429?c===void 0?!0:{retryAfterMs:c}:i>=500}function I(e){const i=e.headers.get("retry-after");if(i===null)return;const c=Number(i);return Number.isFinite(c)&&c>=0?c*1e3:void 0}function U(e){return`u!${Buffer.from(e,"utf8").toString("base64url")}`}function Y(e){if(e.kind==="chat")return`/chats/${e.chatId}/messages/${e.messageId}`;const i=`/teams/${e.teamId}/channels/${e.channelId}/messages`;return e.replyToId&&e.replyToId!==e.messageId?`${i}/${e.replyToId}/replies/${e.messageId}`:`${i}/${e.messageId}`}function M(e){if(!(e.contentType!==_||!e.contentUrl))return{contentType:_,contentUrl:e.contentUrl,...e.id?{uniqueId:e.id}:{},...e.name?{name:e.name}:{}}}function j(e){if(!e.id||e.messageType&&e.messageType!=="message"||e.deletedDateTime)return;const i=e.createdDateTime?Date.parse(e.createdDateTime):Number.NaN,c=e.from?.user,u=e.from?.application,y=e.body?.content??"";return{id:e.id,...Number.isFinite(i)?{at:i}:{},...c?.id?{user:{id:c.id,...c.displayName?{name:c.displayName}:{}}}:{},...u?.id?{app:{id:u.id}}:{},html:e.body?.contentType==="text"?y.replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;"):y,appMentions:(e.mentions??[]).flatMap(m=>{const $=m.mentioned?.application?.id;return $&&m.id!==void 0?[{id:String(m.id),appId:$}]:[]}),files:(e.attachments??[]).flatMap(m=>M(m)??[])}}function G(e,i){return/^\d+$/.test(e)&&/^\d+$/.test(i)?Number(e)<Number(i):e!==i}function D(e){return[...e].sort((i,c)=>(i.at??0)-(c.at??0)||(G(i.id,c.id)?-1:1))}function Q(e){let i;async function c(o){if(i&&i.expiresAt>Date.now())return i.token;const r=await fetch(`${v}/${e.tenantId}/oauth2/v2.0/token`,{method:"POST",headers:{"content-type":"application/x-www-form-urlencoded"},body:new URLSearchParams({client_id:e.clientId,client_secret:e.clientSecret,grant_type:"client_credentials",scope:"https://graph.microsoft.com/.default"}),signal:o}).catch(n=>{throw new l(`could not reach Entra: ${n.message}`,0)});if(!r.ok)throw new l(`Entra refused the app credentials with ${r.status}`,r.status,I(r));const t=await r.json();if(!t.access_token)throw new l("Entra returned no access token",0);const s=(t.expires_in??3600)*1e3;return i={token:t.access_token,expiresAt:Date.now()+s-H},i.token}async function u(o,r,t){return b(async()=>{const s=await fetch(o.startsWith(`${g}/`)?o:`${g}${o}`,{headers:{authorization:`Bearer ${await c(t)}`},signal:t}).catch(n=>{throw new l(`could not reach Graph: ${n.message}`,0)});if(!s.ok)throw new l(`Graph answered ${r} with ${s.status}`,s.status,I(s));return await s.json()},{what:`graph ${r}`,retryable:S,attempts:O,maxDelayMs:x})}async function y(o,r,t,s){return b(async()=>{const n=await fetch(`${g}${o}`,{method:"PUT",headers:{authorization:`Bearer ${await c(s)}`,"content-type":"application/octet-stream","content-length":String(t.byteLength)},body:t,signal:s}).catch(a=>{throw new l(`could not reach Graph: ${a.message}`,0)});if(!n.ok)throw new l(`Graph answered ${r} with ${n.status}`,n.status,I(n));return await n.json()},{what:`graph ${r}`,retryable:S,attempts:z,maxDelayMs:q})}async function m(o,r,t,s){try{const a=(await u(`/drives/${encodeURIComponent(o)}/items/${encodeURIComponent(r)}?$select=sharepointIds`,"the SharePoint identifiers of an uploaded file",s)).sharepointIds?.listItemUniqueId;if(a)return a}catch(n){p.debug({err:n,itemId:r},"could not read the SharePoint ID of an uploaded file")}return t?.match(/\{([0-9a-fA-F-]{36})\}/)?.[1]}async function $(o,r){const t=await u(`/teams/${encodeURIComponent(o.teamId)}/channels/${encodeURIComponent(o.channelId)}/filesFolder`,"a channel files folder",r);if(!t.id||!t.parentReference?.driveId)throw new l("Graph returned an incomplete channel files folder",0);return{driveId:t.parentReference.driveId,folderId:t.id}}async function R(o,r){try{return await u(`/shares/${U(o)}/driveItem`,"a shared file",r)}catch(t){const s=t instanceof l?t.status:0;return p.warn({err:t,status:s},"could not read a file shared in a Teams conversation"),s===401||s===403?{refused:"Bishop is not allowed to read files in this Microsoft 365 tenant"}:void 0}}async function T(o,r,t,s){const n=[];let a=o;try{for(let f=0;a&&f<r;f+=1){const d=await u(a,t,s);n.push(...d.value??[]);const h=d["@odata.nextLink"];if(a=h?.startsWith(`${g}/`)?h:void 0,h&&!a)return p.warn({what:t},"Graph offered a next page somewhere other than Graph"),{raw:n,complete:!1,failed:!1}}return{raw:n,complete:!a,failed:!1}}catch(f){return p.warn({err:f,what:t,read:n.length},"could not read the whole conversation"),{raw:n,complete:!1,failed:!0}}}return{async conversation(o){const r=AbortSignal.timeout(L),t=d=>{const h=new Set;return d.flatMap(k=>{const w=j(k);return!w||!G(w.id,o.messageId)||h.has(w.id)?[]:(h.add(w.id),[w])})};if(o.kind==="chat"){const d=await T(`/chats/${o.chatId}/messages?$top=${E}&$orderby=createdDateTime desc`,C,"a chat",r);return d.failed&&d.raw.length===0?void 0:{messages:D(t(d.raw)),complete:d.complete}}const s=o.replyToId;if(!s||s===o.messageId)return{messages:[],complete:!0};const n=`/teams/${o.teamId}/channels/${o.channelId}/messages/${s}`,[a,f]=await Promise.all([u(n,"the message that opened a thread",r).then(d=>d,d=>{p.warn({err:d},"could not read the message that opened a Teams thread")}),T(`${n}/replies?$top=${E}`,B,"a thread",r)]);if(!(!a&&f.failed&&f.raw.length===0))return{messages:D(t([...a?[a]:[],...f.raw])),complete:!!a&&f.complete}},async files(o){const r=AbortSignal.timeout(N);let t;try{t=await u(Y(o),"a message",r)}catch(n){return p.warn({err:n,address:o.kind},"could not read a Teams message from Graph"),[]}const s=(t.attachments??[]).flatMap(n=>M(n)??[]);return Promise.all(s.map(async n=>{const a=await R(n.contentUrl,r);return a?"refused"in a?{...n,unavailable:a.refused}:{...n,name:n.name??a.name,...a.eTag?{etag:a.eTag}:{},...a.size===void 0?{}:{size:a.size},...a.file?.mimeType?{mimeType:a.file.mimeType}:{}}:n}))},async fetchShared(o){const r=AbortSignal.timeout(A),t=await fetch(`${g}/shares/${U(o)}/driveItem/content`,{headers:{authorization:`Bearer ${await c(r)}`},redirect:"manual",signal:r});if(t.status>=300&&t.status<400){const s=t.headers.get("location");if(!s)throw new Error("Graph redirected the download to nowhere");const n=await fetch(s,{signal:AbortSignal.timeout(A)});if(!n.ok)throw new Error(`the download returned ${n.status}`);if(!n.body)throw new Error("the download sent no body");return n.body}if(!t.ok)throw new Error(`Graph answered the download with ${t.status}`);if(!t.body)throw new Error("the download sent no body");return t.body},async uploadChannelFile(o,r){const t=AbortSignal.timeout(F),s=new Uint8Array(await P(r.path)),n=await $(o,t),a=await y(`/drives/${encodeURIComponent(n.driveId)}/items/${encodeURIComponent(n.folderId)}:/${encodeURIComponent(r.name)}:/content`,`the file ${r.name} in a Teams channel`,s,t);if(!a.id||!a.webUrl)throw new l(`Graph returned an incomplete upload for ${r.name}`,0);return{id:a.id,uniqueId:await m(n.driveId,a.id,a.eTag,t),name:a.name??r.name,webUrl:a.webUrl,fileType:a.file?.mimeType??r.mimeType}}}}export{S as graphRetryable,Q as teamsGraph};
@@ -0,0 +1 @@
1
+ import{describedFile as w}from"../files.js";import{declareUnread as y,trimHistory as b,withinFileBudget as v}from"../interface/history.js";import{log as p}from"../log.js";import{describeTeamsFile as I}from"./files.js";import{messageText as h}from"./html.js";import{threadIdOf as k}from"./thread.js";function C(t){if(t.conversationType==="personal")return!1;if(t.conversationType==="groupChat")return!0;const e=k(t.conversationId);return!!e&&e!==t.id}async function H(t){const{read:e}=t;if(!e)return p.warn("could not read the Teams conversation to catch the agent up"),[{kind:"omitted",reason:"unavailable"}];for(const n of e.messages)n.user&&n.at!==void 0&&t.saw?.(n.user.id,n.at);const a=new Map,s=new Map(e.messages.flatMap(n=>n.user?[[n.user.id,n.user]]:[]));await Promise.all([...s.values()].map(async n=>{try{a.set(n.id,await t.sender(n))}catch(d){p.warn({err:d,who:n.id},"could not check a Teams sender for history")}}));const i=B(e.messages,t,a),o=b(i.entries),c=o[1]?.kind==="omitted"&&o[1].reason==="trimmed",r=!e.complete&&!t.openedBy&&c?o.slice(1):o,f=v(r).map(n=>T(n,t));return e.complete?f:y(f,i.opensWithParent)}function B(t,e,a){const s=[];let i=!1,o=0;const c=()=>{o&&(s.push({kind:"omitted",reason:"unauthorized",count:o}),o=0)};for(const r of t){const u=r.id===e.openedBy,f=!!e.selfAppId&&r.app?.id===e.selfAppId,n=r.user?a.get(r.user.id):void 0;if(!f&&!n?.allowed){u&&(i=!0),o+=1;continue}const d=new Set(r.appMentions.filter(l=>l.appId===e.selfAppId).map(l=>l.id));if(!h(r.html,(l,m)=>d.has(l)?"":`@${m}`)&&r.files.length===0)continue;const g=h(r.html,(l,m)=>d.has(l)&&e.selfName?e.selfName:`@${m}`);u&&(i=!0),c(),s.push({kind:"message",principal:f?{id:e.selfAppId,...e.selfName?{displayName:e.selfName.replace(/^@/,"")}:{}}:n.principal,text:g,...f?{self:!0}:{},...r.files.length>0?{shared:r.files}:{}})}return c(),{entries:s,opensWithParent:i}}function T(t,e){if(t.kind!=="message")return t;const{shared:a,...s}=t;if(!a?.length)return s;const i=a.map(o=>{const c=w(I(o)),r=e.remember(o);return r?{...c,ref:r}:{...c,unavailable:"Bishop could not record a reference for it"}});return{...s,files:i}}export{C as landedMidConversation,H as teamsHistory};
@@ -0,0 +1,6 @@
1
+ const s={amp:"&",lt:"<",gt:">",quot:'"',apos:"'",nbsp:" "};function a(e){return e.replace(/&(#x[0-9a-f]+|#\d+|[a-z]+);/gi,(r,t)=>t.startsWith("#x")||t.startsWith("#X")?String.fromCodePoint(Number.parseInt(t.slice(2),16)):t.startsWith("#")?String.fromCodePoint(Number.parseInt(t.slice(1),10)):s[t.toLowerCase()]??r)}function c(e){return a(e.replace(/<br\s*\/?>/gi,`
2
+ `).replace(/<[^>]*>/g,""))}function g(e,r){const t=e.replace(/<at\b[^>]*?\bid="([^"]*)"[^>]*>([\s\S]*?)<\/at>/gi,(p,n,o)=>{const i=o.replace(/<[^>]*>/g,"").trim();return r?r(n,i):`@${i}`}).replace(/<img\b[^>]*>/gi,"[image]").replace(/<\/(p|div|li|h[1-6]|blockquote|pre)>/gi,`
3
+ `);return c(t).replace(/[ \t]+\n/g,`
4
+ `).replace(/\n{3,}/g,`
5
+
6
+ `).trim()}export{a as decodeEntities,c as htmlText,g as messageText};
@@ -1,4 +1,4 @@
1
- import{DEFAULT_VERBOSITY as U}from"../config.js";import{BishopError as E}from"../errors.js";import{etiquetteNote as Y,threadParticipants as $}from"../interface/etiquette.js";import{splitMessage as z}from"../interface/format.js";import{showsProgress as W,showsTasks as L}from"../interface/verbosity.js";import{log as c}from"../log.js";import{describeTeamsFile as q,parseTeamsFile as Q,teamsAttachment as N,teamsFilePayload as V}from"./files.js";import{teamsPresence as X}from"./presence.js";import{addressesAgent as g,routeActivity as J}from"./route.js";import{baseConversation as Z,conversationOf as A,destinationHandleOf as tt,destinationOfHandle as nt,fileRouteOf as S,sameConversation as et,scopeOf as ot,threadKeyOf as at}from"./thread.js";function it(e){return[...rt(e),...st].join(`
2
- `)}function rt(e){return W(e)?["This conversation is in Microsoft Teams. Each thing you write is posted as its own message","while you work, so somebody 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.","",L(e)?"Your tool calls show as one message that is rewritten as you work, and replaced by a count of them once you speak again.":"Your tool calls are not shown, so a stretch of them is a silence unless you say what you are doing.","","Nothing more can reach this conversation once the turn ends, so never say you will follow up","separately: finish what you were asked before you stop.",""]:["This conversation is in Microsoft Teams. Only the last thing you write reaches it, and","anything you say on the way there is dropped, so put the whole answer in the message you","finish with. Never say you will follow up separately: nothing more can reach this","conversation 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, and there is no","typing indicator to say you are still going.",""]}const st=["Teams renders a narrower markdown than Slack does. Bold, italics, links, inline code, fenced","code blocks and lists work. Tables do not, so write a list or plain prose where you would","otherwise reach for one.","","A file somebody attaches reaches you in a direct chat and in a channel, and so does an image","pasted into a message anywhere. One Bishop could not download arrives saying why instead of","going missing, so you can tell somebody what happened to it. A group chat is the one place a","file may not reach you at all: if somebody there asks about one you were given nothing for,","say it never arrived and ask them to send it in a direct chat.","","A conversation can hold people talking to each other and not to 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 @mention 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."],ct="You are not on this agent\u2019s allow list, so it can\u2019t answer you.",B="There was no text in that message for me to act on. Say what you need in words, and @mention me so I see it.",lt=B+" If you attached a file, nothing about it reached me, so send it in a direct chat.",ht=2e4;function kt(e){const f=new Set,p=new Map,v=$(),T=e.verbosity??U,_=it(T);let y,b=!1;function F(){return{name:"teams",guidance:_,...y?{self:y}:{}}}async function x(t,n){const o=n.from.aadObjectId;if(!o)return;const i=p.get(o);if(i)return i;const a=Z(t);try{const r=await e.api.member(a,n.from.id);return r&&p.set(o,r),r}catch(r){c.warn({err:r,conversation:a},"could not read the Teams roster");return}}async function w(t,n,o=[],i){let a;const r=n?z(n,ht):[""];for(const[u,d]of r.entries()){const h=u===r.length-1&&(o.length>0||i)?{...o.length>0?{files:o}:{},...i?{channel:i}:{}}:void 0,m=await e.api.send(t,d,h);a=m?{id:m.id,text:d}:void 0}return a}function k(t,n){return[t,n?.userPrincipalName,n?.email].filter(o=>!!o).map(o=>o.trim().toLowerCase())}function P(t,n){const o=V(n);if(!o)return;const i=q(n);return e.attachments.remember(t,"teams",{id:i.id,name:i.name,payload:o})}async function j(t,n){return n.files.length===0?[]:Promise.all(n.files.map(async o=>{const i=P(t.id,o),a=await e.files.save(t.dirKey,N(o,e.api));return i?{...a,ref:i}:a}))}function K(t,n){return{id:t.from.aadObjectId??t.from.id,...t.from.name?{displayName:t.from.name}:{},...n?.name?{realName:n.name}:{},...n?.email?{email:n.email}:{}}}function M(t,n,o,i){const a=A(n),r=a.split(";").find(s=>s.startsWith("messageid="))?.slice(10),u=n.channel?{teamId:n.channel.teamId,channelId:n.channel.channelId,...r?{threadId:r}:{}}:void 0,d=Y({participants:v.count(t.threadKey),mentioned:g(n)});return{thread:t,isNewThread:i,origin:F(),...d?{note:d}:{},async messages(){const s=await j(t,n);return[{principal:o,text:n.text,...s.length>0?{files:s}:{}}]},presence:()=>X({send:(s,h)=>w(a,s,h,u),post:s=>e.api.send(a,s),update:(s,h)=>e.api.update(a,s,h),threadKey:t.threadKey,verbosity:()=>T,...S(ot(n),u)==="none"?{share:!1}:{},...d?{mayDecline:!0}:{}}),async notify(s){await w(a,s)},async destination(){return{interface:"teams",handle:tt(n),label:dt(n)}}}}async function C(t){if(b)return;if(await e.events.seen(`teams:${t.id}`)){c.debug({activity:t.id},"already answered this Teams activity");return}!y&&t.recipientName&&(y={address:`@${t.recipientName}`},c.info({address:y.address},"the agent is known by this name in Teams"));const n=at(t),o=await e.threads.has(n),i=J(t,{threadKnown:o});if(i.action==="ignore"){c.debug({activity:t.id,thread:n,reason:i.reason},"leaving a Teams message alone");return}const a=t.from.aadObjectId;if(!a){c.warn({activity:t.id},"refusing a Teams message with no Entra object ID");return}v.saw(n,a,Date.now());const r=A(t),u=e.allow.open?void 0:await x(r,t);if(!e.allow.open&&!k(a,u).some(l=>e.allow.allows(l))){c.warn({activity:t.id,who:a,conversation:r},"refusing an unauthorized Teams message"),g(t)&&await w(r,ct).catch(l=>{c.debug({err:l},"could not tell somebody they are not on the allow list")});return}const d=new Set(t.files.map(l=>l.contentUrl)),s=(await e.api.sharedFiles(t)).filter(l=>!d.has(l.contentUrl)),h=s.length>0?{...t,files:[...t.files,...s]}:t;if(!h.text.trim()&&h.files.length===0){if(c.info({activity:t.id},"a Teams message had nothing to act on"),g(t)){const l=t.conversationType==="personal"?B:lt;await w(r,l).catch(H=>{c.debug({err:H},"could not answer a message with nothing in it")})}return}const m=await e.threads.remember(n),G=K(h,u);c.info({thread:n,who:a,newThread:!o},"running a turn from Teams");const I=e.run(M(m,h,G,!o)).catch(l=>{c.error({thread:n,err:l},"a turn from Teams failed")}).finally(()=>{f.delete(I)});f.add(I)}async function O(){for(;f.size>0;)await Promise.allSettled([...f])}function D(t){return t==="groupChat"?"Bishop cannot send files to Teams group chats yet.":t==="channel"?"Bishop has no Graph address for this Teams channel, so it cannot send files there.":"Bishop cannot send files to this older Teams destination. Ask for a new report in the conversation first."}async function R(t){const n=nt(t.handle),o=t.files??[],i=o.map(a=>a.name).join(", ");if(o.length>0&&S(n.scope,n.channel)==="none")throw new E(D(n.scope));try{await w(n.conversation,t.text,o,n.channel)}catch(a){if(!o.length)throw a;c.warn({handle:t.handle,files:i,err:a},"could not attach files to a Teams report"),await w(n.conversation,`${t.text}
1
+ import{runCommand as W}from"../commands/run.js";import{BishopError as K}from"../errors.js";import{etiquetteNote as q,threadParticipants as Q}from"../interface/etiquette.js";import{splitMessage as V}from"../interface/format.js";import{showsProgress as X,showsTasks as J,threadVerbosity as Z}from"../interface/verbosity.js";import{log as s}from"../log.js";import{teamsCommandLine as tt}from"./command.js";import{describeTeamsFile as nt,parseTeamsFile as et,teamsAttachment as M,teamsFilePayload as ot}from"./files.js";import{landedMidConversation as at,teamsHistory as it}from"./history.js";import{teamsPresence as rt}from"./presence.js";import{addressesAgent as O,routeActivity as st}from"./route.js";import{baseConversation as ct,conversationOf as I,destinationHandleOf as lt,destinationOfHandle as ht,fileRouteOf as _,sameConversation as dt,scopeOf as ut,threadIdOf as x,threadKeyOf as ft}from"./thread.js";function mt(e){return[...wt(e),...yt].join(`
2
+ `)}function wt(e){return X(e)?["This conversation is in Microsoft Teams. Each thing you write is posted as its own message","while you work, so somebody 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.","",J(e)?"Your tool calls show as one message that is rewritten as you work, and replaced by a count of them once you speak again.":"Your tool calls are not shown, so a stretch of them is a silence unless you say what you are doing.","","Nothing more can reach this conversation once the turn ends, so never say you will follow up","separately: finish what you were asked before you stop.",""]:["This conversation is in Microsoft Teams. Only the last thing you write reaches it, and","anything you say on the way there is dropped, so put the whole answer in the message you","finish with. Never say you will follow up separately: nothing more can reach this","conversation 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, and there is no","typing indicator to say you are still going.",""]}const yt=["Teams renders a narrower markdown than Slack does. Bold, italics, links, inline code, fenced","code blocks and lists work. Tables do not, so write a list or plain prose where you would","otherwise reach for one.","","A file somebody attaches reaches you in a direct chat and in a channel, and so does an image","pasted into a message anywhere. One Bishop could not download arrives saying why instead of","going missing, so you can tell somebody what happened to it. A group chat is the one place a","file may not reach you at all: if somebody there asks about one you were given nothing for,","say it never arrived and ask them to send it in a direct chat.","","A conversation can hold people talking to each other and not to 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 @mention 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."],pt="You are not on this agent\u2019s allow list, so it can\u2019t answer you.",C="There was no text in that message for me to act on. Say what you need in words, and @mention me so I see it.",gt=C+" If you attached a file, nothing about it reached me, so send it in a direct chat.",vt=2e4;function xt(e){const y=new Set,p=new Map,g=Q(),v=Z(e.meta,"teams",e.verbosity),F={verbosity:v,model:e.model,configured:e.configuredModel??{},stop:e.stop},T=new Map;let m,N=!1;function P(t){return{name:"teams",guidance:mt(v.of(t.id).level),...m?{self:m}:{}}}async function A(t,n,a){if(!n)return;const r=p.get(n);if(r)return r;const o=ct(t);try{const i=await e.api.member(o,a);return i&&p.set(n,i),i}catch(i){s.warn({err:i,conversation:o},"could not read the Teams roster");return}}async function w(t,n,a=[],r){let o;const i=n?V(n,vt):[""];for(const[c,h]of i.entries()){const f=c===i.length-1&&(a.length>0||r)?{...a.length>0?{files:a}:{},...r?{channel:r}:{}}:void 0,u=await e.api.send(t,h,f);o=u?{id:u.id,text:h}:void 0}return o}function b(t,n){return[t,n?.userPrincipalName,n?.email].filter(a=>!!a).map(a=>a.trim().toLowerCase())}function E(t,n){const a=ot(n);if(!a)return;const r=nt(n);return e.attachments.remember(t,"teams",{id:r.id,name:r.name,payload:a})}async function j(t,n){return n.files.length===0?[]:Promise.all(n.files.map(async a=>{const r=E(t.id,a),o=await e.files.save(t.dirKey,M(a,e.api));return r?{...o,ref:r}:o}))}function D(t,n){return{id:t.from.aadObjectId??t.from.id,...t.from.name?{displayName:t.from.name}:{},...n?.name?{realName:n.name}:{},...n?.email?{email:n.email}:{}}}async function H(t,n){const a=I(n),r=x(a),o=n.recipientId?.replace(/^28:/,"");return it({read:await e.api.history(n),...r?{openedBy:r}:{},...o?{selfAppId:o}:{},...m?{selfName:m.address}:{},sender:async i=>{const c=e.allow.open?void 0:await A(a,i.id,i.id);return{allowed:e.allow.open||b(i.id,c).some(h=>e.allow.allows(h)),principal:{id:i.id,...i.name?{displayName:i.name}:{},...c?.name?{realName:c.name}:{},...c?.email?{email:c.email}:{}}}},remember:i=>E(t.id,i),saw:(i,c)=>g.saw(t.threadKey,i,c)})}function G(t,n,a,r){const o=I(n),i=x(o),c=n.channel?{teamId:n.channel.teamId,channelId:n.channel.channelId,...i?{threadId:i}:{}}:void 0,h=q({participants:g.count(t.threadKey),mentioned:O(n)});return{thread:t,isNewThread:r,get origin(){return P(t)},...h?{note:h}:{},...at(n)?{history:()=>H(t,n)}:{},async messages(){const d=await j(t,n);return[{principal:a,text:n.text,...d.length>0?{files:d}:{}}]},presence:()=>{const d=rt({send:(f,u)=>w(o,f,u,c),post:f=>e.api.send(o,f),update:(f,u)=>e.api.update(o,f,u),threadKey:t.threadKey,verbosity:()=>v.of(t.id).level,..._(ut(n),c)==="none"?{share:!1}:{},...h?{mayDecline:!0}:{}});return T.set(t.threadKey,d),{...d,finish:async()=>{try{await d.finish()}finally{T.delete(t.threadKey)}}}},async notify(d){await w(o,d)},async destination(){return{interface:"teams",handle:lt(n),label:Tt(n)}}}}async function R(t){if(N)return;if(await e.events.seen(`teams:${t.id}`)){s.debug({activity:t.id},"already answered this Teams activity");return}!m&&t.recipientName&&(m={address:`@${t.recipientName}`},s.info({address:m.address},"the agent is known by this name in Teams"));const n=ft(t),a=await e.threads.has(n),r=st(t,{threadKnown:a});if(r.action==="ignore"){s.debug({activity:t.id,thread:n,reason:r.reason},"leaving a Teams message alone");return}const o=t.from.aadObjectId;if(!o){s.warn({activity:t.id},"refusing a Teams message with no Entra object ID");return}g.saw(n,o,Date.now());const i=I(t),c=e.allow.open?void 0:await A(i,o,t.from.id);if(!e.allow.open&&!b(o,c).some(l=>e.allow.allows(l))){s.warn({activity:t.id,who:o,conversation:i},"refusing an unauthorized Teams message"),O(t)&&await w(i,pt).catch(l=>{s.debug({err:l},"could not tell somebody they are not on the allow list")});return}const h=tt(t);if(h){const l=await e.threads.remember(n),k=W(h,{thread:l},F);try{await w(i,k),T.get(n)?.interrupted()}catch(L){s.warn({thread:n,err:L},"couldn't answer a command")}s.info({thread:n,who:o},"ran a command");return}const d=new Set(t.files.map(l=>l.contentUrl)),f=(await e.api.sharedFiles(t)).filter(l=>!d.has(l.contentUrl)),u=f.length>0?{...t,files:[...t.files,...f]}:t;if(!u.text.trim()&&u.files.length===0){if(s.info({activity:t.id},"a Teams message had nothing to act on"),O(t)){const l=t.conversationType==="personal"?C:gt;await w(i,l).catch(k=>{s.debug({err:k},"could not answer a message with nothing in it")})}return}const U=await e.threads.remember(n),Y=D(u,c);s.info({thread:n,who:o,newThread:!a},"running a turn from Teams");const B=e.run(G(U,u,Y,!a)).catch(l=>{s.error({thread:n,err:l},"a turn from Teams failed")}).finally(()=>{y.delete(B)});y.add(B)}async function S(){for(;y.size>0;)await Promise.allSettled([...y])}function $(t){return t==="groupChat"?"Bishop cannot send files to Teams group chats yet.":t==="channel"?"Bishop has no Graph address for this Teams channel, so it cannot send files there.":"Bishop cannot send files to this older Teams destination. Ask for a new report in the conversation first."}async function z(t){const n=ht(t.handle),a=t.files??[],r=a.map(o=>o.name).join(", ");if(a.length>0&&_(n.scope,n.channel)==="none")throw new K($(n.scope));try{await w(n.conversation,t.text,a,n.channel)}catch(o){if(!a.length)throw o;s.warn({handle:t.handle,files:r,err:o},"could not attach files to a Teams report"),await w(n.conversation,`${t.text}
3
3
 
4
- _Could not attach ${i}; the report was sent without the file._`,[],n.channel)}return{}}return{name:"teams",handle:C,settle:O,deliver:R,allows(t){return e.allow.open?!0:k(t,p.get(t)).some(n=>e.allow.allows(n))},sameConversation:et,async resolveDestination(){throw new E("Bishop cannot yet name a Teams channel or chat from what you typed. Ask for a report in the conversation you want it in, which names itself.")},attachment:t=>{const n=Q(t);return n?N(n,e.api):void 0},async start(){await e.listener.start(),c.info({allow:e.allow.open?"anyone in the tenant":"a list"},"listening for Teams activities")},async stop(){b=!0,await e.listener.stop(),f.size>0&&(c.info({turns:f.size},"waiting for Teams turns still running"),await O())}}}function dt(e){return e.conversationType==="personal"?`a chat with ${e.from.name??"someone"}`:e.conversationType==="groupChat"?"a Teams group chat":"a Teams channel thread"}export{kt as createTeamsInterface,it as teamsGuidance};
4
+ _Could not attach ${r}; the report was sent without the file._`,[],n.channel)}return{}}return{name:"teams",handle:R,settle:S,deliver:z,allows(t){return e.allow.open?!0:b(t,p.get(t)).some(n=>e.allow.allows(n))},sameConversation:dt,async resolveDestination(){throw new K("Bishop cannot yet name a Teams channel or chat from what you typed. Ask for a report in the conversation you want it in, which names itself.")},attachment:t=>{const n=et(t);return n?M(n,e.api):void 0},async start(){await e.listener.start(),s.info({allow:e.allow.open?"anyone in the tenant":"a list"},"listening for Teams activities")},async stop(){N=!0,await e.listener.stop(),y.size>0&&(s.info({turns:y.size},"waiting for Teams turns still running"),await S())}}}function Tt(e){return e.conversationType==="personal"?`a chat with ${e.from.name??"someone"}`:e.conversationType==="groupChat"?"a Teams group chat":"a Teams channel thread"}export{xt as createTeamsInterface,mt as teamsGuidance};
@@ -1,7 +1,7 @@
1
- import{DEFAULT_VERBOSITY as E}from"../config.js";import{saysNoOutput as K}from"../harness/principal.js";import{summaryPhrase as N,toolPhrase as S,turnTally as $}from"../interface/phrases.js";import{showsCost as O,showsProgress as j,showsTasks as b}from"../interface/verbosity.js";import{log as o}from"../log.js";import{fileShelf as F}from"../outgoing.js";import{teamsTask as H}from"./progress.js";const C="The agent finished without saying anything. Nothing went wrong that it reported, but there is no answer to send.";function q(e){const k=()=>e.verbosity?.()??E,A=Date.now();let l;const y=[];let h=0,s="",i,w="",D=!1,f,I=!1;const u=F();let c=!1;async function g(){const t=l;t&&(l=void 0,await t.close())}async function P(t,n=[]){await g();try{const r=await e.send(t,n);h+=1,s=t,i=r,c=n.length>0}catch(r){if(n.length>0)try{const a=await e.send(t);h+=1,s=t,i=a,c=!1,o.warn({thread:e.threadKey,files:n.map(d=>d.name),err:r},"could not attach files to a Teams message; sent the text without them");return}catch(a){o.debug({thread:e.threadKey,err:a},"could not post the Teams message without its files")}s="",i=void 0,c=!1,o.debug({thread:e.threadKey,err:r},"could not post to Teams while working")}}function _(t){return t.filter(n=>typeof n!="string")}return{...e.share===!1?{}:{share:t=>u.offer(t)},async handle(t){const n=k();switch(b(n)||await g(),t.type){case"text":{if(K(t.text))return;if(!j(n)){y.push(u.strip(t.text));return}const r=u.place(t.text),a=_(r.parts);if(!r.text.trim()&&a.length===0)return;y.push(r.text),await P(r.text,a);return}case"tool":if(!b(n))return;l||(l=H({send:e.post,update:e.update,threadKey:e.threadKey},e.taskIntervalMs),s="",i=void 0),l.note(S(t));return;case"tool_summary":if(!b(n))return;l?.retitle(N(t.summary));return;case"done":D=!0,w=K(t.text)?"":t.text,I=!!e.mayDecline&&K(t.text);return;case"error":f=t.message;return;default:return}},async finish(){if(await g(),I&&!f){o.info({thread:e.threadKey},"the agent stayed out of a Teams conversation");return}const t=w?u.place(w):{parts:[],text:""},n=t.text.trim()||(h===0?y.join(`
1
+ import{DEFAULT_VERBOSITY as E}from"../config.js";import{saysNoOutput as K}from"../harness/principal.js";import{summaryPhrase as N,toolPhrase as S,turnTally as $}from"../interface/phrases.js";import{showsCost as O,showsProgress as j,showsTasks as b}from"../interface/verbosity.js";import{log as o}from"../log.js";import{fileShelf as F}from"../outgoing.js";import{teamsTask as H}from"./progress.js";const C="The agent finished without saying anything. Nothing went wrong that it reported, but there is no answer to send.";function q(e){const k=()=>e.verbosity?.()??E,A=Date.now();let h;const y=[];let c=0,s="",i,w="",D=!1,f,I=!1;const u=F();let l=!1;async function g(){const t=h;t&&(h=void 0,await t.close())}async function P(t,n=[]){await g();try{const r=await e.send(t,n);c+=1,s=t,i=r,l=n.length>0}catch(r){if(n.length>0)try{const a=await e.send(t);c+=1,s=t,i=a,l=!1,o.warn({thread:e.threadKey,files:n.map(d=>d.name),err:r},"could not attach files to a Teams message; sent the text without them");return}catch(a){o.debug({thread:e.threadKey,err:a},"could not post the Teams message without its files")}s="",i=void 0,l=!1,o.debug({thread:e.threadKey,err:r},"could not post to Teams while working")}}function _(t){return t.filter(n=>typeof n!="string")}return{...e.share===!1?{}:{share:t=>u.offer(t)},interrupted(){s="",i=void 0,l=!1},async handle(t){const n=k();switch(b(n)||await g(),t.type){case"text":{if(K(t.text))return;if(!j(n)){y.push(u.strip(t.text));return}const r=u.place(t.text),a=_(r.parts);if(!r.text.trim()&&a.length===0)return;y.push(r.text),await P(r.text,a);return}case"tool":if(!b(n))return;h||(h=H({send:e.post,update:e.update,threadKey:e.threadKey},e.taskIntervalMs),s="",i=void 0),h.note(S(t));return;case"tool_summary":if(!b(n))return;h?.retitle(N(t.summary));return;case"done":D=!0,w=K(t.text)?"":t.text,I=!!e.mayDecline&&K(t.text);return;case"error":f=t.message;return;default:return}},async finish(){if(await g(),I&&!f){o.info({thread:e.threadKey},"the agent stayed out of a Teams conversation");return}const t=w?u.place(w):{parts:[],text:""},n=t.text.trim()||(c===0?y.join(`
2
2
 
3
- `).trim():""),r=_(t.parts),a=B(n),d=O(k())?`_${$(Date.now()-A,D||!!f)}_`:"";if(!a&&d&&i&&!c)try{await e.update(i.id,`${i.text}
3
+ `).trim():""),r=_(t.parts),a=B(n),d=O(k())?`_${$(Date.now()-A,D||!!f)}_`:"";if(!a&&d&&i&&!l)try{await e.update(i.id,`${i.text}
4
4
 
5
5
  ${d}`);return}catch(m){o.debug({thread:e.threadKey,err:m},"could not close a Teams turn in place")}const T=[a,d].filter(Boolean).join(`
6
6
 
7
- `),p=u.unplaced();if(!(!T&&r.length===0&&p.length===0)){try{await e.send(T,r),h+=1,s=a,c=r.length>0,a&&o.info({thread:e.threadKey,chars:a.length},"answered in Teams")}catch(m){if(r.length>0)try{await e.send(T),h+=1,s=a,c=!1,o.warn({thread:e.threadKey,files:r.map(x=>x.name),err:m},"could not attach files to the final Teams message; sent the text without them")}catch(x){o.debug({thread:e.threadKey,err:x},"could not post the final Teams message without its files")}a?o.error({thread:e.threadKey,err:m},"could not post the answer to Teams"):o.debug({thread:e.threadKey,err:m},"could not say what a Teams turn cost")}p.length>0&&await P("",p)}}};function B(t){return f?`Something went wrong: ${f}`:t?t===s.trim()?"":t:h===0?C:""}}export{q as teamsPresence};
7
+ `),p=u.unplaced();if(!(!T&&r.length===0&&p.length===0)){try{await e.send(T,r),c+=1,s=a,l=r.length>0,a&&o.info({thread:e.threadKey,chars:a.length},"answered in Teams")}catch(m){if(r.length>0)try{await e.send(T),c+=1,s=a,l=!1,o.warn({thread:e.threadKey,files:r.map(x=>x.name),err:m},"could not attach files to the final Teams message; sent the text without them")}catch(x){o.debug({thread:e.threadKey,err:x},"could not post the final Teams message without its files")}a?o.error({thread:e.threadKey,err:m},"could not post the answer to Teams"):o.debug({thread:e.threadKey,err:m},"could not say what a Teams turn cost")}p.length>0&&await P("",p)}}};function B(t){return f?`Something went wrong: ${f}`:t?t===s.trim()?"":t:c===0?C:""}}export{q as teamsPresence};
@@ -1 +1 @@
1
- const s=new Set(["personal","groupChat"]),p=/^[1-9]\d*$/;function u(n){const e=n.conversationId;return s.has(n.conversationType)||e.includes(";messageid=")?e:p.test(n.id)?`${e};messageid=${n.id}`:e}function f(n){return n.conversationType==="personal"?"personal":n.conversationType==="groupChat"?"groupChat":"channel"}function $(n,e){return n==="personal"?"consent":n==="channel"&&e?"upload":"none"}function v(n){const e=u(n),t=f(n);return t!=="channel"?`${e};scope=${t}`:n.channel?`${e};scope=channel;teamid=${encodeURIComponent(n.channel.teamId)};channelid=${encodeURIComponent(n.channel.channelId)}`:`${e};scope=channel`}function C(n){const[e="",...t]=n.split(";"),o=new Map(t.map(i=>{const a=i.indexOf("=");return a<0?[i,""]:[i.slice(0,a),i.slice(a+1)]})),c=o.get("messageid"),d=c?`${e};messageid=${c}`:e,r=o.get("scope");if(r==="personal")return{conversation:d,scope:r};if(r==="groupChat")return{conversation:d,scope:r};if(r==="channel"){const i=o.get("teamid"),a=o.get("channelid");return!i||!a?{conversation:d,scope:r}:{conversation:d,scope:r,channel:{teamId:decodeURIComponent(i),channelId:decodeURIComponent(a),...c?{threadId:c}:{}}}}return{conversation:d,scope:"unknown"}}function x(n){return`teams:${u(n)}`}function h(n){return n.split(";")[0]??n}function O(n,e){return l(n)===l(e)}function l(n){const[e="",...t]=n.split(";"),o=t.find(c=>c.startsWith("messageid="));return o?`${e};${o}`:e}function m(n){if(n.conversationType==="personal")return;if(s.has(n.conversationType))return{kind:"chat",chatId:h(n.conversationId),messageId:n.id};if(!n.channel)return;const e=I(n);return{kind:"channel",teamId:n.channel.teamId,channelId:n.channel.channelId,messageId:n.id,...e?{replyToId:e}:{}}}function I(n){const e=n.conversationId.split(";").find(t=>t.startsWith("messageid="))?.slice(10);return e&&e!==n.id?e:void 0}export{h as baseConversation,u as conversationOf,v as destinationHandleOf,C as destinationOfHandle,$ as fileRouteOf,m as graphAddressOf,O as sameConversation,f as scopeOf,x as threadKeyOf};
1
+ const s=new Set(["personal","groupChat"]),f=/^[1-9]\d*$/;function u(n){const e=n.conversationId;return s.has(n.conversationType)||e.includes(";messageid=")?e:f.test(n.id)?`${e};messageid=${n.id}`:e}function l(n){return n.conversationType==="personal"?"personal":n.conversationType==="groupChat"?"groupChat":"channel"}function v(n,e){return n==="personal"?"consent":n==="channel"&&e?"upload":"none"}function x(n){const e=u(n),i=l(n);return i!=="channel"?`${e};scope=${i}`:n.channel?`${e};scope=channel;teamid=${encodeURIComponent(n.channel.teamId)};channelid=${encodeURIComponent(n.channel.channelId)}`:`${e};scope=channel`}function C(n){const[e="",...i]=n.split(";"),t=new Map(i.map(r=>{const a=r.indexOf("=");return a<0?[r,""]:[r.slice(0,a),r.slice(a+1)]})),c=t.get("messageid"),d=c?`${e};messageid=${c}`:e,o=t.get("scope");if(o==="personal")return{conversation:d,scope:o};if(o==="groupChat")return{conversation:d,scope:o};if(o==="channel"){const r=t.get("teamid"),a=t.get("channelid");return!r||!a?{conversation:d,scope:o}:{conversation:d,scope:o,channel:{teamId:decodeURIComponent(r),channelId:decodeURIComponent(a),...c?{threadId:c}:{}}}}return{conversation:d,scope:"unknown"}}function O(n){return`teams:${u(n)}`}function h(n){return n.split(";")[0]??n}function m(n,e){return p(n)===p(e)}function p(n){const[e="",...i]=n.split(";"),t=i.find(c=>c.startsWith("messageid="));return t?`${e};${t}`:e}function y(n){if(n.conversationType==="personal")return;if(s.has(n.conversationType))return{kind:"chat",chatId:h(n.conversationId),messageId:n.id};if(!n.channel)return;const e=I(n);return{kind:"channel",teamId:n.channel.teamId,channelId:n.channel.channelId,messageId:n.id,...e?{replyToId:e}:{}}}function I(n){const e=$(n.conversationId);return e&&e!==n.id?e:void 0}function $(n){return n.split(";").find(e=>e.startsWith("messageid="))?.slice(10)}export{h as baseConversation,u as conversationOf,x as destinationHandleOf,C as destinationOfHandle,v as fileRouteOf,y as graphAddressOf,m as sameConversation,l as scopeOf,$ as threadIdOf,O as threadKeyOf};
package/dist/worktrees.js CHANGED
@@ -1,2 +1,3 @@
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};
1
+ import{createHash as w}from"node:crypto";import{existsSync as a,mkdirSync as h}from"node:fs";import{join as p,resolve as u}from"node:path";import{BishopError as l}from"./errors.js";import{exec as i}from"./exec.js";const d=64,s="bishop-",g=".claude/worktrees";function R(t){const r=t.replaceAll(/[^a-zA-Z0-9]+/g,"-").replaceAll(/-+/g,"-").replaceAll(/^-+|-+$/g,""),e=`${s}${r}`;if(e.length<=d)return e;const o=w("sha256").update(t).digest("hex").slice(0,8),n=d-s.length-o.length-1;return`${s}${r.slice(0,n)}-${o}`}function m(t,r){return u(t,g,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 a(p(t,".git"))}async function y(t){const r=await i("git",["rev-parse","--show-prefix"],{cwd:t});return r.code===0?r.stdout.trim():""}async function M(t,r){const e=m(t,r),o=await y(t);if(x(e))return f(e,o);if(a(e))throw new l(`${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 n=await i("git",["worktree","add","-b",r,e,"HEAD"],{cwd:t});if(n.code!==0){await i("git",["worktree","prune"],{cwd:t});const c=await i("git",["worktree","add",e,r],{cwd:t});if(c.code!==0)throw new l(`Couldn't create a git worktree for this thread at ${e}: ${k(n,c)}`)}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=u(t,r);return h(e,{recursive:!0}),e}async function P(t){const r=await i("git",["rev-parse","--path-format=absolute","--git-dir","--git-common-dir"],{cwd:t}),e=r.stdout.split(`
3
+ `).filter(o=>o.trim()!=="");return r.code!==0||e.length!==2?[]:[...new Set(e)]}async function S(t){return(await i("git",["rev-parse","--show-toplevel"],{cwd:t})).code===0}async function W(t){const r=await i("git",["config","--get","user.email"],{cwd:t}),e=await i("git",["config","--get","user.name"],{cwd:t});return r.code===0&&r.stdout.trim()!==""&&e.code===0&&e.stdout.trim()!==""}export{g as WORKTREE_DIR,M as ensureWorktree,P as gitDirs,W as hasGitIdentity,S as isGitRepo,N as isolates,I as sharedDirNotice,R as worktreeName,m as worktreePath};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agentdeploymentco/bishop",
3
- "version": "0.3.2",
3
+ "version": "0.3.4",
4
4
  "description": "An agent gateway. Connects an agent to Slack and email.",
5
5
  "license": "UNLICENSED",
6
6
  "type": "module",
@@ -1,3 +0,0 @@
1
- import{Command as L,CommanderError as N,InvalidArgumentError as g}from"commander";import{VERBOSITY as y}from"../../config.js";import{log as b}from"../../log.js";import{isHarnessName as S,MODEL_SETTINGS as O}from"../../thread-model.js";import{parseVerbosity as E}from"../verbosity.js";import{commandOutput as T,tokenize as j}from"./syntax.js";const a="default";function k(o){if(o.trim().toLowerCase()===a)return a;const e=E(o);if(!e)throw new g(`Use one of ${y.join(", ")}, or ${a}.`);return e}function l(o){const e=o.trim();if(!e)throw new g(`Name one, or ${a} to follow the configured one.`);if(!S(e))throw new g(`"${e}" is not a model name. Letters, digits, and . : / @ - _ only.`);return e}function I(o,e,t){return e?`${o} is ${e}, set in this thread`:t?`${o} is ${t}, the default`:`${o} is whatever the harness runs by default`}function C(o,e){const t=e.model.of(o);return[I("Model",t.model,e.configured.model),I("Effort",t.effort,e.configured.effort)]}function H(o,e,t){const u=[];let h;const f=new L;f.name("!").description("Bishop commands. These are handled here and never reach the agent.").exitOverride().configureOutput({writeOut:r=>u.push(r),writeErr:r=>u.push(r),outputError:(r,n)=>n(r)}),f.command("verbosity").description("How much of a turn this thread shows while it is running.").argument("[level]",`${y.join(", ")}, or ${a} to follow the configured level. Omit it to see the current one.`,k).action(r=>{if(!r){const c=t.verbosity.of(e.thread.id),d=c.fromThread?"":" (the configured default)";h=`Thread verbosity is ${c.level}${d}`;return}if(r===a){t.verbosity.clear(e.thread.id),h=`Thread verbosity follows the configured default, which is ${t.verbosity.of(e.thread.id).level}`;return}const n=E(r);n&&(t.verbosity.set(e.thread.id,n),h=`Thread verbosity set to ${n}`)}),f.command("model").description("Which model this thread runs, and how hard it thinks.").argument("[model]",`a model name in the harness's own naming, or ${a} to follow the configured one. Omit everything to see where the thread stands.`,l).argument("[effort]",`how hard it should think, in the harness's own naming, or ${a}.`,l).option("-m, --model <model>","the model, named as a flag rather than in order",l).option("-e, --effort <effort>","the effort, named as a flag rather than in order",l).action((r,n,c)=>{const d=e.thread.id,s={...c},w=O.filter(i=>s[i]===void 0),v=[r,n].filter(i=>i!==void 0);if(v.length>w.length){h="Name each half once: a model and an effort, in order or as flags.";return}if(v.forEach((i,m)=>{const p=w[m];p&&(s[p]=i)}),!s.model&&!s.effort){h=C(d,t).join(`
2
- `);return}const $={};for(const i of O){const m=s[i];m&&(m.toLowerCase()===a?t.model.clear(d,i):$[i]=m)}t.model.set(d,$),b.info({thread:e.thread.threadKey,...s},"a thread changed the model it runs"),h=["Set. It takes effect on this thread's next turn.",...C(d,t)].join(`
3
- `)});try{f.parse(j(o),{from:"user"})}catch(r){if(!(r instanceof N)){const n=r instanceof Error?r.message:String(r);return b.debug({thread:e.thread.threadKey,line:o,err:r},"a command failed"),T(n)}}return T(h??(u.join("").trim()||f.helpInformation()))}export{H as runCommand};
@@ -1,3 +0,0 @@
1
- const f="!>";function o(i){const n=i.trim();if(n.startsWith("```")&&n.endsWith("```")&&n.length>=6){const t=n.slice(3,-3);return t.includes("```")?void 0:t}if(n.startsWith("`")&&n.endsWith("`")&&n.length>=2){const t=n.slice(1,-1);return t.includes("`")?void 0:t}}const c=/^-{0,2}[a-z][a-z0-9-]*$/i;function d(i){const n=o(i)?.trim();if(!n)return;const t=/^!\s+(\S+)(?:\s+([\s\S]*))?$/.exec(n),e=t?.[1];if(!e||!c.test(e))return;const r=t?.[2]?.trim();return r?`${e} ${r}`:e}function l(i){const n=[];let t="",e=!1,r;for(let u=0;u<i.length;u+=1){const s=i[u];if(s==="\\"&&r!=="'"&&u+1<i.length){t+=i[++u],e=!0;continue}if(r){s===r?r=void 0:t+=s;continue}if(s==='"'||s==="'"){r=s,e=!0;continue}if(/\s/.test(s)){e&&n.push(t),t="",e=!1;continue}t+=s,e=!0}if(r)throw new Error(`unterminated ${r==='"'?"double":"single"} quote`);return e&&n.push(t),n}function a(i){const n=i.trim().split(`
2
- `),[t="",...e]=n;return["```",`!> ${t}`,...e,"```"].join(`
3
- `)}export{d as commandLine,a as commandOutput,l as tokenize};
@@ -1 +0,0 @@
1
- import{DEFAULT_VERBOSITY as l,VERBOSITY as f}from"../config.js";import{log as a}from"../log.js";const n="slack.verbosity";function u(t){const o=t.trim().toLowerCase();return f.includes(o)?o:void 0}function m(t,o=l){const s=new Set;return{of(e){const r=t.get(e,n);if(r===void 0)return{level:o,fromThread:!1};const i=u(r);return i?{level:i,fromThread:!0}:(s.has(`${e} ${r}`)||(s.add(`${e} ${r}`),a.warn({thread:e,stored:r},"unknown verbosity on this thread; using the default")),{level:o,fromThread:!1})},set(e,r){t.set(e,n,r)},clear(e){t.clear(e,n)}}}export{u as parseVerbosity,m as threadVerbosity};