@superclawd-ai/fda 0.1.24 → 0.1.26

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.
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  import { createRequire as __cr } from 'module'; const require = __cr(import.meta.url);
3
- import{execFile as mp,spawn as va}from"node:child_process";import fp from"node:crypto";import j from"node:fs";import ot from"node:os";import ie from"node:path";import _s from"node:fs";import Bt from"node:os";import me from"node:path";import{createRequire as Oa}from"node:module";import{fileURLToPath as $a}from"node:url";var Da=Oa(import.meta.url);function Br(s){return s==="~"?Bt.homedir():s.startsWith("~/")?me.join(Bt.homedir(),s.slice(2)):me.resolve(s)}var ut=Br(process.env.FDA_HOME||me.join(Bt.homedir(),".superclawd","fda")),N={home:ut,db:me.join(ut,"fda.db"),key:process.env.FDA_KEY_FILE||me.join(ut,"fda.key"),machine:me.join(ut,"fda.json"),initialPassword:me.join(ut,"initial-password"),requests:me.join(ut,"requests"),uploads:me.join(ut,"uploads"),worktrees:Br(process.env.FDA_WORKTREE_ROOT||me.join(Bt.homedir(),".fda-worktrees")),cliConfig:me.join(process.env.SUPERCLAWD_HOME||me.join(Bt.homedir(),".superclawd"),"config.json")};function ks(s,e){if(!s)return e;let t=Number(s);if(!Number.isInteger(t)||t<1||t>65535)throw new Error(`Invalid port: ${s}`);return t}var yt=ks(process.env.FDA_PORT,63220),bs=ks(process.env.FDA_MCP_PORT,63221),jr=ks(process.env.FDA_PROXY_PORT,63222),Hr=process.env.SUPERCLAWD_MCP_API_URL||"https://mcp-api.superclawd.com",Wr="fda-1",xe=(()=>{for(let s of["../package.json","../../package.json"])try{let e=Da(s);if(e.name==="@superclawd-ai/fda"&&e.version)return e.version}catch{}return"0.0.0-dev"})();function ct(){for(let s of[N.home,N.requests,N.uploads])_s.mkdirSync(s,{recursive:!0,mode:448});try{_s.chmodSync(N.home,448)}catch{}}function pe(s){return me.join(N.requests,s)}function dt(s){return pe(s)}function cn(s){if(!process.argv[1])return!1;try{return $a(s)===_s.realpathSync(process.argv[1])}catch{return!1}}import qa from"node:fs";import{createRequire as Ma}from"node:module";var Ua=/SQLite is an experimental feature/,Fa=process.emitWarning.bind(process);process.emitWarning=(s,...e)=>{let t=typeof s=="string"?s:s?.message;if(!(t&&Ua.test(t)))return Fa(s,...e)};var{DatabaseSync:Ba}=Ma(import.meta.url)("node:sqlite"),ys=[{id:1,sql:`
3
+ import{execFile as mp,spawn as va}from"node:child_process";import fp from"node:crypto";import j from"node:fs";import ot from"node:os";import se from"node:path";import _s from"node:fs";import Bt from"node:os";import me from"node:path";import{createRequire as Oa}from"node:module";import{fileURLToPath as $a}from"node:url";var Da=Oa(import.meta.url);function Br(s){return s==="~"?Bt.homedir():s.startsWith("~/")?me.join(Bt.homedir(),s.slice(2)):me.resolve(s)}var ut=Br(process.env.FDA_HOME||me.join(Bt.homedir(),".superclawd","fda")),N={home:ut,db:me.join(ut,"fda.db"),key:process.env.FDA_KEY_FILE||me.join(ut,"fda.key"),machine:me.join(ut,"fda.json"),initialPassword:me.join(ut,"initial-password"),requests:me.join(ut,"requests"),uploads:me.join(ut,"uploads"),worktrees:Br(process.env.FDA_WORKTREE_ROOT||me.join(Bt.homedir(),".fda-worktrees")),cliConfig:me.join(process.env.SUPERCLAWD_HOME||me.join(Bt.homedir(),".superclawd"),"config.json")};function ks(s,e){if(!s)return e;let t=Number(s);if(!Number.isInteger(t)||t<1||t>65535)throw new Error(`Invalid port: ${s}`);return t}var yt=ks(process.env.FDA_PORT,63220),bs=ks(process.env.FDA_MCP_PORT,63221),jr=ks(process.env.FDA_PROXY_PORT,63222),Hr=process.env.SUPERCLAWD_MCP_API_URL||"https://mcp-api.superclawd.com",Wr="fda-1",xe=(()=>{for(let s of["../package.json","../../package.json"])try{let e=Da(s);if(e.name==="@superclawd-ai/fda"&&e.version)return e.version}catch{}return"0.0.0-dev"})();function ct(){for(let s of[N.home,N.requests,N.uploads])_s.mkdirSync(s,{recursive:!0,mode:448});try{_s.chmodSync(N.home,448)}catch{}}function pe(s){return me.join(N.requests,s)}function dt(s){return pe(s)}function cn(s){if(!process.argv[1])return!1;try{return $a(s)===_s.realpathSync(process.argv[1])}catch{return!1}}import qa from"node:fs";import{createRequire as Ma}from"node:module";var Ua=/SQLite is an experimental feature/,Fa=process.emitWarning.bind(process);process.emitWarning=(s,...e)=>{let t=typeof s=="string"?s:s?.message;if(!(t&&Ua.test(t)))return Fa(s,...e)};var{DatabaseSync:Ba}=Ma(import.meta.url)("node:sqlite"),ys=[{id:1,sql:`
4
4
  CREATE TABLE requests (
5
5
  id TEXT PRIMARY KEY,
6
6
  title TEXT NOT NULL DEFAULT '',
@@ -583,23 +583,23 @@ _Hand-off for <@${r(t.requester)}>: react with \u2705 to approve it (only <@${r(
583
583
  (SELECT COALESCE(SUM(t.cost_usd), 0) FROM turns t WHERE t.request_id = r.id) AS cost_usd,
584
584
  (SELECT GROUP_CONCAT(a.url_or_path, char(10)) FROM artifacts a WHERE a.request_id = r.id AND a.kind = 'pr') AS prs
585
585
  FROM requests r LEFT JOIN channels c ON c.id = r.channel_id`,b=c=>({id:c.id,title:r(c.title)??"",status:c.status,requesterId:c.requester_id,requesterName:c.requester_name,channelId:c.channel_id,channelName:c.channel_name,permalink:c.permalink,branch:c.branch,planStatus:c.plan_status,createdAt:c.created_at,lastActivityAt:c.last_activity_at,costUsd:Math.round((c.cost_usd??0)*1e4)/1e4,prs:c.prs?c.prs.split(`
586
- `):[]});t.get("/api/requests",d(c=>{let p=[],g=[],_=String(c.query.status??"").split(",").map(se=>se.trim()).filter(se=>/^[a-z_]{3,20}$/.test(se));_.length&&(p.push(`r.status IN (${_.map(()=>"?").join(",")})`),g.push(..._)),typeof c.query.requester=="string"&&gr.user.test(c.query.requester)&&(p.push("r.requester_id = ?"),g.push(c.query.requester)),typeof c.query.channel=="string"&&gr.channel.test(c.query.channel)&&(p.push("r.channel_id = ?"),g.push(c.query.channel));let y=typeof c.query.q=="string"?c.query.q.trim().slice(0,100):"";y&&(p.push("(r.title LIKE ? ESCAPE '\\' OR EXISTS (SELECT 1 FROM messages m WHERE m.request_id = r.id AND m.text LIKE ? ESCAPE '\\'))"),g.push(Uo(y),Uo(y)));let P=Hc.has(Number(c.query.pageSize))?Number(c.query.pageSize):25,H=mr(c.query.page,1,1,1e5),F=p.length?` WHERE ${p.join(" AND ")}`:"",x=c.query.sort==="recent"?"r.last_activity_at DESC":"CASE WHEN r.status IN ('completed','failed','cancelled') THEN 1 ELSE 0 END, r.last_activity_at DESC",J=s.db.get(`SELECT COUNT(*) AS n FROM requests r${F}`,...g).n;return{items:s.db.all(`${m}${F} ORDER BY ${x} LIMIT ? OFFSET ?`,...g,P,(H-1)*P).map(b),total:J,page:H,pageSize:P}}));let S=c=>{if(!/^[0-9a-f-]{36}$/i.test(c))throw new I(404,"not_found");let p=s.db.get(`${m} WHERE r.id = ?`,c);if(!p)throw new I(404,"not_found");return p};t.get("/api/requests/:id",d(c=>{let p=S(c.params.id),g=s.db.all("SELECT id, source, author_id, author_name, text, state, delivered_turn_id, received_at FROM messages WHERE request_id = ? ORDER BY received_at, rowid",p.id),_=s.db.all("SELECT * FROM turns WHERE request_id = ? ORDER BY seq",p.id),y=s.db.all("SELECT version, status, markdown, decided_by, decided_at, feedback, created_at FROM plans WHERE request_id = ? ORDER BY version",p.id),P=s.db.all("SELECT id, kind, url_or_path, meta, created_at FROM artifacts WHERE request_id = ? ORDER BY created_at",p.id),H=s.db.all("SELECT id, type, summary, ts FROM events WHERE request_id = ? ORDER BY id DESC LIMIT 200",p.id).reverse();return{prFollows:s.store.prFollowsFor(p.id).map(x=>({url:x.url,status:x.status,statusReason:x.status_reason,ci:x.ci_state,mergeable:x.mergeable,ciAttempts:x.ci_attempts,lastCheckedAt:x.last_checked_at,nextCheckAt:x.next_check_at,lastError:x.last_error})),request:{...b(p),cwd:p.cwd,workdir:p.workdir,pendingQuestion:r(p.pending_question),currentSessionId:p.current_session_id},messages:g.map(x=>({id:x.id,source:x.source,authorId:x.author_id,authorName:x.author_name,text:r(x.text)??"",state:x.state,deliveredTurnId:x.delivered_turn_id,receivedAt:x.received_at})),turns:_.map(x=>({id:x.id,seq:x.seq,mode:x.mode,trigger:x.trigger,status:x.status,outcome:x.outcome,stopReason:x.stop_reason,errorText:r(x.error_text),resultText:r(x.result_text),startedAt:x.started_at,endedAt:x.ended_at,durationMs:x.duration_ms,inputTokens:x.input_tokens,outputTokens:x.output_tokens,costUsd:x.cost_usd})),plans:y.map(x=>({version:x.version,status:x.status,markdown:r(x.markdown)??"",decidedBy:x.decided_by,decidedAt:x.decided_at,feedback:r(x.feedback),createdAt:x.created_at})),artifacts:P.map(x=>{let J=null;try{J=JSON.parse(x.meta??"{}").title??null}catch{J=null}return{id:x.id,kind:x.kind,urlOrPath:x.kind==="pr"?x.url_or_path:Q.basename(x.url_or_path),title:r(J),createdAt:x.created_at}}),events:H.map(x=>({...x,summary:r(x.summary)??""}))}})),t.get("/api/requests/:id/stream",(c,p)=>{let g;try{g=S(c.params.id)}catch{p.status(404).json({error:{code:"not_found"}});return}let _=a(c);p.writeHead(200,{"Content-Type":"text/event-stream","Cache-Control":"no-store",Connection:"keep-alive","X-Accel-Buffering":"no"});let y=(je,ve)=>p.write(`event: ${je}
586
+ `):[]});t.get("/api/requests",d(c=>{let p=[],g=[],_=String(c.query.status??"").split(",").map(re=>re.trim()).filter(re=>/^[a-z_]{3,20}$/.test(re));_.length&&(p.push(`r.status IN (${_.map(()=>"?").join(",")})`),g.push(..._)),typeof c.query.requester=="string"&&gr.user.test(c.query.requester)&&(p.push("r.requester_id = ?"),g.push(c.query.requester)),typeof c.query.channel=="string"&&gr.channel.test(c.query.channel)&&(p.push("r.channel_id = ?"),g.push(c.query.channel));let y=typeof c.query.q=="string"?c.query.q.trim().slice(0,100):"";y&&(p.push("(r.title LIKE ? ESCAPE '\\' OR EXISTS (SELECT 1 FROM messages m WHERE m.request_id = r.id AND m.text LIKE ? ESCAPE '\\'))"),g.push(Uo(y),Uo(y)));let P=Hc.has(Number(c.query.pageSize))?Number(c.query.pageSize):25,H=mr(c.query.page,1,1,1e5),F=p.length?` WHERE ${p.join(" AND ")}`:"",x=c.query.sort==="recent"?"r.last_activity_at DESC":"CASE WHEN r.status IN ('completed','failed','cancelled') THEN 1 ELSE 0 END, r.last_activity_at DESC",J=s.db.get(`SELECT COUNT(*) AS n FROM requests r${F}`,...g).n;return{items:s.db.all(`${m}${F} ORDER BY ${x} LIMIT ? OFFSET ?`,...g,P,(H-1)*P).map(b),total:J,page:H,pageSize:P}}));let S=c=>{if(!/^[0-9a-f-]{36}$/i.test(c))throw new I(404,"not_found");let p=s.db.get(`${m} WHERE r.id = ?`,c);if(!p)throw new I(404,"not_found");return p};t.get("/api/requests/:id",d(c=>{let p=S(c.params.id),g=s.db.all("SELECT id, source, author_id, author_name, text, state, delivered_turn_id, received_at FROM messages WHERE request_id = ? ORDER BY received_at, rowid",p.id),_=s.db.all("SELECT * FROM turns WHERE request_id = ? ORDER BY seq",p.id),y=s.db.all("SELECT version, status, markdown, decided_by, decided_at, feedback, created_at FROM plans WHERE request_id = ? ORDER BY version",p.id),P=s.db.all("SELECT id, kind, url_or_path, meta, created_at FROM artifacts WHERE request_id = ? ORDER BY created_at",p.id),H=s.db.all("SELECT id, type, summary, ts FROM events WHERE request_id = ? ORDER BY id DESC LIMIT 200",p.id).reverse();return{prFollows:s.store.prFollowsFor(p.id).map(x=>({url:x.url,status:x.status,statusReason:x.status_reason,ci:x.ci_state,mergeable:x.mergeable,ciAttempts:x.ci_attempts,lastCheckedAt:x.last_checked_at,nextCheckAt:x.next_check_at,lastError:x.last_error})),request:{...b(p),cwd:p.cwd,workdir:p.workdir,pendingQuestion:r(p.pending_question),currentSessionId:p.current_session_id},messages:g.map(x=>({id:x.id,source:x.source,authorId:x.author_id,authorName:x.author_name,text:r(x.text)??"",state:x.state,deliveredTurnId:x.delivered_turn_id,receivedAt:x.received_at})),turns:_.map(x=>({id:x.id,seq:x.seq,mode:x.mode,trigger:x.trigger,status:x.status,outcome:x.outcome,stopReason:x.stop_reason,errorText:r(x.error_text),resultText:r(x.result_text),startedAt:x.started_at,endedAt:x.ended_at,durationMs:x.duration_ms,inputTokens:x.input_tokens,outputTokens:x.output_tokens,costUsd:x.cost_usd})),plans:y.map(x=>({version:x.version,status:x.status,markdown:r(x.markdown)??"",decidedBy:x.decided_by,decidedAt:x.decided_at,feedback:r(x.feedback),createdAt:x.created_at})),artifacts:P.map(x=>{let J=null;try{J=JSON.parse(x.meta??"{}").title??null}catch{J=null}return{id:x.id,kind:x.kind,urlOrPath:x.kind==="pr"?x.url_or_path:Q.basename(x.url_or_path),title:r(J),createdAt:x.created_at}}),events:H.map(x=>({...x,summary:r(x.summary)??""}))}})),t.get("/api/requests/:id/stream",(c,p)=>{let g;try{g=S(c.params.id)}catch{p.status(404).json({error:{code:"not_found"}});return}let _=a(c);p.writeHead(200,{"Content-Type":"text/event-stream","Cache-Control":"no-store",Connection:"keep-alive","X-Accel-Buffering":"no"});let y=(je,ve)=>p.write(`event: ${je}
587
587
  data: ${JSON.stringify(ve)}
588
588
 
589
- `),P="",H=s.db.get("SELECT MAX(id) AS id FROM events WHERE request_id = ?",g.id)?.id??0,F=()=>{let je=s.db.get("SELECT status, title, plan_status, last_activity_at FROM requests WHERE id = ?",g.id);if(!je)return;let ve={status:je.status,title:r(je.title)??"",planStatus:je.plan_status,lastActivityAt:je.last_activity_at},Fr=JSON.stringify(ve);Fr!==P&&(P=Fr,y("snapshot",ve))},x=()=>{let je=s.db.all("SELECT id, type, summary, ts FROM events WHERE request_id = ? AND id > ? ORDER BY id LIMIT 100",g.id,H);for(let ve of je)H=ve.id,(ve.type==="tool"||ve.type==="text")&&y("activity",{ts:ve.ts,kind:ve.type,text:r(ve.summary)??""})};F();let J=0,Re=setInterval(()=>{J++;try{if(J%30===0&&!s.auth.validate(_,{touch:!1}))return y("expired",{}),se();F(),x(),J%15===0&&y("ping",{})}catch{se()}},1e3);Re.unref();let se=()=>{clearInterval(Re),p.end()};c.on("close",()=>clearInterval(Re))}),t.post("/api/requests/:id/cancel",d(c=>{let p=S(c.params.id);if(s.cancel(p.id,`admin:${c.user.username}`)==="nothing")throw new I(409,"not_cancellable");return i(c.user.username,"request_cancel",{id:p.id},c.ip2??null),{ok:!0}})),t.post("/api/requests/:id/retry",d(c=>{let p=S(c.params.id);if(!s.retry(p.id,`admin:${c.user.username}`))throw new I(409,"not_retryable");return i(c.user.username,"request_retry",{id:p.id},c.ip2??null),{ok:!0}}));let w=()=>({settings:s.settings.all(),secrets:s.settings.maskedSecrets()});t.get("/api/settings",d(w));let T=c=>{if(c===null)return null;if(typeof c!="string")throw new I(400,"invalid_setting","Pick a folder.","workdir");let p=Pt(c,n);if(!p.ok)throw new I(400,"invalid_setting",p.message,"workdir");return p.path};t.put("/api/settings/general",d(c=>{let p=c.body??{},g=Object.create(null);for(let[_,y]of Object.entries(p)){if(_==="__proto__"||_==="constructor"||_==="prototype")throw new I(400,"invalid_setting","Unknown setting.",_);if(_==="publicHostname"&&y!==null&&!Kc(y))throw new I(400,"invalid_setting","Use the full public host name (e.g. fda.example.com).",_);if(Wc.has(_))throw new I(400,"invalid_setting",`${_} can't be changed here.`,_);g[_]=_==="workdir"?T(y):y}try{s.settings.set(g)}catch(_){if(_ instanceof st){let y=/(?:Unknown setting:|Invalid value for) (\w+)/.exec(_.message)?.[1];throw new I(400,"invalid_setting",_.message,y)}throw _}return i(c.user.username,"settings_changed",{changed:Object.keys(g)},c.ip2??null),s.onChanged(),{settings:s.settings.all()}})),t.get("/api/agents",d(async()=>{let c=s.settings.all(),p=c.agentName?{id:c.agentId,name:c.agentName}:null,g=await s.agents();return g.ok?{items:g.data.map(_=>({id:_.id,name:_.name,slug:ke(_.name),description:_.description,model:_.model})),selected:p}:{items:[],selected:p,error:g.kind==="auth"?"This machine isn't logged in to SuperClawd (run `superclawd login` on it).":g.kind==="not_found"?"This SuperClawd server doesn't support FDA machines yet.":"Can't reach SuperClawd right now."}})),t.put("/api/settings/agent",d(async c=>{let p=W(c.body?.agentId,64),g=await s.agents();if(!g.ok)throw new I(503,"cloud_unavailable","Can't reach SuperClawd right now.");let _=g.data.find(y=>y.id===p);if(!_)throw new I(400,"invalid_setting","That agent isn't available.","agentId");return s.settings.set({agentId:_.id,agentName:ke(_.name)}),i(c.user.username,"agent_changed",{agent:ke(_.name)},c.ip2??null),s.onChanged(),{settings:s.settings.all()}})),t.put("/api/settings/anthropic",d(c=>{let p=W(c.body?.apiKey,300).trim();if(!/^sk-ant-[A-Za-z0-9_-]{20,}$/.test(p))throw new I(400,"invalid_key","That isn't an Anthropic API key.");return s.settings.setSecret("anthropicApiKey",p),s.redactor.addSecrets([p]),i(c.user.username,"settings_changed",{changed:["anthropicApiKey"]},c.ip2??null),s.onChanged(),{secrets:s.settings.maskedSecrets()}})),t.post("/api/settings/anthropic/test",d(async()=>{let c=s.settings.getSecret("anthropicApiKey");if(!c)return{ok:!1,message:"No API key is set."};let p=await s.testAnthropic(c);return{ok:p.ok,message:r(p.message)??""}})),t.put("/api/settings/slack",d(async c=>{let p=c.body?.botToken==null?null:W(c.body.botToken,300).trim(),g=c.body?.appToken==null?null:W(c.body.appToken,300).trim();if(p&&!/^xoxb-[A-Za-z0-9-]{10,250}$/.test(p))throw new I(400,"invalid_setting","The bot token starts with xoxb-.","botToken");if(g&&!/^xapp-\d-[A-Za-z0-9-]{10,250}$/.test(g))throw new I(400,"invalid_setting","The app-level token starts with xapp-.","appToken");if(!p&&!g)throw new I(400,"bad_request");return s.db.tx(()=>{p&&s.settings.setSecret("slackBotToken",p),g&&s.settings.setSecret("slackAppToken",g)}),s.redactor.addSecrets([p,g]),i(c.user.username,"settings_changed",{changed:[p&&"slackBotToken",g&&"slackAppToken"].filter(Boolean)},c.ip2??null),s.onChanged(),await s.reloadSlack(),{secrets:s.settings.maskedSecrets()}})),t.post("/api/settings/slack/test",d(async()=>{let c=s.settings.getSecret("slackBotToken");if(!c)return{ok:!1,message:"No bot token is set."};let p=await s.testSlack(c);return{...p,message:r(p.message??null)??void 0}})),t.get("/api/settings/slack/manifest",d(c=>{let p=typeof c.query.name=="string"&&c.query.name.trim()?c.query.name.slice(0,35):s.settings.get("slackBotName")??s.settings.get("agentName")??"SuperClawd FDA";return{manifest:Gn(p)}})),t.get("/api/settings/access",d(()=>({entries:s.store.allowlistEntries().map(c=>({kind:c.kind,slackId:c.slack_id,label:c.label}))}))),t.put("/api/settings/access",d(c=>{let p=c.body?.entries;if(!Array.isArray(p)||p.length>500)throw new I(400,"bad_request");let g=p.map(_=>{let y=_?.kind;if(y!=="channel"&&y!=="user"&&y!=="usergroup")throw new I(400,"bad_request");let P=W(_.slackId,30);if(!gr[y].test(P))throw new I(400,"invalid_setting",`That doesn't look like a Slack ${y} id.`,"entries");let H=_.label==null?null:W(_.label,120);return{kind:y,slackId:P,label:H}});return s.store.setAllowlist(g),i(c.user.username,"access_changed",{count:g.length},c.ip2??null),s.onChanged(),{entries:s.store.allowlistEntries().map(_=>({kind:_.kind,slackId:_.slack_id,label:_.label}))}}));let f=new Map,E=async(c,p)=>{let g=f.get(c);if(g&&e()-g.at<5*6e4)return g.items;let _=await p();return f.set(c,{at:e(),items:_}),_},R=()=>{let c=s.slack();if(!c)throw new I(503,"slack_unavailable","Slack isn't connected.");return c},L=(c,...p)=>!c||p.some(g=>g?.toLowerCase().includes(c));t.get("/api/slack/channels",d(async c=>{let p=R(),g=String(c.query.q??"").toLowerCase().slice(0,80);return{items:(await E("channels",()=>p.listChannels())).filter(y=>L(g,y.name,y.id)).slice(0,200).map(y=>({id:y.id,name:y.name,isPrivate:y.isPrivate,isMember:y.isMember}))}})),t.get("/api/slack/users",d(async c=>{let p=R(),g=String(c.query.q??"").toLowerCase().slice(0,80);return{items:(await E("users",()=>p.listUsers())).filter(y=>!y.isBot&&L(g,y.name,y.id)).slice(0,200).map(y=>({id:y.id,name:y.name,realName:null}))}})),t.get("/api/slack/usergroups",d(async()=>{let c=R();return{items:await E("usergroups",()=>c.listUsergroups())}})),t.get("/api/fs/list",d(c=>{let p=typeof c.query.path=="string"&&c.query.path?c.query.path:n,g;try{g=Z.realpathSync(p.replace(/^~(?=$|\/)/,n))}catch{throw new I(404,"not_found")}if(!(g===n||g.startsWith(n+Q.sep)))throw new I(403,"outside_home");if(Q.relative(n,g).split(Q.sep).some(or))throw new I(403,"hidden");let _=[];for(let y of Z.readdirSync(g,{withFileTypes:!0})){if(!y.isDirectory()||y.name.startsWith("."))continue;let P=Q.join(g,y.name);if(_.push({name:y.name,path:P,isRepo:Z.existsSync(Q.join(P,".git"))}),_.length>=500)break}return _.sort((y,P)=>y.name.localeCompare(P.name)),{path:g,parent:g===n?null:Q.dirname(g),dirs:_}}));let q=c=>{let p=Ce(c,e()),g=c.last_req_status!==void 0?{status:c.last_req_status}:c.last_request_id?s.store.getRequest(c.last_request_id):void 0,_=c.channel_name!==void 0?{name:c.channel_name}:s.store.getChannel(c.channel_id);return{id:c.id,title:r(c.title)??"",prompt:r(c.prompt)??"",cron:c.cron,when:p,once:c.once===1,timezone:c.timezone,channelId:c.channel_id,channelName:_?.name??null,createdBy:c.created_by,createdByName:c.created_by_name,status:c.status,statusReason:c.status_reason,nextRunAt:c.next_run_at,lastRunAt:c.last_run_at,lastRequestId:c.last_request_id,lastRunStatus:c.last_status==="missed"?"missed":g?.status??null,runCount:c.run_count,createdAt:c.created_at}},M=c=>{let p=/^[0-9a-f-]{36}$/i.test(c)?s.store.getSchedule(c):void 0;if(!p||p.status==="cancelled")throw new I(404,"not_found");return p};t.get("/api/schedules",d(()=>({items:s.db.all(`SELECT s.*, r.status AS last_req_status, c.name AS channel_name FROM schedules s
589
+ `),P="",H=s.db.get("SELECT MAX(id) AS id FROM events WHERE request_id = ?",g.id)?.id??0,F=()=>{let je=s.db.get("SELECT status, title, plan_status, last_activity_at FROM requests WHERE id = ?",g.id);if(!je)return;let ve={status:je.status,title:r(je.title)??"",planStatus:je.plan_status,lastActivityAt:je.last_activity_at},Fr=JSON.stringify(ve);Fr!==P&&(P=Fr,y("snapshot",ve))},x=()=>{let je=s.db.all("SELECT id, type, summary, ts FROM events WHERE request_id = ? AND id > ? ORDER BY id LIMIT 100",g.id,H);for(let ve of je)H=ve.id,(ve.type==="tool"||ve.type==="text")&&y("activity",{ts:ve.ts,kind:ve.type,text:r(ve.summary)??""})};F();let J=0,Re=setInterval(()=>{J++;try{if(J%30===0&&!s.auth.validate(_,{touch:!1}))return y("expired",{}),re();F(),x(),J%15===0&&y("ping",{})}catch{re()}},1e3);Re.unref();let re=()=>{clearInterval(Re),p.end()};c.on("close",()=>clearInterval(Re))}),t.post("/api/requests/:id/cancel",d(c=>{let p=S(c.params.id);if(s.cancel(p.id,`admin:${c.user.username}`)==="nothing")throw new I(409,"not_cancellable");return i(c.user.username,"request_cancel",{id:p.id},c.ip2??null),{ok:!0}})),t.post("/api/requests/:id/retry",d(c=>{let p=S(c.params.id);if(!s.retry(p.id,`admin:${c.user.username}`))throw new I(409,"not_retryable");return i(c.user.username,"request_retry",{id:p.id},c.ip2??null),{ok:!0}}));let w=()=>({settings:s.settings.all(),secrets:s.settings.maskedSecrets()});t.get("/api/settings",d(w));let T=c=>{if(c===null)return null;if(typeof c!="string")throw new I(400,"invalid_setting","Pick a folder.","workdir");let p=Pt(c,n);if(!p.ok)throw new I(400,"invalid_setting",p.message,"workdir");return p.path};t.put("/api/settings/general",d(c=>{let p=c.body??{},g=Object.create(null);for(let[_,y]of Object.entries(p)){if(_==="__proto__"||_==="constructor"||_==="prototype")throw new I(400,"invalid_setting","Unknown setting.",_);if(_==="publicHostname"&&y!==null&&!Kc(y))throw new I(400,"invalid_setting","Use the full public host name (e.g. fda.example.com).",_);if(Wc.has(_))throw new I(400,"invalid_setting",`${_} can't be changed here.`,_);g[_]=_==="workdir"?T(y):y}try{s.settings.set(g)}catch(_){if(_ instanceof st){let y=/(?:Unknown setting:|Invalid value for) (\w+)/.exec(_.message)?.[1];throw new I(400,"invalid_setting",_.message,y)}throw _}return i(c.user.username,"settings_changed",{changed:Object.keys(g)},c.ip2??null),s.onChanged(),{settings:s.settings.all()}})),t.get("/api/agents",d(async()=>{let c=s.settings.all(),p=c.agentName?{id:c.agentId,name:c.agentName}:null,g=await s.agents();return g.ok?{items:g.data.map(_=>({id:_.id,name:_.name,slug:ke(_.name),description:_.description,model:_.model})),selected:p}:{items:[],selected:p,error:g.kind==="auth"?"This machine isn't logged in to SuperClawd (run `superclawd login` on it).":g.kind==="not_found"?"This SuperClawd server doesn't support FDA machines yet.":"Can't reach SuperClawd right now."}})),t.put("/api/settings/agent",d(async c=>{let p=W(c.body?.agentId,64),g=await s.agents();if(!g.ok)throw new I(503,"cloud_unavailable","Can't reach SuperClawd right now.");let _=g.data.find(y=>y.id===p);if(!_)throw new I(400,"invalid_setting","That agent isn't available.","agentId");return s.settings.set({agentId:_.id,agentName:ke(_.name)}),i(c.user.username,"agent_changed",{agent:ke(_.name)},c.ip2??null),s.onChanged(),{settings:s.settings.all()}})),t.put("/api/settings/anthropic",d(c=>{let p=W(c.body?.apiKey,300).trim();if(!/^sk-ant-[A-Za-z0-9_-]{20,}$/.test(p))throw new I(400,"invalid_key","That isn't an Anthropic API key.");return s.settings.setSecret("anthropicApiKey",p),s.redactor.addSecrets([p]),i(c.user.username,"settings_changed",{changed:["anthropicApiKey"]},c.ip2??null),s.onChanged(),{secrets:s.settings.maskedSecrets()}})),t.post("/api/settings/anthropic/test",d(async()=>{let c=s.settings.getSecret("anthropicApiKey");if(!c)return{ok:!1,message:"No API key is set."};let p=await s.testAnthropic(c);return{ok:p.ok,message:r(p.message)??""}})),t.put("/api/settings/slack",d(async c=>{let p=c.body?.botToken==null?null:W(c.body.botToken,300).trim(),g=c.body?.appToken==null?null:W(c.body.appToken,300).trim();if(p&&!/^xoxb-[A-Za-z0-9-]{10,250}$/.test(p))throw new I(400,"invalid_setting","The bot token starts with xoxb-.","botToken");if(g&&!/^xapp-\d-[A-Za-z0-9-]{10,250}$/.test(g))throw new I(400,"invalid_setting","The app-level token starts with xapp-.","appToken");if(!p&&!g)throw new I(400,"bad_request");return s.db.tx(()=>{p&&s.settings.setSecret("slackBotToken",p),g&&s.settings.setSecret("slackAppToken",g)}),s.redactor.addSecrets([p,g]),i(c.user.username,"settings_changed",{changed:[p&&"slackBotToken",g&&"slackAppToken"].filter(Boolean)},c.ip2??null),s.onChanged(),await s.reloadSlack(),{secrets:s.settings.maskedSecrets()}})),t.post("/api/settings/slack/test",d(async()=>{let c=s.settings.getSecret("slackBotToken");if(!c)return{ok:!1,message:"No bot token is set."};let p=await s.testSlack(c);return{...p,message:r(p.message??null)??void 0}})),t.get("/api/settings/slack/manifest",d(c=>{let p=typeof c.query.name=="string"&&c.query.name.trim()?c.query.name.slice(0,35):s.settings.get("slackBotName")??s.settings.get("agentName")??"SuperClawd FDA";return{manifest:Gn(p)}})),t.get("/api/settings/access",d(()=>({entries:s.store.allowlistEntries().map(c=>({kind:c.kind,slackId:c.slack_id,label:c.label}))}))),t.put("/api/settings/access",d(c=>{let p=c.body?.entries;if(!Array.isArray(p)||p.length>500)throw new I(400,"bad_request");let g=p.map(_=>{let y=_?.kind;if(y!=="channel"&&y!=="user"&&y!=="usergroup")throw new I(400,"bad_request");let P=W(_.slackId,30);if(!gr[y].test(P))throw new I(400,"invalid_setting",`That doesn't look like a Slack ${y} id.`,"entries");let H=_.label==null?null:W(_.label,120);return{kind:y,slackId:P,label:H}});return s.store.setAllowlist(g),i(c.user.username,"access_changed",{count:g.length},c.ip2??null),s.onChanged(),{entries:s.store.allowlistEntries().map(_=>({kind:_.kind,slackId:_.slack_id,label:_.label}))}}));let f=new Map,E=async(c,p)=>{let g=f.get(c);if(g&&e()-g.at<5*6e4)return g.items;let _=await p();return f.set(c,{at:e(),items:_}),_},R=()=>{let c=s.slack();if(!c)throw new I(503,"slack_unavailable","Slack isn't connected.");return c},L=(c,...p)=>!c||p.some(g=>g?.toLowerCase().includes(c));t.get("/api/slack/channels",d(async c=>{let p=R(),g=String(c.query.q??"").toLowerCase().slice(0,80);return{items:(await E("channels",()=>p.listChannels())).filter(y=>L(g,y.name,y.id)).slice(0,200).map(y=>({id:y.id,name:y.name,isPrivate:y.isPrivate,isMember:y.isMember}))}})),t.get("/api/slack/users",d(async c=>{let p=R(),g=String(c.query.q??"").toLowerCase().slice(0,80);return{items:(await E("users",()=>p.listUsers())).filter(y=>!y.isBot&&L(g,y.name,y.id)).slice(0,200).map(y=>({id:y.id,name:y.name,realName:null}))}})),t.get("/api/slack/usergroups",d(async()=>{let c=R();return{items:await E("usergroups",()=>c.listUsergroups())}})),t.get("/api/fs/list",d(c=>{let p=typeof c.query.path=="string"&&c.query.path?c.query.path:n,g;try{g=Z.realpathSync(p.replace(/^~(?=$|\/)/,n))}catch{throw new I(404,"not_found")}if(!(g===n||g.startsWith(n+Q.sep)))throw new I(403,"outside_home");if(Q.relative(n,g).split(Q.sep).some(or))throw new I(403,"hidden");let _=[];for(let y of Z.readdirSync(g,{withFileTypes:!0})){if(!y.isDirectory()||y.name.startsWith("."))continue;let P=Q.join(g,y.name);if(_.push({name:y.name,path:P,isRepo:Z.existsSync(Q.join(P,".git"))}),_.length>=500)break}return _.sort((y,P)=>y.name.localeCompare(P.name)),{path:g,parent:g===n?null:Q.dirname(g),dirs:_}}));let q=c=>{let p=Ce(c,e()),g=c.last_req_status!==void 0?{status:c.last_req_status}:c.last_request_id?s.store.getRequest(c.last_request_id):void 0,_=c.channel_name!==void 0?{name:c.channel_name}:s.store.getChannel(c.channel_id);return{id:c.id,title:r(c.title)??"",prompt:r(c.prompt)??"",cron:c.cron,when:p,once:c.once===1,timezone:c.timezone,channelId:c.channel_id,channelName:_?.name??null,createdBy:c.created_by,createdByName:c.created_by_name,status:c.status,statusReason:c.status_reason,nextRunAt:c.next_run_at,lastRunAt:c.last_run_at,lastRequestId:c.last_request_id,lastRunStatus:c.last_status==="missed"?"missed":g?.status??null,runCount:c.run_count,createdAt:c.created_at}},M=c=>{let p=/^[0-9a-f-]{36}$/i.test(c)?s.store.getSchedule(c):void 0;if(!p||p.status==="cancelled")throw new I(404,"not_found");return p};t.get("/api/schedules",d(()=>({items:s.db.all(`SELECT s.*, r.status AS last_req_status, c.name AS channel_name FROM schedules s
590
590
  LEFT JOIN requests r ON r.id = s.last_request_id
591
591
  LEFT JOIN channels c ON c.id = s.channel_id
592
- WHERE s.status != 'cancelled' ORDER BY s.created_at DESC LIMIT 500`).map(q)}))),t.get("/api/schedules/:id",d(c=>{let p=M(c.params.id),g=s.store.scheduleRuns(p.id,20).map(_=>({requestId:_.id,status:_.status,createdAt:_.created_at,permalink:_.permalink}));return{schedule:q(p),runs:g}})),t.patch("/api/schedules/:id",d(c=>{let p=M(c.params.id),g=c.body??{},_={};g.title!==void 0&&(_.title=W(g.title,80).trim()||p.title),g.prompt!==void 0&&(_.prompt=W(g.prompt,Xt).trim()||p.prompt);let y=g.cron!==void 0?W(g.cron,100).trim():p.cron,P=g.timezone!==void 0?W(g.timezone,60).trim():p.timezone;if(g.cron!==void 0||g.timezone!==void 0){let F;try{F=Ne(y)}catch(J){throw new I(400,"invalid_setting",J.message,"cron")}if(!xt(P))throw new I(400,"invalid_setting","Unknown timezone.","timezone");let x=s.settings.get("scheduleMinIntervalMin");if(!p.once&&Rn(F,P,e())<x*6e4)throw new I(400,"invalid_setting",`Runs can be at most every ${x} minutes.`,"cron");_.cron=F.source,_.timezone=P}if(g.status!==void 0){if(g.status!=="active"&&g.status!=="paused")throw new I(400,"invalid_setting","Status must be active or paused.","status");if(p.status==="pending")throw new I(409,"not_confirmed","The requester hasn't confirmed this schedule yet.");_.status=g.status,_.status_reason=g.status==="paused"?"Paused by an admin.":null,_.paused_by=g.status==="paused"?"admin":null}let H=_.status??p.status;return H==="active"&&(_.next_run_at=_e(Ne(_.cron??p.cron),_.timezone??p.timezone,e())),H==="paused"&&(_.next_run_at=null),s.store.updateSchedule(p.id,_,e()),i(c.user.username,"schedule_changed",{id:p.id,changed:Object.keys(_)},c.ip2??null),s.scheduler?.wake(),{schedule:q(s.store.getSchedule(p.id))}}));let z=c=>({repoPath:c.repo_path,script:c.script,onFailure:c.on_failure,timeoutSec:c.timeout_sec,enabled:c.enabled===1,proposal:c.proposed_script?{script:c.proposed_script,reason:c.proposed_reason,requestId:c.proposed_request_id,at:c.proposed_at}:null,updatedBy:c.updated_by,updatedAt:c.updated_at,runs:s.store.setupRuns({repoPath:c.repo_path},10).map(p=>({id:p.id,requestId:p.request_id,status:p.status,exitCode:p.exit_code,log:p.log_tail,durationMs:p.duration_ms,startedAt:p.started_at}))}),A=c=>{let p=W(c,1e3).trim(),g;try{g=Z.realpathSync(p)}catch{throw new I(400,"invalid_setting","That folder doesn't exist.","repoPath")}if(!Z.existsSync(Q.join(g,".git")))throw new I(400,"invalid_setting","That folder isn't a git repository.","repoPath");return g};t.get("/api/setup",d(()=>{let c=s.settings.get("workdir"),p=null,g=!1;try{c&&(p=Z.realpathSync(c),g=Z.existsSync(Q.join(p,".git")))}catch{}return{workdir:p,isRepo:g,scripts:s.store.listSetupScripts().map(z)}})),t.put("/api/setup",d(c=>{let p=c.body??{},g=A(p.repoPath),_=W(p.script,Ln).trim()||null,y=p.onFailure==="fail"?"fail":"continue",P=Number(p.timeoutSec??600);if(!Number.isInteger(P)||P<10||P>3600)throw new I(400,"invalid_setting","Timeout must be 10 to 3600 seconds.","timeoutSec");return s.store.saveSetupScript(g,{script:_,onFailure:y,timeoutSec:P,enabled:p.enabled!==!1,by:c.user.username,clearProposal:p.clearProposal===!0},e()),i(c.user.username,"setup_script_saved",{repoPath:g,onFailure:y,enabled:p.enabled!==!1},c.ip2??null),{script:z(s.store.getSetupScript(g))}})),t.post("/api/setup/proposal",d(c=>{let p=c.body??{},g=A(p.repoPath),_=s.store.getSetupScript(g);if(!_?.proposed_script)throw new I(404,"not_found","There's no proposal for this repository.");let y=p.action==="approve";if(y&&Number(p.proposedAt)!==_.proposed_at)throw new I(409,"proposal_changed","The proposal changed since you opened it. Review it again.");return y?s.store.saveSetupScript(g,{script:_.proposed_script,onFailure:_.on_failure,timeoutSec:_.timeout_sec,enabled:!0,by:c.user.username,clearProposal:!0},e()):s.store.clearSetupProposal(g,e()),i(c.user.username,y?"setup_proposal_approved":"setup_proposal_rejected",{repoPath:g},c.ip2??null),{script:z(s.store.getSetupScript(g))}})),t.delete("/api/setup",d(c=>{let p=W(c.body?.repoPath,1e3);if(!s.store.deleteSetupScript(p))throw new I(404,"not_found");return i(c.user.username,"setup_script_deleted",{repoPath:p},c.ip2??null),{ok:!0}})),t.delete("/api/schedules/:id",d(c=>{let p=M(c.params.id);return s.store.updateSchedule(p.id,{status:"cancelled",next_run_at:null},e()),i(c.user.username,"schedule_deleted",{id:p.id,title:p.title},c.ip2??null),s.scheduler?.wake(),{ok:!0}})),t.post("/api/schedules/:id/run",d(async c=>{let p=M(c.params.id);if(p.status==="pending")throw new I(409,"not_confirmed","The requester hasn't confirmed this schedule yet.");if(!s.scheduler)throw new I(503,"unavailable");let g=await s.scheduler.runNow(p.id);if(i(c.user.username,"schedule_run",{id:p.id,ok:g.ok},c.ip2??null),!g.ok)throw new I(409,"not_started",g.message);return{ok:!0}}));let $=()=>{if(!s.tickets)throw new I(404,"not_available");return s.tickets},le=c=>`${o(c)?"https":"http"}://${String(c.headers.host??"")}/trackers/linear/callback`,De=c=>`${o(c)?"https":"http"}://${String(c.headers.host??"")}/trackers/jira/callback`,Ut=()=>{let c=$().jira;if(!c)throw new I(404,"not_available");return c},K=c=>{let p=$(),g=s.settings.all(),_=p.store.list(100).map(y=>{let P=y.request_id?s.store.getRequest(y.request_id):void 0;return{id:y.id,tracker:y.tracker,key:y.key,url:y.url,title:r(y.title)??"",state:y.state,statusNote:y.status_note,delegator:y.delegator_name,prUrl:y.pr_url,requestId:y.request_id,requestStatus:P?.status??null,costUsd:y.request_id?p.store.requestCost(y.request_id):0,updatedAt:y.updated_at}});return{settings:{enabled:g.ticketsEnabled,atATime:g.ticketsAtATime,budgetUsd:g.ticketBudgetUsd,dailyBudgetUsd:g.ticketDailyBudgetUsd,autoMerge:g.ticketsAutoMerge,slackChannel:g.ticketsSlackChannel,statusOnStart:g.ticketsStatusOnStart,statusOnPr:g.ticketsStatusOnPr},linear:{...p.linear.status(),clientId:g.linearClientId,hasSecret:!!s.settings.getSecret("linearClientSecret"),teams:g.linearTeams,redirectUri:le(c)},github:{repos:g.githubIssueRepos,label:g.githubIssueLabel,people:g.githubIssuePeople},jira:p.jira?{...p.jira.status(),available:!0,clientId:g.jiraClientId,hasSecret:!!s.settings.getSecret("jiraClientSecret"),siteInput:g.jiraSite,projects:g.jiraProjects,label:g.jiraLabel,people:g.jiraPeople,redirectUri:De(c)}:{available:!1},bridge:{enabled:g.ticketsBridge,linear:{url:g.ticketsBridgeLinear?.url??null,hasSecret:!!s.settings.getSecret("linearWebhookSecret")},github:{url:g.ticketsBridgeGithub?.url??null,hasSecret:!!s.settings.getSecret("githubWebhookSecret")}},items:_}},an=async c=>{try{return await c()}catch(p){throw p instanceof Be||p instanceof ge?new I(400,p.code,p.message):p}};t.get("/api/tickets",d(c=>K(c))),t.put("/api/tickets",d(c=>{$();let p=c.body??{},g={enabled:"ticketsEnabled",atATime:"ticketsAtATime",budgetUsd:"ticketBudgetUsd",dailyBudgetUsd:"ticketDailyBudgetUsd",autoMerge:"ticketsAutoMerge",slackChannel:"ticketsSlackChannel",statusOnStart:"ticketsStatusOnStart",statusOnPr:"ticketsStatusOnPr",linearClientId:"linearClientId",linearTeams:"linearTeams",githubRepos:"githubIssueRepos",githubLabel:"githubIssueLabel",githubPeople:"githubIssuePeople",jiraClientId:"jiraClientId",jiraSite:"jiraSite",jiraProjects:"jiraProjects",jiraLabel:"jiraLabel",jiraPeople:"jiraPeople",bridge:"ticketsBridge"},_={};for(let[y,P]of Object.entries(g))p[y]!==void 0&&(_[P]=p[y]===""?null:p[y]);try{Object.keys(_).length&&s.settings.set(_)}catch(y){throw new I(400,"invalid_setting",y.message)}return typeof p.linearWebhookSecret=="string"&&p.linearWebhookSecret.trim()&&s.settings.setSecret("linearWebhookSecret",p.linearWebhookSecret.trim().slice(0,200)),typeof p.linearClientSecret=="string"&&p.linearClientSecret.trim()&&s.settings.setSecret("linearClientSecret",p.linearClientSecret.trim().slice(0,500)),typeof p.jiraClientSecret=="string"&&p.jiraClientSecret.trim()&&s.settings.setSecret("jiraClientSecret",p.jiraClientSecret.trim().slice(0,500)),i(c.user.username,"tickets_settings",{changed:[...Object.keys(_),...p.linearClientSecret?["linearClientSecret"]:[],...p.jiraClientSecret?["jiraClientSecret"]:[]]},c.ip2??null),s.onChanged(),K(c)})),t.post("/api/tickets/bridge/:tracker",d(async c=>{let p=$(),g=c.params.tracker;if(g!=="linear"&&g!=="github")throw new I(404,"not_found");if(!p.bridge)throw new I(404,"not_available");let _=null;g==="github"&&(c.body?.rotate===!0||!s.settings.getSecret("githubWebhookSecret"))&&(_=jc.randomBytes(24).toString("hex"),s.settings.setSecret("githubWebhookSecret",_));let y=await p.bridge.register(g);if("error"in y)throw new I(409,"bridge_failed",y.error);return i(c.user.username,"ticket_bridge_registered",{tracker:g},c.ip2??null),{...K(c),url:y.url,secret:_}})),t.post("/api/tickets/linear/connect",d(c=>an(()=>({url:$().linear.authUrl(le(c))})))),t.post("/api/tickets/linear/exchange",d(async c=>{let p=W(c.body?.code,2e3),g=W(c.body?.state,200);if(!p||!g)throw new I(400,"bad_request");let _=await an(()=>$().linear.exchange(p,g));return i(c.user.username,"linear_connected",{workspace:_.workspace},c.ip2??null),s.onChanged(),K(c)})),t.delete("/api/tickets/linear/connection",d(c=>($().linear.disconnect(),i(c.user.username,"linear_disconnected",void 0,c.ip2??null),s.onChanged(),K(c)))),t.post("/api/tickets/jira/connect",d(c=>an(()=>({url:Ut().authUrl(De(c))})))),t.post("/api/tickets/jira/exchange",d(async c=>{let p=W(c.body?.code,2e3),g=W(c.body?.state,200);if(!p||!g)throw new I(400,"bad_request");let _=await an(()=>Ut().exchange(p,g));return i(c.user.username,"jira_connected",{site:_.site,account:_.account},c.ip2??null),s.onChanged(),K(c)})),t.delete("/api/tickets/jira/connection",d(c=>(Ut().disconnect(),i(c.user.username,"jira_disconnected",void 0,c.ip2??null),s.onChanged(),K(c)))),t.post("/api/tickets/poll",d(async c=>{let p=$(),g=await p.service.pollOnce("linear")+await p.service.pollOnce("github")+await p.service.pollOnce("jira");return{...K(c),handled:g}}));let Ft=()=>{if(!s.calendar)throw new I(404,"not_available");return s.calendar},Dr=c=>`${o(c)?"https":"http"}://${String(c.headers.host??"")}/calendar/callback`,ln=c=>({...Ft().status(),clientId:s.settings.get("googleClientId"),hasSecret:!!s.settings.getSecret("googleClientSecret"),allowExternal:s.settings.get("calendarAllowExternal"),redirectUri:Dr(c)}),qr=async c=>{try{return await c()}catch(p){throw p instanceof Fe?new I(400,p.code,p.message):p}};t.get("/api/calendar",d(c=>ln(c))),t.put("/api/calendar",d(c=>{Ft();let p=c.body??{},g={};p.clientId!==void 0&&(g.googleClientId=p.clientId===""||p.clientId===null?null:W(p.clientId,300).trim()),p.enabled!==void 0&&(g.calendarEnabled=p.enabled===!0),p.allowExternal!==void 0&&(g.calendarAllowExternal=p.allowExternal===!0);try{Object.keys(g).length&&s.settings.set(g)}catch(_){throw new I(400,"invalid_setting",_.message,"clientId")}return typeof p.clientSecret=="string"&&p.clientSecret.trim()&&s.settings.setSecret("googleClientSecret",p.clientSecret.trim().slice(0,500)),i(c.user.username,"calendar_settings",{changed:[...Object.keys(g),...p.clientSecret?["clientSecret"]:[]]},c.ip2??null),s.onChanged(),ln(c)})),t.post("/api/calendar/connect",d(c=>qr(()=>({url:Ft().authUrl(Dr(c))})))),t.post("/api/calendar/exchange",d(async c=>{let p=W(c.body?.code,2e3),g=W(c.body?.state,200);if(!p||!g)throw new I(400,"bad_request");let _=await qr(()=>Ft().exchange(p,g));return i(c.user.username,"calendar_connected",{account:_.account},c.ip2??null),s.onChanged(),ln(c)})),t.delete("/api/calendar/connection",d(async c=>(await Ft().disconnect(),i(c.user.username,"calendar_disconnected",void 0,c.ip2??null),s.onChanged(),ln(c))));let Mr=c=>Q.join((s.requestDir??pe)(c),"out"),Ur=(c,p="",g=0)=>{let _;try{_=Z.readdirSync(c,{withFileTypes:!0})}catch{return[]}let y=[];for(let P of _){if(!qo.test(P.name))continue;let H=Q.join(c,P.name);if(P.isDirectory()&&g<1){y.push(...Ur(H,`${p}${P.name}/`,g+1));continue}if(!P.isFile()||!dr[Q.extname(P.name).toLowerCase()])continue;let F=Z.lstatSync(H,{throwIfNoEntry:!1});!F?.isFile()||F.size>Mo||y.push({name:p+P.name,size:F.size,mtime:F.mtimeMs})}return y};t.get("/api/requests/:id/screenshots",d(c=>{let p=S(c.params.id);return{items:Ur(Mr(p.id)).sort((_,y)=>y.mtime-_.mtime).slice(0,200)}})),t.get("/api/requests/:id/screenshots/*",d((c,p)=>{let g=S(c.params.id),_=String(c.params[0]??""),y=_.split("/");if(y.length>2||!y.every(J=>qo.test(J))||!dr[Q.extname(_).toLowerCase()])throw new I(404,"not_found");let P=Mr(g.id),H=P;for(let J of y.slice(0,-1))if(H=Q.join(H,J),!Z.lstatSync(H,{throwIfNoEntry:!1})?.isDirectory())throw new I(404,"not_found");let F=Q.join(P,...y),x;try{x=Z.openSync(F,Z.constants.O_RDONLY|Z.constants.O_NOFOLLOW)}catch{throw new I(404,"not_found")}try{let J=Z.fstatSync(x);if(!J.isFile()||J.size>Mo)throw new I(404,"not_found");let Re=Buffer.alloc(J.size);Z.readSync(x,Re,0,J.size,0),p.setHeader("Content-Type",dr[Q.extname(_).toLowerCase()]),p.setHeader("Content-Security-Policy","default-src 'none'; sandbox"),p.setHeader("Cache-Control","private, max-age=300"),p.setHeader("Content-Disposition",`inline; filename="${Q.basename(_)}"`),p.end(Re)}finally{Z.closeSync(x)}}));let nt=()=>{if(!s.browser)throw new I(404,"not_available");return s.browser},un=async c=>{try{return await c()}catch(p){throw p instanceof V?new I(p.code==="not_open"||p.code==="not_installed"?409:400,p.code,p.message):p}},bt=()=>{let c=nt();return{installed:c.session.installed(),enabled:s.settings.get("browserEnabled"),session:{...c.session.status(),sandboxed:c.session.isSandboxed()},sites:c.logins.sites()}};if(t.get("/api/browser",d(bt)),t.post("/api/browser/session",d(async c=>{let p=nt(),g=W(c.body?.url,2e3),_=p.session.status().open;return await un(()=>p.session.open(g)),_||i(c.user.username,"browser_open",{url:Zc(g)},c.ip2??null),bt()})),t.post("/api/browser/session/input",d(async c=>{let p=nt(),g=c.body;if(!g||typeof g!="object")throw new I(400,"bad_input");return await un(()=>p.session.input(g)),{ok:!0}})),t.post("/api/browser/session/go",d(async c=>{let p=nt(),g=W(c.body?.action,10);if(g!=="back"&&g!=="forward"&&g!=="reload")throw new I(400,"bad_input");return await un(()=>p.session.go(g)),{ok:!0}})),t.post("/api/browser/session/save",d(async c=>{let p=nt(),g=await un(()=>p.session.storageState()),_=p.logins.load(),y=new Set(g.cookies.map(F=>F.domain)),P=new Set(g.origins.map(F=>F.origin)),H={cookies:[...(_?.cookies??[]).filter(F=>!y.has(F.domain)),...g.cookies],origins:[...(_?.origins??[]).filter(F=>!P.has(F.origin)),...g.origins]};try{p.logins.save(H)}catch(F){throw new I(400,"save_failed",String(F.message))}return c.body?.close===!0&&await p.session.close(),i(c.user.username,"browser_save",{sites:p.logins.sites().map(F=>F.site)},c.ip2??null),bt()})),t.delete("/api/browser/session",d(async c=>{let p=nt();return p.session.status().open&&(await p.session.close(),i(c.user.username,"browser_close",void 0,c.ip2??null)),bt()})),t.delete("/api/browser/sites",d(c=>(nt().logins.clear(),i(c.user.username,"browser_sign_out_all",void 0,c.ip2??null),bt()))),t.delete("/api/browser/sites/:site",d(c=>{let p=nt(),g=W(c.params.site,255).toLowerCase();if(!p.logins.signOut(g))throw new I(404,"not_found");return i(c.user.username,"browser_sign_out",{site:g},c.ip2??null),bt()})),t.get("/api/browser/session/stream",(c,p)=>{if(!s.browser){p.status(404).json({error:{code:"not_available"}});return}let g=s.browser,_=a(c);p.writeHead(200,{"Content-Type":"text/event-stream","Cache-Control":"no-store",Connection:"keep-alive","X-Accel-Buffering":"no"});let y=null,P=!1,H=()=>{if(!y||P)return;let se=y;y=null,P=!p.write(`event: frame
593
- data: ${JSON.stringify({seq:se.seq,url:se.url,title:se.title,tabs:se.tabs,data:se.data})}
592
+ WHERE s.status != 'cancelled' ORDER BY s.created_at DESC LIMIT 500`).map(q)}))),t.get("/api/schedules/:id",d(c=>{let p=M(c.params.id),g=s.store.scheduleRuns(p.id,20).map(_=>({requestId:_.id,status:_.status,createdAt:_.created_at,permalink:_.permalink}));return{schedule:q(p),runs:g}})),t.patch("/api/schedules/:id",d(c=>{let p=M(c.params.id),g=c.body??{},_={};g.title!==void 0&&(_.title=W(g.title,80).trim()||p.title),g.prompt!==void 0&&(_.prompt=W(g.prompt,Xt).trim()||p.prompt);let y=g.cron!==void 0?W(g.cron,100).trim():p.cron,P=g.timezone!==void 0?W(g.timezone,60).trim():p.timezone;if(g.cron!==void 0||g.timezone!==void 0){let F;try{F=Ne(y)}catch(J){throw new I(400,"invalid_setting",J.message,"cron")}if(!xt(P))throw new I(400,"invalid_setting","Unknown timezone.","timezone");let x=s.settings.get("scheduleMinIntervalMin");if(!p.once&&Rn(F,P,e())<x*6e4)throw new I(400,"invalid_setting",`Runs can be at most every ${x} minutes.`,"cron");_.cron=F.source,_.timezone=P}if(g.status!==void 0){if(g.status!=="active"&&g.status!=="paused")throw new I(400,"invalid_setting","Status must be active or paused.","status");if(p.status==="pending")throw new I(409,"not_confirmed","The requester hasn't confirmed this schedule yet.");_.status=g.status,_.status_reason=g.status==="paused"?"Paused by an admin.":null,_.paused_by=g.status==="paused"?"admin":null}let H=_.status??p.status;return H==="active"&&(_.next_run_at=_e(Ne(_.cron??p.cron),_.timezone??p.timezone,e())),H==="paused"&&(_.next_run_at=null),s.store.updateSchedule(p.id,_,e()),i(c.user.username,"schedule_changed",{id:p.id,changed:Object.keys(_)},c.ip2??null),s.scheduler?.wake(),{schedule:q(s.store.getSchedule(p.id))}}));let z=c=>({repoPath:c.repo_path,script:c.script,onFailure:c.on_failure,timeoutSec:c.timeout_sec,enabled:c.enabled===1,proposal:c.proposed_script?{script:c.proposed_script,reason:c.proposed_reason,requestId:c.proposed_request_id,at:c.proposed_at}:null,updatedBy:c.updated_by,updatedAt:c.updated_at,runs:s.store.setupRuns({repoPath:c.repo_path},10).map(p=>({id:p.id,requestId:p.request_id,status:p.status,exitCode:p.exit_code,log:p.log_tail,durationMs:p.duration_ms,startedAt:p.started_at}))}),A=c=>{let p=W(c,1e3).trim(),g;try{g=Z.realpathSync(p)}catch{throw new I(400,"invalid_setting","That folder doesn't exist.","repoPath")}if(!Z.existsSync(Q.join(g,".git")))throw new I(400,"invalid_setting","That folder isn't a git repository.","repoPath");return g};t.get("/api/setup",d(()=>{let c=s.settings.get("workdir"),p=null,g=!1;try{c&&(p=Z.realpathSync(c),g=Z.existsSync(Q.join(p,".git")))}catch{}return{workdir:p,isRepo:g,scripts:s.store.listSetupScripts().map(z)}})),t.put("/api/setup",d(c=>{let p=c.body??{},g=A(p.repoPath),_=W(p.script,Ln).trim()||null,y=p.onFailure==="fail"?"fail":"continue",P=Number(p.timeoutSec??600);if(!Number.isInteger(P)||P<10||P>3600)throw new I(400,"invalid_setting","Timeout must be 10 to 3600 seconds.","timeoutSec");return s.store.saveSetupScript(g,{script:_,onFailure:y,timeoutSec:P,enabled:p.enabled!==!1,by:c.user.username,clearProposal:p.clearProposal===!0},e()),i(c.user.username,"setup_script_saved",{repoPath:g,onFailure:y,enabled:p.enabled!==!1},c.ip2??null),{script:z(s.store.getSetupScript(g))}})),t.post("/api/setup/proposal",d(c=>{let p=c.body??{},g=A(p.repoPath),_=s.store.getSetupScript(g);if(!_?.proposed_script)throw new I(404,"not_found","There's no proposal for this repository.");let y=p.action==="approve";if(y&&Number(p.proposedAt)!==_.proposed_at)throw new I(409,"proposal_changed","The proposal changed since you opened it. Review it again.");return y?s.store.saveSetupScript(g,{script:_.proposed_script,onFailure:_.on_failure,timeoutSec:_.timeout_sec,enabled:!0,by:c.user.username,clearProposal:!0},e()):s.store.clearSetupProposal(g,e()),i(c.user.username,y?"setup_proposal_approved":"setup_proposal_rejected",{repoPath:g},c.ip2??null),{script:z(s.store.getSetupScript(g))}})),t.delete("/api/setup",d(c=>{let p=W(c.body?.repoPath,1e3);if(!s.store.deleteSetupScript(p))throw new I(404,"not_found");return i(c.user.username,"setup_script_deleted",{repoPath:p},c.ip2??null),{ok:!0}})),t.delete("/api/schedules/:id",d(c=>{let p=M(c.params.id);return s.store.updateSchedule(p.id,{status:"cancelled",next_run_at:null},e()),i(c.user.username,"schedule_deleted",{id:p.id,title:p.title},c.ip2??null),s.scheduler?.wake(),{ok:!0}})),t.post("/api/schedules/:id/run",d(async c=>{let p=M(c.params.id);if(p.status==="pending")throw new I(409,"not_confirmed","The requester hasn't confirmed this schedule yet.");if(!s.scheduler)throw new I(503,"unavailable");let g=await s.scheduler.runNow(p.id);if(i(c.user.username,"schedule_run",{id:p.id,ok:g.ok},c.ip2??null),!g.ok)throw new I(409,"not_started",g.message);return{ok:!0}}));let $=()=>{if(!s.tickets)throw new I(404,"not_available");return s.tickets},le=c=>`${o(c)?"https":"http"}://${String(c.headers.host??"")}/trackers/linear/callback`,De=c=>`${o(c)?"https":"http"}://${String(c.headers.host??"")}/trackers/jira/callback`,Ut=()=>{let c=$().jira;if(!c)throw new I(404,"not_available");return c},K=c=>{let p=$(),g=s.settings.all(),_=p.store.list(100).map(y=>{let P=y.request_id?s.store.getRequest(y.request_id):void 0;return{id:y.id,tracker:y.tracker,key:y.key,url:y.url,title:r(y.title)??"",state:y.state,statusNote:y.status_note,delegator:y.delegator_name,prUrl:y.pr_url,requestId:y.request_id,requestStatus:P?.status??null,costUsd:y.request_id?p.store.requestCost(y.request_id):0,updatedAt:y.updated_at}});return{settings:{enabled:g.ticketsEnabled,atATime:g.ticketsAtATime,budgetUsd:g.ticketBudgetUsd,dailyBudgetUsd:g.ticketDailyBudgetUsd,autoMerge:g.ticketsAutoMerge,slackChannel:g.ticketsSlackChannel,statusOnStart:g.ticketsStatusOnStart,statusOnPr:g.ticketsStatusOnPr},linear:{...p.linear.status(),clientId:g.linearClientId,hasSecret:!!s.settings.getSecret("linearClientSecret"),teams:g.linearTeams,redirectUri:le(c)},github:{repos:g.githubIssueRepos,label:g.githubIssueLabel,people:g.githubIssuePeople},jira:p.jira?{...p.jira.status(),available:!0,clientId:g.jiraClientId,hasSecret:!!s.settings.getSecret("jiraClientSecret"),siteInput:g.jiraSite,projects:g.jiraProjects,label:g.jiraLabel,people:g.jiraPeople,redirectUri:De(c)}:{available:!1},bridge:{enabled:g.ticketsBridge,linear:{url:g.ticketsBridgeLinear?.url??null,hasSecret:!!s.settings.getSecret("linearWebhookSecret")},github:{url:g.ticketsBridgeGithub?.url??null,hasSecret:!!s.settings.getSecret("githubWebhookSecret")}},items:_}},an=async c=>{try{return await c()}catch(p){throw p instanceof Be||p instanceof ge?new I(400,p.code,p.message):p}};t.get("/api/tickets",d(c=>K(c))),t.put("/api/tickets",d(c=>{$();let p=c.body??{},g={enabled:"ticketsEnabled",atATime:"ticketsAtATime",budgetUsd:"ticketBudgetUsd",dailyBudgetUsd:"ticketDailyBudgetUsd",autoMerge:"ticketsAutoMerge",slackChannel:"ticketsSlackChannel",statusOnStart:"ticketsStatusOnStart",statusOnPr:"ticketsStatusOnPr",linearClientId:"linearClientId",linearTeams:"linearTeams",githubRepos:"githubIssueRepos",githubLabel:"githubIssueLabel",githubPeople:"githubIssuePeople",jiraClientId:"jiraClientId",jiraSite:"jiraSite",jiraProjects:"jiraProjects",jiraLabel:"jiraLabel",jiraPeople:"jiraPeople",bridge:"ticketsBridge"},_={};for(let[y,P]of Object.entries(g))p[y]!==void 0&&(_[P]=p[y]===""?null:p[y]);try{Object.keys(_).length&&s.settings.set(_)}catch(y){throw new I(400,"invalid_setting",y.message)}return typeof p.linearWebhookSecret=="string"&&p.linearWebhookSecret.trim()&&s.settings.setSecret("linearWebhookSecret",p.linearWebhookSecret.trim().slice(0,200)),typeof p.linearClientSecret=="string"&&p.linearClientSecret.trim()&&s.settings.setSecret("linearClientSecret",p.linearClientSecret.trim().slice(0,500)),typeof p.jiraClientSecret=="string"&&p.jiraClientSecret.trim()&&s.settings.setSecret("jiraClientSecret",p.jiraClientSecret.trim().slice(0,500)),i(c.user.username,"tickets_settings",{changed:[...Object.keys(_),...p.linearClientSecret?["linearClientSecret"]:[],...p.jiraClientSecret?["jiraClientSecret"]:[]]},c.ip2??null),s.onChanged(),K(c)})),t.post("/api/tickets/bridge/:tracker",d(async c=>{let p=$(),g=c.params.tracker;if(g!=="linear"&&g!=="github")throw new I(404,"not_found");if(!p.bridge)throw new I(404,"not_available");let _=null;g==="github"&&(c.body?.rotate===!0||!s.settings.getSecret("githubWebhookSecret"))&&(_=jc.randomBytes(24).toString("hex"),s.settings.setSecret("githubWebhookSecret",_));let y=await p.bridge.register(g);if("error"in y)throw new I(409,"bridge_failed",y.error);return i(c.user.username,"ticket_bridge_registered",{tracker:g},c.ip2??null),{...K(c),url:y.url,secret:_}})),t.post("/api/tickets/linear/connect",d(c=>an(()=>({url:$().linear.authUrl(le(c))})))),t.post("/api/tickets/linear/exchange",d(async c=>{let p=W(c.body?.code,2e3),g=W(c.body?.state,200);if(!p||!g)throw new I(400,"bad_request");let _=await an(()=>$().linear.exchange(p,g));return i(c.user.username,"linear_connected",{workspace:_.workspace},c.ip2??null),s.onChanged(),K(c)})),t.delete("/api/tickets/linear/connection",d(c=>($().linear.disconnect(),i(c.user.username,"linear_disconnected",void 0,c.ip2??null),s.onChanged(),K(c)))),t.post("/api/tickets/jira/connect",d(c=>an(()=>({url:Ut().authUrl(De(c))})))),t.post("/api/tickets/jira/exchange",d(async c=>{let p=W(c.body?.code,2e3),g=W(c.body?.state,200);if(!p||!g)throw new I(400,"bad_request");let _=await an(()=>Ut().exchange(p,g));return i(c.user.username,"jira_connected",{site:_.site,account:_.account},c.ip2??null),s.onChanged(),K(c)})),t.delete("/api/tickets/jira/connection",d(c=>(Ut().disconnect(),i(c.user.username,"jira_disconnected",void 0,c.ip2??null),s.onChanged(),K(c)))),t.post("/api/tickets/poll",d(async c=>{let p=$(),g=await p.service.pollOnce("linear")+await p.service.pollOnce("github")+await p.service.pollOnce("jira");return{...K(c),handled:g}}));let Ft=()=>{if(!s.calendar)throw new I(404,"not_available");return s.calendar},Dr=c=>`${o(c)?"https":"http"}://${String(c.headers.host??"")}/calendar/callback`,ln=c=>({...Ft().status(),clientId:s.settings.get("googleClientId"),hasSecret:!!s.settings.getSecret("googleClientSecret"),allowExternal:s.settings.get("calendarAllowExternal"),redirectUri:Dr(c)}),qr=async c=>{try{return await c()}catch(p){throw p instanceof Fe?new I(400,p.code,p.message):p}};t.get("/api/calendar",d(c=>ln(c))),t.put("/api/calendar",d(c=>{Ft();let p=c.body??{},g={};p.clientId!==void 0&&(g.googleClientId=p.clientId===""||p.clientId===null?null:W(p.clientId,300).trim()),p.enabled!==void 0&&(g.calendarEnabled=p.enabled===!0),p.allowExternal!==void 0&&(g.calendarAllowExternal=p.allowExternal===!0);try{Object.keys(g).length&&s.settings.set(g)}catch(_){throw new I(400,"invalid_setting",_.message,"clientId")}return typeof p.clientSecret=="string"&&p.clientSecret.trim()&&s.settings.setSecret("googleClientSecret",p.clientSecret.trim().slice(0,500)),i(c.user.username,"calendar_settings",{changed:[...Object.keys(g),...p.clientSecret?["clientSecret"]:[]]},c.ip2??null),s.onChanged(),ln(c)})),t.post("/api/calendar/connect",d(c=>qr(()=>({url:Ft().authUrl(Dr(c))})))),t.post("/api/calendar/exchange",d(async c=>{let p=W(c.body?.code,2e3),g=W(c.body?.state,200);if(!p||!g)throw new I(400,"bad_request");let _=await qr(()=>Ft().exchange(p,g));return i(c.user.username,"calendar_connected",{account:_.account},c.ip2??null),s.onChanged(),ln(c)})),t.delete("/api/calendar/connection",d(async c=>(await Ft().disconnect(),i(c.user.username,"calendar_disconnected",void 0,c.ip2??null),s.onChanged(),ln(c))));let Mr=c=>Q.join((s.requestDir??pe)(c),"out"),Ur=(c,p="",g=0)=>{let _;try{_=Z.readdirSync(c,{withFileTypes:!0})}catch{return[]}let y=[];for(let P of _){if(!qo.test(P.name))continue;let H=Q.join(c,P.name);if(P.isDirectory()&&g<1){y.push(...Ur(H,`${p}${P.name}/`,g+1));continue}if(!P.isFile()||!dr[Q.extname(P.name).toLowerCase()])continue;let F=Z.lstatSync(H,{throwIfNoEntry:!1});!F?.isFile()||F.size>Mo||y.push({name:p+P.name,size:F.size,mtime:F.mtimeMs})}return y};t.get("/api/requests/:id/screenshots",d(c=>{let p=S(c.params.id);return{items:Ur(Mr(p.id)).sort((_,y)=>y.mtime-_.mtime).slice(0,200)}})),t.get("/api/requests/:id/screenshots/*",d((c,p)=>{let g=S(c.params.id),_=String(c.params[0]??""),y=_.split("/");if(y.length>2||!y.every(J=>qo.test(J))||!dr[Q.extname(_).toLowerCase()])throw new I(404,"not_found");let P=Mr(g.id),H=P;for(let J of y.slice(0,-1))if(H=Q.join(H,J),!Z.lstatSync(H,{throwIfNoEntry:!1})?.isDirectory())throw new I(404,"not_found");let F=Q.join(P,...y),x;try{x=Z.openSync(F,Z.constants.O_RDONLY|Z.constants.O_NOFOLLOW)}catch{throw new I(404,"not_found")}try{let J=Z.fstatSync(x);if(!J.isFile()||J.size>Mo)throw new I(404,"not_found");let Re=Buffer.alloc(J.size);Z.readSync(x,Re,0,J.size,0),p.setHeader("Content-Type",dr[Q.extname(_).toLowerCase()]),p.setHeader("Content-Security-Policy","default-src 'none'; sandbox"),p.setHeader("Cache-Control","private, max-age=300"),p.setHeader("Content-Disposition",`inline; filename="${Q.basename(_)}"`),p.end(Re)}finally{Z.closeSync(x)}}));let nt=()=>{if(!s.browser)throw new I(404,"not_available");return s.browser},un=async c=>{try{return await c()}catch(p){throw p instanceof V?new I(p.code==="not_open"||p.code==="not_installed"?409:400,p.code,p.message):p}},bt=()=>{let c=nt();return{installed:c.session.installed(),enabled:s.settings.get("browserEnabled"),session:{...c.session.status(),sandboxed:c.session.isSandboxed()},sites:c.logins.sites()}};if(t.get("/api/browser",d(bt)),t.post("/api/browser/session",d(async c=>{let p=nt(),g=W(c.body?.url,2e3),_=p.session.status().open;return await un(()=>p.session.open(g)),_||i(c.user.username,"browser_open",{url:Zc(g)},c.ip2??null),bt()})),t.post("/api/browser/session/input",d(async c=>{let p=nt(),g=c.body;if(!g||typeof g!="object")throw new I(400,"bad_input");return await un(()=>p.session.input(g)),{ok:!0}})),t.post("/api/browser/session/go",d(async c=>{let p=nt(),g=W(c.body?.action,10);if(g!=="back"&&g!=="forward"&&g!=="reload")throw new I(400,"bad_input");return await un(()=>p.session.go(g)),{ok:!0}})),t.post("/api/browser/session/save",d(async c=>{let p=nt(),g=await un(()=>p.session.storageState()),_=p.logins.load(),y=new Set(g.cookies.map(F=>F.domain)),P=new Set(g.origins.map(F=>F.origin)),H={cookies:[...(_?.cookies??[]).filter(F=>!y.has(F.domain)),...g.cookies],origins:[...(_?.origins??[]).filter(F=>!P.has(F.origin)),...g.origins]};try{p.logins.save(H)}catch(F){throw new I(400,"save_failed",String(F.message))}return c.body?.close===!0&&await p.session.close(),i(c.user.username,"browser_save",{sites:p.logins.sites().map(F=>F.site)},c.ip2??null),bt()})),t.delete("/api/browser/session",d(async c=>{let p=nt();return p.session.status().open&&(await p.session.close(),i(c.user.username,"browser_close",void 0,c.ip2??null)),bt()})),t.delete("/api/browser/sites",d(c=>(nt().logins.clear(),i(c.user.username,"browser_sign_out_all",void 0,c.ip2??null),bt()))),t.delete("/api/browser/sites/:site",d(c=>{let p=nt(),g=W(c.params.site,255).toLowerCase();if(!p.logins.signOut(g))throw new I(404,"not_found");return i(c.user.username,"browser_sign_out",{site:g},c.ip2??null),bt()})),t.get("/api/browser/session/stream",(c,p)=>{if(!s.browser){p.status(404).json({error:{code:"not_available"}});return}let g=s.browser,_=a(c);p.writeHead(200,{"Content-Type":"text/event-stream","Cache-Control":"no-store",Connection:"keep-alive","X-Accel-Buffering":"no"});let y=null,P=!1,H=()=>{if(!y||P)return;let re=y;y=null,P=!p.write(`event: frame
593
+ data: ${JSON.stringify({seq:re.seq,url:re.url,title:re.title,tabs:re.tabs,data:re.data})}
594
594
 
595
- `)};p.on("drain",()=>{P=!1,H()});let F=g.session.subscribe(se=>{y=se,H()}),x=0,J=setInterval(()=>{if(x++,x%2===0&&!s.auth.validate(_,{touch:!1}))return p.write(`event: expired
595
+ `)};p.on("drain",()=>{P=!1,H()});let F=g.session.subscribe(re=>{y=re,H()}),x=0,J=setInterval(()=>{if(x++,x%2===0&&!s.auth.validate(_,{touch:!1}))return p.write(`event: expired
596
596
  data: {}
597
597
 
598
- `),Re();let se=g.session.status();if(!se.open)return p.write(`event: closed
598
+ `),Re();let re=g.session.status();if(!re.open)return p.write(`event: closed
599
599
  data: {}
600
600
 
601
601
  `),Re();p.write(`event: ping
602
- data: ${JSON.stringify({idleClosesAt:se.idleClosesAt})}
602
+ data: ${JSON.stringify({idleClosesAt:re.idleClosesAt})}
603
603
 
604
604
  `)},15e3);J.unref();let Re=()=>{clearInterval(J),F(),p.end()};c.on("close",()=>{clearInterval(J),F()})}),t.get("/api/audit",d(c=>{let p=mr(c.query.pageSize,50,10,200),g=mr(c.query.page,1,1,1e5),_=s.db.get("SELECT COUNT(*) AS n FROM audit_log").n;return{items:s.db.all("SELECT id, ts, actor, action, detail, ip FROM audit_log ORDER BY id DESC LIMIT ? OFFSET ?",p,(g-1)*p),total:_,page:g,pageSize:p}})),t.all("/api/*",(c,p)=>{p.status(404).json({error:{code:"not_found"}})}),s.staticDir&&Z.existsSync(Q.join(s.staticDir,"index.html"))){let c=Q.join(s.staticDir,"index.html");t.use(cr.static(s.staticDir,{index:!1,maxAge:"1h"})),t.get("*",(p,g)=>{g.setHeader("Cache-Control","no-store"),g.sendFile(c)})}return t.use((c,p,g,_)=>{if(g.headersSent)return;if(c instanceof I){g.status(c.status).json({error:{code:c.code,message:c.message,field:c.field}});return}let y=c?.status;g.status(y&&y>=400&&y<500?y:500).json({error:{code:y&&y<500?"bad_request":"internal"}})}),t}function Bo(s,e){let t=Vc(s);return new Promise((n,r)=>{let i=t.listen(e,"127.0.0.1",()=>n(i));i.once("error",r)})}function Zc(s){try{return new URL(/^[a-z][a-z0-9+.-]*:/i.test(s)?s:`https://${s}`).origin}catch{return"(invalid)"}}var Qc="https://www.googleapis.com/calendar/v3",$t=class extends Error{constructor(t,n){super(n);this.status=t}},es=class{constructor(e){this.d=e}async call(e,t,n){let r=await this.d.token();if(!r)throw new $t(401,"Google Calendar isn't connected.");let i=await(this.d.fetch??fetch)(`${this.d.base??Qc}${t}`,{method:e,headers:{authorization:`Bearer ${r}`,...n?{"content-type":"application/json"}:{}},body:n?JSON.stringify(n):void 0,redirect:"error",signal:AbortSignal.timeout(2e4)});if(i.status===204)return;let o=await i.json().catch(()=>({}));if(!i.ok)throw new $t(i.status,`Google Calendar: ${String(o.error?.message??i.status).slice(0,300)}`);return o}async freeBusy(e,t,n){let r=await this.call("POST","/freeBusy",{timeMin:new Date(t).toISOString(),timeMax:new Date(n).toISOString(),items:e.map(o=>({id:o}))}),i={};for(let[o,a]of Object.entries(r.calendars??{}))i[o]={busy:a.busy??[],...a.errors?.length?{error:a.errors[0].reason??"unavailable"}:{}};return i}listEvents(e,t,n){let r=new URLSearchParams({timeMin:new Date(e).toISOString(),timeMax:new Date(t).toISOString(),singleEvents:"true",orderBy:"startTime",maxResults:"50",...n?{q:n}:{}});return this.call("GET",`/calendars/primary/events?${r}`)}getEvent(e){return this.call("GET",`/calendars/primary/events/${encodeURIComponent(e)}`)}insertEvent(e,t){return this.call("POST",`/calendars/primary/events?sendUpdates=all${t?"&conferenceDataVersion=1":""}`,e)}patchEvent(e,t){return this.call("PATCH",`/calendars/primary/events/${encodeURIComponent(e)}?sendUpdates=all`,t)}deleteEvent(e){return this.call("DELETE",`/calendars/primary/events/${encodeURIComponent(e)}?sendUpdates=all`)}};var jo=s=>s.normalize("NFKD").replace(/[̀-ͯ]/g,"").toLowerCase().trim();function Ho(s,e){let t=e.trim().replace(/^<@([UW][A-Z0-9]+)(\|[^>]*)?>$/,"$1").replace(/^@/,""),n=s.filter(a=>!a.isBot&&!a.deleted),r=a=>a.map(l=>({id:l.id,name:l.realName&&l.realName!==l.name?`${l.name} (${l.realName})`:l.name,email:l.email??null}));if(/^[UW][A-Z0-9]{5,}$/.test(t))return r(n.filter(a=>a.id===t));if(/^[^@\s]+@[^@\s]+$/.test(t))return r(n.filter(a=>(a.email??"").toLowerCase()===t.toLowerCase()));let i=jo(t).split(/\s+/).filter(Boolean);if(!i.length)return[];let o=n.filter(a=>{let l=jo(`${a.name} ${a.realName??""}`);return i.every(u=>l.split(/[\s._-]+/).some(d=>d.startsWith(u)))});return r(o.slice(0,10))}import ed from"node:crypto";var Wo=s=>`tk:${s}`,en=s=>s.startsWith("tk:"),td=new Set(["key","url","title","team","priority","request_id","session_id","state","delegator_name","delegator_email","pr_url","status_note"]),ts=class{constructor(e){this.db=e}get(e,t){return this.db.get("SELECT * FROM tickets WHERE tracker = ? AND external_id = ?",e,t)}byRequest(e){return this.db.get("SELECT * FROM tickets WHERE request_id = ?",e)}upsert(e,t){let n=this.get(e.tracker,e.externalId);if(n)return this.update(n.id,{key:e.key,url:e.url,title:e.title,team:e.team,priority:e.priority,...t.sessionId?{session_id:t.sessionId}:{},...t.delegatorName?{delegator_name:t.delegatorName}:{},...t.delegatorEmail?{delegator_email:t.delegatorEmail}:{}},t.now),this.get(e.tracker,e.externalId);let r=ed.randomUUID();return this.db.run(`INSERT INTO tickets (id, tracker, external_id, key, url, title, team, priority, session_id, state, delegator_name, delegator_email, created_at, updated_at)
605
605
  VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, 'queued', ?, ?, ?, ?)`,r,e.tracker,e.externalId,e.key,e.url,e.title.slice(0,300),e.team,e.priority,t.sessionId,t.delegatorName,t.delegatorEmail,t.now,t.now),this.get(e.tracker,e.externalId)}update(e,t,n){let r=Object.keys(t).filter(i=>td.has(i));r.length&&this.db.run(`UPDATE tickets SET ${r.map(i=>`${i} = ?`).join(", ")}, updated_at = ? WHERE id = ?`,...r.map(i=>t[i]),n,e)}list(e=200){return this.db.all("SELECT * FROM tickets ORDER BY updated_at DESC LIMIT ?",e)}isSeen(e,t){return!!this.db.get("SELECT 1 AS x FROM ticket_seen WHERE tracker = ? AND event_id = ?",e,t)}markSeen(e,t,n){return this.db.run("INSERT OR IGNORE INTO ticket_seen (tracker, event_id, seen_at) VALUES (?, ?, ?)",e,t,n).changes>0}pruneSeen(e){this.db.run("DELETE FROM ticket_seen WHERE seen_at < ?",e)}cursor(e){return this.db.get("SELECT cursor FROM tracker_cursors WHERE tracker = ?",e)?.cursor??null}setCursor(e,t,n){this.db.run("INSERT INTO tracker_cursors (tracker, cursor, updated_at) VALUES (?, ?, ?) ON CONFLICT(tracker) DO UPDATE SET cursor = excluded.cursor, updated_at = excluded.updated_at",e,t,n)}requestCost(e){return this.db.get("SELECT SUM(cost_usd) AS c FROM turns WHERE request_id = ?",e)?.c??0}ticketCostSince(e){return this.db.get(`SELECT SUM(t.cost_usd) AS c FROM turns t JOIN requests r ON r.id = t.request_id
@@ -632,9 +632,9 @@ Reply with your choice.`:""}`,this.d.tickets.update(r.id,{state:"waiting"},o)}el
632
632
  <!-- fda:${this.d.marker()} -->`})}async started(){}async pullRequest(e,t){await this.post(e,"response",`Pull request: ${t}`)}};var os=class{constructor(e){this.d=e}running=new Set;stopped=!0;registeredAt=new Map;secretOf(e){return this.d.settings.getSecret(e==="linear"?"linearWebhookSecret":"githubWebhookSecret")}regOf(e){return this.d.settings.get(e==="linear"?"ticketsBridgeLinear":"ticketsBridgeGithub")}start(){if(this.stopped=!1,!(!this.d.settings.get("ticketsBridge")||!this.d.settings.get("ticketsEnabled")))for(let e of["linear","github"])this.secretOf(e)&&!this.running.has(e)&&this.loop(e)}stop(){this.stopped=!0}async register(e){let t=this.secretOf(e),n=this.d.machineId();if(!t)return{error:"Set the webhook secret first."};if(!n)return{error:"This machine isn't registered with SuperClawd yet."};let r=await this.d.cloud.bridgeRegister(n,e,t);return r.ok?(this.d.settings.set(e==="linear"?{ticketsBridgeLinear:r.data}:{ticketsBridgeGithub:r.data}),this.registeredAt.set(e,(this.d.now??Date.now)()),{url:r.data.url}):{error:r.kind==="not_found"?"This SuperClawd server doesn't have the ticket bridge yet.":"Couldn't reach SuperClawd's bridge."}}pause(e){return(this.d.sleep??kd)(e)}async loop(e){this.running.add(e);try{for(;!this.stopped&&this.d.settings.get("ticketsBridge")&&this.secretOf(e);){let t=(this.d.now??Date.now)();if(!this.regOf(e)||t-(this.registeredAt.get(e)??0)>432e5){let i=await this.register(e);if("error"in i){this.d.log?.(`tickets: bridge (${e}): ${i.error}`),await this.pause(3e4*10);continue}}let n=this.regOf(e),r=await this.d.cloud.bridgeWait(n.token);r.ok?r.data.rings>0&&this.d.onRing(e):r.kind==="not_found"?(this.registeredAt.delete(e),this.d.settings.set(e==="linear"?{ticketsBridgeLinear:null}:{ticketsBridgeGithub:null}),await this.pause(5e3)):await this.pause(3e4)}}finally{this.running.delete(e)}}};function kd(s){return new Promise(e=>setTimeout(e,s).unref?.())}import{fileURLToPath as Kd}from"node:url";import Ze from"node:path";import{z as Ye}from"zod";import bd from"node:fs";import as from"node:path";var yd=60*6e4,Sd=6*60*6e4,Td=10*6e4,Zo=25*1024*1024,Ed=5,Rd=5*6e4,Qo=10*6e4,ls=class{constructor(e){this.d=e;this.now=e.now??Date.now}busy=!1;again=!1;names=new Map;groups=new Map;now;receive(e){let t=wr(e,this.d.settings.get("slackBotUserId")),n=t.key??(e.envelope_id?`env:${e.envelope_id}`:null);return n?(t.kind==="ignore"&&t.peer&&this.d.store.markPeerDirty(t.peer.channelId,t.peer.threadTs),this.d.store.inboxInsert(n,t.kind==="ignore"?{type:"ignored"}:e,this.now()),n):null}inflight=null;stopped=!1;stop(){return this.stopped=!0,this.inflight??Promise.resolve()}drain(){return this.stopped?Promise.resolve():this.busy?(this.again=!0,this.inflight??Promise.resolve()):(this.inflight=this.drainOnce().finally(()=>{this.inflight=null}),this.inflight)}async drainOnce(){this.busy=!0;try{do{this.again=!1;let e=this.d.store.inboxPending(50),t=0;for(let n of e){if(this.stopped)break;try{let r=JSON.parse(n.payload);await this.process(wr(r,this.d.settings.get("slackBotUserId"))),this.d.store.inboxDone(n.key,this.now()),t++}catch(r){this.d.store.inboxFailed(n.key,String(r),n.attempts+1>=Ed,this.now())}}e.length===50&&t>0&&(this.again=!0)}while(this.again&&!this.stopped)}finally{this.busy=!1}}startLive(e,t){let{store:n,notifier:r}=this.d;n.updateRequest(e,{live_since:t,live_handoff:0},t),r.liveStart(n.getRequest(e)),setTimeout(()=>{try{n.db.tx(()=>{let o=n.getRequest(e);!o||o.live_since!==t||o.live_handoff||(n.updateRequest(e,{live_handoff:1},o.updated_at),r.status(n.getRequest(e)))})}catch{}},this.d.liveHandoffMs??this.d.settings.get("liveHandoffSec")*1e3).unref?.()}nameMap(){let e=new Map;for(let[t,n]of this.names)e.set(t,n.name);return e}channelName(e){return ea(e)?"direct message":this.d.store.getChannel(e)?.name??null}async process(e){switch(e.kind){case"message":return this.onMessage(e);case"edit":{let t=this.d.store.getRequestByThread(e.channelId,e.threadTs);t&&this.d.store.editPendingMessage(t.id,e.ts,e.text);return}case"action":return this.onAction(e);case"reaction":return this.onReaction(e);case"view_submission":return this.onViewSubmission(e);case"home_opened":return this.publishHome(e.userId);case"home_action":return this.onHomeAction(e);case"ignore":return}}async onMessage(e){let{store:t,settings:n}=this.d,r=n.get("slackBotUserId"),i=e.threadTs?t.getRequestByThread(e.channelId,e.threadTs):void 0;if(i)return this.onFollowUp(i,e);if(e.dm||r&&e.mentions.includes(r))return this.onNewRequest(e)}async onNewRequest(e){let{store:t,settings:n,notifier:r}=this.d,i=n.all(),o=e.threadTs??e.ts;if(t.getRequestByThread(e.channelId,o))return;let a=this.d.acceptingRequests();if(!i.agentName||!i.workdir||!a.ok){r.ephemeral(e.channelId,e.userId,e.threadTs,a.reason??"I'm not set up yet. Ask the operator to finish setting me up.");return}let l=await this.access(e.channelId,e.userId,e.userTeam);if(l!=="allow"){r.ephemeral(e.channelId,e.userId,e.threadTs,Ei(l,i.slackBotName||"this agent",i.operatorContact));return}let u=this.d.slack(),d=await this.user(e.userId),h=ns(e.text,i.slackBotUserId??"",!0);if(_r(h)){r.ephemeral(e.channelId,e.userId,e.threadTs,tn(this.helpOptions()));return}let k=[];if(e.threadTs&&u&&i.threadContextMessages>0)try{let f=await u.replies(e.channelId,e.threadTs,void 0,2e3);for(let E of f)E.ts!==e.ts&&Number(E.ts)<Number(e.ts)&&E.user&&E.user!==i.slackBotUserId&&k.push({ts:E.ts,user:E.user,text:E.botId?E.text.slice(0,2e3):E.text})}catch{}let m=t.allowlist(),b=k.slice(-i.threadContextMessages);for(let f of b)f.user&&await this.user(f.user);let S=this.now(),w;t.db.tx(()=>{w=t.createRequest({channelId:e.channelId,threadTs:o,originTs:e.ts,requesterId:e.userId,requesterName:d?.name??null,agentId:i.agentId,agentName:i.agentName,workdir:i.workdir,cwd:i.workdir,title:vd(h),planRequired:Tt(i.planPolicy,h),now:S});for(let R of b)t.addMessage({requestId:w.id,source:"prior_context",slackTs:R.ts,authorId:R.user,authorName:this.names.get(R.user)?.name??null,authorAllowlisted:!!R.user&&m.users.has(R.user),text:R.text,now:S-1});t.addMessage({requestId:w.id,source:"mention",slackTs:e.ts,authorId:e.userId,authorName:d?.name??null,authorAllowlisted:!0,text:h,files:e.files,now:S});let f=t.getRequest(w.id);t.enqueueTurn(w.id,{trigger:"new_request",mode:oe({planRequired:f.plan_required===1,planStatus:"none"},"new_request"),notBefore:S+(e.files.length?Qo:0),now:S}),t.addEvent(w.id,"created",`by ${e.userId}`),t.updateRequest(w.id,{tagged_ts:e.ts},S);let E=this.d.holdNote?.()??null;E?r.info(t.getRequest(w.id),E):this.startLive(w.id,S)});let T=u?await u.permalink(e.channelId,e.ts).catch(()=>null):null;T&&t.updateRequest(w.id,{permalink:T}),await this.fetchAttachments(w.id,e.ts,e.files),e.files.length&&t.setQueuedNotBefore(w.id,this.now()+i.debounceMs),this.d.worker.wake()}async onFollowUp(e,t){let{store:n,settings:r,notifier:i}=this.d,o=r.all(),a=o.slackBotUserId??"",l=!!a&&t.mentions.includes(a),u=[];for(let T of t.mentions.filter(f=>f!==a)){let f=await this.user(T);if(!f)throw new Error("slack: couldn't look up a mentioned user");f.isBot&&u.push(T)}let d=await this.access(t.channelId,t.userId,t.userTeam),h=d==="allow";if(h&&l&&n.bumpTag(e.id,"tagged_ts",t.ts),h&&u.length&&n.bumpTag(e.id,"other_tagged_ts",t.ts,u[0]),u.length&&!l)return this.keepAsContext(e,t,d);let k=ns(t.text,a,!0),m=h?ss(ns(t.text,a)):null;if(!l&&m?.cmd!=="stop"&&await this.anotherFdaAddressedLast(e,t))return this.keepAsContext(e,t,d);if(m)return this.onCommand(e,t,m,l);if(d==="deny_external")return;let b=await this.user(t.userId),S=this.now(),w=!1;n.db.tx(()=>{if(w=n.addMessage({requestId:e.id,source:h?"follow_up":"unlisted",slackTs:t.ts,authorId:t.userId,authorName:b?.name??null,authorAllowlisted:h,text:k,files:h?t.files:[],now:S}).inserted,!w||!h)return;let f=!!a&&t.mentions.includes(a);f&&n.updateRequest(e.id,{peer_dirty:1},S),f&&!n.getRequest(e.id).live_since?this.startLive(e.id,S):i.received(t.channelId,t.ts,e.id);let E=n.getRequest(e.id);if(o.planPolicy==="auto"&&Yr(k)&&!E.plan_required&&n.updateRequest(e.id,{plan_required:1},S),E.plan_status==="proposed"){t.userId===E.requester_id&&(n.requestPlanChanges(e.id,t.userId,k,{now:S}),i.status(n.getRequest(e.id)));return}if(n.runningTurnFor(e.id))return;let R=n.getRequest(e.id);n.enqueueTurn(e.id,{trigger:"follow_up",mode:oe({planRequired:R.plan_required===1,planStatus:R.plan_status},"follow_up"),notBefore:S+(t.files.length?Qo:t.mentions.includes(a)?0:o.debounceMs),now:S}),R.status!=="queued"&&(n.updateRequest(e.id,{status:"queued",pending_question:null,pending_choices:null},S),i.status(n.getRequest(e.id)))}),w&&h&&(await this.fetchAttachments(e.id,t.ts,t.files),t.files.length&&n.setQueuedNotBefore(e.id,this.now()+o.debounceMs),this.d.worker.wake())}async anotherFdaAddressedLast(e,t){let{store:n}=this.d,r=n.getRequest(e.id),i=r.tagged_ts??r.origin_ts;if(!r.other_tagged_ts||Number(r.other_tagged_ts)<=Number(i)||t.userId===r.requester_id&&(r.status==="awaiting_input"||r.status==="awaiting_approval"||r.plan_status==="proposed"))return!1;let o=r.other_tagged_bot;if(!o)return!1;if(n.isKnownFda(o))return!0;let a=`${e.id}:${o}`,l=this.fdaProbe.get(a)??0;return this.d.refreshPeers&&this.now()-l>Rd&&(this.fdaProbe.set(a,this.now()),this.fdaProbe.size>500&&this.fdaProbe.clear(),await Promise.race([this.d.refreshPeers(r).catch(()=>{}),new Promise(u=>setTimeout(u,4e3))])),n.isKnownFda(o)}fdaProbe=new Map;async keepAsContext(e,t,n){if(n==="deny_external")return;let r=await this.user(t.userId);this.d.store.addMessage({requestId:e.id,source:"prior_context",slackTs:t.ts,authorId:t.userId,authorName:r?.name??null,authorAllowlisted:n==="allow",text:t.text.slice(0,4e3),now:this.now()})}async onCommand(e,t,n,r=!0){let{store:i,notifier:o,worker:a}=this.d,l=u=>o.ephemeral(t.channelId,t.userId,e.thread_ts,u);switch(i.addEvent(e.id,"command",`${n.cmd} by ${t.userId}`),n.cmd){case"help":l(tn(this.helpOptions()));return;case"stop":{let u=i.setRequestPrFollowing(e.id,!1,`stopped by <@${t.userId}>`,this.now());a.cancel(e.id,t.userId)==="nothing"&&(u||r)&&l(u?"Stopped following this request's pull requests. Say `follow` to resume.":"There's nothing running for this request.");return}case"follow":case"unfollow":{let u=n.cmd==="follow";if(!i.prFollowsFor(e.id).length)return l("This request has no pull requests to follow.");let d=i.setRequestPrFollowing(e.id,u,u?null:`stopped by <@${t.userId}>`,this.now());l(u?d?"Following this request's pull requests again (CI, conflicts and reviews).":"Already following (or the pull requests are merged or closed).":d?"Stopped following this request's pull requests. Say `follow` to resume.":"I'm not following any open pull request here.");return}case"status":{let u=i.getRequest(e.id),d=u.status==="queued"?i.queuePosition(e.id,this.now()):null,h=a.status().find(m=>m.requestId===e.id),k=h?.startedAt?` for ${Math.round((this.now()-h.startedAt)/6e4)} min`:"";l(`Status: ${u.status.replace("_"," ")}${k}${d?`, position ${d} in the queue`:""}.`);return}case"approve":case"reject":{if(t.userId!==e.requester_id)return l("Only the requester can approve or change this plan.");let u=i.getRequest(e.id);return u.plan_status!=="proposed"?l("There's no plan waiting for approval."):n.cmd==="approve"?this.approve(u,u.plan_version,t.userId,t.channelId):n.feedback?this.requestChanges(u,u.plan_version,t.userId,n.feedback,t.channelId):l("Add what should change, e.g. `reject keep the old endpoint`.")}case"retry":a.retry(e.id,t.userId)||l("Only a failed or stopped request can be retried.");return}}approve(e,t,n,r){let{store:i,notifier:o}=this.d,a=this.now(),l=!1;i.db.tx(()=>{if(l=i.approvePlan(e.id,t,n,a),!l)return;i.addMessage({requestId:e.id,source:"synthetic",slackTs:null,authorId:null,authorName:null,authorAllowlisted:!0,text:`Plan v${t} approved by the requester. Execute it.`,now:a}),i.enqueueTurn(e.id,{trigger:"plan_approved",mode:"normal",notBefore:a,now:a}),i.updateRequest(e.id,{status:"queued"},a);let u=i.getPlan(e.id,t);if(u?.message_ts){let d=Us(t,`approved by <@${n}>`,u.markdown);o.update(r,e.id,u.message_ts,d.text,d.blocks)}o.status(i.getRequest(e.id))}),l?this.d.worker.wake():o.ephemeral(r,n,e.thread_ts,"This plan is no longer current.")}requestChanges(e,t,n,r,i){let{store:o,notifier:a}=this.d,l=!1;o.db.tx(()=>{if(l=o.requestPlanChanges(e.id,n,r,{version:t,now:this.now()}),!l)return;let u=o.getPlan(e.id,t);if(u?.message_ts){let d=Us(t,`changes requested by <@${n}>`,u.markdown);a.update(i,e.id,u.message_ts,d.text,d.blocks)}a.status(o.getRequest(e.id))}),l?this.d.worker.wake():a.ephemeral(i,n,e.thread_ts,"This plan is no longer current.")}async onReaction(e){let{store:t,settings:n}=this.d,r=n.all(),i=r.slackBotUserId;if(!i||!e.itemUser||!r.handoffSources.includes(e.itemUser))return;let o=this.d.slack();if(!o)return;let l=(await o.replies(e.channelId,e.itemTs,void 0,200,!1,!0)).find(S=>S.ts===e.itemTs);if(!l||l.user!==e.itemUser||l.metadata?.event_type!=="fda_handoff")return;let u=xd.safeParse(l.metadata.event_payload);if(!u.success)return;let d=u.data;if(d.to!==i||d.from!==e.itemUser||e.userId!==d.requester||d.hops>1||await this.access(e.channelId,d.requester,null)!=="allow"||!t.acceptHandoff({id:d.id,fromBot:d.from,approvedBy:e.userId,now:this.now()}))return;let h=l.threadTs??l.ts,k=`Hand-off from <@${d.from}>, approved by <@${d.requester}>:
633
633
  ${d.task}`,m=t.getRequestByThread(e.channelId,h);if(m){await this.onFollowUp(m,{kind:"message",key:`handoff:${d.id}`,channelId:e.channelId,ts:l.ts,threadTs:h,userId:d.requester,userTeam:null,text:`<@${i}> ${k}`,files:[],mentions:[i]}),t.linkHandoff(d.id,m.id);return}await this.onNewRequest({kind:"message",key:`handoff:${d.id}`,channelId:e.channelId,ts:l.ts,threadTs:l.threadTs??null,userId:d.requester,userTeam:null,text:`<@${i}> ${k}`,files:[],mentions:[i]});let b=t.getRequestByThread(e.channelId,h);b&&(t.updateRequest(b.id,{handoff_hops:d.hops,handoff_from:d.from},this.now()),t.linkHandoff(d.id,b.id),t.addEvent(b.id,"handoff_accepted",`from ${d.from}, approved by ${d.requester}`))}async onStopButton(e){let{store:t,notifier:n,worker:r}=this.d,i=t.getRequest(e.value);if(!i||i.channel_id!==e.channelId)return;let o=a=>n.ephemeral(e.channelId,e.userId,i.thread_ts,a);if(e.userId!==i.requester_id&&await this.access(e.channelId,e.userId,null)!=="allow")return o("Only the requester or someone allowed to use me can stop this.");t.addEvent(i.id,"command",`stop (button) by ${e.userId}`),t.setRequestPrFollowing(i.id,!1,`stopped by <@${e.userId}>`,this.now()),r.cancel(i.id,e.userId)==="nothing"&&o("There's nothing running for this request.")}helpOptions(){let e=this.d.settings.all();return{name:e.slackBotName||e.agentName||"the agent",directMessages:e.allowDirectMessages,stopButton:e.stopButton,prFollow:e.prMonitorEnabled,operatorContact:e.operatorContact}}async publishHome(e){let t=this.d.slack();if(!t?.publishHome)return;let n=await this.isAllowedAsync(e);if(n===null)return;let{store:r}=this.d;await t.publishHome(e,Yo({help:this.helpOptions(),allowed:n,requests:n?r.recentRequestsBy(e,8):[],schedules:n?r.listSchedules({createdBy:e}):[],now:this.now()}))}async onHomeAction(e){let{store:t}=this.d,n=e.actionId==="home_sched_pause";if(!n&&e.actionId!=="home_sched_resume")return;let r=t.getSchedule(e.value);if(r&&r.created_by===e.userId&&await this.isAllowedAsync(e.userId)===!0){let i=this.now();n&&r.status==="active"?(t.updateSchedule(r.id,{status:"paused",status_reason:null,paused_by:"creator"},i),this.d.onSchedulesChanged?.()):!n&&r.status==="paused"&&r.paused_by==="creator"&&(t.updateSchedule(r.id,{status:"active",status_reason:null,paused_by:null,next_run_at:_e(Ne(r.cron),r.timezone,i)},i),this.d.onSchedulesChanged?.())}await this.publishHome(e.userId)}async onAction(e){let{store:t,notifier:n}=this.d;if(e.actionId.startsWith("choice_"))return this.onChoiceAction(e);if(e.actionId==="schedule_confirm"||e.actionId==="schedule_cancel")return this.onScheduleAction(e);if(e.actionId==="live_stop")return this.onStopButton(e);if(e.actionId!=="plan_approve"&&e.actionId!=="plan_changes")return;let r;try{r=JSON.parse(e.value)}catch{return}let i=r.r?t.getRequest(r.r):void 0;if(!i||typeof r.v!="number"||i.channel_id!==e.channelId)return;let o=l=>n.ephemeral(e.channelId,e.userId,i.thread_ts,l);if(e.userId!==i.requester_id)return o("Only the requester can approve or change this plan.");if(await this.access(e.channelId,e.userId,null)!=="allow")return o("You're no longer on the access list.");if(i.plan_status!=="proposed"||i.plan_version!==r.v)return o("This plan is no longer current.");if(e.actionId==="plan_approve")return this.approve(i,r.v,e.userId,e.channelId);let a=this.d.slack();a&&await a.openModal(e.triggerId,vi(i.id,r.v))}async onScheduleAction(e){let{store:t,notifier:n}=this.d,r;try{r=JSON.parse(e.value)}catch{return}let i=r.s?t.getSchedule(r.s):void 0,o=h=>n.ephemeral(e.channelId,e.userId,e.threadTs,h);if(!i)return o("That schedule no longer exists.");if(e.userId!==i.created_by)return o(`Only ${i.created_by_name??"the person who asked for it"} can confirm this schedule.`);if(await this.access(e.channelId,e.userId,null)!=="allow")return o("You're no longer on the access list.");if(i.status!=="pending")return o("This schedule was already decided.");let a=this.now(),l=e.actionId==="schedule_confirm",u=l?i.once&&i.next_run_at!==null?Math.max(i.next_run_at,a):xn(i,a):null,d=Ce({...i,next_run_at:u??i.next_run_at},a);t.db.tx(()=>{if(!t.decidePendingSchedule(i.id,l,u,a))return;let h=Pi({title:i.title,when:d,timezone:i.timezone,channelId:i.channel_id,outcome:l?`Created by <@${e.userId}>${u&&!i.once?` \xB7 next run ${En(u,i.timezone)}`:""}`:`Cancelled by <@${e.userId}>`});n.update(e.channelId,i.source_request_id,e.messageTs,h.text,h.blocks)}),this.d.onSchedulesChanged?.()}async userTimezone(e){let t=this.d.slack();return t?(await t.userInfo(e).catch(()=>null))?.tz??null:null}async currentChoices(e,t,n,r){let i;try{i=JSON.parse(e)}catch{return null}let o=i.r?this.d.store.getRequest(i.r):void 0;if(!o||n&&o.channel_id!==n)return null;if(t!==o.requester_id)return r?.(o,"Only the requester can answer this question. You can still reply in the thread."),null;if(await this.access(o.channel_id,t,null)!=="allow")return r?.(o,"You're no longer on the access list."),null;let a=Is(o.pending_choices);return!a||a.id!==i.q||o.status!=="awaiting_input"?(r?.(o,"This question was already answered."),null):{req:o,c:a,i:typeof i.i=="number"?i.i:void 0}}async onChoiceAction(e){let t=(i,o)=>this.d.notifier.ephemeral(e.channelId,e.userId,i.thread_ts,o),n=await this.currentChoices(e.value,e.userId,e.channelId,t);if(!n)return;if(e.actionId==="choice_open"){let i=this.d.slack();i&&await i.openModal(e.triggerId,Ai(n.req.id,n.c));return}let r=n.i!==void 0?n.c.options[n.i]:void 0;r!==void 0&&this.answerChoices(n.req,n.c,[r],null,e.userId)}answerChoices(e,t,n,r,i){let{store:o,notifier:a}=this.d,l=[];n.length&&l.push(n.length===1?n[0]:n.join(", ")),r&&l.push(n.length?`Also: ${r}`:r);let u=l.join(`
634
634
  `),d=`${n.length>1?"My answers":"My answer"}: ${u}`,h=this.now();o.db.tx(()=>{o.addMessage({requestId:e.id,source:"follow_up",slackTs:null,authorId:i,authorName:this.names.get(i)?.name??e.requester_name,authorAllowlisted:!0,text:d,now:h}),o.updateRequest(e.id,{status:"queued",pending_question:null,pending_choices:null},h);let k=o.getRequest(e.id);if(o.enqueueTurn(e.id,{trigger:"follow_up",mode:oe({planRequired:k.plan_required===1,planStatus:k.plan_status},"follow_up"),notBefore:h,now:h}),o.addEvent(e.id,"answered",`by ${i}`),t.messageTs){let m=Ni(t.question,u,i);a.update(e.channel_id,e.id,t.messageTs,m.text,m.blocks)}a.status(o.getRequest(e.id))}),this.d.worker.wake()}async onViewSubmission(e){if(e.callbackId==="choice_answer"){let i=await this.currentChoices(e.privateMetadata,e.userId,null,null);if(!i)return;let o=(e.selections.choice??[]).map(l=>i.c.options[Number(l)]).filter(l=>l!==void 0),a=(e.values.other??"").trim().slice(0,2e3)||null;if(!o.length&&!a)return;this.answerChoices(i.req,i.c,i.c.multiple?o:o.slice(0,1),a,e.userId);return}if(e.callbackId!=="plan_feedback")return;let t;try{t=JSON.parse(e.privateMetadata)}catch{return}let n=t.r?this.d.store.getRequest(t.r):void 0;if(!n||typeof t.v!="number"||e.userId!==n.requester_id||await this.access(n.channel_id,e.userId,null)!=="allow")return;let r=(e.values.feedback??"").trim();r&&this.requestChanges(n,t.v,e.userId,r,n.channel_id)}async access(e,t,n){let{store:r,settings:i}=this.d,o=r.allowlist(),a=ea(e),l=a?null:await this.channel(e),u=i.get("allowAnyone"),d=u&&!o.users.has(t)?await this.user(t):null;if(u&&!o.users.has(t)&&!d)throw new Error("slack: couldn't look up the person (retrying)");let h;return o.usergroups.size&&!o.users.has(t)&&(h=await this.groupsOf(t,o.usergroups)),Ti(o,{channelId:e,userId:t,userGroups:h,channelExtShared:!!l?.is_ext_shared,userTeam:n,installedTeam:i.get("slackTeamId"),directMessage:a,dmEnabled:i.get("allowDirectMessages"),anyChannel:i.get("allowAnyChannel"),anyone:u&&!!d&&!d.isBot,userIsGuest:!!d?.isGuest,userDeleted:!!d?.deleted})}async channel(e){let{store:t}=this.d,n=t.getChannel(e);if(n&&this.now()-n.checked_at<yd)return n;let r=this.d.slack(),i=r?await r.channelInfo(e):null;return i&&t.upsertChannel({id:e,name:i.name,isPrivate:i.isPrivate,isExtShared:i.isExtShared,botIsMember:i.isMember},this.now()),t.getChannel(e)??n}async user(e){let t=this.names.get(e);if(t&&this.now()-t.at<Sd)return{name:t.name,isBot:t.isBot,teamId:t.team,isGuest:t.guest,deleted:t.deleted};let n=this.d.slack(),r=n?await n.userInfo(e):null;return r&&this.names.set(e,{name:r.name,isBot:r.isBot,team:r.teamId,guest:!!r.isGuest,deleted:!!r.deleted,at:this.now()}),r?{name:r.name,isBot:r.isBot,teamId:r.teamId,isGuest:!!r.isGuest,deleted:!!r.deleted}:null}async groupsOf(e,t){let n=this.d.slack(),r=[];for(let i of t){let o=this.groups.get(i);if(!o||this.now()-o.at>Td){let a=null;try{a=n?await n.usergroupMembers(i):null}catch{a=null}if(a)o={members:new Set(a),at:this.now()},this.groups.set(i,o);else if(!o)throw new Error("usergroup lookup unavailable")}o.members.has(e)&&r.push(i)}return r}isAllowedNow(e){if(!e)return!1;let t=this.d.store.allowlist();if(t.users.has(e))return!0;for(let n of t.usergroups)if(this.groups.get(n)?.members.has(e))return!0;if(this.d.settings.get("allowAnyone")){let n=this.names.get(e);return!(n&&(n.guest||n.deleted||n.isBot))}return!1}async isAllowedAsync(e){if(this.d.settings.get("allowAnyone")){let n=await this.user(e).catch(()=>null);return n?!n.isGuest&&!n.deleted&&!n.isBot:null}if(this.isAllowedNow(e))return!0;let t=this.d.store.allowlist();if(!t.usergroups.size)return!1;try{return(await this.groupsOf(e,t.usergroups)).length>0}catch{return null}}invalidateGroups(){this.groups.clear()}async fetchAttachments(e,t,n){let r=this.d.slack();if(!r||!n.length)return;let i=as.join(pe(e),"attachments");bd.mkdirSync(i,{recursive:!0,mode:448});let o=[];for(let a of n){if(a.size>Zo){o.push({...a,skipped:"too large"});continue}try{let l=await r.download(a.url,Zo),u=`${a.id.replace(/[^A-Za-z0-9]/g,"")}-${as.basename(a.name).replace(/[^A-Za-z0-9._-]/g,"_").slice(0,120)}`,d=as.join(i,u);if(as.dirname(d)!==i)throw new Error("bad name");gt(d,l),o.push({...a,localPath:d})}catch(l){o.push({...a,skipped:String(l).slice(0,80)})}}this.d.store.db.run("UPDATE messages SET files = ? WHERE request_id = ? AND slack_ts = ?",JSON.stringify(o),e,t)}};function vd(s){let e=(s.split(`
635
- `).find(r=>r.trim())??"").trim();if(e.length<=80)return e;let t=e.slice(0,79),n=t.lastIndexOf(" ");return`${(n>40?t.slice(0,n):t).replace(/[\s,.;:]+$/,"")}\u2026`}function ea(s){return s.startsWith("D")}var xd=Ye.object({v:Ye.literal(1),id:Ye.string().uuid(),from:Ye.string().regex(/^[UW][A-Z0-9]+$/),to:Ye.string().regex(/^[UW][A-Z0-9]+$/),requester:Ye.string().regex(/^[UW][A-Z0-9]+$/),task:Ye.string().min(1).max(4e3),hops:Ye.number().int().min(1).max(10),request:Ye.string().max(100).optional()});import oa from"node:crypto";import re from"node:fs";import ye from"node:path";import Tr from"express";import{z as X}from"zod";import{McpServer as Cd}from"@modelcontextprotocol/sdk/server/mcp.js";import{StreamableHTTPServerTransport as ia}from"@modelcontextprotocol/sdk/server/streamableHttp.js";import{Readable as Pd}from"node:stream";import Id from"node:crypto";import{z as B}from"zod";import{McpServer as Ad}from"@modelcontextprotocol/sdk/server/mcp.js";function us(s,e){let t=new Intl.DateTimeFormat("en-US",{timeZone:e,year:"numeric",month:"2-digit",day:"2-digit",hour:"2-digit",minute:"2-digit",weekday:"short",hourCycle:"h23"}).formatToParts(new Date(s)),n=i=>t.find(o=>o.type===i)?.value??"0",r=["Sun","Mon","Tue","Wed","Thu","Fri","Sat"];return{y:Number(n("year")),m:Number(n("month")),d:Number(n("day")),h:Number(n("hour"))%24,mi:Number(n("minute")),weekday:r.indexOf(n("weekday"))}}function cs(s,e,t,n,r,i){let o=Date.UTC(s,e-1,t,n,r),a=o;for(let l=0;l<3;l++){let u=us(a,i),d=Date.UTC(u.y,u.m-1,u.d,u.h,u.mi);if(d===o)return a;a+=o-d}return a}function Ve(s,e){let t=s.trim();if(/([zZ]|[+-]\d{2}:?\d{2})$/.test(t)){let r=Date.parse(t);return Number.isFinite(r)?r:null}let n=/^(\d{4})-(\d{2})-(\d{2})(?:[T ](\d{2}):(\d{2}))?/.exec(t);return n?cs(+n[1],+n[2],+n[3],n[4]?+n[4]:0,n[5]?+n[5]:0,e):null}function ta(s){let e=s.durationMin*6e4,t=(s.stepMin??30)*6e4,n=s.max??5,r=s.perDay??2,i=[...s.busy].sort((u,d)=>u.start-d.start),o=(u,d)=>!i.some(h=>h.start<d&&h.end>u),a=[],l=us(s.from,s.tz);for(let u=0;u<62&&a.length<n;u++){let d=cs(l.y,l.m,l.d,0,0,s.tz);if(d>s.to)break;let h=us(d+12*36e5,s.tz),k=0;if(h.weekday>=1&&h.weekday<=5){let b=cs(l.y,l.m,l.d,s.workStart,0,s.tz),S=cs(l.y,l.m,l.d,s.workEnd,0,s.tz),w=Math.max(b,s.from);for(w=b+Math.ceil((w-b)/t)*t;w+e<=Math.min(S,s.to)&&k<r&&a.length<n;w+=t)o(w,w+e)&&(a.push({start:w,end:w+e}),k++,w+=e-t)}l=us(d+36*36e5,s.tz)}return a}function yr(s,e){let t=new Intl.DateTimeFormat("en-US",{timeZone:e,weekday:"short",month:"short",day:"numeric"}).format(new Date(s.start)),n=r=>new Intl.DateTimeFormat("en-US",{timeZone:e,hour:"2-digit",minute:"2-digit",hourCycle:"h23"}).format(new Date(r));return`${t}, ${n(s.start)}\u2013${n(s.end)}`}var ds=B.string().trim().email().max(254),be=(s,e=!1)=>({content:[{type:"text",text:s}],...e?{isError:!0}:{}});function na(s,e){return e.allowExternal?[]:s.filter(t=>{let n=t.toLowerCase().split("@")[1];return!e.domain||n!==e.domain&&!n.endsWith("calendar.google.com")})}function sa(s,e){return be(`Not sent: ${s.join(", ")} ${s.length===1?"is":"are"} outside ${e.domain??"the calendar's domain"}, and this agent may only invite people from ${e.domain??"its own domain"}. Tell the requester; the machine's operator can allow external guests in the admin (Calendar).`,!0)}function ps(s,e){return s&&xt(s)?s:e.timezone}function Sr(s,e){let t=s.start?.dateTime?Date.parse(s.start.dateTime):null,n=s.end?.dateTime?Date.parse(s.end.dateTime):null;return t&&n?yr({start:t,end:n},e):s.start?.date?`${s.start.date} (all day)`:"unknown time"}function Nd(s,e){let t=(s.attendees??[]).filter(n=>!n.self).map(n=>`${n.email} (${n.responseStatus??"needsAction"})`).join(", ");return`\u2022 ${s.summary??"(no title)"} \xB7 ${Sr(s,e)} \xB7 id ${s.id}${t?` \xB7 attendees: ${t}`:""}${s.htmlLink?` \xB7 ${s.htmlLink}`:""}`}async function nn(s){try{return await s()}catch(e){if(e instanceof $t)return be(e.message,!0);throw e}}function ra(s){let e=s.now??Date.now,t=new Ad({name:"fda-calendar",version:"1.0.0"});return t.tool("suggest_time","Find meeting times when all attendees are free (their Google Calendar availability, plus this calendar's own events). Times are on weekdays within working hours in the given timezone (default: the requester's). Returns up to 5 options; people whose availability can't be seen are listed.",{attendees:B.array(ds).min(1).max(30),duration_minutes:B.number().int().min(5).max(480),from:B.string().optional().describe("Start of the search window (ISO; default: now)"),to:B.string().optional().describe("End of the search window (ISO; default: 7 days later)"),timezone:B.string().optional(),working_hours:B.object({start:B.number().int().min(0).max(23),end:B.number().int().min(1).max(24)}).optional().describe("Hours of the day, e.g. {start: 9, end: 18} (the default)")},n=>nn(async()=>{let r=ps(n.timezone,s),i=Math.max(e(),n.from&&Ve(n.from,r)||e()),o=n.to&&Ve(n.to,r)||i+7*864e5;if(o<=i)return be("The search window ends before it starts.",!0);if(o-i>62*864e5)return be("Search at most two months at a time.",!0);let a=[...new Set([...n.attendees.map(b=>b.toLowerCase()),"primary"])],l=await s.api.freeBusy(a,i,o),u=[],d=[];for(let b of a){let S=l[b];if(!S||S.error){b!=="primary"&&d.push(b);continue}for(let w of S.busy)u.push({start:Date.parse(w.start),end:Date.parse(w.end)})}let h=n.working_hours??{start:9,end:18},k=ta({busy:u,from:i,to:o,durationMin:n.duration_minutes,tz:r,workStart:h.start,workEnd:Math.max(h.end,h.start+1)}),m=k.length?k.map(b=>`\u2022 ${yr(b,r)} (${new Date(b.start).toISOString()} \u2192 ${new Date(b.end).toISOString()})`):["No time works for everyone in that window. Try a longer window or fewer people."];return be([`The earliest times when everyone is free (${r}; at most 2 per day, so other times that day may be free too; search a narrower window for more):`,...m,...d.length?[`Availability not visible for: ${d.join(", ")} (outside the organization or not shared); check with them.`]:[]].join(`
635
+ `).find(r=>r.trim())??"").trim();if(e.length<=80)return e;let t=e.slice(0,79),n=t.lastIndexOf(" ");return`${(n>40?t.slice(0,n):t).replace(/[\s,.;:]+$/,"")}\u2026`}function ea(s){return s.startsWith("D")}var xd=Ye.object({v:Ye.literal(1),id:Ye.string().uuid(),from:Ye.string().regex(/^[UW][A-Z0-9]+$/),to:Ye.string().regex(/^[UW][A-Z0-9]+$/),requester:Ye.string().regex(/^[UW][A-Z0-9]+$/),task:Ye.string().min(1).max(4e3),hops:Ye.number().int().min(1).max(10),request:Ye.string().max(100).optional()});import oa from"node:crypto";import ie from"node:fs";import ye from"node:path";import Tr from"express";import{z as X}from"zod";import{McpServer as Cd}from"@modelcontextprotocol/sdk/server/mcp.js";import{StreamableHTTPServerTransport as ia}from"@modelcontextprotocol/sdk/server/streamableHttp.js";import{Readable as Pd}from"node:stream";import Id from"node:crypto";import{z as B}from"zod";import{McpServer as Ad}from"@modelcontextprotocol/sdk/server/mcp.js";function us(s,e){let t=new Intl.DateTimeFormat("en-US",{timeZone:e,year:"numeric",month:"2-digit",day:"2-digit",hour:"2-digit",minute:"2-digit",weekday:"short",hourCycle:"h23"}).formatToParts(new Date(s)),n=i=>t.find(o=>o.type===i)?.value??"0",r=["Sun","Mon","Tue","Wed","Thu","Fri","Sat"];return{y:Number(n("year")),m:Number(n("month")),d:Number(n("day")),h:Number(n("hour"))%24,mi:Number(n("minute")),weekday:r.indexOf(n("weekday"))}}function cs(s,e,t,n,r,i){let o=Date.UTC(s,e-1,t,n,r),a=o;for(let l=0;l<3;l++){let u=us(a,i),d=Date.UTC(u.y,u.m-1,u.d,u.h,u.mi);if(d===o)return a;a+=o-d}return a}function Ve(s,e){let t=s.trim();if(/([zZ]|[+-]\d{2}:?\d{2})$/.test(t)){let r=Date.parse(t);return Number.isFinite(r)?r:null}let n=/^(\d{4})-(\d{2})-(\d{2})(?:[T ](\d{2}):(\d{2}))?/.exec(t);return n?cs(+n[1],+n[2],+n[3],n[4]?+n[4]:0,n[5]?+n[5]:0,e):null}function ta(s){let e=s.durationMin*6e4,t=(s.stepMin??30)*6e4,n=s.max??5,r=s.perDay??2,i=[...s.busy].sort((u,d)=>u.start-d.start),o=(u,d)=>!i.some(h=>h.start<d&&h.end>u),a=[],l=us(s.from,s.tz);for(let u=0;u<62&&a.length<n;u++){let d=cs(l.y,l.m,l.d,0,0,s.tz);if(d>s.to)break;let h=us(d+12*36e5,s.tz),k=0;if(h.weekday>=1&&h.weekday<=5){let b=cs(l.y,l.m,l.d,s.workStart,0,s.tz),S=cs(l.y,l.m,l.d,s.workEnd,0,s.tz),w=Math.max(b,s.from);for(w=b+Math.ceil((w-b)/t)*t;w+e<=Math.min(S,s.to)&&k<r&&a.length<n;w+=t)o(w,w+e)&&(a.push({start:w,end:w+e}),k++,w+=e-t)}l=us(d+36*36e5,s.tz)}return a}function yr(s,e){let t=new Intl.DateTimeFormat("en-US",{timeZone:e,weekday:"short",month:"short",day:"numeric"}).format(new Date(s.start)),n=r=>new Intl.DateTimeFormat("en-US",{timeZone:e,hour:"2-digit",minute:"2-digit",hourCycle:"h23"}).format(new Date(r));return`${t}, ${n(s.start)}\u2013${n(s.end)}`}var ds=B.string().trim().email().max(254),be=(s,e=!1)=>({content:[{type:"text",text:s}],...e?{isError:!0}:{}});function na(s,e){return e.allowExternal?[]:s.filter(t=>{let n=t.toLowerCase().split("@")[1];return!e.domain||n!==e.domain&&!n.endsWith("calendar.google.com")})}function sa(s,e){return be(`Not sent: ${s.join(", ")} ${s.length===1?"is":"are"} outside ${e.domain??"the calendar's domain"}, and this agent may only invite people from ${e.domain??"its own domain"}. Tell the requester; the machine's operator can allow external guests in the admin (Calendar).`,!0)}function ps(s,e){return s&&xt(s)?s:e.timezone}function Sr(s,e){let t=s.start?.dateTime?Date.parse(s.start.dateTime):null,n=s.end?.dateTime?Date.parse(s.end.dateTime):null;return t&&n?yr({start:t,end:n},e):s.start?.date?`${s.start.date} (all day)`:"unknown time"}function Nd(s,e){let t=(s.attendees??[]).filter(n=>!n.self).map(n=>`${n.email} (${n.responseStatus??"needsAction"})`).join(", ");return`\u2022 ${s.summary??"(no title)"} \xB7 ${Sr(s,e)} \xB7 id ${s.id}${t?` \xB7 attendees: ${t}`:""}${s.htmlLink?` \xB7 ${s.htmlLink}`:""}`}async function nn(s){try{return await s()}catch(e){if(e instanceof $t)return be(e.message,!0);throw e}}function ra(s){let e=s.now??Date.now,t=new Ad({name:"fda-calendar",version:"1.0.0"});return t.tool("suggest_time","Find meeting times when all attendees are free (their Google Calendar availability, plus this calendar's own events). Times are on weekdays within working hours in the given timezone (default: the requester's). Returns up to 5 options; people whose availability can't be seen are listed.",{attendees:B.array(ds).min(1).max(30),duration_minutes:B.number().int().min(5).max(480),from:B.string().optional().describe("Start of the search window (ISO; default: now)"),to:B.string().optional().describe("End of the search window (ISO; default: 7 days later)"),timezone:B.string().optional(),working_hours:B.object({start:B.number().int().min(0).max(23),end:B.number().int().min(1).max(24)}).optional().describe("Hours of the day, e.g. {start: 9, end: 18} (the default)")},n=>nn(async()=>{let r=ps(n.timezone,s),i=Math.max(e(),n.from&&Ve(n.from,r)||e()),o=n.to&&Ve(n.to,r)||i+7*864e5;if(o<=i)return be("The search window ends before it starts.",!0);if(o-i>62*864e5)return be("Search at most two months at a time.",!0);let a=[...new Set([...n.attendees.map(b=>b.toLowerCase()),"primary"])],l=await s.api.freeBusy(a,i,o),u=[],d=[];for(let b of a){let S=l[b];if(!S||S.error){b!=="primary"&&d.push(b);continue}for(let w of S.busy)u.push({start:Date.parse(w.start),end:Date.parse(w.end)})}let h=n.working_hours??{start:9,end:18},k=ta({busy:u,from:i,to:o,durationMin:n.duration_minutes,tz:r,workStart:h.start,workEnd:Math.max(h.end,h.start+1)}),m=k.length?k.map(b=>`\u2022 ${yr(b,r)} (${new Date(b.start).toISOString()} \u2192 ${new Date(b.end).toISOString()})`):["No time works for everyone in that window. Try a longer window or fewer people."];return be([`The earliest times when everyone is free (${r}; at most 2 per day, so other times that day may be free too; search a narrower window for more):`,...m,...d.length?[`Availability not visible for: ${d.join(", ")} (outside the organization or not shared); check with them.`]:[]].join(`
636
636
  `))})),t.tool("create_event","Create an event on this calendar and email invitations to the attendees (guests can change the event). Only after the requester agreed to the time, unless they asked you to just book it. Adds a Google Meet link by default.",{title:B.string().min(1).max(200),start:B.string().describe("ISO date-time; without an offset it's in `timezone`"),end:B.string().optional().describe("ISO date-time (or use duration_minutes)"),duration_minutes:B.number().int().min(5).max(1440).optional(),timezone:B.string().optional(),attendees:B.array(ds).max(50).default([]),description:B.string().max(4e3).optional(),location:B.string().max(300).optional(),add_meet:B.boolean().default(!0)},n=>nn(async()=>{let r=ps(n.timezone,s),i=Ve(n.start,r),o=n.end?Ve(n.end,r):i!==null&&n.duration_minutes?i+n.duration_minutes*6e4:null;if(i===null||o===null||o<=i)return be("Give a valid start, and an end after it (or duration_minutes).",!0);let a=na(n.attendees,s);if(a.length)return sa(a,s);let l=await s.api.insertEvent({summary:n.title,description:n.description,location:n.location,start:{dateTime:new Date(i).toISOString(),timeZone:r},end:{dateTime:new Date(o).toISOString(),timeZone:r},attendees:n.attendees.map(u=>({email:u})),guestsCanModify:!0,reminders:{useDefault:!0},...n.add_meet?{conferenceData:{createRequest:{requestId:Id.randomUUID(),conferenceSolutionKey:{type:"hangoutsMeet"}}}}:{}},n.add_meet);return be(`Created "${l.summary??n.title}" \xB7 ${Sr(l,r)} \xB7 invitations sent to ${n.attendees.length?n.attendees.join(", "):"nobody"} \xB7 id ${l.id}${l.htmlLink?` \xB7 ${l.htmlLink}`:""}${l.hangoutLink?` \xB7 Meet: ${l.hangoutLink}`:""}`)})),t.tool("update_event","Change an event on this calendar (time, title, description, attendees). Attendees get the update by email.",{event_id:B.string().min(1).max(1024),start:B.string().optional(),end:B.string().optional(),duration_minutes:B.number().int().min(5).max(1440).optional(),timezone:B.string().optional(),title:B.string().min(1).max(200).optional(),description:B.string().max(4e3).optional(),add_attendees:B.array(ds).max(50).optional(),remove_attendees:B.array(ds).max(50).optional()},n=>nn(async()=>{let r=ps(n.timezone,s),i=await s.api.getEvent(n.event_id),o={};if(n.title&&(o.summary=n.title),n.description!==void 0&&(o.description=n.description),n.start||n.end||n.duration_minutes){let l=i.start?.dateTime?Date.parse(i.start.dateTime):null,u=i.end?.dateTime?Date.parse(i.end.dateTime):null,d=n.start?Ve(n.start,r):l,h=l&&u?u-l:30*6e4,k=n.end?Ve(n.end,r):d!==null?d+(n.duration_minutes?n.duration_minutes*6e4:h):null;if(d===null||k===null||k<=d)return be("Give a valid new start/end.",!0);o.start={dateTime:new Date(d).toISOString(),timeZone:r},o.end={dateTime:new Date(k).toISOString(),timeZone:r}}if(n.add_attendees?.length||n.remove_attendees?.length){let l=na(n.add_attendees??[],s);if(l.length)return sa(l,s);let u=new Set((n.remove_attendees??[]).map(h=>h.toLowerCase())),d=(i.attendees??[]).filter(h=>!u.has(h.email.toLowerCase()));for(let h of n.add_attendees??[])d.some(k=>k.email.toLowerCase()===h.toLowerCase())||d.push({email:h});o.attendees=d}if(!Object.keys(o).length)return be("Nothing to change.",!0);let a=await s.api.patchEvent(n.event_id,o);return be(`Updated "${a.summary??""}" \xB7 ${Sr(a,r)} \xB7 attendees notified \xB7 id ${a.id}`)})),t.tool("delete_event","Cancel an event on this calendar; attendees get a cancellation email. Only when the requester asked for it.",{event_id:B.string().min(1).max(1024)},n=>nn(async()=>(await s.api.deleteEvent(n.event_id),be(`Cancelled event ${n.event_id}; attendees were notified.`)))),t.tool("list_events","List events on this calendar in a time window (default: the next 7 days), with attendees' responses (accepted/declined/tentative).",{from:B.string().optional(),to:B.string().optional(),query:B.string().max(200).optional().describe("Free-text search (title, description, attendees)"),timezone:B.string().optional()},n=>nn(async()=>{let r=ps(n.timezone,s),i=n.from&&Ve(n.from,r)||e(),o=n.to&&Ve(n.to,r)||i+7*864e5,l=((await s.api.listEvents(i,o,n.query)).items??[]).filter(u=>u.status!=="cancelled");return be(l.length?l.map(u=>Nd(u,r)).join(`
637
- `):"No events in that window.")})),t}var Ld=3,Od=3e4,aa=25*1024*1024,$d=/^https:\/\/github\.com\/([\w.-]+)\/([\w.-]+)\/pull\/(\d+)\/?$/,Dd=new Set([".superclawd",".claude",".ssh",".config",".gnupg",".aws",".git"]),qd=/^(\.env(\..*)?|.*\.(pem|key|p12|pfx)|id_[a-z0-9]+|credentials(\.json)?|\.npmrc|\.netrc|\.pgpass)$/i;function U(s){return{content:[{type:"text",text:s}]}}function sn(s,e){return s===e||s.startsWith(e+ye.sep)}function la(s){let e=[];for(let t of s)try{e.push(re.realpathSync(t))}catch{}return e}function ua(s,e){let t=e.find(i=>sn(s,i));if(!t)throw new Error("only files inside the working directory or the request's output folder can be shared");if(ye.relative(t,s).split(ye.sep).some(i=>Dd.has(i))||qd.test(ye.basename(s)))throw new Error("that file can't be shared");let r=re.realpathSync(N.home);if(sn(s,r)&&!e.some(i=>i!==t&&sn(i,r)&&sn(s,i))&&!sn(t,r))throw new Error("that file can't be shared");if(s.startsWith("/proc")||s.startsWith("/sys")||s.startsWith("/dev"))throw new Error("that file can't be shared")}function Md(s,e){if(!s||typeof s!="string")throw new Error("path required");let t=la(e);if(!t.length)throw new Error("nothing can be shared right now");let n=re.realpathSync(ye.resolve(t[0],s));ua(n,t);let r=re.statSync(n);if(!r.isFile())throw new Error("not a regular file");if(r.size>aa)throw new Error("file too large (25 MB max)");return n}function Ud(s,e){let t=re.openSync(s,re.constants.O_RDONLY|(re.constants.O_NOFOLLOW??0));try{let n=s;try{n=re.readlinkSync(`/proc/self/fd/${t}`)}catch{}ua(n,la(e));let r=re.fstatSync(t);if(!r.isFile())throw new Error("not a regular file");if(r.size>aa)throw new Error("file too large (25 MB max)");let i=Buffer.alloc(r.size),o=0;for(;o<r.size;){let a=re.readSync(t,i,o,r.size-o,o);if(a<=0)break;o+=a}return i.subarray(0,o)}finally{re.closeSync(t)}}function Er(s){try{let e=ye.join(re.realpathSync(s),".git","config");return re.lstatSync(e).isSymbolicLink()?[]:[...re.readFileSync(e,"utf8").matchAll(/^\s*url\s*=\s*(\S+)\s*$/gm)].map(t=>t[1])}catch{return[]}}function Rr(s){let e=/(?:^|[@/])github\.com[:/]([\w.-]+)\/([\w.-]+?)(?:\.git)?\/?$/.exec(s);return e?`${e[1]}/${e[2]}`.toLowerCase():null}function Fd(s,e){let t=e.now??Date.now,n=new Cd({name:"fda",version:"1.0.0"}),r=()=>{let i=e.store.getRequest(s.requestId);if(!i)throw new Error("request not found");return i};return n.tool("set_title","Set a short title (max 80 characters) for the current request. Call once, early.",{title:X.string().min(1).max(80)},async({title:i})=>(s.title=e.redactor.redact(i.trim()).text,U("Title set."))),n.tool("post_update","Post a short progress update to the requester's Slack thread. Use sparingly, only for meaningful progress (at most one every 30 seconds).",{text:X.string().min(1).max(2e3)},async({text:i})=>t()-s.lastUpdateAt<Od?U("Too soon since the last update; keep working and update later."):(s.lastUpdateAt=t(),e.store.db.tx(()=>e.notifier.progress(r(),i)),U("Update posted."))),n.tool("ask_question",'Ask the requester a question you need answered before you can continue. When there are a few clear answers, pass them as `options` (2-10 short labels; `multiple: true` if several can be picked): the requester picks in Slack and can still write their own answer. The question (with its options) is posted in Slack as a card, so after calling this, end your turn without restating it: your final message should be empty or one short line that adds something new (e.g. "Nothing is booked yet."). Their answer will continue the conversation.',{question:X.string().min(1).max(3e3),options:X.array(X.string().trim().min(1).max(75)).min(2).max(10).optional(),multiple:X.boolean().optional()},async({question:i,options:o,multiple:a})=>{s.question=i;let l=o?[...new Set(o)]:[];return s.questionOptions=l.length>=2?{options:l,multiple:!!a}:null,U("Question recorded and shown to the requester as a card. End your turn now without repeating it (at most one short new line); their reply will continue this conversation.")}),n.tool("propose_plan","Propose a step-by-step plan for the requester to approve before you start non-trivial or risky work. The plan is shown to them as-is, so include any decision you need from them in it. After calling this, end your turn without making changes or restating the plan.",{markdown:X.string().min(1).max(2e4)},async({markdown:i})=>(s.plan=i,U("Plan recorded. End your turn now without making changes; you'll continue once it's approved."))),n.tool("upload_file","Share a file (for example a screenshot) in the requester's Slack thread. The file must be inside the working directory or the request's output folder.",{path:X.string().min(1).max(1e3),title:X.string().max(200).optional()},async({path:i,title:o})=>{let a=r(),l=dt(a.id),u=ye.join(l,"out"),d=ye.join(l,"tmp"),h,k;try{let w=We()==="bwrap",T=w?[a.cwd,u,d]:[a.cwd,u],f=w&&(i==="/tmp"||i.startsWith("/tmp/"))?ye.join(d,i.slice(4)):i;h=Md(f,T),k=Ud(h,T)}catch(w){return U(`Can't share that file: ${w.message}.`)}if(e.redactor.redact(k.toString("latin1")).hits.length)return U("Can't share that file: it appears to contain credentials or secrets.");let m=ye.join(N.uploads,a.id);re.mkdirSync(m,{recursive:!0,mode:448});let b=ye.basename(h).replace(/[^A-Za-z0-9._-]/g,"_").slice(0,120),S=ye.join(m,`${oa.randomUUID()}-${b}`);return re.writeFileSync(S,k,{mode:384}),s.uploads.push({path:S,title:o??null}),e.store.db.tx(()=>{e.notifier.upload(a,S,b,o??b),e.store.addArtifact(a.id,b.match(/\.(png|jpe?g|gif|webp)$/i)?"screenshot":"file",S,{title:o??null})}),U("File shared in the thread.")}),n.tool("link_pr","Record a pull request you opened for this request (a github.com pull request URL of this repository).",{url:X.string().url().max(300)},async({url:i})=>{let o=$d.exec(i.trim());if(!o)return U("That isn't a GitHub pull request URL.");let a=r(),l=`${o[1]}/${o[2]}`.toLowerCase(),u=Er(a.workdir).map(Rr).filter(Boolean);if(u.length&&!u.includes(l))return U("That pull request isn't in this repository.");s.prs.includes(i.trim())||(s.prs.push(i.trim()),e.store.updateTurn(s.turnId,{side_effects:JSON.stringify({pushed:s.pushed,prs:s.prs})}));let d=Mi(i);return d&&e.store.followPr(a.id,d),d&&e.onPrLinked?.(a,i)?.catch(()=>{}),U("Pull request recorded. I'll follow it: CI failures, merge conflicts and review feedback come back to this conversation.")}),n.tool("update_progress","Keep this thread's progress note current: what's done, key decisions or the root cause found, and what's left. It replaces the previous note. It's your memory if this work resumes in a fresh session, and it's shared (hidden) with other agents tagged in this thread, so write plain facts and never secrets or personal data.",{note:X.string().trim().min(1).max(1e3)},async({note:i})=>{let o=r();return e.redactor.redact(i).hits.length?U("Not saved: the note appears to contain a secret. Describe it without the value."):(e.store.updateRequest(o.id,{state_note:i}),U("Progress note saved."))}),n.tool("propose_setup_script","Propose a setup script for this repository: the shell commands a fresh worktree needs before work can start (install dependencies, copy an example env file, build). It does NOT run now: the machine's operator reviews and approves it in the admin, then it runs in each new worktree. Never put secrets in it.",{script:X.string().trim().min(1).max(Ln),reason:X.string().trim().min(1).max(500)},async({script:i,reason:o})=>{let a=r();return a.worktree_path?e.redactor.redact(i).hits.length?U("Not saved: the script appears to contain a secret. Reference an environment variable or a file the operator provides instead."):(e.store.db.tx(()=>{e.store.proposeSetupScript(re.realpathSync(a.workdir),i,o,a.id,t()),e.store.addEvent(a.id,"setup_proposed",o.slice(0,200))}),U("Proposed. It won't run until the machine's operator approves it in the admin (Setup); tell the requester that.")):U("Setup scripts are only for a repository working directory (this request doesn't have its own worktree).")}),e.settings&&Hd(n,s,e,r),e.calendar?.active()&&Bd(n,e),e.settings?.get("handoffTargets").length&&!e.store.getRequest(s.requestId)?.channel_id.startsWith("tk:")&&jd(n,s,e,r),n}function Bd(s,e){s.tool("find_people","Look up people in this Slack workspace by name (or Slack user id) to get their work email, e.g. to invite them with the calendar tools. Returns up to 5 matches per name; if a name matches several people, ask the requester which one.",{names:X.array(X.string().min(1).max(80)).min(1).max(20)},async({names:t})=>{let n=[];for(let r of t){let i=await e.calendar.findPeople(r).catch(()=>null);if(i===null){n.push(`${r}: couldn't look people up right now.`);continue}n.push(i.length?`${r}: ${i.slice(0,5).map(o=>`${o.name} <${o.email??"no email on their Slack profile"}> (${o.id})`).join("; ")}`:`${r}: nobody by that name.`)}return{content:[{type:"text",text:n.join(`
637
+ `):"No events in that window.")})),t}var Ld=3,Od=3e4,aa=25*1024*1024,$d=/^https:\/\/github\.com\/([\w.-]+)\/([\w.-]+)\/pull\/(\d+)\/?$/,Dd=new Set([".superclawd",".claude",".ssh",".config",".gnupg",".aws",".git"]),qd=/^(\.env(\..*)?|.*\.(pem|key|p12|pfx)|id_[a-z0-9]+|credentials(\.json)?|\.npmrc|\.netrc|\.pgpass)$/i;function U(s){return{content:[{type:"text",text:s}]}}function sn(s,e){return s===e||s.startsWith(e+ye.sep)}function la(s){let e=[];for(let t of s)try{e.push(ie.realpathSync(t))}catch{}return e}function ua(s,e){let t=e.find(i=>sn(s,i));if(!t)throw new Error("only files inside the working directory or the request's output folder can be shared");if(ye.relative(t,s).split(ye.sep).some(i=>Dd.has(i))||qd.test(ye.basename(s)))throw new Error("that file can't be shared");let r=ie.realpathSync(N.home);if(sn(s,r)&&!e.some(i=>i!==t&&sn(i,r)&&sn(s,i))&&!sn(t,r))throw new Error("that file can't be shared");if(s.startsWith("/proc")||s.startsWith("/sys")||s.startsWith("/dev"))throw new Error("that file can't be shared")}function Md(s,e){if(!s||typeof s!="string")throw new Error("path required");let t=la(e);if(!t.length)throw new Error("nothing can be shared right now");let n=ie.realpathSync(ye.resolve(t[0],s));ua(n,t);let r=ie.statSync(n);if(!r.isFile())throw new Error("not a regular file");if(r.size>aa)throw new Error("file too large (25 MB max)");return n}function Ud(s,e){let t=ie.openSync(s,ie.constants.O_RDONLY|(ie.constants.O_NOFOLLOW??0));try{let n=s;try{n=ie.readlinkSync(`/proc/self/fd/${t}`)}catch{}ua(n,la(e));let r=ie.fstatSync(t);if(!r.isFile())throw new Error("not a regular file");if(r.size>aa)throw new Error("file too large (25 MB max)");let i=Buffer.alloc(r.size),o=0;for(;o<r.size;){let a=ie.readSync(t,i,o,r.size-o,o);if(a<=0)break;o+=a}return i.subarray(0,o)}finally{ie.closeSync(t)}}function Er(s){try{let e=ye.join(ie.realpathSync(s),".git","config");return ie.lstatSync(e).isSymbolicLink()?[]:[...ie.readFileSync(e,"utf8").matchAll(/^\s*url\s*=\s*(\S+)\s*$/gm)].map(t=>t[1])}catch{return[]}}function Rr(s){let e=/(?:^|[@/])github\.com[:/]([\w.-]+)\/([\w.-]+?)(?:\.git)?\/?$/.exec(s);return e?`${e[1]}/${e[2]}`.toLowerCase():null}function Fd(s,e){let t=e.now??Date.now,n=new Cd({name:"fda",version:"1.0.0"}),r=()=>{let i=e.store.getRequest(s.requestId);if(!i)throw new Error("request not found");return i};return n.tool("set_title","Set a short title (max 80 characters) for the current request. Call once, early.",{title:X.string().min(1).max(80)},async({title:i})=>(s.title=e.redactor.redact(i.trim()).text,U("Title set."))),n.tool("post_update","Post a short progress update to the requester's Slack thread. Use sparingly, only for meaningful progress (at most one every 30 seconds).",{text:X.string().min(1).max(2e3)},async({text:i})=>t()-s.lastUpdateAt<Od?U("Too soon since the last update; keep working and update later."):(s.lastUpdateAt=t(),e.store.db.tx(()=>e.notifier.progress(r(),i)),U("Update posted."))),n.tool("ask_question",'Ask the requester a question you need answered before you can continue. When there are a few clear answers, pass them as `options` (2-10 short labels; `multiple: true` if several can be picked): the requester picks in Slack and can still write their own answer. The question (with its options) is posted in Slack as a card, so after calling this, end your turn without restating it: your final message should be empty or one short line that adds something new (e.g. "Nothing is booked yet."). Their answer will continue the conversation.',{question:X.string().min(1).max(3e3),options:X.array(X.string().trim().min(1).max(75)).min(2).max(10).optional(),multiple:X.boolean().optional()},async({question:i,options:o,multiple:a})=>{s.question=i;let l=o?[...new Set(o)]:[];return s.questionOptions=l.length>=2?{options:l,multiple:!!a}:null,U("Question recorded and shown to the requester as a card. End your turn now without repeating it (at most one short new line); their reply will continue this conversation.")}),n.tool("propose_plan","Propose a step-by-step plan for the requester to approve before you start non-trivial or risky work. The plan is shown to them as-is, so include any decision you need from them in it. After calling this, end your turn without making changes or restating the plan.",{markdown:X.string().min(1).max(2e4)},async({markdown:i})=>(s.plan=i,U("Plan recorded. End your turn now without making changes; you'll continue once it's approved."))),n.tool("upload_file","Share a file (for example a screenshot) in the requester's Slack thread. The file must be inside the working directory or the request's output folder.",{path:X.string().min(1).max(1e3),title:X.string().max(200).optional()},async({path:i,title:o})=>{let a=r(),l=dt(a.id),u=ye.join(l,"out"),d=ye.join(l,"tmp"),h,k;try{let w=We()==="bwrap",T=w?[a.cwd,u,d]:[a.cwd,u],f=w&&(i==="/tmp"||i.startsWith("/tmp/"))?ye.join(d,i.slice(4)):i;h=Md(f,T),k=Ud(h,T)}catch(w){return U(`Can't share that file: ${w.message}.`)}if(e.redactor.redact(k.toString("latin1")).hits.length)return U("Can't share that file: it appears to contain credentials or secrets.");let m=ye.join(N.uploads,a.id);ie.mkdirSync(m,{recursive:!0,mode:448});let b=ye.basename(h).replace(/[^A-Za-z0-9._-]/g,"_").slice(0,120),S=ye.join(m,`${oa.randomUUID()}-${b}`);return ie.writeFileSync(S,k,{mode:384}),s.uploads.push({path:S,title:o??null}),e.store.db.tx(()=>{e.notifier.upload(a,S,b,o??b),e.store.addArtifact(a.id,b.match(/\.(png|jpe?g|gif|webp)$/i)?"screenshot":"file",S,{title:o??null})}),U("File shared in the thread.")}),n.tool("link_pr","Record a pull request you opened for this request (a github.com pull request URL of this repository).",{url:X.string().url().max(300)},async({url:i})=>{let o=$d.exec(i.trim());if(!o)return U("That isn't a GitHub pull request URL.");let a=r(),l=`${o[1]}/${o[2]}`.toLowerCase(),u=Er(a.workdir).map(Rr).filter(Boolean);if(u.length&&!u.includes(l))return U("That pull request isn't in this repository.");s.prs.includes(i.trim())||(s.prs.push(i.trim()),e.store.updateTurn(s.turnId,{side_effects:JSON.stringify({pushed:s.pushed,prs:s.prs})}));let d=Mi(i);return d&&e.store.followPr(a.id,d),d&&e.onPrLinked?.(a,i)?.catch(()=>{}),U("Pull request recorded. I'll follow it: CI failures, merge conflicts and review feedback come back to this conversation.")}),n.tool("update_progress","Keep this thread's progress note current: what's done, key decisions or the root cause found, and what's left. It replaces the previous note. It's your memory if this work resumes in a fresh session, and it's shared (hidden) with other agents tagged in this thread, so write plain facts and never secrets or personal data.",{note:X.string().trim().min(1).max(1e3)},async({note:i})=>{let o=r();return e.redactor.redact(i).hits.length?U("Not saved: the note appears to contain a secret. Describe it without the value."):(e.store.updateRequest(o.id,{state_note:i}),U("Progress note saved."))}),n.tool("propose_setup_script","Propose a setup script for this repository: the shell commands a fresh worktree needs before work can start (install dependencies, copy an example env file, build). It does NOT run now: the machine's operator reviews and approves it in the admin, then it runs in each new worktree. Never put secrets in it.",{script:X.string().trim().min(1).max(Ln),reason:X.string().trim().min(1).max(500)},async({script:i,reason:o})=>{let a=r();return a.worktree_path?e.redactor.redact(i).hits.length?U("Not saved: the script appears to contain a secret. Reference an environment variable or a file the operator provides instead."):(e.store.db.tx(()=>{e.store.proposeSetupScript(ie.realpathSync(a.workdir),i,o,a.id,t()),e.store.addEvent(a.id,"setup_proposed",o.slice(0,200))}),U("Proposed. It won't run until the machine's operator approves it in the admin (Setup); tell the requester that.")):U("Setup scripts are only for a repository working directory (this request doesn't have its own worktree).")}),e.settings&&Hd(n,s,e,r),e.calendar?.active()&&Bd(n,e),e.settings?.get("handoffTargets").length&&!e.store.getRequest(s.requestId)?.channel_id.startsWith("tk:")&&jd(n,s,e,r),n}function Bd(s,e){s.tool("find_people","Look up people in this Slack workspace by name (or Slack user id) to get their work email, e.g. to invite them with the calendar tools. Returns up to 5 matches per name; if a name matches several people, ask the requester which one.",{names:X.array(X.string().min(1).max(80)).min(1).max(20)},async({names:t})=>{let n=[];for(let r of t){let i=await e.calendar.findPeople(r).catch(()=>null);if(i===null){n.push(`${r}: couldn't look people up right now.`);continue}n.push(i.length?`${r}: ${i.slice(0,5).map(o=>`${o.name} <${o.email??"no email on their Slack profile"}> (${o.id})`).join("; ")}`:`${r}: nobody by that name.`)}return{content:[{type:"text",text:n.join(`
638
638
  `)}]}})}function jd(s,e,t,n){let r=t.settings,i=r.get("handoffTargets");s.tool("hand_off",`Hand part of this request to another agent, when it's clearly their role. A person must approve it before they start. Agents: ${i.map(o=>`${o.name}${o.role?` (${o.role})`:""}`).join("; ")}. Give a self-contained task (they see this thread and your notes, not your session).`,{agent:X.string().trim().min(1).max(40).describe("The agent's name, from the list."),task:X.string().trim().min(1).max(2e3)},async({agent:o,task:a})=>{let l=n(),u=i.find(m=>m.name.toLowerCase()===o.toLowerCase()||m.botUserId===o);if(!u)return U(`Unknown agent. You can hand work to: ${i.map(m=>m.name).join(", ")}.`);if(l.handoff_hops>=1)return U("This work already came from a hand-off, so it can't be handed on again. Tell the requester what's left and who should do it.");let d=r.get("slackBotUserId");if(!d)return U("Hand-offs aren't available right now.");let h=t.redactor.redact(a).text,k=oa.randomUUID();return t.notifier.handoff(l,{to:u.botUserId,requester:l.requester_id,task:h},{event_type:"fda_handoff",event_payload:{v:1,id:k,from:d,to:u.botUserId,requester:l.requester_id,task:h,hops:l.handoff_hops+1,request:l.id}}),t.store.addEvent(l.id,"handoff_proposed",`to ${u.name}`,e.turnId),U(`Posted the hand-off to ${u.name}. It starts once <@${l.requester_id}> approves it with \u2705; say so in your reply.`)})}function Hd(s,e,t,n){let r=t.settings,i=t.now??Date.now;s.tool("schedule_task","Set up a task the agent runs on its own: recurring (e.g. daily insights at 9:00 in a channel) or one-time with `once: true` (reminders like \"remind me Friday at 10am\", or any one-off task at a set time). Use a 5-field cron (minute hour day month weekday) in the requester's timezone; with `once: true` it runs a single time at the next matching time (e.g. `0 10 6 11 *` for 6 Nov at 10:00) and then finishes, so always pass `once: true` for reminders and one-off tasks (without it, a date cron repeats every year). The requester must confirm the card that appears in Slack; until then it's not active, so say that it awaits their confirmation.",{title:X.string().trim().min(1).max(80),prompt:X.string().trim().min(1).max(Xt).describe("What to do on each run, self-contained (the run starts without this conversation)."),cron:X.string().trim().min(9).max(100),timezone:X.string().trim().max(60).optional().describe("IANA name; defaults to the requester's Slack timezone."),channel_id:X.string().trim().max(20).optional().describe("Where results go; defaults to this channel."),once:X.boolean().optional().describe("Run a single time at the next matching time, then finish (reminders, one-off tasks).")},async o=>{let a=n(),l;try{l=Ne(o.cron)}catch(f){return U(`That schedule isn't valid: ${f.message}.`)}let u=o.timezone||await t.userTimezone?.(a.requester_id)||"UTC";if(!xt(u))return U(`Unknown timezone "${u}". Use an IANA name like America/Sao_Paulo.`);let d=_e(l,u,i());if(d===null)return U("That schedule never runs. Check the day and month fields.");let h=o.once===!0,k=r.get("scheduleMinIntervalMin")*6e4;if(!h&&Rn(l,u,i())<k)return U(`Schedules can run at most every ${r.get("scheduleMinIntervalMin")} minutes.`);if(t.store.countLiveSchedules()>=r.get("scheduleMax"))return U("This machine already has the maximum number of schedules. Remove one in the admin panel first.");if(t.store.countPendingSchedules(a.requester_id)>=Ld)return U("There are already proposals waiting for this person's confirmation. Ask them to confirm or cancel those first.");let m=o.channel_id||a.channel_id;if(m.startsWith("D")&&m!==a.channel_id||!Sn(t.store.allowlist(),m,r.get("allowDirectMessages"),r.get("allowAnyChannel")))return U("Results can only go to a channel this agent is allowed in (or back to this direct message).");let S=await t.channelProblem?.(m,m===a.channel_id?null:a.requester_id)??null;if(S)return U(`Results can't go to that channel: ${S}`);let w=t.store.createSchedule({title:t.redactor.redact(o.title).text,prompt:t.redactor.redact(o.prompt).text,cron:l.source,timezone:u,channelId:m,createdBy:a.requester_id,createdByName:a.requester_name,sourceRequestId:a.id,once:h,nextRunAt:h?d:null,now:i()}),T=Ce(w,i());return t.store.db.tx(()=>t.notifier.scheduleCard(a,{id:w.id,title:w.title,prompt:w.prompt,when:T,timezone:u,channelId:m,once:h})),e.schedules.push(w.id),U(`Proposed "${w.title}": ${T} (${u}), results in <#${m}>. It starts once the requester confirms the card in Slack. Schedule id: ${w.id}`)}),s.tool("list_schedules","List the schedules on this machine (title, when, channel, status, who set it up, id).",{},async()=>{let o=t.store.listSchedules({createdBy:n().requester_id});return o.length?U(o.map(a=>`- ${a.title} \xB7 ${Ce(a,i())} (${a.timezone}) \xB7 <#${a.channel_id}> \xB7 ${a.status}${a.status_reason?` (${a.status_reason})`:""} \xB7 by ${a.created_by_name??a.created_by} \xB7 id ${a.id}`).join(`
639
639
  `)):U("No schedules yet.")}),s.tool("update_schedule","Pause, resume or cancel a schedule. Only the person who set it up can change it here (admins can in the admin panel).",{schedule_id:X.string().trim().min(1).max(64),action:X.enum(["pause","resume","cancel"])},async({schedule_id:o,action:a})=>{let l=n(),u=t.store.getSchedule(o);if(!u||u.status==="cancelled")return U("There's no such schedule.");if(u.created_by!==l.requester_id)return U(`Only ${u.created_by_name??"the person who set it up"} can change this schedule (or an admin in the admin panel).`);if(u.status==="pending")return U("That schedule hasn't been confirmed yet; use the card in Slack.");if(a==="resume"&&u.status==="paused"&&u.paused_by!=="creator")return U(`It was paused ${u.paused_by==="admin"?"by an admin":"automatically"}${u.status_reason?` (${u.status_reason})`:""}; only the admin panel can resume it.`);let d=a==="pause"?{status:"paused",status_reason:null,paused_by:"creator"}:a==="resume"?{status:"active",status_reason:null,next_run_at:_e(Ne(u.cron),u.timezone,i())}:{status:"cancelled",status_reason:null,next_run_at:null};return t.store.updateSchedule(u.id,d,i()),t.onSchedulesChanged?.(),U(`"${u.title}" is now ${d.status}.`)})}function Wd(s){return!!s&&(s==="127.0.0.1"||s==="::1"||s==="::ffff:127.0.0.1")}function Gd(s,e){let t=Tr();t.disable("x-powered-by");let n=new Set([`127.0.0.1:${e}`,`localhost:${e}`]);t.use((a,l,u)=>{if(!Wd(a.socket.remoteAddress)||a.headers["x-forwarded-for"]||a.headers["x-forwarded-host"]||a.headers.forwarded)return l.status(403).end();if(!n.has(String(a.headers.host??"")))return l.status(421).end();u()}),t.post("/mcp",(a,l,u)=>{let d=String(a.headers.authorization??""),h=d.startsWith("Bearer ")?s.registry.lookup(d.slice(7).trim()):void 0;if(!h)return l.status(401).json({error:"unauthorized"});if(!(s.peerCheck??Wt)(a.socket,h.turnId))return l.status(401).json({error:"unauthorized"});l.locals.ctx=h,u()});let r=(a,l,u)=>{let d=String(a.headers.authorization??""),h=d.startsWith("Bearer ")?s.registry.lookup(d.slice(7).trim()):void 0;if(!h||!(s.peerCheck??Wt)(a.socket,h.turnId))return l.status(401).json({error:"unauthorized"});if(!s.calendar?.active())return l.status(404).json({error:"calendar_off"});l.locals.ctx=h,u()},i=async(a,l)=>{if(a.method!=="POST")return l.status(405).end();let u=s.calendar,d=l.locals.ctx,h;try{h=JSON.parse(Buffer.isBuffer(a.body)?a.body.toString("utf8"):"")}catch{return l.status(400).json({error:"bad_request"})}let k=s.store.getRequest(d.requestId),m=k&&await s.userTimezone?.(k.requester_id).catch(()=>null)||"UTC",b=ra({api:u.api,timezone:m,domain:u.accountDomain(),allowExternal:u.allowExternal(),now:s.now}),S=new ia({sessionIdGenerator:void 0});l.on("close",()=>{S.close(),b.close()});try{await b.connect(S),await S.handleRequest(a,l,h)}catch{l.headersSent||l.status(500).json({error:"internal"})}},o=async(a,l,u)=>{let d=s.calendar;if(d.backend()!=="google-mcp")return i(a,l,u);let h=Buffer.isBuffer(a.body)&&a.body.length?a.body:void 0,k=h?zd(h,d):null;if(k)return l.status(200).json(k);let m=await d.accessToken();if(!m)return l.status(503).json({jsonrpc:"2.0",id:null,error:{code:-32e3,message:"Google Calendar isn't connected right now."}});let b={authorization:`Bearer ${m}`};for(let f of["content-type","accept","mcp-session-id","mcp-protocol-version","last-event-id"]){let E=a.headers[f];typeof E=="string"&&(b[f]=E)}let S=new AbortController;l.on("close",()=>S.abort());let w;try{w=await(d.fetch??fetch)(d.upstream??vo,{method:a.method,headers:b,body:h?new Uint8Array(h):void 0,redirect:"error",signal:S.signal})}catch{l.headersSent||l.status(502).json({jsonrpc:"2.0",id:null,error:{code:-32e3,message:"Couldn't reach Google Calendar."}});return}l.status(w.status);for(let f of["content-type","mcp-session-id","cache-control"]){let E=w.headers.get(f);E&&l.setHeader(f,E)}if(!w.body)return void l.end();let T=Pd.fromWeb(w.body);T.on("error",()=>l.destroy()),T.pipe(l)};return t.post("/gcal",r,Tr.raw({type:()=>!0,limit:"2mb"}),o),t.get("/gcal",r,o),t.delete("/gcal",r,o),t.all("/gcal",(a,l)=>l.status(405).end()),t.use(Tr.json({limit:"1mb"})),t.post("/mcp",async(a,l)=>{let u=l.locals.ctx,d=Fd(u,s),h=new ia({sessionIdGenerator:void 0});l.on("close",()=>{h.close(),d.close()});try{await d.connect(h),await h.handleRequest(a,l,a.body)}catch{l.headersSent||l.status(500).json({error:"internal"})}}),t.all("/mcp",(a,l)=>l.status(405).end()),t.use((a,l,u,d)=>{u.status(a?.status&&a.status>=400&&a.status<500?a.status:500).json({error:"bad_request"})}),t}function ca(s,e){let t=Gd(s,e);return new Promise((n,r)=>{let i=t.listen(e,"127.0.0.1",()=>n(i));i.once("error",r)})}function zd(s,e){if(e.allowExternal())return null;let t=e.accountDomain(),n;try{n=JSON.parse(s.toString("utf8"))}catch{return null}let r=Array.isArray(n)?n:[n];for(let i of r){if(i?.method!=="tools/call"||!xo.has(String(i.params?.name)))continue;let o=[...Jn(i.params?.arguments)].filter(a=>!t||a.split("@")[1]!==t&&!a.endsWith(".calendar.google.com"));if(o.length)return{jsonrpc:"2.0",id:Array.isArray(n)?null:i.id??null,result:{isError:!0,content:[{type:"text",text:`Not sent: ${o.join(", ")} ${o.length===1?"is":"are"} outside ${t??"the calendar's domain"}, and this agent may only invite people from ${t??"its own domain"}. Tell the requester; the machine's operator can allow external guests in the admin (Calendar).`}]}}}return null}function rn(){try{let s=JSON.parse(Qe.readFileSync(N.machine,"utf8"));return s&&typeof s.machineId=="string"?s:null}catch{return null}}function ha(s){Qe.writeFileSync(N.machine,JSON.stringify(s,null,2),{mode:384})}function _t(){try{let s=JSON.parse(Qe.readFileSync(N.cliConfig,"utf8"));return s.credentials?.keyId&&s.credentials?.secret?{keyId:s.credentials.keyId,secret:s.credentials.secret}:null}catch{return null}}var Jd=new Set(["auth","superclawd_auth","cli_missing","agent_unavailable","workspace_invalid"]),Yd=5*6e4,Vd=6e4,Zd=3*6e4,pa=7*24*36e5,Qd=5*6e4,ep=14*24*36e5,tp=new Set(["invalid_auth","not_authed","token_revoked","token_expired","account_inactive","missing_scope","not_allowed_token_type"]);function vr(){let s=[process.env.FDA_BROWSER_MCP,Ze.join(N.home,"browser","node_modules",".bin","playwright-mcp")];for(let e of s)if(e&&Qe.existsSync(e))return e;return null}function np(s=Ze.join(hs.homedir(),".claude","settings.json")){try{let e=Qe.lstatSync(s);if(!e.isFile()||e.size>256*1024)return null;let t=JSON.parse(Qe.readFileSync(s,"utf8")).model;return typeof t=="string"&&/^[A-Za-z0-9][A-Za-z0-9.:_[\]-]{0,99}$/.test(t)?t:null}catch{return null}}async function sp(s){try{let e=await fetch("https://api.anthropic.com/v1/models?limit=1",{headers:{"x-api-key":s,"anthropic-version":"2023-06-01"},redirect:"error",signal:AbortSignal.timeout(1e4)});return await e.body?.cancel().catch(()=>{}),e.ok?{ok:!0,message:"The key works."}:e.status===401||e.status===403?{ok:!1,message:"Anthropic rejected this key."}:{ok:!1,message:`Anthropic answered with HTTP ${e.status}.`}}catch{return{ok:!1,message:"Couldn't reach Anthropic."}}}var gs=class{constructor(e=t=>console.log(t)){this.log=e;ct(),this.db=dn(N.db),this.db.migrate(),this.db.restrictFileModes(),this.store=new Et(this.db),this.settings=new St(this.db),this.google=new Kn({settings:this.settings,onLost:n=>{this.health.set("calendar","warn",n),this.notifyOperator(n)}}),this.keyProxy=new bn({apiKey:()=>this.settings.getSecret("anthropicApiKey"),upstream:process.env.FDA_ANTHROPIC_URL||void 0,onCreditsExhausted:(n,r)=>this.onCreditsExhausted(r)}),this.notifier=new Bn(this.store,this.redactor,()=>{this.sender?.wake(),this.ticketSender?.wake()}),this.tickets=new ts(this.db),this.linear=new Vn({settings:this.settings}),this.jira=new Zn({settings:this.settings}),this.ticketSender=new Zt({lane:"tickets",store:this.store,slack:()=>null,watchUrl:()=>null,onFatal:()=>{},onDropped:(n,r)=>this.log(`tickets: dropped an update to ${r} (${n})`),tickets:(n,r)=>this.ticketService.deliver(n,r)}),this.sender=new Zt({lane:"slack",store:this.store,slack:()=>this.slackPort,watchUrl:n=>{let r=this.settings.get("adminPublicUrl");return r?`${r.replace(/\/$/,"")}/requests/${n}`:null},onFatal:n=>this.health.set("slack_auth","blocked",`Slack rejected the bot token (${n}). Update the Slack tokens.`),onDropped:(n,r)=>this.log(`slack: dropped a message to ${r} (${n})`),threadBlock:(n,r)=>this.threadBlock(n,r),stopButton:()=>this.settings.get("stopButton"),presenceBlock:n=>this.threadBlock(n,"","working")}),this.worker=new Dn({store:this.store,settings:this.settings,notifier:this.notifier,registry:this.registry,redactor:this.redactor,mcpUrl:`http://127.0.0.1:${bs}/mcp`,workspaceSlug:()=>this.machine?.workspaceSlug??null,canDispatch:()=>this.health.canDispatch(),isAllowed:n=>this.ticketPersonAllowedNow(n),onBreaker:(n,r)=>this.onBreaker(n,r),names:()=>this.intake.nameMap(),channelName:n=>this.intake.channelName(n),browserSites:()=>this.logins.sites().map(n=>n.site),browser:n=>{let r=vr();if(!r)return null;let i=Ze.join(dt(n.id),"out");Qe.mkdirSync(i,{recursive:!0,mode:448});let o=this.logins.writeForTurn(pe(n.id));return{command:r,args:["--headless","--browser","chromium","--isolated",...o?["--storage-state",o]:[],"--output-dir",i],ro:o?[o]:[]}},anthropicAuth:(n,r)=>{if(!this.settings.getSecret("anthropicApiKey"))return{apiKey:null,baseUrl:null};let i=this.keyProxy.issue(n,r);return this.redactor.addSecrets([i]),{apiKey:i,baseUrl:this.keyProxy.baseUrl()}},prepareCwd:n=>wi(n),ensureCwd:n=>_i(n),setupCwd:n=>Zi(this.store,this.redactor,n),githubToken:()=>this.githubToken(),sessionModel:()=>np(),tickets:{forRequest:n=>{if(!en(n.channel_id))return null;let r=this.tickets.byRequest(n.id);return r?{key:r.key,url:r.url,tracker:r.tracker,delegator:r.delegator_name}:null},budgetLeft:n=>{let r=this.settings.get("ticketBudgetUsd");return en(n.channel_id)&&r!==null?r-this.tickets.requestCost(n.id):null},markOverBudget:n=>{let r=this.tickets.byRequest(n.id);return!r||r.status_note==="over budget"?!1:(this.tickets.update(r.id,{status_note:"over budget"},Date.now()),!0)},dailyCapReached:()=>{let n=this.settings.get("ticketDailyBudgetUsd");if(n===null)return!1;let r=new Date;return r.setHours(0,0,0,0),this.tickets.ticketCostSince(r.getTime())>=n}},calendar:()=>this.google.active()?{account:this.settings.get("calendarAccount"),allowExternal:this.settings.get("calendarAllowExternal")}:null,creditsFlag:n=>this.keyProxy.takeCreditsFlag(n),unreachableFlag:n=>this.keyProxy.takeUnreachableFlag(n),cliSupports:n=>this.cliFeatures.has(n),refreshPeers:async n=>{await zs({store:this.store,slack:this.slackPort,selfInstance:this.machine?.machineId??null},n)},gitState:async n=>{let r=await ki(n);r&&this.store.updateRequest(n.id,{git_state:JSON.stringify(r)})},log:this.log}),this.intake=new ls({store:this.store,settings:this.settings,notifier:this.notifier,worker:this.worker,slack:()=>this.slackPort,acceptingRequests:()=>this.health.acceptingRequests(),onSchedulesChanged:()=>this.scheduler.wake(),holdNote:()=>this.health.list().some(n=>n.id==="credits")?pt("credits"):null,refreshPeers:async n=>{await zs({store:this.store,slack:this.slackPort,selfInstance:this.machine?.machineId??null},n)}}),this.scheduler=new In({store:this.store,settings:this.settings,notifier:this.notifier,slack:()=>this.slackPort,isAllowed:n=>this.intake.isAllowedAsync(n),channelProblem:n=>this.channelProblem(n,null),wakeWorker:()=>this.worker.wake(),log:this.log}),this.prMonitor=new Nn({store:this.store,settings:this.settings,notifier:this.notifier,redactor:this.redactor,github:this.gh,isAllowed:n=>this.ticketPersonAllowedNow(n),canDispatch:()=>this.health.canDispatch(),wakeWorker:()=>this.worker.wake(),setHealth:(n,r)=>r?this.health.set(n,"warn",r):this.health.clear(n),onMerged:(n,r)=>this.ticketService.onPrMerged(n,r),log:this.log}),this.ticketService=new rs({store:this.store,tickets:this.tickets,settings:this.settings,notifier:this.notifier,worker:this.worker,adapters:[this.linear,new is({settings:this.settings,token:()=>this.githubToken(),marker:()=>this.machine?.machineId??"fda",agentName:()=>this.settings.get("slackBotName")||this.settings.get("agentName")||"FDA"}),this.jira],person:(n,r)=>this.ticketPerson(n,r),acceptingRequests:()=>this.health.acceptingRequests(),holdNote:()=>this.health.list().some(n=>n.id==="credits")?pt("credits"):null,help:()=>this.intake.helpOptions(),names:()=>this.intake.nameMap(),enableAutoMerge:(n,r)=>this.gh.enableAutoMerge(n,r),workdirRepos:()=>{let n=this.settings.get("workdir");return n?Er(n).map(Rr).filter(Boolean):[]},log:this.log}),this.health.onChange(()=>this.worker.wake()),this.auth=new wt(this.db);let t=new Ct({credential:()=>_t(),workspaceSlug:()=>this.machine?.workspaceSlug??null});this.cloudClient=t,this.ticketBridge=new os({cloud:t,settings:this.settings,machineId:()=>this.machine?.machineId??null,onRing:n=>void this.ticketService.pollOnce(n),log:this.log}),this.cloud=new Hn({client:t,machineId:()=>this.machine?.machineId??null,heartbeat:()=>this.heartbeat(),agentName:()=>this.settings.get("agentName"),agentId:()=>this.settings.get("agentId"),setAgentId:n=>this.settings.set({agentId:n}),setAgentName:n=>this.settings.set({agentName:n}),setCondition:(n,r,i)=>this.health.set(n,r,i),clearCondition:n=>this.health.clear(n),log:this.log})}db;store;settings;redactor=new Mn;health=new Un;registry=new Fn;notifier;worker;intake;sender;ticketSender;slackPort=null;socket=null;toolServer=null;timers=[];machine=null;startedAt=Date.now();cloud;auth;keyProxy;scheduler;prMonitor;google;tickets;linear;jira;ticketService;ticketBridge;gh=new An;peopleCache=null;logins=new ft(Ze.join(N.home,"browser-logins.enc"));loginBrowser=new Qn({logins:this.logins,mcpBin:()=>vr()});sandbox=null;cloudClient;adminServer=null;starting=null;stopping=!1;connecting=Promise.resolve();slackGen=0;slackFailures=0;slackRetry=null;disconnectedAt=null;backfilling=!1;adminDeps(){return{db:this.db,store:this.store,settings:this.settings,health:this.health,auth:this.auth,redactor:this.redactor,browser:{logins:this.logins,session:this.loginBrowser},calendar:this.google,tickets:{store:this.tickets,service:this.ticketService,linear:this.linear,jira:this.jira,bridge:this.ticketBridge},staticDir:Ze.resolve(Ze.dirname(Kd(import.meta.url)),"..","dist"),info:()=>({version:xe,startedAt:this.startedAt,machineId:this.machine?.machineId??null,workspaceSlug:this.machine?.workspaceSlug??null,slackConnected:!!this.socket&&!this.health.list().some(e=>e.id==="slack_socket")}),cancel:(e,t)=>(this.store.setRequestPrFollowing(e,!1,"stopped from the admin"),this.worker.cancel(e,t)),retry:(e,t)=>this.worker.retry(e,t),agents:()=>this.cloudClient.agents(),slack:()=>this.slackPort,testSlack:async e=>{try{let t=await new Qt(e).authTest();return{ok:!0,botName:t.user,teamId:t.teamId}}catch(t){return{ok:!1,message:t instanceof te?t.code:"Couldn't reach Slack."}}},testAnthropic:sp,reloadSlack:()=>this.connectSlack(),scheduler:this.scheduler,onChanged:()=>{this.settings.invalidate(),this.intake.invalidateGroups(),this.checkConfig(),this.cloud.reset(),this.worker.wake(),this.prMonitor.wake(),this.ticketBridge.start()}}}creditsProbe=null;onCreditsExhausted(e){let t=this.health.list().some(n=>n.id==="credits");this.health.set("credits","degraded",`Anthropic refused calls: ${this.redactor.redact(e).text.slice(0,160)} Requests are on hold and resume on their own once there's credit (Anthropic Console \u2192 Billing / limits).`),!t&&(this.log(`credits: Anthropic refused calls (${e.slice(0,120)})`),this.notifyOperator("Anthropic refused my calls: the account is out of credits or hit a spend/usage limit. Requests are saved and will resume on their own once there's credit again."),this.creditsProbe||(this.creditsProbe=setInterval(()=>void this.probeCredits(),Zd),this.creditsProbe.unref()))}async probeCredits(){let e=this.settings.getSecret("anthropicApiKey");if(!e)return;let t=!1;try{let n=await fetch(`${process.env.FDA_ANTHROPIC_URL||"https://api.anthropic.com"}/v1/messages`,{method:"POST",headers:{"x-api-key":e,"anthropic-version":"2023-06-01","content-type":"application/json"},body:JSON.stringify({model:"claude-haiku-5-5",max_tokens:1,messages:[{role:"user",content:"ok"}]}),signal:AbortSignal.timeout(2e4)});n.ok?await n.body?.cancel().catch(()=>{}):t=!!Os(n.status,await n.text())}catch{return}t||(this.health.clear("credits"),this.creditsProbe&&clearInterval(this.creditsProbe),this.creditsProbe=null,this.log("credits: calls go through again; resuming held requests"),this.worker.wake())}async notifyOperator(e){let t=this.settings.get("operatorContact")??"",n=/\b(U[A-Z0-9]{6,})\b/.exec(t)?.[1];!n||!this.slackPort||await this.slackPort.postMessage({channel:n,text:e}).catch(()=>this.log("credits: couldn't DM the operator"))}cliFeatures=new Set;async probeCli(){let{cmd:e,baseArgs:t}=It(),n=await new Promise(r=>da(e,[...t,"version","--json"],{timeout:15e3},(i,o)=>r(String(o??""))));try{this.cliFeatures=new Set(JSON.parse(n).features??[])}catch{this.cliFeatures=new Set}}ghToken=null;async githubToken(){if(this.ghToken&&Date.now()-this.ghToken.at<5*6e4)return this.ghToken.value;let e=await new Promise(t=>da(process.env.FDA_GH_BIN||"gh",["auth","token"],{timeout:5e3,env:{...process.env,GH_PROMPT_DISABLED:"1"}},(n,r)=>{let i=String(r??"").trim();t(!n&&/^[A-Za-z0-9_]{20,255}$/.test(i)?i:null)}));return this.ghToken={at:e?Date.now():Date.now()-4.5*6e4,value:e},e||this.log("github: no token from `gh auth token` (turns run without GH_TOKEN)"),e}threadBlock(e,t,n){let r=this.store.getRequest(e),i=this.machine?.machineId;if(!r||!i)return null;let o=this.store.turnsFor(r.id).filter(l=>l.result_text).pop(),a=this.settings.all();return Qi({instance:i,agent:a.slackBotName||a.agentName||"SuperClawd FDA",request:r.id,status:n??r.status,at:Date.now(),seq:o?.seq??null,branch:r.branch,prs:[...new Set([...this.store.artifactsFor(r.id).filter(l=>l.kind==="pr").map(l=>l.url_or_path),...this.store.prFollowsFor(r.id).map(l=>l.url)])],question:r.pending_question,...Js(r),state:r.state_note,summary:t||null},l=>this.redactor.redact(l).text)}heartbeat(){let e=this.db.get(`SELECT (SELECT COUNT(*) FROM turns WHERE status = 'running') AS running,
640
640
  (SELECT COUNT(*) FROM turns WHERE status = 'queued') AS queued,
@@ -679,23 +679,23 @@ Usage: superclawd fda <command>
679
679
 
680
680
  ${Or}`,64)}}finally{n.close()}}function yp(){return{io:{input:process.stdin,output:process.stdout,isTTY:!!process.stdin.isTTY&&!!process.stdout.isTTY},cloud:s=>new Ct({credential:()=>_t(),workspaceSlug:()=>s}),credential:()=>_t(),cliWorkspace:()=>{try{let s=JSON.parse(j.readFileSync(N.cliConfig,"utf8"));return s.credentials?.workspace??s.workspace??null}catch{return null}}}}function Lr(s,e){let t=s.indexOf(e);return t>=0?s[t+1]:void 0}var Ea=/^[a-z0-9][a-z0-9-]{0,62}$/i;async function xa(s,e=yp()){try{await Sp(s,e)}finally{qt(e.io.input)}}async function Sp(s,e){let t=u=>e.io.output.write(`${u}
681
681
  `);if(!e.credential())throw new D("Log in first: superclawd login",77);let n=rn(),r=Lr(s,"--workspace");if(r!==void 0){if(!Ea.test(r))throw new D(`That isn't a workspace slug: ${r}`,64)}else if(e.io.isTTY){let u=await e.cloud(null).workspaces();if(!u.ok&&u.kind==="auth")throw new D("Log in again: superclawd login",77);if(u.ok)if(u.data.length)if(u.data.length===1)r=u.data[0].slug,t(`Using workspace "${u.data[0].name}" (your only one).`);else{let d=n?.workspaceSlug??e.cliWorkspace(),h=u.data.findIndex(m=>m.slug===d),k=await Pr(e.io,"Which SuperClawd workspace should this machine work for?",u.data.map(m=>({label:m.name,hint:[m.slug,m.role].filter(Boolean).join(", "),key:m.slug,value:m.slug})),{defaultIndex:h>=0?h:void 0});if(k===null||k==="skip")throw new D("Cancelled; nothing changed.",130);r=k}else throw new D("Your SuperClawd account has no workspaces. Create one in the SuperClawd App first.",1);else{t("Couldn't list your workspaces right now.");let d=await Se(e.io,"Workspace slug: ");if(!d||!Ea.test(d))throw new D("No valid workspace slug given.",64);r=d}}else throw new D("Pass --workspace <slug> (there's no terminal to choose from).",64);ct();let o=!s.includes("--reset-id")&&n?.machineId?n.machineId:fp.randomUUID();ha({machineId:o,workspaceSlug:r}),t(`This machine is linked to workspace "${r}".`);let a=e.cloud(r);if(!s.includes("--no-register")){let u=await a.register(o);t(u.ok?"Registered in the SuperClawd App.":bo(u.kind))}let l=Lr(s,"--agent");if(l!==void 0||e.io.isTTY){let u=await a.agents(),{db:d,settings:h}=at();try{let k=h.all();if(!u.ok)t(u.kind==="not_found"?"Choose the agent later in the admin panel (this SuperClawd server can't list them yet).":"Couldn't list the workspace's agents; choose one later in the admin panel.");else{let m=u.data;n?.workspaceSlug&&n.workspaceSlug!==r&&k.agentName&&!m.some(S=>ke(S.name)===k.agentName)&&(h.set({agentId:null,agentName:null}),t("The previously selected agent belongs to the old workspace; it was cleared."));let b=null;if(l!==void 0){if(b=m.find(S=>S.id===l||ke(S.name)===l)??null,!b)throw new D(`No enabled Primary agent "${l}" in this workspace.`,64)}else if(!m.length)t("This workspace has no enabled Primary agents yet. Create one in the SuperClawd App (Agents \u2192 Primary).");else{let S=m.findIndex(T=>ke(T.name)===h.get("agentName")),w=S>=0?S:m.length===1?0:-1;if(b=await Pr(e.io,"Which Primary agent should run on this machine?",m.map(T=>({label:T.name,hint:T.description??void 0,key:ke(T.name),value:T})),{defaultIndex:w>=0?w:void 0,allowSkip:!0}),b===null)throw new D("Cancelled (the workspace link was saved).",130)}b&&b!=="skip"&&(h.set({agentId:b.id,agentName:ke(b.name)}),t(`Primary agent: ${b.name}.`))}}finally{d.close()}}if(!s.includes("--skip-workdir")){let{db:u,settings:d}=at();try{let h=Lr(s,"--workdir"),k=(m,b)=>m==="repo"?`Work directory: ${b} (a git repository: each request gets its own worktree).`:`Work directory: ${b} (a folder: requests work in it directly; repos inside it are used as they are).`;if(h!==void 0){let m=Pt(h??"");if(!m.ok)throw new D(`--workdir: ${m.message}`,64);d.set({workdir:m.path}),t(k(m.kind,m.path))}else if(e.io.isTTY){let m=d.get("workdir");t(`
682
- Which folder should the agent work in? A git repository, or a folder holding several repos.`);let b=!1;for(let S=0;S<5&&!b;S++){let w=await Se(e.io,m?`Work directory [${m}]: `:"Work directory (Enter to skip for now): ");if(w===null)break;let T=w||m||"";if(!T)break;let f=Pt(T);if(!f.ok){t(f.message);continue}d.set({workdir:f.path}),t(k(f.kind,f.path)),b=!0}!b&&!d.get("workdir")&&t("No work directory yet: set it in the admin (Settings \u2192 General) or with `superclawd fda config set workdir=<path>`.")}else d.get("workdir")||t("No work directory yet: pass --workdir <path>, or set it later in the admin (Settings \u2192 General).")}finally{u.close()}}Mt(tt())}var Te="/etc/systemd/system/superclawd-fda.service";function lt(s,e){return new Promise(t=>{let n=va(s,e,{stdio:"inherit"});n.on("error",()=>t(127)),n.on("close",r=>t(r??1))})}async function Ia(){if($e()){O("SuperClawd FDA is already running.");return}if(!j.existsSync(Te)){O("The service isn't installed. Install it with `superclawd fda service install`, or run in the foreground: superclawd fda run"),process.exitCode=1;return}let s=await lt("sudo",["systemctl","start","superclawd-fda"]);if(s!==0)throw new D("Couldn't start the service (sudo systemctl start superclawd-fda).",s);for(let e=0;e<20&&!$e();e++)await new Promise(t=>setTimeout(t,500));O($e()?"Started.":"Started the service; check `superclawd fda logs` if it doesn't come up.")}async function $r(s,e){if(e.includes("--yes")||e.includes("-y"))return!0;if(!process.stdin.isTTY)throw new D(`${s} Re-run with --yes to confirm.`,64);let t={input:process.stdin,output:process.stdout};try{let n=await Se(t,`${s} [y/N] `);return/^y(es)?$/i.test(n??"")}finally{qt(t.input)}}function Tp(){return j.realpathSync(process.argv[1]??"")}function Ep(s){return s.template.replaceAll("__USER__",s.user).replaceAll("__HOME__",s.home).replaceAll("__NODE_BIN__",ie.dirname(s.node)).replaceAll("__NODE__",s.node).replaceAll("__CLI__",s.cli)}async function kt(s,e){return e===void 0?lt("sudo",s):new Promise(t=>{let n=va("sudo",s,{stdio:["pipe","ignore","inherit"]});n.on("error",()=>t(127)),n.on("close",r=>t(r??1)),n.stdin.end(e)})}async function Aa(s){let[e,...t]=s;if(ot.platform()!=="linux"||!j.existsSync("/run/systemd/system"))throw new D("The service needs Linux with systemd. Run in the foreground instead: superclawd fda run",78);if(e==="install"){let n=Tp(),r=ie.resolve(ie.dirname(n),"..","scripts","superclawd-fda.service.tmpl");if(!j.existsSync(r))throw new D(`Service template not found at ${r}`);let i=Ep({user:ot.userInfo().username,home:ot.homedir(),node:process.execPath,cli:n,template:j.readFileSync(r,"utf8")}),o=ie.join(N.home,"env");if(j.existsSync(o)||(j.mkdirSync(N.home,{recursive:!0,mode:448}),j.writeFileSync(o,`DISABLE_AUTOUPDATER=1
682
+ Which folder should the agent work in? A git repository, or a folder holding several repos.`);let b=!1;for(let S=0;S<5&&!b;S++){let w=await Se(e.io,m?`Work directory [${m}]: `:"Work directory (Enter to skip for now): ");if(w===null)break;let T=w||m||"";if(!T)break;let f=Pt(T);if(!f.ok){t(f.message);continue}d.set({workdir:f.path}),t(k(f.kind,f.path)),b=!0}!b&&!d.get("workdir")&&t("No work directory yet: set it in the admin (Settings \u2192 General) or with `superclawd fda config set workdir=<path>`.")}else d.get("workdir")||t("No work directory yet: pass --workdir <path>, or set it later in the admin (Settings \u2192 General).")}finally{u.close()}}Mt(tt())}var Te="/etc/systemd/system/superclawd-fda.service";function lt(s,e){return new Promise(t=>{let n=va(s,e,{stdio:"inherit"});n.on("error",()=>t(127)),n.on("close",r=>t(r??1))})}async function Ia(){if($e()){O("SuperClawd FDA is already running.");return}if(!j.existsSync(Te)){O("The service isn't installed. Install it with `superclawd fda service install`, or run in the foreground: superclawd fda run"),process.exitCode=1;return}let s=await lt("sudo",["systemctl","start","superclawd-fda"]);if(s!==0)throw new D("Couldn't start the service (sudo systemctl start superclawd-fda).",s);for(let e=0;e<20&&!$e();e++)await new Promise(t=>setTimeout(t,500));O($e()?"Started.":"Started the service; check `superclawd fda logs` if it doesn't come up.")}async function $r(s,e){if(e.includes("--yes")||e.includes("-y"))return!0;if(!process.stdin.isTTY)throw new D(`${s} Re-run with --yes to confirm.`,64);let t={input:process.stdin,output:process.stdout};try{let n=await Se(t,`${s} [y/N] `);return/^y(es)?$/i.test(n??"")}finally{qt(t.input)}}function Tp(){return j.realpathSync(process.argv[1]??"")}function Ep(s){return s.template.replaceAll("__USER__",s.user).replaceAll("__HOME__",s.home).replaceAll("__NODE_BIN__",se.dirname(s.node)).replaceAll("__NODE__",s.node).replaceAll("__CLI__",s.cli)}async function kt(s,e){return e===void 0?lt("sudo",s):new Promise(t=>{let n=va("sudo",s,{stdio:["pipe","ignore","inherit"]});n.on("error",()=>t(127)),n.on("close",r=>t(r??1)),n.stdin.end(e)})}async function Aa(s){let[e,...t]=s;if(ot.platform()!=="linux"||!j.existsSync("/run/systemd/system"))throw new D("The service needs Linux with systemd. Run in the foreground instead: superclawd fda run",78);if(e==="install"){let n=Tp(),r=se.resolve(se.dirname(n),"..","scripts","superclawd-fda.service.tmpl");if(!j.existsSync(r))throw new D(`Service template not found at ${r}`);let i=Ep({user:ot.userInfo().username,home:ot.homedir(),node:process.execPath,cli:n,template:j.readFileSync(r,"utf8")}),o=se.join(N.home,"env");if(j.existsSync(o)||(j.mkdirSync(N.home,{recursive:!0,mode:448}),j.writeFileSync(o,`DISABLE_AUTOUPDATER=1
683
683
  `,{mode:384})),O(`This writes ${Te}:
684
684
 
685
685
  ${i}`),O(`and runs: sudo systemctl daemon-reload && sudo systemctl enable superclawd-fda
686
- `),!await $r("Install the service?",t))return O("Nothing changed.");if(await kt(["tee",Te],i)!==0)throw new D(`Couldn't write ${Te}.`);if(await kt(["systemctl","daemon-reload"])!==0)throw new D("systemctl daemon-reload failed.");if(await kt(["systemctl","enable","superclawd-fda"])!==0)throw new D("systemctl enable failed.");O("\u2713 Service installed (starts on boot). Start it now: superclawd fda start");return}if(e==="uninstall"){if(!j.existsSync(Te))return O("The service isn't installed.");if(O(`This stops and disables superclawd-fda and deletes ${Te} (your data stays in ${N.home}).`),!await $r("Remove the service?",t))return O("Nothing changed.");if(await kt(["systemctl","disable","--now","superclawd-fda"]),await kt(["rm","-f",Te])!==0)throw new D(`Couldn't delete ${Te}.`);await kt(["systemctl","daemon-reload"]),O("\u2713 Service removed.");return}throw new D("Usage: superclawd fda service install|uninstall [--yes]",64)}var Ra="0.0.83";async function Rp(s){let[e,...t]=s;if(e!=="install")throw new D("Usage: superclawd fda browser install [--yes]",64);let n=ie.join(N.home,"browser");if(O(["This installs the optional browser for agent tasks:",` 1. @playwright/mcp@${Ra} into ${n} (npm, no sudo)`," 2. Chromium for Playwright into ~/.cache/ms-playwright (no sudo)"," 3. Chromium's system libraries: sudo npx playwright install-deps chromium (apt)",`Then turn it on in the admin: Settings \u2192 General \u2192 Browser.
686
+ `),!await $r("Install the service?",t))return O("Nothing changed.");if(await kt(["tee",Te],i)!==0)throw new D(`Couldn't write ${Te}.`);if(await kt(["systemctl","daemon-reload"])!==0)throw new D("systemctl daemon-reload failed.");if(await kt(["systemctl","enable","superclawd-fda"])!==0)throw new D("systemctl enable failed.");O("\u2713 Service installed (starts on boot). Start it now: superclawd fda start");return}if(e==="uninstall"){if(!j.existsSync(Te))return O("The service isn't installed.");if(O(`This stops and disables superclawd-fda and deletes ${Te} (your data stays in ${N.home}).`),!await $r("Remove the service?",t))return O("Nothing changed.");if(await kt(["systemctl","disable","--now","superclawd-fda"]),await kt(["rm","-f",Te])!==0)throw new D(`Couldn't delete ${Te}.`);await kt(["systemctl","daemon-reload"]),O("\u2713 Service removed.");return}throw new D("Usage: superclawd fda service install|uninstall [--yes]",64)}var Ra="0.0.83";async function Rp(s){let[e,...t]=s;if(e!=="install")throw new D("Usage: superclawd fda browser install [--yes]",64);let n=se.join(N.home,"browser");if(O(["This installs the optional browser for agent tasks:",` 1. @playwright/mcp@${Ra} into ${n} (npm, no sudo)`," 2. Chromium for Playwright into ~/.cache/ms-playwright (no sudo)"," 3. Chromium's system libraries: playwright install-deps chromium, with sudo (apt)",`Then turn it on in the admin: Settings \u2192 General \u2192 Browser.
687
687
  `].join(`
688
- `)),!await $r("Install the browser?",t))return O("Nothing changed.");if(j.mkdirSync(n,{recursive:!0,mode:448}),j.existsSync(ie.join(n,"package.json"))||j.writeFileSync(ie.join(n,"package.json"),JSON.stringify({name:"fda-browser",private:!0},null,2)),await lt("npm",["i","--prefix",n,"--no-fund","--no-audit",`@playwright/mcp@${Ra}`])!==0)throw new D("npm couldn't install @playwright/mcp.");let i=JSON.parse(j.readFileSync(ie.join(n,"node_modules","playwright","package.json"),"utf8"));if(await lt("npx",["-y",`playwright@${i.version}`,"install","chromium"])!==0)throw new D("Couldn't download Chromium.");if(await kt(["npx","-y",`playwright@${i.version}`,"install-deps","chromium"])!==0)throw new D("Couldn't install Chromium's system libraries (sudo npx playwright install-deps chromium).");O("\u2713 Browser installed. Turn it on in the admin: Settings \u2192 General \u2192 Browser.")}async function vp(s){if(!j.existsSync(Te)){O("The service isn't installed; in the foreground the daemon logs to its terminal.");return}let e=s.includes("-f")||s.includes("--follow");process.exitCode=await lt("journalctl",["-u","superclawd-fda","--no-pager",...e?["-f"]:["-n","200"]])}async function xp(){let s=$e();if(!s){O("SuperClawd FDA is not running.");return}if(j.existsSync(Te)&&await lt("systemctl",["is-active","--quiet","superclawd-fda"])===0){let t=await lt("sudo",["systemctl","stop","superclawd-fda"]);if(t!==0)throw new D("Couldn't stop the service (sudo systemctl stop superclawd-fda).",t);O("Stopped.");return}process.kill(s,"SIGTERM");let e=Date.now()+45e3;for(;Date.now()<e;)if(await new Promise(t=>setTimeout(t,500)),$e()!==s){O("Stopped.");return}throw new D(`The daemon (pid ${s}) didn't stop within 45s.`,1)}function Ip(){let{db:s}=at();try{let e=new wt(s).initialPassword();if(!e){O("No one-time password is pending (it was changed, or the daemon hasn't started yet). Use `superclawd fda reset-password` if you're locked out.");return}O(`Admin user: admin
688
+ `)),!await $r("Install the browser?",t))return O("Nothing changed.");if(j.mkdirSync(n,{recursive:!0,mode:448}),j.existsSync(se.join(n,"package.json"))||j.writeFileSync(se.join(n,"package.json"),JSON.stringify({name:"fda-browser",private:!0},null,2)),await lt("npm",["i","--prefix",n,"--no-fund","--no-audit",`@playwright/mcp@${Ra}`])!==0)throw new D("npm couldn't install @playwright/mcp.");let i=JSON.parse(j.readFileSync(se.join(n,"node_modules","playwright","package.json"),"utf8"));if(await lt("npx",["-y",`playwright@${i.version}`,"install","chromium"])!==0)throw new D("Couldn't download Chromium.");if(await kt([process.execPath,se.join(n,"node_modules","playwright","cli.js"),"install-deps","chromium"])!==0)throw new D("Couldn't install Chromium's system libraries (sudo playwright install-deps chromium).");O("\u2713 Browser installed. Turn it on in the admin: Settings \u2192 General \u2192 Browser.")}async function vp(s){if(!j.existsSync(Te)){O("The service isn't installed; in the foreground the daemon logs to its terminal.");return}let e=s.includes("-f")||s.includes("--follow");process.exitCode=await lt("journalctl",["-u","superclawd-fda","--no-pager",...e?["-f"]:["-n","200"]])}async function xp(){let s=$e();if(!s){O("SuperClawd FDA is not running.");return}if(j.existsSync(Te)&&await lt("systemctl",["is-active","--quiet","superclawd-fda"])===0){let t=await lt("sudo",["systemctl","stop","superclawd-fda"]);if(t!==0)throw new D("Couldn't stop the service (sudo systemctl stop superclawd-fda).",t);O("Stopped.");return}process.kill(s,"SIGTERM");let e=Date.now()+45e3;for(;Date.now()<e;)if(await new Promise(t=>setTimeout(t,500)),$e()!==s){O("Stopped.");return}throw new D(`The daemon (pid ${s}) didn't stop within 45s.`,1)}function Ip(){let{db:s}=at();try{let e=new wt(s).initialPassword();if(!e){O("No one-time password is pending (it was changed, or the daemon hasn't started yet). Use `superclawd fda reset-password` if you're locked out.");return}O(`Admin user: admin
689
689
  One-time password: ${e}
