@agentdeploymentco/bishop 0.3.4 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/README.md +90 -9
  2. package/dist/cli.js +19 -17
  3. package/dist/config.js +4 -4
  4. package/dist/db.js +57 -11
  5. package/dist/files.js +2 -2
  6. package/dist/gmail/context.js +1 -0
  7. package/dist/gmail/create.js +1 -1
  8. package/dist/gmail/history.js +1 -1
  9. package/dist/gmail/interface.js +2 -2
  10. package/dist/harness/claude.js +6 -5
  11. package/dist/harness/codex.js +5 -5
  12. package/dist/harness/create.js +1 -1
  13. package/dist/harness/principal.js +14 -10
  14. package/dist/ids.js +1 -1
  15. package/dist/interface/authorize.js +1 -1
  16. package/dist/interface/context.js +1 -0
  17. package/dist/nono-setup.js +7 -0
  18. package/dist/outgoing.js +2 -2
  19. package/dist/principals.js +6 -0
  20. package/dist/sandbox.js +20 -0
  21. package/dist/schedule/cli.js +7 -7
  22. package/dist/schedule/prompt.js +2 -2
  23. package/dist/schedule/runner.js +3 -3
  24. package/dist/schedule/store.js +2 -2
  25. package/dist/sessions/store.js +13 -7
  26. package/dist/slack/app.js +2 -2
  27. package/dist/slack/context.js +1 -0
  28. package/dist/slack/handlers.js +1 -1
  29. package/dist/slack/history.js +1 -1
  30. package/dist/slack/interface.js +2 -2
  31. package/dist/slack/members.js +1 -1
  32. package/dist/slack/principal.js +1 -0
  33. package/dist/slack/users.js +1 -1
  34. package/dist/teams/context.js +1 -0
  35. package/dist/teams/create.js +2 -2
  36. package/dist/teams/fake.js +1 -1
  37. package/dist/teams/graph.js +1 -1
  38. package/dist/teams/history.js +1 -1
  39. package/dist/teams/interface.js +3 -3
  40. package/dist/teams/principal.js +1 -0
  41. package/dist/teams/provision.js +1 -1
  42. package/dist/teams/setup.js +39 -39
  43. package/dist/tools/files.js +2 -2
  44. package/dist/tools/schedules.js +5 -5
  45. package/dist/tools/server.js +1 -1
  46. package/dist/transcript.js +3 -0
  47. package/dist/turn.js +3 -1
  48. package/dist/worktrees.js +3 -3
  49. package/package.json +13 -8