690
690
 
691
691
  You'll be asked to change it at the first sign-in.`)}finally{s.close()}}async function Ap(){let{db:s}=at();try{let e=await new wt(s).resetPassword();s.run("INSERT INTO audit_log (actor, action, detail, ip, ts) VALUES ('cli', 'password_reset', NULL, NULL, ?)",Date.now()),O(`Admin user: ${e.username}
692
692
  One-time password: ${e.password}
693
693
 
694
- Every admin session was signed out.`)}finally{s.close()}}function Np(){let s=$e();if(!s){O("SuperClawd FDA is not running."),process.exitCode=3;return}let e=null;try{e=JSON.parse(j.readFileSync(on,"utf8"))}catch{}if(O(`SuperClawd FDA is running (pid ${s}).`),!e)return;let t=Math.round((Date.now()-e.startedAt)/6e4);O(` version ${e.version}`),O(` health ${e.state}`),O(` slack ${e.slackConnected?"connected":"not connected"}`);let{db:n}=at();try{let r=n.get("SELECT SUM(status = 'running') AS running, SUM(status = 'queued') AS queued FROM turns WHERE status IN ('running','queued')");O(` queue ${r?.running??0} running, ${r?.queued??0} queued`)}finally{n.close()}O(` uptime ${t} min`);for(let r of e.conditions)O(` [${r.level}] ${r.message}`)}function Ee(s,e,t){return new Promise(n=>{mp(s,e,{timeout:2e4,env:t??process.env},(r,i)=>{let o=r?typeof r.code=="number"?r.code:127:0;n({code:o,stdout:String(i??"")})})})}async function Cp(){if(We()==="off")throw new D((await vt()).message);let s=ot.homedir(),e=j.mkdtempSync(ie.join(N.requests,"sandbox-check-")),t=ie.join(e,"cwd"),n=ie.join(e,"work");for(let w of[t,n,ie.join(n,"tmp")])j.mkdirSync(w,{recursive:!0});let r=$e()??0,i=ie.join(s,".bashrc"),o=j.existsSync(i)?j.readFileSync(i,"utf8"):null,a=w=>`'${w.replace(/'/g,"'\\''")}'`,l=[["read the database",`head -c 1 ${a(N.db)}`],["read the key",`head -c 1 ${a(N.key)}`],["see FDA's files (db, key, machine)",`ls -A ${a(N.home)} | grep -qx -e fda.db -e fda.key -e fda.json -e uploads`],["list other worktrees",`ls -A ${a(N.worktrees)} | grep -q .`],["list Claude projects",`ls -A ${a(ie.join(s,".claude"))} | grep -q .`],["read the daemon's environment",r?`head -c 1 /proc/${r}/environ`:"false"],["signal the daemon",r?`kill -0 ${r}`:"false"],["start a process outside (systemd --user)","systemd-run --user --quiet true"],["read the system journal","journalctl -n 1 -q | grep -q ."],["write ~/.local/bin",`touch ${a(ie.join(s,".local","bin",".fda-probe"))}`]],u=[["write the working tree","echo ok > probe && test -s probe"],["write /tmp","echo ok > /tmp/probe && test -s /tmp/probe"],["run git","git --version"],["run node and the SuperClawd CLI","node -e 0 && superclawd --version"],["resolve DNS","getent hosts api.anthropic.com"],["append to ~/.bashrc (sandbox copy only)","echo '# probe' >> ~/.bashrc"]],d=[...l,...u].map(([,w],T)=>`( ${w} ) >/dev/null 2>&1; echo "${T}:$?"`).join(`
695
- `),h=await Rt("/bin/sh",["-c",d],{cwd:t,rw:[t,n],tmp:ie.join(n,"tmp")});j.rmSync(e,{recursive:!0,force:!0});let k=new Map(h.stdout.split(`
694
+ Every admin session was signed out.`)}finally{s.close()}}function Np(){let s=$e();if(!s){O("SuperClawd FDA is not running."),process.exitCode=3;return}let e=null;try{e=JSON.parse(j.readFileSync(on,"utf8"))}catch{}if(O(`SuperClawd FDA is running (pid ${s}).`),!e)return;let t=Math.round((Date.now()-e.startedAt)/6e4);O(` version ${e.version}`),O(` health ${e.state}`),O(` slack ${e.slackConnected?"connected":"not connected"}`);let{db:n}=at();try{let r=n.get("SELECT SUM(status = 'running') AS running, SUM(status = 'queued') AS queued FROM turns WHERE status IN ('running','queued')");O(` queue ${r?.running??0} running, ${r?.queued??0} queued`)}finally{n.close()}O(` uptime ${t} min`);for(let r of e.conditions)O(` [${r.level}] ${r.message}`)}function Ee(s,e,t){return new Promise(n=>{mp(s,e,{timeout:2e4,env:t??process.env},(r,i)=>{let o=r?typeof r.code=="number"?r.code:127:0;n({code:o,stdout:String(i??"")})})})}async function Cp(){if(We()==="off")throw new D((await vt()).message);let s=ot.homedir(),e=j.mkdtempSync(se.join(N.requests,"sandbox-check-")),t=se.join(e,"cwd"),n=se.join(e,"work");for(let w of[t,n,se.join(n,"tmp")])j.mkdirSync(w,{recursive:!0});let r=$e()??0,i=se.join(s,".bashrc"),o=j.existsSync(i)?j.readFileSync(i,"utf8"):null,a=w=>`'${w.replace(/'/g,"'\\''")}'`,l=[["read the database",`head -c 1 ${a(N.db)}`],["read the key",`head -c 1 ${a(N.key)}`],["see FDA's files (db, key, machine)",`ls -A ${a(N.home)} | grep -qx -e fda.db -e fda.key -e fda.json -e uploads`],["list other worktrees",`ls -A ${a(N.worktrees)} | grep -q .`],["list Claude projects",`ls -A ${a(se.join(s,".claude"))} | grep -q .`],["read the daemon's environment",r?`head -c 1 /proc/${r}/environ`:"false"],["signal the daemon",r?`kill -0 ${r}`:"false"],["start a process outside (systemd --user)","systemd-run --user --quiet true"],["read the system journal","journalctl -n 1 -q | grep -q ."],["write ~/.local/bin",`touch ${a(se.join(s,".local","bin",".fda-probe"))}`]],u=[["write the working tree","echo ok > probe && test -s probe"],["write /tmp","echo ok > /tmp/probe && test -s /tmp/probe"],["run git","git --version"],["run node and the SuperClawd CLI","node -e 0 && superclawd --version"],["resolve DNS","getent hosts api.anthropic.com"],["append to ~/.bashrc (sandbox copy only)","echo '# probe' >> ~/.bashrc"]],d=[...l,...u].map(([,w],T)=>`( ${w} ) >/dev/null 2>&1; echo "${T}:$?"`).join(`
695
+ `),h=await Rt("/bin/sh",["-c",d],{cwd:t,rw:[t,n],tmp:se.join(n,"tmp")});j.rmSync(e,{recursive:!0,force:!0});let k=new Map(h.stdout.split(`
696
696
  `).map(w=>/^(\d+):(\d+)$/.exec(w.trim())).filter(w=>!!w).map(w=>[Number(w[1]),Number(w[2])])),m=0;l.forEach(([w],T)=>{let f=k.has(T)&&k.get(T)!==0;f||m++,O(`${f?"\u2713 blocked":"\u2717 REACHABLE"} ${w}`)}),u.forEach(([w],T)=>{let f=k.get(l.length+T)===0;f||m++,O(`${f?"\u2713 works ":"\u2717 broken "} ${w}`)});let S=(j.existsSync(i)?j.readFileSync(i,"utf8"):null)!==o;S&&m++,O(`${S?"\u2717 PERSISTED":"\u2713 blocked"} changes to ~/.bashrc reaching the real file`),h.code!==0&&!k.size&&O(`sandbox error: ${h.stderr.trim().slice(0,300)}`),O(m?`