@@ -1,8 +1,8 @@
1
- import{z as a}from"zod";import{BishopError as y}from"../errors.js";import{NO_OUTPUT as T}from"../harness/principal.js";import{log as p}from"../log.js";import{describeTime as w,nextRun as R,systemTimeZone as g,validateCron as z,validateTimeZone as _}from"../schedule/cron.js";import{defineTool as l,ToolRefusal as d}from"./types.js";const A=20;function m(e){return`${e.origin.name}:${e.principal.id}`}function $(e){try{return e()}catch(n){throw n instanceof y?new d(n.message):n}}async function k(e){try{return await e()}catch(n){throw n instanceof y?new d(n.message):n}}function B(e,n){const t=[`${e.id} ${e.name}${e.enabled?"":" (disabled)"}`,` runs ${e.cron} (${e.tz})`,` next ${e.enabled?w(e.nextRunAt,n):"never, while it is off"}`,` to ${e.destination.label}`,` prompt ${e.prompt}`,` by ${e.createdBy}`];return e.lastError&&t.push(` error ${e.lastError} (${e.consecutiveFailures} in a row)`),t.join(`
2
- `)}async function b(e,n){const t=await n.destination(),i=m(n);return e.schedules.all().filter(o=>o.createdBy===i||e.sameConversation(o.destination,t))}function E(e){const n=e.now??Date.now;return l({name:"schedule_create",description:["Runs a prompt on a cron schedule and posts the answer where you say.","Each firing is a fresh session with no memory of the last one, so the prompt has to","carry everything it needs.","",`The prompt must say when to stay quiet, and say that answering ${T} is how.`,"A schedule that reports every time it runs is one people learn to ignore."].join(`
3
- `),input:{name:a.string().trim().min(1).describe("What a person sees in a list, and an email subject"),cron:a.string().trim().min(1).describe("Five-field cron, or @daily and friends. No more than once every five minutes"),prompt:a.string().trim().min(1).describe("What the agent is asked at each firing, including when to say nothing"),to:a.string().trim().min(1).optional().describe("Where the answer goes: #channel or @person for Slack, or an email address. Leave it out to post back into this conversation."),tz:a.string().trim().min(1).optional().describe("IANA zone the expression is read in, like Europe/London. Defaults to Bishop's")},async run(t,i){const o=m(i);if(!e.authorized(o))throw new d(`Bishop can't run a schedule created by ${o}, so making one would write something that never fires. Ask an operator to add it with \`bishop schedules add\`.`);const s=e.schedules.all().filter(v=>v.createdBy===o).length;if(s>=A)throw new d(`You already have ${s} schedules, which is the limit. Remove one with schedule_delete before adding another.`);const u=t.tz,c=u?$(()=>_(u)):g();$(()=>z(t.cron,c));const f=t.to,h=f?await k(()=>e.resolve(f)):await i.destination(),r=e.schedules.add({name:t.name,cron:t.cron,tz:c,prompt:t.prompt,destination:h,createdBy:o,nextRunAt:R(t.cron,c,n())});return p.info({schedule:r.id,name:r.name,cron:r.cron,tz:r.tz,by:r.createdBy,to:h.label},"the agent created a schedule"),`Created ${r.id} "${r.name}", running ${r.cron} (${c}).
4
- Next ${w(r.nextRunAt,c)}, posting to ${h.label}.`}})}function N(e){return l({name:"schedule_list",description:"Lists the schedules you created, and any that post into this conversation. Not schedules other people aimed elsewhere.",input:{},async run(n,t){const i=await b(e,t);if(i.length===0)return"Nothing is scheduled here, and you have not made any.";const o=g();return`${i.map(s=>B(s,o)).join(`
1
+ import{z as s}from"zod";import{BishopError as f}from"../errors.js";import{NO_OUTPUT as T}from"../harness/principal.js";import{log as y}from"../log.js";import{principalLabel as R}from"../principals.js";import{describeTime as w,nextRun as z,systemTimeZone as g,validateCron as _,validateTimeZone as A}from"../schedule/cron.js";import{defineTool as l,ToolRefusal as d}from"./types.js";const k=20;function m(e){return e.principal.id}function b(e){try{return e()}catch(n){throw n instanceof f?new d(n.message):n}}async function B(e){try{return await e()}catch(n){throw n instanceof f?new d(n.message):n}}function E(e,n,t){const i=[`${e.id} ${e.name}${e.enabled?"":" (disabled)"}`,` runs ${e.cron} (${e.tz})`,` next ${e.enabled?w(e.nextRunAt,n):"never, while it is off"}`,` to ${e.destination.label}`,` prompt ${e.prompt}`,` by ${R(e.createdBy,t)}`];return e.lastError&&i.push(` error ${e.lastError} (${e.consecutiveFailures} in a row)`),i.join(`
2
+ `)}async function $(e,n){const t=await n.destination(),i=m(n);return e.schedules.all().filter(o=>o.createdBy===i||e.sameConversation(o.destination,t))}function N(e){const n=e.now??Date.now;return l({name:"schedule_create",description:["Runs a prompt on a cron schedule and posts the answer where you say.","Each firing is a fresh session with no memory of the last one, so the prompt has to","carry everything it needs.","",`The prompt must say when to stay quiet, and say that answering ${T} is how.`,"A schedule that reports every time it runs is one people learn to ignore."].join(`
3
+ `),input:{name:s.string().trim().min(1).describe("What a person sees in a list, and an email subject"),cron:s.string().trim().min(1).describe("Five-field cron, or @daily and friends. No more than once every five minutes"),prompt:s.string().trim().min(1).describe("What the agent is asked at each firing, including when to say nothing"),to:s.string().trim().min(1).optional().describe("Where the answer goes: #channel or @person for Slack, or an email address. Leave it out to post back into this conversation."),tz:s.string().trim().min(1).optional().describe("IANA zone the expression is read in, like Europe/London. Defaults to Bishop's")},async run(t,i){const o=m(i);if(!e.authorized(o))throw new d(`Bishop can't run a schedule created by ${i.principal.name}, so making one would write something that never fires. Ask an operator to add it with \`bishop schedules add\`.`);const a=e.schedules.all().filter(v=>v.createdBy===o).length;if(a>=k)throw new d(`You already have ${a} schedules, which is the limit. Remove one with schedule_delete before adding another.`);const u=t.tz,c=u?b(()=>A(u)):g();b(()=>_(t.cron,c));const p=t.to,h=p?await B(()=>e.resolve(p)):await i.destination(),r=e.schedules.add({name:t.name,cron:t.cron,tz:c,prompt:t.prompt,destination:h,createdBy:o,nextRunAt:z(t.cron,c,n())});return y.info({schedule:r.id,name:r.name,cron:r.cron,tz:r.tz,by:r.createdBy,to:h.label},"the agent created a schedule"),`Created ${r.id} "${r.name}", running ${r.cron} (${c}).
4
+ Next ${w(r.nextRunAt,c)}, posting to ${h.label}.`}})}function x(e){return l({name:"schedule_list",description:"Lists the schedules you created, and any that post into this conversation. Not schedules other people aimed elsewhere.",input:{},async run(n,t){const i=await $(e,t);if(i.length===0)return"Nothing is scheduled here, and you have not made any.";const o=g();return`${i.map(a=>E(a,o,e.principals)).join(`
5
5
 
6
6
  `)}
7
7
 
8
- Times in ${o}.`}})}function x(e){return l({name:"schedule_delete",description:"Removes a schedule by id. Only ones schedule_list shows you.",input:{id:a.string().trim().min(1).describe("The id schedule_list reports")},async run(n,t){const o=(await b(e,t)).find(s=>s.id===n.id);if(!o)throw new d(`No schedule ${n.id} that you can remove. schedule_list has the ones you can.`);return e.schedules.remove(o.id),p.info({schedule:o.id,name:o.name,by:m(t)},"the agent removed a schedule"),`Removed ${o.id} "${o.name}".`}})}function F(e){return[E(e),N(e),x(e)]}export{A as MAX_PER_CREATOR,m as createdBy,F as scheduleTools};
8
+ Times in ${o}.`}})}function j(e){return l({name:"schedule_delete",description:"Removes a schedule by id. Only ones schedule_list shows you.",input:{id:s.string().trim().min(1).describe("The id schedule_list reports")},async run(n,t){const o=(await $(e,t)).find(a=>a.id===n.id);if(!o)throw new d(`No schedule ${n.id} that you can remove. schedule_list has the ones you can.`);return e.schedules.remove(o.id),y.info({schedule:o.id,name:o.name,by:m(t)},"the agent removed a schedule"),`Removed ${o.id} "${o.name}".`}})}function U(e){return[N(e),x(e),j(e)]}export{k as MAX_PER_CREATOR,m as createdBy,U as scheduleTools};
@@ -1 +1 @@
1
- import{randomBytes as T,randomUUID as b}from"node:crypto";import{createServer as k}from"node:http";import{McpServer as B}from"@modelcontextprotocol/sdk/server/mcp.js";import{StreamableHTTPServerTransport as O}from"@modelcontextprotocol/sdk/server/streamableHttp.js";import{isInitializeRequest as _}from"@modelcontextprotocol/sdk/types.js";import{log as h}from"../log.js";import{ToolRefusal as y}from"./types.js";const x="bishop",v="/mcp",R="BISHOP_TOOL_TOKEN",A=1048576;function N(i){const r=i.headers.authorization;return r&&/^Bearer\s+(.+)$/i.exec(r.trim())?.[1]?.trim()||void 0}function p(i,r,a){i.writeHead(r,{"content-type":"application/json"}),i.end(JSON.stringify({jsonrpc:"2.0",id:null,error:{code:-32001,message:a}}))}async function I(i){const r=[];let a=0;for await(const n of i){if(a+=n.length,a>A)throw new Error("request body is too large");r.push(n)}if(r.length!==0)return JSON.parse(Buffer.concat(r).toString("utf8"))}const P="Bishop's tools answer to a person, and this message came from another bot. Ask someone in the conversation to do it.";function z(i,r){const a=new B({name:x,version:"1"});for(const n of i)a.registerTool(n.name,{description:n.description,inputSchema:n.input},async w=>{try{if(r.principal.isBot)throw new y(P);return{content:[{type:"text",text:await n.run(w,r)}]}}catch(c){const f=c instanceof Error?c.message:String(c);return c instanceof y?h.info({tool:n.name,thread:r.thread.threadKey,reason:f},"a tool refused"):h.error({tool:n.name,thread:r.thread.threadKey,err:c},"a tool failed"),{content:[{type:"text",text:f}],isError:!0}}});return a}async function D(i){const r=i.host??"127.0.0.1",a=new Map,n=new Map;async function w(e,t){const o=new O({sessionIdGenerator:()=>b(),onsessioninitialized:s=>{if(a.get(e)!==t){o.close().catch(()=>{});return}n.set(s,{token:e,transport:o}),t.sessions.add(s)}});return o.onclose=()=>{const s=o.sessionId;s&&(n.delete(s),t.sessions.delete(s))},await z(t.tools,t.context).connect(o),o}const c=k((e,t)=>{(async()=>{try{if(!e.url?.startsWith(v))return p(t,404,"no such endpoint");const o=N(e),s=o?a.get(o):void 0;if(!s||!o)return p(t,401,"this call carries no valid turn credential");const l=e.method==="POST"?await I(e):void 0,d=e.headers["mcp-session-id"],m=typeof d=="string"?n.get(d):void 0;if(m){if(m.token!==o)return p(t,404,"no such session");await m.transport.handleRequest(e,t,l);return}if(typeof d=="string")return p(t,404,"no such session");if(!_(l))return p(t,400,"the first call on a connection must be initialize");await(await w(o,s)).handleRequest(e,t,l)}catch(o){h.error({err:o},"a tool call could not be handled"),t.headersSent?t.end():p(t,400,"that request could not be handled")}})()});await new Promise((e,t)=>{c.once("error",t),c.listen(0,r,()=>{c.removeListener("error",t),e()})});const f=c.address();if(!f||typeof f=="string")throw new Error("the tool endpoint started without a port");const g=`http://${r}:${f.port}${v}`;return h.info({url:g,tools:i.tools.map(e=>e.name)},"serving the agent its own tools"),{url:g,toolNames:i.tools.map(e=>e.name),grant(e,t){const o=T(32).toString("base64url"),s=t?.only,l=s?i.tools.filter(u=>s.includes(u.name)):i.tools;s&&l.length!==s.length&&h.error({asked:[...s],serving:l.map(u=>u.name)},"a turn asked for tools this endpoint does not serve");const d={context:e,tools:l,sessions:new Set};return a.set(o,d),{access:{url:g,tokenEnvVar:R,token:o},revoke:()=>{if(a.delete(o)){for(const u of d.sessions){const S=n.get(u);n.delete(u),S?.transport.close().catch(E=>{h.debug({err:E},"a tool connection did not close cleanly")})}d.sessions.clear()}}}},async stop(){for(const{transport:e}of n.values())await e.close().catch(()=>{});n.clear(),a.clear(),c.closeAllConnections(),await new Promise(e=>c.close(()=>e()))}}}export{v as PATH,x as SERVER_NAME,R as TOKEN_ENV_VAR,D as startToolServer};
1
+ import{randomBytes as T,randomUUID as b}from"node:crypto";import{createServer as k}from"node:http";import{McpServer as O}from"@modelcontextprotocol/sdk/server/mcp.js";import{StreamableHTTPServerTransport as _}from"@modelcontextprotocol/sdk/server/streamableHttp.js";import{isInitializeRequest as x}from"@modelcontextprotocol/sdk/types.js";import{log as h}from"../log.js";import{ToolRefusal as y}from"./types.js";const B="bishop",v="/mcp",R="BISHOP_TOOL_TOKEN",A=1048576;function N(i){const r=i.headers.authorization;return r&&/^Bearer\s+(.+)$/i.exec(r.trim())?.[1]?.trim()||void 0}function p(i,r,a){i.writeHead(r,{"content-type":"application/json"}),i.end(JSON.stringify({jsonrpc:"2.0",id:null,error:{code:-32001,message:a}}))}async function I(i){const r=[];let a=0;for await(const n of i){if(a+=n.length,a>A)throw new Error("request body is too large");r.push(n)}if(r.length!==0)return JSON.parse(Buffer.concat(r).toString("utf8"))}const P="Bishop's tools answer to a person, and this message came from another bot. Ask someone in the conversation to do it.";function z(i,r){const a=new O({name:B,version:"1"});for(const n of i)a.registerTool(n.name,{description:n.description,inputSchema:n.input},async w=>{try{if(r.principal.kind==="bot")throw new y(P);return{content:[{type:"text",text:await n.run(w,r)}]}}catch(c){const f=c instanceof Error?c.message:String(c);return c instanceof y?h.info({tool:n.name,thread:r.thread.threadKey,reason:f},"a tool refused"):h.error({tool:n.name,thread:r.thread.threadKey,err:c},"a tool failed"),{content:[{type:"text",text:f}],isError:!0}}});return a}async function D(i){const r=i.host??"127.0.0.1",a=new Map,n=new Map;async function w(e,t){const o=new _({sessionIdGenerator:()=>b(),onsessioninitialized:s=>{if(a.get(e)!==t){o.close().catch(()=>{});return}n.set(s,{token:e,transport:o}),t.sessions.add(s)}});return o.onclose=()=>{const s=o.sessionId;s&&(n.delete(s),t.sessions.delete(s))},await z(t.tools,t.context).connect(o),o}const c=k((e,t)=>{(async()=>{try{if(!e.url?.startsWith(v))return p(t,404,"no such endpoint");const o=N(e),s=o?a.get(o):void 0;if(!s||!o)return p(t,401,"this call carries no valid turn credential");const d=e.method==="POST"?await I(e):void 0,l=e.headers["mcp-session-id"],m=typeof l=="string"?n.get(l):void 0;if(m){if(m.token!==o)return p(t,404,"no such session");await m.transport.handleRequest(e,t,d);return}if(typeof l=="string")return p(t,404,"no such session");if(!x(d))return p(t,400,"the first call on a connection must be initialize");await(await w(o,s)).handleRequest(e,t,d)}catch(o){h.error({err:o},"a tool call could not be handled"),t.headersSent?t.end():p(t,400,"that request could not be handled")}})()});await new Promise((e,t)=>{c.once("error",t),c.listen(0,r,()=>{c.removeListener("error",t),e()})});const f=c.address();if(!f||typeof f=="string")throw new Error("the tool endpoint started without a port");const g=`http://${r}:${f.port}${v}`;return h.info({url:g,tools:i.tools.map(e=>e.name)},"serving the agent its own tools"),{url:g,toolNames:i.tools.map(e=>e.name),grant(e,t){const o=T(32).toString("base64url"),s=t?.only,d=s?i.tools.filter(u=>s.includes(u.name)):i.tools;s&&d.length!==s.length&&h.error({asked:[...s],serving:d.map(u=>u.name)},"a turn asked for tools this endpoint does not serve");const l={context:e,tools:d,sessions:new Set};return a.set(o,l),{access:{url:g,tokenEnvVar:R,token:o},revoke:()=>{if(a.delete(o)){for(const u of l.sessions){const S=n.get(u);n.delete(u),S?.transport.close().catch(E=>{h.debug({err:E},"a tool connection did not close cleanly")})}l.sessions.clear()}}}},async stop(){for(const{transport:e}of n.values())await e.close().catch(()=>{});n.clear(),a.clear(),c.closeAllConnections(),await new Promise(e=>c.close(()=>e()))}}}export{v as PATH,B as SERVER_NAME,R as TOKEN_ENV_VAR,D as startToolServer};
@@ -0,0 +1,3 @@
1
+ import{promptParts as o}from"./harness/principal.js";import{log as a}from"./log.js";import{placedRefs as l}from"./outgoing.js";import{SYSTEM_PRINCIPAL as c}from"./principals.js";function h(t){const n=t.prepare(`INSERT INTO transcript
2
+ (thread_id, turn_id, direction, kind, principal_id, external_id, text, files, at)
3
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`);return{record(i){if(i.length!==0)try{t.exec("BEGIN");try{for(const r of i)n.run(r.threadId,r.turnId,r.direction,r.kind,r.principalId??null,r.externalId??null,r.text,r.files?.length?JSON.stringify(r.files):null,r.at);t.exec("COMMIT")}catch(r){throw t.exec("ROLLBACK"),r}}catch(r){a.warn({err:r,thread:i[0]?.threadId,turn:i[0]?.turnId},"couldn't record the transcript")}}}}function u(t){const n=o(t),i={threadId:t.threadId,turnId:t.turnId,direction:"in"},r=[];if(n.history){const e=(t.history??[]).flatMap(d=>d.kind==="message"?d.files??[]:[]);r.push({...i,kind:"history",principalId:c.id,text:n.history,...e.length?{files:e}:{},at:t.at})}return t.messages.forEach((e,d)=>{r.push({...i,kind:"message",principalId:e.principal.id,...e.externalId?{externalId:e.externalId}:{},text:n.messages[d]??"",...e.files?.length?{files:e.files}:{},at:t.at})}),r}function x(t){const n=l(t.text).flatMap(i=>{const r=t.shared.get(i);return r?[r]:[]});return{threadId:t.threadId,turnId:t.turnId,direction:"out",kind:t.kind,...t.agent?{principalId:t.agent.id}:{},text:t.text,...n.length?{files:n}:{},at:t.at}}export{u as inbound,x as outbound,h as transcriptStore};
package/dist/turn.js CHANGED
@@ -1 +1,3 @@
1
- import{log as d}from"./log.js";import{worktreeName as u}from"./worktrees.js";function A(n){const h=new Map,y=new Set;async function w(e){const a=n.sharedDirNotice;if(!(!a||y.has(e.thread.threadKey))){y.add(e.thread.threadKey);try{await e.notify(a)}catch(o){d.debug({thread:e.thread.threadKey,err:o},"could not deliver the shared-directory notice")}}}function K(e,a,o){if(!n.tools||e.tools===!1)return;const i=Array.isArray(e.tools)?e.tools:void 0;if(i&&i.length===0)return;const r=o.share?.bind(o);return n.tools.grant({thread:e.thread,principal:a,origin:e.origin,destination:()=>e.destination(),...r?{share:r}:{}},...i?[{only:i}]:[])}async function p(e){await w(e);const a=new AbortController,o=e.presence();let i;try{h.set(e.thread.threadKey,a);const r=e.freshSession?void 0:await n.sessions.get(e.thread.id),c=await n.cwd(e.thread),[b,g]=await Promise.all([e.messages(),r?void 0:e.history?.().catch(t=>{d.warn({thread:e.thread.threadKey,err:t},"could not read the history")})]),[f,...m]=b;if(!f)throw new Error("a turn with no messages");const k=m.at(-1)?.principal??f.principal;if(a.signal.aborted){d.info({thread:e.thread.threadKey},"turn stopped before the agent started");return}i=K(e,k,o);const l=n.model?.of(e.thread.id)??{};let s;for await(const t of n.harness.runTurn({messages:[f,...m],cwd:c,threadKey:e.thread.threadKey,worktree:u(e.thread.dirKey),...r?{sessionId:r}:{},...l.model?{model:l.model}:{},...l.effort?{effort:l.effort}:{},...e.note?{note:e.note}:{},...g?.length?{history:g}:{},origin:e.origin,...i?{tools:i.access}:{},signal:a.signal}))t.type==="session"&&(t.model&&(s=t.model),t.sessionId!==r&&await n.sessions.set(e.thread.id,t.sessionId)),t.type==="text"&&d.debug({thread:e.thread.threadKey,chars:t.text.length},"agent said something"),t.type==="tool"&&d.debug({thread:e.thread.threadKey,tool:t.name,detail:t.summary??t.detail},"tool activity"),t.type==="done"&&d.info({thread:e.thread.threadKey,...s?{model:s}:{},...t.usage},"turn complete"),t.type==="error"&&d.error({thread:e.thread.threadKey,...s?{model:s}:{},kind:t.kind,err:t.message},"turn errored"),await o.handle(t)}catch(r){const c=r instanceof Error?r.message:String(r);throw d.error({thread:e.thread.threadKey,err:r},"turn failed before the agent answered"),await o.handle({type:"error",kind:"turn_failed",message:c}),r}finally{i?.revoke(),h.get(e.thread.threadKey)===a&&h.delete(e.thread.threadKey),await o.finish()}}return{run:p,stop(e){const a=h.get(e);return a?(a.abort(),!0):!1}}}export{A as makeTurnRunner};
1
+ import{createHash as q,randomUUID as z}from"node:crypto";import{rm as G}from"node:fs/promises";import{join as L}from"node:path";import{contextChangeNote as Q,guidanceChangeNote as V}from"./harness/principal.js";import{mint as W}from"./ids.js";import{log as o}from"./log.js";import{stageFile as X}from"./sandbox.js";import{inbound as Z,outbound as $}from"./transcript.js";import{worktreeName as ee}from"./worktrees.js";function te(r){const{location:s,visibility:b,audience:I}=r;return{...s?{location:{kind:s.kind,...s.name?{name:s.name}:{}}}:{},visibility:b,audience:I}}const D=Symbol("not yet");function re(r){if(r)try{return JSON.parse(r)}catch{return}}function le(r){const s=new Map,b=new Set;async function I(e){const a=r.sharedDirNotice;if(!(!a||b.has(e.thread.threadKey))){b.add(e.thread.threadKey);try{await e.notify(a)}catch(i){o.debug({thread:e.thread.threadKey,err:i},"could not deliver the shared-directory notice")}}}function J(e,a,i,g,f){if(!r.tools||e.tools===!1)return;const c=Array.isArray(e.tools)?e.tools:void 0;if(c&&c.length===0)return;const l=i.share?.bind(i),n=l&&(async d=>{const m=await l(d);return g.set(m.ref,m),m});return r.tools.grant({thread:e.thread,principal:a,origin:e.origin,destination:()=>e.destination(),...n?{share:n}:{},...f?{stage:f}:{}},...c?[{only:c}]:[])}async function M(e){const a=r.principals;if(!a)return;const i=a.agent(e.origin.name);if(i)return i;try{return a.agent((await e.destination()).interface)}catch(g){o.debug({thread:e.thread.threadKey,err:g},"couldn't name the agent's account");return}}async function U(e){await I(e);const a=new AbortController,i=W("trn"),g=new Map,f=e.presence();let c,l;try{s.set(e.thread.threadKey,a);const n=e.observe?.().catch(t=>(o.warn({thread:e.thread.threadKey,err:t},"could not read where this conversation takes place"),{visibility:"unknown",audience:"unknown"})),d=e.freshSession?void 0:await r.sessions.get(e.thread.id),m=await r.cwd(e.thread),[_,p]=await Promise.all([e.messages(),d?void 0:e.history?.().catch(t=>{o.warn({thread:e.thread.threadKey,err:t},"could not read the history")})]),N=n&&await Promise.race([n,Promise.resolve(D)]),S=N===D?void 0:N;N===D&&n&&(o.debug({thread:e.thread.threadKey},"where this conversation takes place isn't read yet; not waiting for it"),n.then(t=>r.contexts?.record(e.thread.id,e.origin.name,t)).catch(t=>{o.warn({thread:e.thread.threadKey,err:t},"could not record where this conversation takes place")}));const[x,...O]=_;if(!x)throw new Error("a turn with no messages");const H=O.at(-1)?.principal??x.principal,R=r.transcript?await M(e):void 0;if(a.signal.aborted){o.info({thread:e.thread.threadKey},"turn stopped before the agent started");return}const K=await r.sandbox?.policy(m,e.thread);let A;if(K&&r.sandbox){const t=L(r.sandbox.staging,z());l=t,A=C=>X(K,C,t)}c=J(e,H,f,g,A),r.contexts?.record(e.thread.id,e.origin.name,S);const B=r.contexts?.told(e.thread.id),h=S?te(S):n&&re(B),y=h&&JSON.stringify(h),E=!!(d&&y&&B!==y);E&&h&&o.info({thread:e.thread.threadKey,...h},"this conversation changed since the agent was told about it; telling it again");const w=e.origin,T=q("sha256").update(w.guidance).digest("hex"),P=r.contexts?.guided(e.thread.id),j=!!(d&&P&&P!==T);j&&o.info({thread:e.thread.threadKey,interface:w.name},"the interface's guidance changed since the agent was given it; giving it again");const v=[e.note,E&&h?Q(h):void 0,j?V(w.guidance):void 0].filter(Boolean).join(`
2
+
3
+ `),Y=h?{...w,context:h}:w,k=r.model?.of(e.thread.id)??{};let u;const F=(t,C)=>r.transcript?.record([$({threadId:e.thread.id,turnId:i,kind:t,agent:R,text:C,shared:g,at:Date.now()})]);r.transcript?.record(Z({threadId:e.thread.id,turnId:i,messages:[x,...O],...v?{note:v}:{},...p?.length?{history:p}:{},at:Date.now()}));for await(const t of r.harness.runTurn({messages:[x,...O],cwd:m,threadKey:e.thread.threadKey,worktree:ee(e.thread.dirKey),...d?{sessionId:d}:{},...k.model?{model:k.model}:{},...k.effort?{effort:k.effort}:{},...v?{note:v}:{},...p?.length?{history:p}:{},origin:Y,...c?{tools:c.access}:{},...K?{sandbox:K}:{},signal:a.signal}))t.type==="session"&&(t.model&&(u=t.model),t.sessionId!==d&&await r.sessions.set(e.thread.id,t.sessionId),y&&r.contexts?.told(e.thread.id)!==y&&r.contexts?.tell(e.thread.id,y),r.contexts?.guided(e.thread.id)!==T&&r.contexts?.guide(e.thread.id,T)),t.type==="text"&&(F("progress",t.text),o.debug({thread:e.thread.threadKey,chars:t.text.length},"agent said something")),t.type==="tool"&&o.debug({thread:e.thread.threadKey,tool:t.name,detail:t.summary??t.detail},"tool activity"),t.type==="done"&&(F("result",t.text),o.info({thread:e.thread.threadKey,turn:i,...u?{model:u}:{},...t.usage},"turn complete")),t.type==="error"&&o.error({thread:e.thread.threadKey,turn:i,...u?{model:u}:{},kind:t.kind,err:t.message},"turn errored"),await f.handle(t)}catch(n){const d=n instanceof Error?n.message:String(n);throw o.error({thread:e.thread.threadKey,turn:i,err:n},"turn failed before the agent answered"),await f.handle({type:"error",kind:"turn_failed",message:d}),n}finally{c?.revoke(),s.get(e.thread.threadKey)===a&&s.delete(e.thread.threadKey),await f.finish(),l&&await G(l,{recursive:!0,force:!0}).catch(n=>o.warn({dir:l,err:n},"couldn't remove a turn's shared-file copies"))}}return{run:U,stop(e){const a=s.get(e);return a?(a.abort(),!0):!1}}}export{le as makeTurnRunner};
package/dist/worktrees.js CHANGED
@@ -1,3 +1,3 @@
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};
1
+ import{createHash as w}from"node:crypto";import{existsSync as a,mkdirSync as p}from"node:fs";import{join as h,resolve as u}from"node:path";import{BishopError as d}from"./errors.js";import{exec as i}from"./exec.js";const l=64,s="bishop-",m=".claude/worktrees";function I(t){const e=t.replaceAll(/[^a-zA-Z0-9]+/g,"-").replaceAll(/-+/g,"-").replaceAll(/^-+|-+$/g,""),r=`${s}${e}`;if(r.length<=l)return r;const o=w("sha256").update(t).digest("hex").slice(0,8),n=l-s.length-o.length-1;return`${s}${e.slice(0,n)}-${o}`}function g(t,e){return u(t,m,e)}function N(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 M(t){return!!(t?.enabled&&t.isRepo)}function x(t){return a(h(t,".git"))}async function y(t){const e=await i("git",["rev-parse","--show-prefix"],{cwd:t});return e.code===0?e.stdout.trim():""}async function P(t,e){const r=g(t,e),o=await y(t);if(x(r))return f(r,o);if(a(r))throw new d(`${r} 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",e,r,"HEAD"],{cwd:t});if(n.code!==0){await i("git",["worktree","prune"],{cwd:t});const c=await i("git",["worktree","add",r,e],{cwd:t});if(c.code!==0)throw new d(`Couldn't create a git worktree for this thread at ${r}: ${k(n,c)}`)}return f(r,o)}function k(t,e){const r=t.stderr.trim(),o=e.stderr.trim();return!o||o===r?r:`${r}
2
+ and, retrying with the existing branch: ${o}`}function f(t,e){if(!e)return t;const r=u(t,e);return p(r,{recursive:!0}),r}async function S(t){const e=await i("git",["rev-parse","--path-format=absolute","--git-dir","--git-common-dir"],{cwd:t}),r=e.stdout.split(`
3
+ `).filter(o=>o.trim()!=="");return e.code!==0||r.length!==2?[]:[...new Set(r)]}async function $(t){const e=await i("git",["rev-parse","--show-toplevel"],{cwd:t});return e.code===0?e.stdout.trim():void 0}async function W(t){return await $(t)!==void 0}async function _(t){const e=await i("git",["config","--get","user.email"],{cwd:t}),r=await i("git",["config","--get","user.name"],{cwd:t});return e.code===0&&e.stdout.trim()!==""&&r.code===0&&r.stdout.trim()!==""}export{m as WORKTREE_DIR,P as ensureWorktree,S as gitDirs,_ as hasGitIdentity,W as isGitRepo,M as isolates,$ as repoRoot,N as sharedDirNotice,I as worktreeName,g as worktreePath};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agentdeploymentco/bishop",
3
- "version": "0.3.4",
3
+ "version": "0.4.0",
4
4
  "description": "An agent gateway. Connects an agent to Slack and email.",
5
5
  "license": "UNLICENSED",
6
6
  "type": "module",
@@ -22,11 +22,16 @@
22
22
  "build:release": "rm -rf dist && tsc -p tsconfig.release.json && node scripts/minify.mjs && chmod +x dist/cli.js",
23
23
  "prepublishOnly": "npm run build:release && node scripts/check-package.mjs",
24
24
  "dev": "tsx src/cli.ts",
25
- "dev:slack": "./scripts/dev.sh start",
26
- "dev:slack:stop": "./scripts/dev.sh stop",
27
- "dev:slack:restart": "./scripts/dev.sh restart",
28
- "dev:slack:status": "./scripts/dev.sh status",
29
- "dev:slack:logs": "./scripts/dev.sh logs",
25
+ "dev:slack": "./scripts/dev.sh slack start",
26
+ "dev:slack:stop": "./scripts/dev.sh slack stop",
27
+ "dev:slack:restart": "./scripts/dev.sh slack restart",
28
+ "dev:slack:status": "./scripts/dev.sh slack status",
29
+ "dev:slack:logs": "./scripts/dev.sh slack logs",
30
+ "dev:teams": "./scripts/dev.sh teams start",
31
+ "dev:teams:stop": "./scripts/dev.sh teams stop",
32
+ "dev:teams:restart": "./scripts/dev.sh teams restart",
33
+ "dev:teams:status": "./scripts/dev.sh teams status",
34
+ "dev:teams:logs": "./scripts/dev.sh teams logs",
30
35
  "typecheck": "tsc --noEmit",
31
36
  "test": "vitest run",
32
37
  "test:watch": "vitest",
@@ -36,10 +41,10 @@
36
41
  "check": "npm run lint && npm run typecheck && npm test"
37
42
  },
38
43
  "dependencies": {
39
- "@anthropic-ai/claude-agent-sdk": "0.3.280",
44
+ "@anthropic-ai/claude-agent-sdk": "0.3.285",
40
45
  "@microsoft/teams.apps": "^2.0.16",
41
46
  "@modelcontextprotocol/sdk": "^1.30.0",
42
- "@openai/codex-sdk": "0.155.1",
47
+ "@openai/codex-sdk": "0.159.2",
43
48
  "@slack/bolt": "^5.0.0",
44
49
  "@slack/web-api": "^8",
45
50
  "commander": "^13",