697
697
  ${m} problem(s).`:`
698
- The sandbox holds.`),m&&(process.exitCode=1)}async function Na(){return(await Ee("claude",["--version"],{...process.env,PATH:`${ie.join(ot.homedir(),".local","bin")}:${process.env.PATH??""}`})).code===0}async function Ca(){let s=It(),e=await Ee(s.cmd,[...s.baseArgs,"version","--json"]);try{return(JSON.parse(e.stdout).features??[]).includes(Wr)}catch{return!1}}function Pa(s,e){let t=e.allowlist();return(s.get("allowAnyChannel")||t.channels.size>0)&&(s.get("allowAnyone")||t.users.size+t.usergroups.size>0)}function La(s,e,t,n){let r=s.allowlistEntries().filter(i=>i.slack_id!==t).map(i=>({kind:i.kind,slackId:i.slack_id,label:i.label}));s.setAllowlist([...r,{kind:e,slackId:t,label:n}])}function et(s){let e=at();try{return s(e)}finally{e.db.close()}}async function Pp(){if(!process.stdin.isTTY)throw new D("The guide is interactive: run it in a terminal. For scripts, see superclawd fda doctor.",64);let s={input:process.stdin,output:process.stdout},e={io:s,nodeOk:()=>{let[t,n]=process.versions.node.split(".").map(Number);return t>22||t===22&&n>=13},isRoot:()=>process.getuid?.()===0,run:async(t,n)=>(await Ee(t,n)).code,interactive:lt,sandboxOk:async()=>{let t=await vt();return t.ok&&!(t.mode==="off"&&ot.platform()==="linux")},bwrapInstalled:async()=>(await Ee("bwrap",["--version"])).code===0,claudeInstalled:Na,cliHasFdaSupport:Ca,npmNeedsSudo:async()=>{let t=(await Ee("npm",["prefix","-g"])).stdout.trim();try{return j.accessSync(t,j.constants.W_OK),!1}catch{return!0}},gitIdentity:async()=>({name:(await Ee("git",["config","user.name"])).stdout.trim(),email:(await Ee("git",["config","user.email"])).stdout.trim()}),ghLoggedIn:async()=>(await Ee("gh",["auth","status"])).code===0,cliLoggedIn:()=>!!_t(),superclawdCmd:()=>{let t=It();return{cmd:t.cmd,args:t.baseArgs}},linked:()=>!!rn()?.workspaceSlug,setup:()=>xa(["--skip-workdir"]),setting:t=>et(({settings:n})=>n.get(t)),setSettings:t=>{et(({settings:n})=>n.set(t)),tt()},hasSecret:t=>et(({settings:n})=>!!n.getSecret(t)),setSecrets:t=>{et(({db:n,settings:r})=>n.tx(()=>{for(let[i,o]of Object.entries(t))o&&r.setSecret(i,o)})),tt()},getSecret:t=>et(({settings:n})=>n.getSecret(t)),checkWorkdir:t=>Pt(t),home:ot.homedir(),allowlistReady:()=>et(({settings:t,store:n})=>Pa(t,n)),allow:(t,n,r)=>{et(({store:i})=>La(i,t,n,r)),tt()},slackManifest:Gn,slack:async(t,n,r)=>{let i=r??et(({settings:o})=>o.getSecret("slackBotToken"));try{return await(await fetch(`https://slack.com/api/${t}`,{method:"POST",headers:{Authorization:`Bearer ${i??""}`},body:new URLSearchParams(n),signal:AbortSignal.timeout(15e3)})).json()}catch(o){return{ok:!1,error:o.message}}},serviceInstalled:()=>j.existsSync(Te),installService:()=>Aa(["install"]),running:()=>!!$e(),start:Ia,latestRequest:()=>et(({store:t})=>t.latestSlackRequest()??null),now:Date.now,sleep:t=>new Promise(n=>setTimeout(n,t)),finish:()=>["",`Admin panel: http://127.0.0.1:${yt} on this machine; from your laptop, e.g. with Tailscale:`,` sudo tailscale serve --bg --https=443 http://127.0.0.1:${yt}`,"First sign-in password: superclawd fda credentials"]};try{await ya(Sa(e),s,{finish:e.finish})}finally{qt(s.input)}}async function Lp(){let s=0,e=(u,d,h="")=>{u||s++,O(`${u?"\u2713":"\u2717"} ${d}${!u&&h?`
698
+ The sandbox holds.`),m&&(process.exitCode=1)}async function Na(){return(await Ee("claude",["--version"],{...process.env,PATH:`${se.join(ot.homedir(),".local","bin")}:${process.env.PATH??""}`})).code===0}async function Ca(){let s=It(),e=await Ee(s.cmd,[...s.baseArgs,"version","--json"]);try{return(JSON.parse(e.stdout).features??[]).includes(Wr)}catch{return!1}}function Pa(s,e){let t=e.allowlist();return(s.get("allowAnyChannel")||t.channels.size>0)&&(s.get("allowAnyone")||t.users.size+t.usergroups.size>0)}function La(s,e,t,n){let r=s.allowlistEntries().filter(i=>i.slack_id!==t).map(i=>({kind:i.kind,slackId:i.slack_id,label:i.label}));s.setAllowlist([...r,{kind:e,slackId:t,label:n}])}function et(s){let e=at();try{return s(e)}finally{e.db.close()}}async function Pp(){if(!process.stdin.isTTY)throw new D("The guide is interactive: run it in a terminal. For scripts, see superclawd fda doctor.",64);let s={input:process.stdin,output:process.stdout},e={io:s,nodeOk:()=>{let[t,n]=process.versions.node.split(".").map(Number);return t>22||t===22&&n>=13},isRoot:()=>process.getuid?.()===0,run:async(t,n)=>(await Ee(t,n)).code,interactive:lt,sandboxOk:async()=>{let t=await vt();return t.ok&&!(t.mode==="off"&&ot.platform()==="linux")},bwrapInstalled:async()=>(await Ee("bwrap",["--version"])).code===0,claudeInstalled:Na,cliHasFdaSupport:Ca,npmNeedsSudo:async()=>{let t=(await Ee("npm",["prefix","-g"])).stdout.trim();try{return j.accessSync(t,j.constants.W_OK),!1}catch{return!0}},gitIdentity:async()=>({name:(await Ee("git",["config","user.name"])).stdout.trim(),email:(await Ee("git",["config","user.email"])).stdout.trim()}),ghLoggedIn:async()=>(await Ee("gh",["auth","status"])).code===0,cliLoggedIn:()=>!!_t(),superclawdCmd:()=>{let t=It();return{cmd:t.cmd,args:t.baseArgs}},linked:()=>!!rn()?.workspaceSlug,setup:()=>xa(["--skip-workdir"]),setting:t=>et(({settings:n})=>n.get(t)),setSettings:t=>{et(({settings:n})=>n.set(t)),tt()},hasSecret:t=>et(({settings:n})=>!!n.getSecret(t)),setSecrets:t=>{et(({db:n,settings:r})=>n.tx(()=>{for(let[i,o]of Object.entries(t))o&&r.setSecret(i,o)})),tt()},getSecret:t=>et(({settings:n})=>n.getSecret(t)),checkWorkdir:t=>Pt(t),home:ot.homedir(),allowlistReady:()=>et(({settings:t,store:n})=>Pa(t,n)),allow:(t,n,r)=>{et(({store:i})=>La(i,t,n,r)),tt()},slackManifest:Gn,slack:async(t,n,r)=>{let i=r??et(({settings:o})=>o.getSecret("slackBotToken"));try{return await(await fetch(`https://slack.com/api/${t}`,{method:"POST",headers:{Authorization:`Bearer ${i??""}`},body:new URLSearchParams(n),signal:AbortSignal.timeout(15e3)})).json()}catch(o){return{ok:!1,error:o.message}}},serviceInstalled:()=>j.existsSync(Te),installService:()=>Aa(["install"]),running:()=>!!$e(),start:Ia,latestRequest:()=>et(({store:t})=>t.latestSlackRequest()??null),now:Date.now,sleep:t=>new Promise(n=>setTimeout(n,t)),finish:()=>["",`Admin panel: http://127.0.0.1:${yt} on this machine; from your laptop, e.g. with Tailscale:`,` sudo tailscale serve --bg --https=443 http://127.0.0.1:${yt}`,"First sign-in password: superclawd fda credentials"]};try{await ya(Sa(e),s,{finish:e.finish})}finally{qt(s.input)}}async function Lp(){let s=0,e=(u,d,h="")=>{u||s++,O(`${u?"\u2713":"\u2717"} ${d}${!u&&h?`
699
699
  ${h}`:""}`)},[t,n]=process.versions.node.split(".").map(Number);e(t>22||t===22&&n>=13,`Node.js ${process.versions.node}`,"Node.js 22.13 or newer is required."),e(process.getuid?.()!==0,"Not running as root","Run SuperClawd FDA as a regular user."),e((await Ee("git",["--version"])).code===0,"git","Install git."),e((await Ee("gh",["--version"])).code===0,"GitHub CLI (gh)","Install it: https://cli.github.com (then gh auth login)");{let u=await vt();e(u.ok&&!(u.mode==="off"&&ot.platform()==="linux"),u.mode==="bwrap"?"Sandbox for agent turns (bubblewrap)":u.message,u.ok?"Remove FDA_SANDBOX=off from ~/.superclawd/fda/env.":u.message)}let r=(await Ee("git",["config","user.name"])).stdout.trim(),i=(await Ee("git",["config","user.email"])).stdout.trim();e(!!r&&!!i,"git identity (commit author)",'Run: git config --global user.name "SuperClawd FDA" && git config --global user.email <email>'),e(await Na(),"Claude Code (claude)","Install it: npm i -g @anthropic-ai/claude-code"),e(await Ca(),"SuperClawd CLI with FDA support","Install or update it: npm i -g superclawd"),e(!!_t(),"SuperClawd CLI logged in","Run: superclawd login"),e(!!rn()?.workspaceSlug,"Machine linked to a workspace","Run: superclawd fda setup --workspace <slug>");let{db:o,settings:a,store:l}=at();try{let u=a.all();e(!!a.getSecret("anthropicApiKey"),"Anthropic API key","Run: superclawd fda config set-key"),e(!!a.getSecret("slackBotToken")&&!!a.getSecret("slackAppToken"),"Slack tokens","Run: superclawd fda config set-slack"),e(!!u.agentName,"Primary agent selected","Run: superclawd fda config set agentName=<agent>"),e(!!u.workdir&&j.existsSync(u.workdir),"Working directory exists","Run: superclawd fda config set workdir=<path>"),e(Pa(a,l),"Allowlist has a channel and a person","Run: superclawd fda config allow channel <C\u2026> / allow user <U\u2026>")}finally{o.close()}O(j.existsSync(Te)?"\u2713 Service installed (systemd)":"\u2013 Service not installed (optional): superclawd fda service install, or run: superclawd fda run"),s?(O(`
700
700
  ${s} check(s) need attention.`),process.exitCode=1):O(`
701
701
  All checks passed.`)}async function Op(s){process.stdout.on("error",n=>{throw n.code==="EPIPE"&&process.exit(0),n});let[e,...t]=s;switch(e){case"run":return Ar();case"status":return Np();case"stop":return xp();case"start":return Ia();case"logs":return vp(t);case"guide":return Pp();case"doctor":return Lp();case"sandbox-check":return Cp();case"setup":return xa(t);case"credentials":return Ip();case"reset-password":return Ap();case"service":return Aa(t);case"browser":return Rp(t);case"config":return bp(t);case"version":case"--version":return O(xe);case void 0:case"help":case"--help":case"-h":return O(Or);default:throw new D(`Unknown command: ${e}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@superclawd-ai/fda",
3
- "version": "0.1.24",
3
+ "version": "0.1.26",
4
4
  "private": false,
5
5
  "description": "SuperClawd FDA — a forward deployed agent for your team: a Slack-driven AI developer that runs on its own machine with your SuperClawd workspace.",
6
6
  "author": "SuperClawd <support@superclawd.com>",
@@ -96,7 +96,8 @@ main() {
96
96
  [ "$(id -u)" -ne 0 ] || die "Run this as your regular user (not root). It uses sudo only where needed."
97
97
  . /etc/os-release 2>/dev/null || true
98
98
  [ "${ID:-}" = "ubuntu" ] || warn "Tested on Ubuntu 24.04+; continuing on ${PRETTY_NAME:-an unknown system}."
99
- sudo -v || die "This needs sudo for system packages."
99
+ # Passwordless sudo first: sudo-rs (Ubuntu 26.04) asks for a password on `sudo -v` even with NOPASSWD.
100
+ sudo -n true 2>/dev/null || sudo -v || die "This needs sudo for system packages."
100
101
 
101
102
  step "System packages (git, curl, jq, bubblewrap, SSH server)"
102
103
  sudo apt-get update -qq