infra-kit 0.4.0 → 0.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. package/dist/chunk-6GWRKIZ7.js +2 -0
  2. package/dist/chunk-6GWRKIZ7.js.map +7 -0
  3. package/dist/{chunk-CHETBZ6M.js → chunk-D4UYDBN5.js} +2 -2
  4. package/dist/{chunk-CHETBZ6M.js.map → chunk-D4UYDBN5.js.map} +2 -2
  5. package/dist/chunk-HXBIE4JP.js +7 -0
  6. package/dist/{chunk-D7QNBU5A.js.map → chunk-HXBIE4JP.js.map} +3 -3
  7. package/dist/{chunk-PVMF4FH7.js → chunk-Q23IJP6D.js} +2 -2
  8. package/dist/chunk-Q4HKBLQG.js +7 -0
  9. package/dist/chunk-Q4HKBLQG.js.map +7 -0
  10. package/dist/chunk-X2POR5VH.js +422 -0
  11. package/dist/chunk-X2POR5VH.js.map +7 -0
  12. package/dist/{chunk-7POMR4QK.js → chunk-XJOY7J3G.js} +1 -1
  13. package/dist/chunk-XJOY7J3G.js.map +7 -0
  14. package/dist/chunk-Y3FV2BRI.js +2 -0
  15. package/dist/chunk-Y3FV2BRI.js.map +7 -0
  16. package/dist/cli.js +12 -12
  17. package/dist/cli.js.map +4 -4
  18. package/dist/dev-server.js +2 -2
  19. package/dist/dev-server.js.map +1 -1
  20. package/dist/{dev-wizard-run-YMH3L7GE.js → dev-wizard-run-FNMMYTDX.js} +2 -2
  21. package/dist/mcp.js +1 -1
  22. package/dist/mcp.js.map +4 -4
  23. package/dist/update-check.js +1 -1
  24. package/dist/update-check.js.map +1 -1
  25. package/package.json +11 -11
  26. package/readme.md +36 -33
  27. package/dist/chunk-7POMR4QK.js.map +0 -7
  28. package/dist/chunk-D7QNBU5A.js +0 -7
  29. package/dist/chunk-NKXTDKWV.js +0 -2
  30. package/dist/chunk-NKXTDKWV.js.map +0 -7
  31. package/dist/chunk-NS5KIBXS.js +0 -2
  32. package/dist/chunk-NS5KIBXS.js.map +0 -7
  33. package/dist/chunk-QRY6BS6K.js +0 -249
  34. package/dist/chunk-QRY6BS6K.js.map +0 -7
  35. package/dist/chunk-Z2SKAHP4.js +0 -6
  36. package/dist/chunk-Z2SKAHP4.js.map +0 -7
  37. /package/dist/{chunk-PVMF4FH7.js.map → chunk-Q23IJP6D.js.map} +0 -0
  38. /package/dist/{dev-wizard-run-YMH3L7GE.js.map → dev-wizard-run-FNMMYTDX.js.map} +0 -0
@@ -1,2 +1,2 @@
1
- import{f as C,i as f,k as g}from"./chunk-D7QNBU5A.js";import{J as u,K as b,L as y,N as k,X as x,da as v}from"./chunk-Z2SKAHP4.js";import{e as w}from"./chunk-CHETBZ6M.js";import"./chunk-QWVLAZ6N.js";import O from"@inquirer/checkbox";import _ from"@inquirer/confirm";import j,{Separator as I}from"@inquirer/select";import{loadDev as K}from"@slip-stream-kit/config/internal";import U from"node:path";import m from"node:process";var W=e=>e.split("/")[0],A=e=>new Map(e.apps.map(r=>[r.name,r])),D=e=>e.localCapable&&e.cloudCapable,N=(e,r,t)=>D(r)?t[`${e} ${r.path}`]??(r.cloudCapable?"cloud":"local"):r.localCapable?"local":"cloud",$=(e,r,t)=>{let a=new Set,o=new Map;for(let n of e){let s=r.get(W(n));for(let c of s?.backends??[]){o.set(c.packageName,c.ownerApp);for(let l of c.routes)N(n,l,t)==="local"&&a.add(c.packageName)}}return{localPkgs:a,ownerByPkg:o}},B=(e,r)=>{let t={},a=!1;for(let o of e?.backends??[]){let n=r.has(o.packageName);for(let s of o.routes){let c=s.localCapable&&n?"local":"cloud";t[s.path]=c,c==="cloud"&&(a=!0)}}return{overrides:t,anyCloud:a}},z=(e,r)=>{let t=A(r),a=e.targets.filter(p=>p.endsWith("/ui")),o=e.targets.filter(p=>p.endsWith("/api")),{localPkgs:n,ownerByPkg:s}=$(a,t,e.sources),c={},l=!1;for(let p of a){let{overrides:d,anyCloud:i}=B(t.get(W(p)),n);i&&(l=!0),c[p]=Object.keys(d).length>0?{proxy:d}:{}}for(let p of o)c[p]??={};for(let p of n){let d=s.get(p);d!=null&&(c[`${d}/api`]??={})}return{presetDef:{apps:c,cmux:e.cmux},anyCloudRoute:l,targetKeys:Object.keys(c).sort()}},M=(e,r)=>{let t=[`--target=${e.targetKeys.join(",")}`];return r.watch&&t.push("--watch"),r.cmux&&t.push("--cmux"),{flags:t.join(" ")}};var h={output:m.stderr,clearPromptOnDone:!0},q={select:e=>g(r=>j({message:e.message,choices:e.choices,default:e.default},r),h),checkbox:e=>g(r=>O({message:e.message,choices:e.choices},r),h),confirm:e=>g(r=>_({message:e.message,default:e.default},r),h)},E=(e,r)=>{let t=new Map;for(let[a,o]of Object.entries(e)){let n={path:a,localCapable:o.from.includes("local"),cloudCapable:o.from.includes("cloud"),default:o.default},s=t.get(o.packageName);s?(s.routes.push(n),s.localCapable=s.localCapable||n.localCapable,s.cloudCapable=s.cloudCapable||n.cloudCapable):t.set(o.packageName,{packageName:o.packageName,routes:[n],localCapable:n.localCapable,cloudCapable:n.cloudCapable,ownerApp:r.get(o.packageName)})}return[...t.values()]},T=async(e,r,t)=>{try{let a=(await K(e))?.proxy?.routes;return a?E(a,t):[]}catch(a){return u.warn(`\u26A0\uFE0F Skipping ${r}: its dev config failed to load \u2014 ${a instanceof Error?a.message:String(a)}`),[]}},V=async e=>{let r=y(e),t=k(e),a=await v(),o=new Map(r.map(i=>[i.packageName,i.name])),n=new Map(r.map(i=>[i.name,i.packageName])),s=new Set(t.map(i=>i.name)),c=new Set(r.map(i=>i.name)),l=[...new Set([...c,...s])].sort(),p=await Promise.all(l.map(async i=>{let P=s.has(i);return{name:i,hasApi:c.has(i),hasUi:P,apiPackage:n.get(i),backends:P?await T(U.join(e,"apps",i,"ui"),i,o):[]}})),d=await C();return{apps:p,presets:Object.keys(a.devServersPresets??{}),environments:Object.keys(d?.envs??{}).sort()}},F=e=>{let r=[];for(let t of e.apps){if(t.hasUi){let a=t.backends.flatMap(n=>n.routes.map(s=>s.path)),o=a.length>0?`frontend \u2014 proxies ${[...a].sort().join(", ")}`:"frontend";r.push({name:`${t.name}/ui`,value:`${t.name}/ui`,description:o})}t.hasApi&&!t.hasUi&&r.push({name:`${t.name}/api`,value:`${t.name}/api`,description:"backend"})}return r},L=e=>{let r={api:e.apps.filter(o=>o.hasApi).map(o=>o.name),ui:e.apps.filter(o=>o.hasUi).map(o=>o.name)},t={},a=new Map;for(let o of e.apps){o.apiPackage!=null&&(t[o.name]=o.apiPackage);for(let n of o.backends)for(let s of n.routes)a.set(`${o.name} ${s.path}`,n.packageName)}return{discovered:r,apiPkgByApp:t,routePkg:(o,n)=>a.get(`${o} ${n}`)}},X=(e,r)=>x({__wizard__:e},L(r)).map(t=>t.message),Z="INFRA_KIT_DEV_WIZARD_CMUX",H=()=>m.env[Z]==="1",G=async(e,r,t)=>{let a=new Map(r.apps.map(n=>[n.name,n])),o={};for(let n of t){let s=a.get(n.split("/")[0]);for(let c of s?.backends??[])for(let l of c.routes){if(!l.localCapable||!l.cloudCapable)continue;let p=await e.select({message:`\u{1F500} ${n} ${l.path} \u2192 local or cloud?`,choices:[{name:"local (this machine)",value:"local"},{name:"cloud",value:"cloud"}],default:l.default??"local"});o[`${n} ${l.path}`]=p}}return o},J=(e,r,t)=>e.apps.find(a=>a.name===r)?.backends.flatMap(a=>a.routes).find(a=>a.path===t),Q=(e,r)=>{let t=[];for(let[a,o]of Object.entries(e.presetDef.apps??{})){if(!a.endsWith("/ui")||!o.proxy)continue;let n=a.split("/")[0];for(let[s,c]of Object.entries(o.proxy)){let l=J(r,n,s),p=l&&l.localCapable&&l.cloudCapable?"":" (only option)";t.push(` ${a} ${s} \u2192 ${c}${p}`)}}if(t.length!==0){u.info("Resolved routes:");for(let a of[...t].sort())u.info(a)}},R=async(e,r)=>{if(r.apps.length===0)return u.warn("No apps discovered to run."),null;let t=await e.checkbox({message:"\u{1F4E6} Which packages?",choices:F(r)});if(t.length===0)return u.warn("No packages selected."),null;let a=t.filter(i=>i.endsWith("/ui")),o=await G(e,r,a),n=await e.confirm({message:"\u{1F440} Rebuild & restart on save (watch)?",default:!1}),s=H()?await e.confirm({message:"\u{1F9E9} Run each app in its own cmux pane?",default:!1}):!1,c={targets:t,sources:o,watch:n,cmux:s},l=z(c,r);if(l.anyCloudRoute){if(r.environments.length===0)return u.warn("\u2601\uFE0F This selection routes to a cloud environment, but no environment token is loaded."),u.warn(" Run `infra-kit env-load` to add one, then re-run the wizard."),null;c.env=await e.select({message:"\u2601\uFE0F Point cloud routes at which environment?",choices:r.environments.map(i=>({name:i,value:i}))}),m.env[w]=c.env}let p=X(l.presetDef,r);if(p.length>0){u.warn("\u26A0\uFE0F Proxy audit found issues with this selection:");for(let i of p)u.warn(` \u2022 ${i}`);return null}Q(l,r),Y(l,c);let d=l.targetKeys.filter(i=>i.endsWith("/api")).map(i=>i.split("/")[0]);return s&&d.length>0?{include:d,presetDef:l.presetDef,watch:n,cmux:!0}:(s&&u.info("\u2139\uFE0F cmux needs at least one local backend (panes are backend-only) \u2014 running in-process instead."),{presetDef:l.presetDef,watch:n,cmux:!1})},Y=(e,r)=>{f.setInteractive();let t=M(e,r);f.addOption(t.flags,!0),f.print(),u.info("\u2139\uFE0F Prefer a short name? Save this selection as a devPreset and run `infra-kit dev <name>`.")},ee=async(e,r)=>{let t=await e.confirm({message:"\u{1F440} Rebuild & restart on save (watch)?",default:!1});return f.setInteractive(),f.addOption(r,!0),t&&f.addOption("--watch",!0),f.print(),{preset:r,watch:t,cmux:!1}},S=" manual",re=async(e,r)=>{if(r.presets.length===0)return R(e,r);let t=await e.select({message:"\u{1F680} Start from a preset, or configure manually?",choices:[...r.presets.map(a=>({name:a,value:a})),new I(" "),{name:"Manual (custom)\u2026",value:S}]});return t===S?R(e,r):ee(e,t)},be=async(e=q,r=b(m.cwd()))=>re(e,await V(r));export{S as MANUAL_CHOICE,Z as WIZARD_CMUX_VAR,X as auditManualPlan,q as defaultPrompts,V as gatherWizardModel,T as loadBackends,be as runDevWizard,re as runWizardFlow};
2
- //# sourceMappingURL=dev-wizard-run-YMH3L7GE.js.map
1
+ import{f as C,i as f,k as g}from"./chunk-HXBIE4JP.js";import{L as u,M as b,N as y,P as k,Z as x,fa as v}from"./chunk-Q4HKBLQG.js";import{e as w}from"./chunk-D4UYDBN5.js";import"./chunk-QWVLAZ6N.js";import O from"@inquirer/checkbox";import _ from"@inquirer/confirm";import j,{Separator as I}from"@inquirer/select";import{loadDev as K}from"@slip-stream-kit/config/internal";import U from"node:path";import m from"node:process";var W=e=>e.split("/")[0],A=e=>new Map(e.apps.map(r=>[r.name,r])),D=e=>e.localCapable&&e.cloudCapable,N=(e,r,t)=>D(r)?t[`${e} ${r.path}`]??(r.cloudCapable?"cloud":"local"):r.localCapable?"local":"cloud",$=(e,r,t)=>{let a=new Set,o=new Map;for(let n of e){let s=r.get(W(n));for(let c of s?.backends??[]){o.set(c.packageName,c.ownerApp);for(let l of c.routes)N(n,l,t)==="local"&&a.add(c.packageName)}}return{localPkgs:a,ownerByPkg:o}},B=(e,r)=>{let t={},a=!1;for(let o of e?.backends??[]){let n=r.has(o.packageName);for(let s of o.routes){let c=s.localCapable&&n?"local":"cloud";t[s.path]=c,c==="cloud"&&(a=!0)}}return{overrides:t,anyCloud:a}},z=(e,r)=>{let t=A(r),a=e.targets.filter(p=>p.endsWith("/ui")),o=e.targets.filter(p=>p.endsWith("/api")),{localPkgs:n,ownerByPkg:s}=$(a,t,e.sources),c={},l=!1;for(let p of a){let{overrides:d,anyCloud:i}=B(t.get(W(p)),n);i&&(l=!0),c[p]=Object.keys(d).length>0?{proxy:d}:{}}for(let p of o)c[p]??={};for(let p of n){let d=s.get(p);d!=null&&(c[`${d}/api`]??={})}return{presetDef:{apps:c,cmux:e.cmux},anyCloudRoute:l,targetKeys:Object.keys(c).sort()}},M=(e,r)=>{let t=[`--target=${e.targetKeys.join(",")}`];return r.watch&&t.push("--watch"),r.cmux&&t.push("--cmux"),{flags:t.join(" ")}};var h={output:m.stderr,clearPromptOnDone:!0},q={select:e=>g(r=>j({message:e.message,choices:e.choices,default:e.default},r),h),checkbox:e=>g(r=>O({message:e.message,choices:e.choices},r),h),confirm:e=>g(r=>_({message:e.message,default:e.default},r),h)},E=(e,r)=>{let t=new Map;for(let[a,o]of Object.entries(e)){let n={path:a,localCapable:o.from.includes("local"),cloudCapable:o.from.includes("cloud"),default:o.default},s=t.get(o.packageName);s?(s.routes.push(n),s.localCapable=s.localCapable||n.localCapable,s.cloudCapable=s.cloudCapable||n.cloudCapable):t.set(o.packageName,{packageName:o.packageName,routes:[n],localCapable:n.localCapable,cloudCapable:n.cloudCapable,ownerApp:r.get(o.packageName)})}return[...t.values()]},T=async(e,r,t)=>{try{let a=(await K(e))?.proxy?.routes;return a?E(a,t):[]}catch(a){return u.warn(`\u26A0\uFE0F Skipping ${r}: its dev config failed to load \u2014 ${a instanceof Error?a.message:String(a)}`),[]}},V=async e=>{let r=y(e),t=k(e),a=await v(),o=new Map(r.map(i=>[i.packageName,i.name])),n=new Map(r.map(i=>[i.name,i.packageName])),s=new Set(t.map(i=>i.name)),c=new Set(r.map(i=>i.name)),l=[...new Set([...c,...s])].sort(),p=await Promise.all(l.map(async i=>{let P=s.has(i);return{name:i,hasApi:c.has(i),hasUi:P,apiPackage:n.get(i),backends:P?await T(U.join(e,"apps",i,"ui"),i,o):[]}})),d=await C();return{apps:p,presets:Object.keys(a.devServersPresets??{}),environments:Object.keys(d?.envs??{}).sort()}},F=e=>{let r=[];for(let t of e.apps){if(t.hasUi){let a=t.backends.flatMap(n=>n.routes.map(s=>s.path)),o=a.length>0?`frontend \u2014 proxies ${[...a].sort().join(", ")}`:"frontend";r.push({name:`${t.name}/ui`,value:`${t.name}/ui`,description:o})}t.hasApi&&!t.hasUi&&r.push({name:`${t.name}/api`,value:`${t.name}/api`,description:"backend"})}return r},L=e=>{let r={api:e.apps.filter(o=>o.hasApi).map(o=>o.name),ui:e.apps.filter(o=>o.hasUi).map(o=>o.name)},t={},a=new Map;for(let o of e.apps){o.apiPackage!=null&&(t[o.name]=o.apiPackage);for(let n of o.backends)for(let s of n.routes)a.set(`${o.name} ${s.path}`,n.packageName)}return{discovered:r,apiPkgByApp:t,routePkg:(o,n)=>a.get(`${o} ${n}`)}},X=(e,r)=>x({__wizard__:e},L(r)).map(t=>t.message),Z="INFRA_KIT_DEV_WIZARD_CMUX",H=()=>m.env[Z]==="1",G=async(e,r,t)=>{let a=new Map(r.apps.map(n=>[n.name,n])),o={};for(let n of t){let s=a.get(n.split("/")[0]);for(let c of s?.backends??[])for(let l of c.routes){if(!l.localCapable||!l.cloudCapable)continue;let p=await e.select({message:`\u{1F500} ${n} ${l.path} \u2192 local or cloud?`,choices:[{name:"local (this machine)",value:"local"},{name:"cloud",value:"cloud"}],default:l.default??"local"});o[`${n} ${l.path}`]=p}}return o},J=(e,r,t)=>e.apps.find(a=>a.name===r)?.backends.flatMap(a=>a.routes).find(a=>a.path===t),Q=(e,r)=>{let t=[];for(let[a,o]of Object.entries(e.presetDef.apps??{})){if(!a.endsWith("/ui")||!o.proxy)continue;let n=a.split("/")[0];for(let[s,c]of Object.entries(o.proxy)){let l=J(r,n,s),p=l&&l.localCapable&&l.cloudCapable?"":" (only option)";t.push(` ${a} ${s} \u2192 ${c}${p}`)}}if(t.length!==0){u.info("Resolved routes:");for(let a of[...t].sort())u.info(a)}},R=async(e,r)=>{if(r.apps.length===0)return u.warn("No apps discovered to run."),null;let t=await e.checkbox({message:"\u{1F4E6} Which packages?",choices:F(r)});if(t.length===0)return u.warn("No packages selected."),null;let a=t.filter(i=>i.endsWith("/ui")),o=await G(e,r,a),n=await e.confirm({message:"\u{1F440} Rebuild & restart on save (watch)?",default:!1}),s=H()?await e.confirm({message:"\u{1F9E9} Run each app in its own cmux pane?",default:!1}):!1,c={targets:t,sources:o,watch:n,cmux:s},l=z(c,r);if(l.anyCloudRoute){if(r.environments.length===0)return u.warn("\u2601\uFE0F This selection routes to a cloud environment, but no environment token is loaded."),u.warn(" Run `infra-kit env-load` to add one, then re-run the wizard."),null;c.env=await e.select({message:"\u2601\uFE0F Point cloud routes at which environment?",choices:r.environments.map(i=>({name:i,value:i}))}),m.env[w]=c.env}let p=X(l.presetDef,r);if(p.length>0){u.warn("\u26A0\uFE0F Proxy audit found issues with this selection:");for(let i of p)u.warn(` \u2022 ${i}`);return null}Q(l,r),Y(l,c);let d=l.targetKeys.filter(i=>i.endsWith("/api")).map(i=>i.split("/")[0]);return s&&d.length>0?{include:d,presetDef:l.presetDef,watch:n,cmux:!0}:(s&&u.info("\u2139\uFE0F cmux needs at least one local backend (panes are backend-only) \u2014 running in-process instead."),{presetDef:l.presetDef,watch:n,cmux:!1})},Y=(e,r)=>{f.setInteractive();let t=M(e,r);f.addOption(t.flags,!0),f.print(),u.info("\u2139\uFE0F Prefer a short name? Save this selection as a devPreset and run `infra-kit dev <name>`.")},ee=async(e,r)=>{let t=await e.confirm({message:"\u{1F440} Rebuild & restart on save (watch)?",default:!1});return f.setInteractive(),f.addOption(r,!0),t&&f.addOption("--watch",!0),f.print(),{preset:r,watch:t,cmux:!1}},S=" manual",re=async(e,r)=>{if(r.presets.length===0)return R(e,r);let t=await e.select({message:"\u{1F680} Start from a preset, or configure manually?",choices:[...r.presets.map(a=>({name:a,value:a})),new I(" "),{name:"Manual (custom)\u2026",value:S}]});return t===S?R(e,r):ee(e,t)},be=async(e=q,r=b(m.cwd()))=>re(e,await V(r));export{S as MANUAL_CHOICE,Z as WIZARD_CMUX_VAR,X as auditManualPlan,q as defaultPrompts,V as gatherWizardModel,T as loadBackends,be as runDevWizard,re as runWizardFlow};
2
+ //# sourceMappingURL=dev-wizard-run-FNMMYTDX.js.map
package/dist/mcp.js CHANGED
@@ -1,2 +1,2 @@
1
- import{a as w,d as p,ea as E,ha as P,r as b}from"./chunk-QRY6BS6K.js";import"./chunk-PVMF4FH7.js";import{a as S}from"./chunk-NS5KIBXS.js";import"./chunk-7POMR4QK.js";import{i as v}from"./chunk-D7QNBU5A.js";import{H as l,I as T,J as a,c as k,da as R}from"./chunk-Z2SKAHP4.js";import"./chunk-CHETBZ6M.js";import"./chunk-QWVLAZ6N.js";import{serveStdio as ce}from"@modelcontextprotocol/server/stdio";import u from"node:process";import{setTimeout as fe}from"node:timers/promises";import m from"node:process";var j=e=>{m.on("uncaughtException",o=>{e.fatal({err:o,msg:"Uncaught Exception"}),e.error(`Uncaught Exception! Check ${l}. Shutting down...`),e.flush(),m.exit(1)}),m.on("unhandledRejection",(o,r)=>{e.fatal({reason:o,promise:r,msg:"Unhandled Rejection"}),e.error(`Unhandled Rejection! Check ${l}. Shutting down...`),e.flush(),m.exit(1)})};import{McpServer as ae}from"@modelcontextprotocol/server";var D=async e=>{};var L="infra-kit://config",q="infra-kit://dev-context",B={loadConfig:R,readDevContext:()=>p()},C=(e,o)=>({contents:[{uri:e,mimeType:"application/json",text:JSON.stringify(o,null,2)}]}),O=async(e,o=B)=>{e.registerResource("infra-kit-config",L,{title:"infra-kit config",description:"The merged infra-kit.json configuration (all override layers applied). Read-only.",mimeType:"application/json"},async r=>{try{return C(r.toString(),await o.loadConfig())}catch(t){return C(r.toString(),{error:t instanceof Error?t.message:String(t)})}}),e.registerResource("infra-kit-dev-context",q,{title:"infra-kit dev context",description:"What `infra-kit dev` last wrote: backends currently up, their ports/origins, and fragment freshness. Read-only and advisory (no liveness probe). No active session resolves to an empty payload.",mimeType:"application/json"},r=>C(r.toString(),o.readDevContext()))};import{z as h}from"zod";import{createRequestStateCodec as J}from"@modelcontextprotocol/server";import{randomBytes as V}from"node:crypto";var W=new Set(["confirm","confirmToken"]),X=600,M=e=>({toolName:e}),g=e=>{if(Array.isArray(e))return e.map(g);if(e!==null&&typeof e=="object"){let o=e;return Object.fromEntries(Object.keys(o).sort().map(r=>[r,g(o[r])]))}return e},y=e=>JSON.stringify(g(e)),f=e=>typeof e!="object"||e===null?{}:Object.fromEntries(Object.entries(e).filter(([o])=>!W.has(o))),_=(e={})=>{let o=J({key:e.key??V(32),ttlSeconds:e.ttlSeconds??X,bind:r=>r.toolName});return{mint:(r,t)=>o.mint(r,M(t)),verify:(r,t)=>o.verify(r,M(t))}},I,G=()=>(I??=_(),I),Z=e=>{if(typeof e!="object"||e===null)return;let{confirmToken:o}=e;return typeof o=="string"?o:void 0},Y=new Set(["malformed","mac","expired","bind"]),Q=e=>{let o=e instanceof Error?e.message:"";return Y.has(o)?o:"malformed"},H=async(e,o,r)=>e.mint({args:y(f(r))},o),$=async(e,o,r)=>{let t=Z(r);if(t===void 0)return{ok:!1,reason:"absent"};let s;try{s=await e.verify(t,o)}catch(i){return{ok:!1,reason:Q(i)}}return s.args!==y(f(r))?{ok:!1,reason:"mismatch"}:{ok:!0}};var ee=e=>typeof e=="object"&&e!==null&&e.confirm===!0,oe=(e,o)=>e===!0?ee(o)?"verify":"gate":"run",A=e=>({content:w(JSON.stringify(e,null,2)),structuredContent:e,isError:!0}),re=async(e,o,r)=>{let t=f(r),s=await H(e,o,r);return A({status:"confirmation_required",tool:o,resolvedArgs:t,confirmToken:s,message:`${o} mutates external state and is gated. It was NOT executed. Re-call ${o} with the same arguments plus "confirm": true and this "confirmToken" to execute.`})},te={absent:'no "confirmToken" was supplied',malformed:'the "confirmToken" is malformed',mac:'the "confirmToken" was not issued by this server',expired:'the "confirmToken" has expired',bind:'the "confirmToken" was issued for a different tool',mismatch:'the arguments differ from the ones the "confirmToken" was issued for'},ne=(e,o)=>A({status:"confirmation_refused",tool:e,reason:o,message:`${e} was NOT executed: ${te[o]}. Call ${e} again WITHOUT "confirm" to receive a fresh gate, then re-call with the same arguments plus "confirm": true and the returned "confirmToken".`}),x=({toolName:e,handler:o,requiresHumanConfirm:r,confirmCodec:t})=>{let s=t??G();return async i=>{a.info({msg:`Tool execution started: ${e}`,params:i});try{await b(),v.reset();let c=oe(r,i);if(c==="gate")return a.info({msg:`Tool execution gated (awaiting confirm): ${e}`}),await re(s,e,i);if(c==="verify"){let d=await $(s,e,i);if(!d.ok)return a.info({msg:`Tool execution refused (${d.reason}): ${e}`}),ne(e,d.reason)}let F=await o({...i,confirmedCommand:!0});return a.info({msg:`Tool execution successful: ${e}`}),F}catch(c){throw a.error({err:c,params:i,msg:`Tool execution failed: ${e}`}),c}}};var U=async e=>{for(let o of E())e.registerTool(o.name,{title:o.title,description:o.description,inputSchema:h.object(o.requiresHumanConfirm===!0?ie(o.inputSchema):o.inputSchema),outputSchema:h.object(o.outputSchema),annotations:o.annotations},se(x({toolName:o.name,handler:o.handler,requiresHumanConfirm:o.requiresHumanConfirm})))},ie=e=>({...e,confirmToken:h.string().optional().describe("Round-2 only: the token returned by the round-1 gate, proving the arguments are unchanged.")}),se=e=>(o,r)=>e(o,r);async function N(){k.enabled=!0;let e=new ae({name:"infra-kit",version:S.version},{capabilities:{resources:{listChanged:!0},tools:{},prompts:{}}});return await D(e),await O(e),await U(e),e}P();var n=T(),me=async()=>{try{return await N()}catch(e){n.error({err:e,msg:"Failed to create MCP server"}),n.flush(),u.exit(1)}};j(n);var ue=ce(me,{onerror:e=>{n.error({err:e,msg:"MCP stdio entry error"}),n.flush()}});n.info({msg:"MCP stdio entry started."});var z=!1,K=async e=>{if(!z){z=!0,n.info({msg:`Received ${e}. Shutting down...`});try{await Promise.race([ue.close(),fe(1500)])}catch(o){n.error({err:o,msg:"MCP stdio close failed during shutdown"})}n.flush(),u.exit(0)}};u.on("SIGINT",()=>{K("SIGINT")});u.on("SIGTERM",()=>{K("SIGTERM")});
1
+ import{a as E,d as w,fa as O,ia as N,s as I}from"./chunk-X2POR5VH.js";import"./chunk-Q23IJP6D.js";import{a as D}from"./chunk-6GWRKIZ7.js";import"./chunk-XJOY7J3G.js";import{i as _}from"./chunk-HXBIE4JP.js";import{J as g,K as A,L as i,c as S,fa as P}from"./chunk-Q4HKBLQG.js";import"./chunk-D4UYDBN5.js";import"./chunk-QWVLAZ6N.js";import{serveStdio as je}from"@modelcontextprotocol/server/stdio";import f from"node:process";import{setTimeout as Le}from"node:timers/promises";import h from"node:process";var F=e=>{h.on("uncaughtException",t=>{e.fatal({err:t,msg:"Uncaught Exception"}),e.error(`Uncaught Exception! Check ${g}. Shutting down...`),e.flush(),h.exit(1)}),h.on("unhandledRejection",(t,r)=>{e.fatal({reason:t,promise:r,msg:"Unhandled Rejection"}),e.error(`Unhandled Rejection! Check ${g}. Shutting down...`),e.flush(),h.exit(1)})};import{McpServer as qe}from"@modelcontextprotocol/server";var M='# release-create \u2014 cutting a release through infra-kit\n\nThe tool is `mcp__infra-kit__release-create`. Everything below is about calling that tool.\n\nDo not shell out. A `Bash` call running `git switch`, `git push` or `gh pr create` reproduces none\nof the preconditions below and bypasses the confirm gate in section 2 \u2014 which is the only place a\nhuman approves the release.\n\n## 1. Preconditions\n\nCheck these before the first call; each one is a refusal the human has to clear, not something to\nwork around.\n\n- **The main repository checkout, not a linked worktree.** The tool refuses outright from inside a\n linked worktree.\n- **A clean working tree.** Uncommitted changes are refused; the human commits or stashes.\n- **No other worktree holding the base branch.** Regular releases branch off `dev`, hotfixes off\n `main`. If a linked worktree has that branch checked out, the tool refuses and names the path.\n- **Jira configured.** Every release gets a matching fix version, so `JIRA_BASE_URL`,\n `JIRA_EMAIL`, `JIRA_PROJECT_ID` and `JIRA_TOKEN` (or `JIRA_API_TOKEN`) must be in the\n environment \u2014 load them with `ik env-load` and source the file it returns. The check runs before\n anything is cut.\n\n**You do not have to already be on the base branch.** The tool runs `git fetch origin`,\n`git switch <base>` and `git pull --ff-only` itself. That is a real side effect on the human\'s\ncheckout: say so before call 2.\n\n## 2. The two-call confirm protocol\n\n`release-create` is gated. **The first call never executes anything.**\n\n**Call 1** \u2014 send the real arguments, with no `confirm` and no `confirmToken`. The result is a gate\npayload, `{"status": "confirmation_required", ...}`, and it carries `"isError": true`.\n\n**That `isError` does not mean the call failed.** It is set because the payload is a gate rather\nthan the tool\'s declared output. Nothing was created, nothing was pushed, nothing was switched. An\nagent that reads it as a failure \u2014 and gives up, or retries, or falls back to `Bash` \u2014 has skipped\nthe human approval this protocol exists for. Do none of those.\n\nThe gate payload carries two things you need:\n\n- `resolvedArgs` \u2014 exactly the arguments the server bound. **Show these to the human.** This is the\n approval moment; there is no other one.\n- `confirmToken` \u2014 an HMAC bound to the tool name and to those exact arguments. It expires 600\n seconds after it is minted.\n\n**Call 2** \u2014 repeat the **same arguments**, unchanged, plus `"confirm": true` and the\n`confirmToken` from call 1.\n\nChange any argument between the two calls and round 2 comes back as\n`{"status": "confirmation_refused", "reason": "mismatch"}`. That is terminal \u2014 it is not a second\ngate. The other reasons are `absent`, `malformed`, `mac`, `expired` and `bind`, and every one of\nthem recovers the same way: call again **without** `confirm` to mint a fresh gate, then re-call\nwith those arguments and the new token. Never retry call 2 with the old token.\n\nIf the human wants different arguments, go back to call 1 with the new arguments. Do not edit the\narguments and reuse the token \u2014 that is exactly what `mismatch` refuses.\n\n## 3. What goes in `releases`\n\n`releases` is an array with at least one entry. Every entry carries **exactly one** of:\n\n- `version` \u2014 a semver string such as `"1.64.0"`, or the literal token `"next"`.\n- `name` \u2014 a free-form kebab-case identifier such as `"checkout-redesign"`.\n\nThey are mutually exclusive and one is required. An entry with both, or with neither, is rejected\nby the schema before the tool runs.\n\nEach entry also carries `type` (`"regular"` or `"hotfix"`, default `"regular"`) and an optional\n`description`, which becomes the Jira fix version\'s description and feeds the PR body.\n\n### The `"next"` token\n\n`"next"` is version-only \u2014 a named release never auto-bumps. It resolves against the union of the\nremote `release/v*` branches on `origin` and the project\'s Jira fix versions:\n\n- `"regular"` bumps the minor and resets the patch: `1.63.2` becomes `1.64.0`.\n- `"hotfix"` bumps the patch on the highest minor: `1.63.2` becomes `1.63.3`.\n\nSeveral `"next"` entries in one call advance sequentially rather than all resolving to the same\nversion.\n\n**Be honest about what `"next"` could see.** The two sources are queried in parallel and a source\nthat fails is logged and dropped, not raised \u2014 if the Jira call fails, `"next"` is computed from\nthe remote branches alone and can land on a version Jira already knows about. If neither source\nyields a prior version the tool refuses and asks for an explicit one. When the exact number\nmatters, pass the semver instead of the token.\n\n### Reading `$ARGUMENTS`\n\nThe `/infra-kit:release-create` command hands you `$ARGUMENTS` verbatim, and its argument hint is\n`[--hotfix] [--desc <text>] [<version|name>]`. **Those two flags are conventions of this command, not\nCLI flags** \u2014 `infra-kit release create` accepts neither, and the tool takes neither. They exist so a\nhuman can type the whole request on one line, and it is your job to translate them:\n\n- `--hotfix` \u2192 `type: "hotfix"` on every entry you build. Its absence means `"regular"`.\n- `--desc <text>` \u2192 `description` on the entry. The text runs to the end of the argument string.\n- The bare token \u2192 `version` when it is a semver or the literal `next`, `name` when it is kebab-case.\n\nSo `--hotfix --desc "Card expiry fix" 1.63.3` is one entry:\n`{version: "1.63.3", type: "hotfix", description: "Card expiry fix"}`.\n\nIf `$ARGUMENTS` is empty, ask the human what to cut rather than guessing a version \u2014 and read the\n`"next"` caveats above before offering it.\n\n**Precedence, when a form is also involved.** If the server answers with an argument form and the\nhuman edits it, **the form wins field by field wherever the human supplied a value, and the values\nyou parsed from `$ARGUMENTS` win everywhere else.** A human who typed `--hotfix` and then picked\n`regular` in the form gets `regular` \u2014 they saw the field and changed it. A human who typed\n`--hotfix` and left `type` untouched gets `hotfix`. Never rebuild the entry from the form alone: that\nconverts every untouched field into a silent overwrite by a value the human never saw.\n\n### Batches\n\nOne call may create several releases, but **all entries must share the same `type`**. Regular and\nhotfix branch off different bases, so a mixed batch is rejected \u2014 cut them in separate\ninvocations.\n\nA batch does not stop at the first failure. Each entry is attempted and the result reports\n`successCount`, `failureCount`, `createdBranches` and `failedReleases`. Read all four before\ntelling the human the release was created: partial success is a normal outcome here.\n\n## 4. What one release does\n\nPer entry, in order: fetch and switch to the base branch, cut `release/v<semver>` (or\n`release/<name>`), open a GitHub release PR, and create or reuse the Jira fix version (`v<semver>`\nor `<name>`).\n\nTwo consequences worth stating before call 2:\n\n- An existing fix version is **reused**, and a `description` that differs is written through to it,\n so the PR and the fix version cannot disagree.\n- A fix version that is already released or archived is **refused**, not reused. The human either\n picks a different version or un-releases it in Jira.\n\n## 5. What not to do\n\n- Do not work around a refusal with `git` or `gh`. Every refusal in section 1 is a state only the\n human can clear.\n- Do not invent a list of candidate versions for the human. Pass `"next"` and let the server\n resolve it from the real branches and fix versions, or ask for the exact semver.\n- Do not read `isError: true` on a `confirmation_required` payload as a failure. See section 2.\n';var q='# setup \u2014 bringing a machine to a working infra-kit\n\nThe tool is `mcp__infra-kit__setup`. Everything below is about calling that tool.\n\nThe same code runs behind `infra-kit setup` in a terminal, so this body describes both spellings: the\nCLI flag first, the tool field it sets second. They are one implementation, not two.\n\nDo not shell out. A `Bash` call running `brew install`, `curl \u2026 | bash` or the writers in section 1\nreproduces none of the refusals in section 3 and bypasses the confirm gate in section 4 \u2014 which is the\nonly place a human approves an install.\n\n**If all you want is to know what this machine looks like, call `doctor` instead.** It reports the same\nfive tools plus the rest of the setup, mutates nothing, and raises no confirmation prompt. `infra-kit setup` is\nthe write path; `doctor` is the read path, and they are separate tools precisely so that asking a\nquestion does not cost an approval.\n\n## 1. What one call does, in order\n\nTwo halves, and **both always run**. Neither short-circuits the other: an init-half failure is recorded\nand the dependency half still runs.\n\n### Step 1 \u2014 the init half: local, offline, additive, near-instant\n\nIn this order:\n\n1. the managed block in `~/.zshrc` \u2014 the shell integration\n2. the four config migrations, in their recorded order\n3. the user-global config seed\n4. the repo\'s agent-instruction files (`CLAUDE.md` guidance blocks) \u2014 **non-fatal**; a repo it cannot\n resolve is warned about, not failed on\n5. the git-root resolution for writes, warning when the two root gates disagree\n6. the Claude Code plugin pointer \u2014 `.claude/settings.json`, `.mcp.json`, and the plugin install\n7. the per-project config reseed\n8. a warning when `$SHELL` is not zsh\n\nEvery writer here is additive and never overwrites. Nothing in this half installs software and nothing\nreaches the network.\n\n**It runs first deliberately.** Step 1.6 is what makes the MCP surface usable at all, so it must not sit\nbehind a network converge that can be slow or fail.\n\n### Step 2 \u2014 the dependency converge\n\nFive tools, serially, in registry order: **brew, aws, gh, doppler, portless**. Serial and ordered\nbecause the recipes have prerequisites \u2014 `gh` and `doppler` both need `brew`, and doppler\'s own two\nsteps (gnupg, then the tap) must not interleave with another tool\'s.\n\nPer tool: install it when it is absent, update it when it is present, skip it when its manager is not\none infra-kit manages. A recipe the risk predicate refuses is **printed, not run** \u2014 section 3.\n\n### Step 3 \u2014 one combined summary\n\nOne line per tool, then the exact argv for every refused recipe, then the `source ~/.zshrc` reminder\nlast of all.\n\n### How to read the result\n\n- `init` \u2014 one entry per step above, each with an `outcome` of `written`, `unchanged`, `skipped` or\n `warned`, and the same message a human would have read.\n- `tools` \u2014 one entry per requested dependency: `action` (`installed`, `updated`, `skipped`, `refused`\n or `failed`), the `before` state, the `commands` that were run or would have been, and a one-line\n `detail`.\n- `converged` \u2014 whether the dependency step could act at all. `false` under `skipTools`.\n- `changed` \u2014 whether anything was installed or updated.\n- `allSucceeded` \u2014 whether no tool **failed**. **A refusal is not a failure**, so this stays `true` when\n a recipe was printed instead of run.\n\nThe process exits non-zero when either half hard-failed. **Do not read a success as "everything is\ninstalled"** \u2014 read `tools[].action`, and tell the human about every `refused` entry.\n\n## 2. The flags, and what each one narrows\n\nThe default \u2014 no flag, no field \u2014 converges all five tools.\n\n- `--tools <ids...>` \u2192 `tools: ["gh", "doppler"]`. Converge **only those ids**. Same behaviour per\n tool, smaller set. The ids are `brew`, `aws`, `gh`, `doppler` and `portless`.\n- `--update [ids...]` \u2192 `mode: "update"`. **Never installs.** A tool that is present is updated; a tool\n that is absent is reported `skipped` with the reason, and its install recipe is not run. Given ids, it\n also narrows the set, so `--update gh` is "update gh, and nothing else, and only if it is there".\n- `--skip-tools` \u2192 `skipTools: true`. A **read-only probe**. The init half still runs \u2014 it is local and\n additive \u2014 and then each tool is reported with what it needs and the exact argv that would fix it.\n Nothing is installed and nothing is updated.\n\n**`--skip-tools` with `--tools` or `--update` is a usage error, not a precedence rule.** Every\nprecedence answer is wrong: honouring `--skip-tools` would ignore a set the caller chose, and honouring\nthe other would install software the caller asked not to install. The call is refused instead.\n\n## 3. Recipes that are printed rather than run\n\nWhether a recipe may run unattended is **computed**, not a per-recipe flag someone set. Two of the four\nconjuncts are static and are applied to every recipe unconditionally:\n\n- **`needs-sudo`** \u2014 the recipe escalates privilege.\n- **`fetches-network-script`** \u2014 the recipe pipes a script fetched at run time into a shell.\n\nTwo are detection-based: **`manager-absent`** (the package manager the recipe drives is not on this\nhost) and **`manager-mismatch`** (a different manager owns the binary \u2014 running `brew upgrade` against\nan npm install, or the vendor\'s self-updater against a Homebrew keg, is the split-brain infra-kit\nrefuses for itself).\n\n**Two recipes fail the static conjuncts, and they are the two bootstraps:**\n\n- **the Homebrew bootstrap** \u2014 `/bin/bash -c "$(curl -fsSL \u2026/install.sh)"` \u2014 fails **both**: it pipes a\n network-fetched script **and** needs sudo on macOS.\n- **the first AWS CLI install** \u2014 `curl -fsSL https://awscli.amazonaws.com/v2/install.sh | bash` \u2014\n needs no sudo (it installs under `$HOME`), but it is still a network-fetched script.\n\nBoth are the tools\' own documented installers, and neither is a bug to route around. They are refused\n**by computation, applied before any detection runs**, which is what makes the refusal trustworthy: it\ncannot be widened by a probe getting something wrong, only narrowed.\n\nOnce Homebrew exists, `gh` and `doppler` install through it and run unattended; once the AWS CLI exists,\n`aws update` is a plain no-sudo recipe and runs. The refusals are a first-install cost, not permanent.\n\nWhat to do with one: the entry\'s `commands` array is the exact argv, one string per step. **Show it to\nthe human and let them run it themselves.** Do not reconstruct it as a `Bash` call \u2014 that is the same\nunattended `sudo` and the same piped script, with the control removed.\n\n## 4. The confirm gate\n\n`mcp__infra-kit__setup` is gated, and **both gates fire on every call \u2014 `skipTools` included**.\n\n**Call 1** \u2014 send the real arguments, with no `confirm` and no `confirmToken`. The result is a gate\npayload, `{"status": "confirmation_required", \u2026}`, carrying `"isError": true`.\n\n**That `isError` does not mean the call failed.** Nothing was written and nothing was installed. An\nagent that reads it as a failure \u2014 and gives up, or retries, or falls back to `Bash` \u2014 has skipped the\nhuman approval this protocol exists for. Do none of those. Show the human `resolvedArgs`; that is the\napproval moment.\n\n**Call 2** \u2014 repeat the **same arguments**, unchanged, plus `"confirm": true` and the `confirmToken`\nfrom call 1. Change any argument between the two and round 2 comes back\n`{"status": "confirmation_refused", "reason": "mismatch"}`, which is terminal, not a second gate. Every\nrefusal reason recovers the same way: call again **without** `confirm` for a fresh gate, then re-call\nwith the new token. Never retry with the old one.\n\nThe tool also carries `anthropic/requiresUserInteraction`, so the host prompts a human even where an\nallow rule would otherwise skip it. One install therefore costs two prompts. That is intended: neither\ngate substitutes for the other, and neither substitutes for the computed refusals in section 3, which\nare the only control that ships inside the CLI itself.\n\n## 5. There is no `init` command\n\nIt was removed outright. The binary rejects the name \u2014 an instruction that still says to run it fails\nat the parser rather than quietly doing something else.\n\nRepos can still say it. A consumer\'s committed CLAUDE.md block is rewritten only from inside that repo,\nso a repo upgrades the global CLI without its own text changing and can sit arbitrarily far behind. So\nwhen a human asks for "init", or a repo\'s instructions still name it:\n\n- Run **`infra-kit setup`** if they want the tools installed or updated too.\n- Run **`infra-kit setup --skip-tools`** (`skipTools: true`) for the additive local writes with nothing\n installed. That is the whole reason the flag exists: without it, removing `init` would have deleted a\n capability rather than renamed one.\n\nSay which one you chose. Running `infra-kit audit --fix` inside that repo rewrites the stale block.\n\n## 6. What not to do\n\n- Do not work around a refusal in section 3 with `Bash`. The refusal is the control.\n- Do not read `isError: true` on a `confirmation_required` payload as a failure. See section 4.\n- Do not report success from the exit status alone. Read every `tools[].action` and name the refusals.\n- Do not call this tool to answer a question. Call `doctor` \u2014 it changes nothing and prompts no one.\n';var m={"release-create":M.trimEnd(),setup:q.trimEnd()};var j=async e=>{e.registerPrompt("release-create",{title:"Cut a release",description:'The procedure for cutting a release with infra-kit: preconditions, the two-call confirm protocol, and what "next" resolves against.'},()=>({messages:[{role:"user",content:{type:"text",text:m["release-create"]}}]}))};var ce="infra-kit://config",de="infra-kit://dev-context",ue="infra-kit://workflow/release-create",he="infra-kit://workflow/setup",me={loadConfig:P,readDevContext:()=>w()},y=(e,t)=>({contents:[{uri:e,mimeType:"application/json",text:JSON.stringify(t,null,2)}]}),L=(e,t)=>{e.registerResource(`infra-kit-workflow-${t.key}`,t.uri,{title:t.title,description:t.description,mimeType:"text/markdown"},r=>({contents:[{uri:r.toString(),mimeType:"text/markdown",text:m[t.key]}]}))},$=async(e,t=me)=>{e.registerResource("infra-kit-config",ce,{title:"infra-kit config",description:"The merged infra-kit.json configuration (all override layers applied). Read-only.",mimeType:"application/json"},async r=>{try{return y(r.toString(),await t.loadConfig())}catch(o){return y(r.toString(),{error:o instanceof Error?o.message:String(o)})}}),e.registerResource("infra-kit-dev-context",de,{title:"infra-kit dev context",description:"What `infra-kit dev` last wrote: backends currently up, their ports/origins, and fragment freshness. Read-only and advisory (no liveness probe). No active session resolves to an empty payload.",mimeType:"application/json"},r=>y(r.toString(),t.readDevContext())),L(e,{key:"release-create",uri:ue,title:"release-create procedure",description:'How to cut a release with the release-create tool: the preconditions, the two-call confirm protocol, and what the "next" token actually resolves against. Read this before calling mcp__infra-kit__release-create.'}),L(e,{key:"setup",uri:he,title:"setup procedure",description:"How to set a machine up with the setup tool: the ordered local writes, then the dependency converge; what tools/mode/skipTools each narrow; which recipes are printed instead of run and why; and what to run when a repo still tells you to set it up some older way. Read this before calling mcp__infra-kit__setup."})};import{z as T}from"zod";import{createRequestStateCodec as fe}from"@modelcontextprotocol/server";import{randomBytes as pe}from"node:crypto";var ge=new Set(["confirm","confirmToken"]),we=600,G=e=>({toolName:e}),v=e=>{if(Array.isArray(e))return e.map(v);if(e!==null&&typeof e=="object"){let t=e;return Object.fromEntries(Object.keys(t).sort().map(r=>[r,v(t[r])]))}return e},b=e=>JSON.stringify(v(e)),c=e=>typeof e!="object"||e===null?{}:Object.fromEntries(Object.entries(e).filter(([t])=>!ge.has(t))),U=(e={})=>{let t=fe({key:e.key??pe(32),ttlSeconds:e.ttlSeconds??we,bind:r=>r.toolName});return{mint:(r,o)=>t.mint(r,G(o)),verify:(r,o)=>t.verify(r,G(o))}},W,H=()=>(W??=U(),W),ye=e=>{if(typeof e!="object"||e===null)return;let{confirmToken:t}=e;return typeof t=="string"?t:void 0},ve=new Set(["malformed","mac","expired","bind"]),be=e=>{let t=e instanceof Error?e.message:"";return ve.has(t)?t:"malformed"},K=async(e,t,r)=>e.mint({args:b(c(r))},t),B=async(e,t,r)=>{let o=ye(r);if(o===void 0)return{ok:!1,reason:"absent"};let n;try{n=await e.verify(o,t)}catch(a){return{ok:!1,reason:be(a)}}return n.args!==b(c(r))?{ok:!1,reason:"mismatch"}:{ok:!0}};import{acceptedContent as ke,inputRequired as J,inputResponse as xe}from"@modelcontextprotocol/server";var k="args",Z=3e3,V=e=>{let t=xe(e,k);return t.kind==="elicit"?t.action:"missing"},Ce=async(e,t)=>new Promise(r=>{let o=setTimeout(()=>{r(null)},t);e.then(n=>{clearTimeout(o),r(n)},()=>{clearTimeout(o),r(null)})}),Y=async(e,t,r)=>{try{return await Ce(e.buildRequestedSchema(t),r)}catch{return null}},X=async(e,t,r)=>{let o=await Y(e,t,r);if(o===null)return null;try{return J({inputRequests:{[k]:J.elicit({message:e.message,requestedSchema:o})}})}catch{return null}},Q=async(e,t,r,o)=>{let n=await Y(e,t,o);if(n===null)return null;let a;try{a=ke(r,k,n)}catch{return null}if(a===void 0)return null;try{return e.toArgs(a,t)}catch{return null}},z=e=>typeof e=="object"&&e!==null&&!Array.isArray(e),ee=(e,t)=>{if(!z(e))return!1;if(!z(t))return!0;for(let[r,o]of Object.entries(e)){if(!(r in t))return!0;let n=t[r];if(Array.isArray(o)&&(!Array.isArray(n)||n.length!==o.length))return!0}return!1};var Te=e=>typeof e=="object"&&e!==null&&e.confirm===!0,Re=e=>e.gated?e.gated&&!e.confirmed&&e.responses===void 0&&e.canForm&&e.hasProvider&&e.formable?"form":e.gated&&!e.confirmed&&e.responses!==void 0&&!e.accepted?"declined":e.gated&&!e.confirmed&&(e.responses===void 0||e.accepted)?"gate":"verify":"run",x=e=>({content:E(JSON.stringify(e,null,2)),structuredContent:e,isError:!0}),Se="The values you submitted in the form could NOT be applied and were DISCARDED \u2014 they failed validation or narrowed the arguments \u2014 so the arguments shown above are the ORIGINAL ones, not your selection.",Ae="This tool does not prompt for its arguments; you are being asked to approve the values shown above.",Ee=async(e,t,r,o)=>{let n=c(r),a=await K(e,t,r),s=[o.formDiscarded?Se:void 0,`${t} mutates external state and is gated. It was NOT executed. Re-call ${t} with the same arguments plus "confirm": true and this "confirmToken" to execute.`,o.hasProvider?void 0:Ae].filter(p=>p!==void 0).join(" ");return x({status:"confirmation_required",tool:t,resolvedArgs:n,confirmToken:a,formDiscarded:o.formDiscarded,message:s})},De=(e,t)=>x({status:"form_declined",tool:e,action:t,message:`${e} was NOT executed: the argument form came back as "${t}". No confirmation is pending \u2014 call ${e} again to start over.`}),Pe={absent:'no "confirmToken" was supplied',malformed:'the "confirmToken" is malformed',mac:'the "confirmToken" was not issued by this server',expired:'the "confirmToken" has expired',bind:'the "confirmToken" was issued for a different tool',mismatch:'the arguments differ from the ones the "confirmToken" was issued for'},_e=(e,t)=>x({status:"confirmation_refused",tool:e,reason:t,message:`${e} was NOT executed: ${Pe[t]}. Call ${e} again WITHOUT "confirm" to receive a fresh gate, then re-call with the same arguments plus "confirm": true and the returned "confirmToken".`}),te=e=>{try{return e()}catch{return!1}},Ie=async(e,t,r,o)=>{let n=e.formProvider;if(n===void 0||o!=="accept")return{params:t,formDiscarded:!1};let a=await Q(n,t,r,e.formDeadlineMs);return a===null?(i.info({msg:`Tool execution form discarded (validation): ${e.toolName}`}),{params:t,formDiscarded:!0}):ee(c(t),a)?(i.info({msg:`Tool execution form discarded (narrowed): ${e.toolName}`}),{params:t,formDiscarded:!0}):{params:a,formDiscarded:!1}},Oe=async(e,t,r,o,n)=>{if(o==="form"&&e.formProvider!==void 0){let s=await X(e.formProvider,t,e.formDeadlineMs);if(s!==null)return i.info({msg:`Tool execution form requested: ${e.toolName}`}),s;i.info({msg:`Tool execution form unavailable: ${e.toolName}`})}let a=await Ie(e,t,r,n);return i.info({msg:`Tool execution gated (awaiting confirm): ${e.toolName}`}),await Ee(e.codec,e.toolName,a.params,{formDiscarded:a.formDiscarded,hasProvider:e.formProvider!==void 0})},Ne=async(e,t,r)=>{let o=r?.mcpReq?.inputResponses,n=V(o),a=Re({gated:e.requiresHumanConfirm===!0,confirmed:Te(t),responses:o,canForm:te(()=>e.getClientCapabilities?.()?.elicitation?.form!==void 0),hasProvider:e.formProvider!==void 0,formable:te(()=>e.formProvider?.isFormable(t)===!0),accepted:n==="accept"});if(a==="run")return null;if(a==="declined")return i.info({msg:`Tool execution form declined (${n}): ${e.toolName}`}),De(e.toolName,n);if(a==="verify"){let s=await B(e.codec,e.toolName,t);return s.ok?null:(i.info({msg:`Tool execution refused (${s.reason}): ${e.toolName}`}),_e(e.toolName,s.reason))}return await Oe(e,t,o,a,n)},C=({toolName:e,handler:t,requiresHumanConfirm:r,formProvider:o,getClientCapabilities:n,confirmCodec:a,formDeadlineMs:s})=>{let p={toolName:e,codec:a??H(),requiresHumanConfirm:r,formProvider:o,getClientCapabilities:n,formDeadlineMs:s??Z};return async(u,R)=>{i.info({msg:`Tool execution started: ${e}`,params:u,sessionId:R?.sessionId});try{await I(),_.reset();let d=await Ne(p,u,R);if(d!==null)return d;let ie=await t({...u,confirmedCommand:!0});return i.info({msg:`Tool execution successful: ${e}`}),ie}catch(d){throw i.error({err:d,params:u,msg:`Tool execution failed: ${e}`}),d}}};var re=async e=>{for(let t of O())e.registerTool(t.name,{title:t.title,description:t.description,inputSchema:T.object(t.requiresHumanConfirm===!0?Fe(t.inputSchema):t.inputSchema),outputSchema:T.object(t.outputSchema),annotations:t.annotations,_meta:t.meta},Me(C({toolName:t.name,handler:t.handler,requiresHumanConfirm:t.requiresHumanConfirm,formProvider:t.formProvider,getClientCapabilities:()=>e.server.getClientCapabilities()})))},Fe=e=>({...e,confirmToken:T.string().optional().describe("Round-2 only: the token returned by the round-1 gate, proving the arguments are unchanged.")}),Me=e=>(t,r)=>e(t,r);async function oe(){S.enabled=!0;let e=new qe({name:"infra-kit",version:D.version},{capabilities:{resources:{listChanged:!0},tools:{},prompts:{}}});return await j(e),await $(e),await re(e),e}N();var l=A(),$e=async()=>{try{return await oe()}catch(e){l.error({err:e,msg:"Failed to create MCP server"}),l.flush(),f.exit(1)}};F(l);var Ge=je($e,{onerror:e=>{l.error({err:e,msg:"MCP stdio entry error"}),l.flush()}});l.info({msg:"MCP stdio entry started."});var ne=!1,ae=async e=>{if(!ne){ne=!0,l.info({msg:`Received ${e}. Shutting down...`});try{await Promise.race([Ge.close(),Le(1500)])}catch(t){l.error({err:t,msg:"MCP stdio close failed during shutdown"})}l.flush(),f.exit(0)}};f.on("SIGINT",()=>{ae("SIGINT")});f.on("SIGTERM",()=>{ae("SIGTERM")});
2
2
  //# sourceMappingURL=mcp.js.map
package/dist/mcp.js.map CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
- "sources": ["../src/entry/mcp.ts", "../src/lib/error-handlers/index.ts", "../src/mcp/server.ts", "../src/mcp/prompts/index.ts", "../src/mcp/resources/index.ts", "../src/mcp/tools/index.ts", "../src/lib/tool-handler/confirm-token.ts", "../src/lib/tool-handler/tool-handler.ts"],
4
- "sourcesContent": ["import { serveStdio } from '@modelcontextprotocol/server/stdio'\nimport process from 'node:process'\nimport { setTimeout as delay } from 'node:timers/promises'\n\nimport { setupErrorHandlers } from 'src/lib/error-handlers'\nimport { initLoggerMcp } from 'src/lib/logger'\nimport { suppressTypelessPackageJsonWarning } from 'src/lib/node-warnings'\n\nimport { createMcpServer } from '../mcp/server'\n\n// The MCP tools run the same commands as the CLI, so they hit the same consumer `.ts` configs.\n// This keeps the client's log pane clean; it cannot affect protocol framing either way, since\n// the stdio transport frames JSON-RPC on stdout and `emitWarning` only ever writes to stderr.\nsuppressTypelessPackageJsonWarning()\n\nconst logger = initLoggerMcp()\n\n/**\n * `serveStdio` calls this LAZILY, on the first inbound message, and may call it TWICE on one\n * connection via the probe-then-legacy discard path. The factory must therefore stay free of\n * non-idempotent process-scope side effects.\n */\nconst buildOrDie = async () => {\n try {\n return await createMcpServer()\n } catch (error) {\n logger.error({ err: error, msg: 'Failed to create MCP server' })\n logger.flush()\n\n process.exit(1)\n }\n}\n\nsetupErrorHandlers(logger)\n\nconst handle = serveStdio(buildOrDie, {\n onerror: (error) => {\n logger.error({ err: error, msg: 'MCP stdio entry error' })\n logger.flush()\n },\n})\n\n// NOT \"listening\": `serveStdio` returns while the transport start is still pending, so nothing is\n// established at this point beyond the entry having been wired up. Claiming readiness here would\n// print \"listening\" on a transport that failed to start.\nlogger.info({ msg: 'MCP stdio entry started.' })\n\nlet isShuttingDown = false\n\nconst shutdown = async (signal: NodeJS.Signals) => {\n // A second signal inside the 1500 ms window must not start a second teardown.\n if (isShuttingDown) {\n return\n }\n\n isShuttingDown = true\n\n logger.info({ msg: `Received ${signal}. Shutting down...` })\n\n // Exit 0 is unconditional: a rejecting close() must not escape to `unhandledRejection` and turn a\n // clean shutdown into exit 1. The SDK already routes close errors to `onerror`; this is the belt.\n try {\n await Promise.race([handle.close(), delay(1500)])\n } catch (error) {\n logger.error({ err: error, msg: 'MCP stdio close failed during shutdown' })\n }\n\n logger.flush()\n\n process.exit(0)\n}\n\nprocess.on('SIGINT', () => {\n void shutdown('SIGINT')\n})\n\nprocess.on('SIGTERM', () => {\n void shutdown('SIGTERM')\n})\n", "import process from 'node:process'\nimport type { Logger } from 'pino'\n\nimport { LOG_FILE_PATH } from '../logger/index'\n\n/**\n * Setup handlers for fatal, non-signal process events.\n *\n * @param logger - The logger instance\n *\n * ONLY FOR SERVER!\n *\n * Signal handling deliberately does NOT live here: a bounded teardown needs the\n * `StdioServerHandle` returned by `serveStdio`, which only the MCP entry holds, so\n * `SIGINT`/`SIGTERM` are registered in `src/entry/mcp.ts` instead. The arms removed from\n * here called `process.exit(0)` without flushing the logger, discarding the final log\n * lines on every clean shutdown; the entry's `shutdown` flushes before exiting.\n */\nexport const setupErrorHandlers = (logger: Logger) => {\n process.on('uncaughtException', (error) => {\n logger.fatal({ err: error, msg: 'Uncaught Exception' })\n logger.error(`Uncaught Exception! Check ${LOG_FILE_PATH}. Shutting down...`)\n logger.flush()\n process.exit(1)\n })\n\n process.on('unhandledRejection', (reason, promise) => {\n logger.fatal({ reason, promise, msg: 'Unhandled Rejection' })\n logger.error(`Unhandled Rejection! Check ${LOG_FILE_PATH}. Shutting down...`)\n logger.flush()\n process.exit(1)\n })\n}\n", "import { McpServer } from '@modelcontextprotocol/server'\n\nimport { mcpMode } from 'src/lib/mcp-mode'\n\nimport packageJson from '../../package.json' with { type: 'json' }\nimport { initializePrompts } from './prompts'\nimport { initializeResources } from './resources'\nimport { initializeTools } from './tools'\n\nexport async function createMcpServer() {\n // Marks `process.stdin` as the JSON-RPC transport for every prompt guard in the\n // process. Set HERE rather than in `entry/mcp.ts` (the sole caller) because that\n // module starts a server at import time and so can never be unit-tested \u2014 a test\n // there could only stub the flag, which proves nothing about it ever being set.\n // This runs before `server.connect`, so no tool handler can outrun it.\n mcpMode.enabled = true\n\n const server = new McpServer(\n {\n name: 'infra-kit',\n version: packageJson.version,\n },\n {\n capabilities: {\n // `initializeResources` registers read-only resources below; the SDK's\n // `registerResource` also declares `resources.listChanged` on top of this.\n resources: { listChanged: true },\n tools: {},\n prompts: {},\n },\n },\n )\n\n await initializePrompts(server)\n await initializeResources(server)\n await initializeTools(server)\n\n return server\n}\n", "import type { McpServer } from '@modelcontextprotocol/server'\n\nexport const initializePrompts = async (_server: McpServer) => {}\n", "import type { McpServer } from '@modelcontextprotocol/server'\n\nimport type { InfraKitConfig } from 'src/lib/infra-kit-config'\nimport { getInfraKitConfig } from 'src/lib/infra-kit-config'\n\nimport type { DevContextSnapshot } from './dev-context'\nimport { readDevContext } from './dev-context'\n\n/** Stable URI of the merged-config resource. */\nexport const CONFIG_RESOURCE_URI = 'infra-kit://config'\n\n/** Stable URI of the dev-context resource. */\nexport const DEV_CONTEXT_RESOURCE_URI = 'infra-kit://dev-context'\n\n/**\n * The two disk reads the resources need, injected so the registration is unit-testable without touching\n * the filesystem or the config-loader's mtime cache. Production uses the real readers by default.\n */\nexport interface ResourceDeps {\n /** Loads the merged `infra-kit.json` (all override layers applied). May throw when no config exists. */\n loadConfig: () => Promise<InfraKitConfig>\n /** Reads the dev-context fragments `infra-kit dev` last wrote. Never throws; absent session \u21D2 empty. */\n readDevContext: () => DevContextSnapshot\n}\n\nconst defaultDeps: ResourceDeps = {\n loadConfig: getInfraKitConfig,\n readDevContext: () => {\n return readDevContext()\n },\n}\n\n/** Serialize a resource body as pretty JSON text \u2014 the wire form every MCP client can read. */\nconst jsonResource = (uri: string, value: unknown): { contents: { uri: string; mimeType: string; text: string }[] } => {\n return { contents: [{ uri, mimeType: 'application/json', text: JSON.stringify(value, null, 2) }] }\n}\n\n/**\n * Register infra-kit's READ-ONLY MCP resources so an agent can inspect the repo's config and dev state\n * without calling a tool (or mutating anything):\n *\n * - `infra-kit://config` \u2014 the merged `infra-kit.json` an agent would run against.\n * - `infra-kit://dev-context` \u2014 what `infra-kit dev` last wrote (backends up, ports, freshness); an\n * absent dev session resolves to an empty `session: 'none'` payload, NOT\n * an error.\n *\n * Both handlers are side-effect free and swallow their reader's failure into an `{ error }` payload rather\n * than throwing, so a bad on-disk config can never crash the long-lived MCP server.\n */\nexport const initializeResources = async (server: McpServer, deps: ResourceDeps = defaultDeps) => {\n server.registerResource(\n 'infra-kit-config',\n CONFIG_RESOURCE_URI,\n {\n title: 'infra-kit config',\n description: 'The merged infra-kit.json configuration (all override layers applied). Read-only.',\n mimeType: 'application/json',\n },\n async (uri) => {\n try {\n return jsonResource(uri.toString(), await deps.loadConfig())\n } catch (error) {\n return jsonResource(uri.toString(), { error: error instanceof Error ? error.message : String(error) })\n }\n },\n )\n\n server.registerResource(\n 'infra-kit-dev-context',\n DEV_CONTEXT_RESOURCE_URI,\n {\n title: 'infra-kit dev context',\n description:\n 'What `infra-kit dev` last wrote: backends currently up, their ports/origins, and fragment ' +\n 'freshness. Read-only and advisory (no liveness probe). No active session resolves to an empty payload.',\n mimeType: 'application/json',\n },\n // The SDK's `ReadResourceCallback` accepts a synchronous return; the disk read is sync, so no `async`.\n (uri) => {\n return jsonResource(uri.toString(), deps.readDevContext())\n },\n )\n}\n", "import type { McpServer } from '@modelcontextprotocol/server'\nimport { z } from 'zod'\n\nimport { getExposedMcpTools } from 'src/lib/command-catalog'\nimport { createToolHandler } from 'src/lib/tool-handler'\nimport type { ToolCallContext } from 'src/lib/tool-handler'\n\nexport const initializeTools = async (server: McpServer) => {\n // The registered tool set is derived from the single command catalog, filtered\n // by its explicit `mcpExposed` allowlist. doctor is intentionally excluded there\n // (host-inspecting) and must never be registered here.\n for (const tool of getExposedMcpTools()) {\n server.registerTool(\n tool.name,\n {\n // Display label; hosts fall back to `name` when absent. TOP-LEVEL `title` only \u2014\n // `annotations.title` is deliberately left unset, because some hosts prefer the latter when\n // present and two titles are a divergence waiting to happen.\n title: tool.title,\n description: tool.description,\n // Wrapped HERE, at the single `registerTool` call site, rather than in the 24\n // `defineMcpTool` definitions or in `CatalogMcpTool`. SDK v2 offers a deprecated\n // raw-shape overload that would auto-wrap, but it types the shape as its own\n // `ZodRawShape` (`Record<string, ZodType>`) while zod 4's `z.ZodRawShape` is the\n // looser `Readonly<{ [k: string]: $ZodType }>` \u2014 `$ZodType` lacks `def`/`type`/`_def`\n // and ~42 other members, so the raw shape is NOT assignable and the overload is\n // unreachable for us. `z.object()` closes that gap and lands on v2's PREFERRED\n // (non-deprecated) Standard Schema overload, since a `ZodObject` carries `~standard`.\n //\n // Wrapping here keeps the authoring shape in all 24 `src/commands/**` definitions\n // unchanged and leaves `defineMcpTool`'s `z.infer<z.ZodObject<TIn>>` handler typing\n // untouched \u2014 the migration needs zero edits under `src/commands/`.\n //\n // Gated tools additionally accept `confirmToken` here, at the boundary: `z.object` strips\n // undeclared keys, so without this the round-2 token the gate hands out would never reach\n // `createToolHandler`'s verification and every confirmation would be refused as absent.\n inputSchema: z.object(\n tool.requiresHumanConfirm === true ? withConfirmToken(tool.inputSchema) : tool.inputSchema,\n ),\n outputSchema: z.object(tool.outputSchema),\n // ADVISORY protocol hints, derived in the catalog from `mutating`. The authority for\n // destructive operations remains `requiresHumanConfirm` + `lib/tool-handler`'s confirm gate;\n // nothing in that gate reads these, and the spec forbids a client treating them as security.\n annotations: tool.annotations,\n },\n // The SDK's second callback argument (session, request state, signal) is threaded through so\n // the handler signature lands once; the gate binds tokens by tool name and needs none of it.\n wrapForRegistration(\n createToolHandler({\n toolName: tool.name,\n handler: tool.handler,\n requiresHumanConfirm: tool.requiresHumanConfirm,\n }),\n ),\n )\n }\n}\n\nconst withConfirmToken = (shape: z.ZodRawShape): z.ZodRawShape => {\n return {\n ...shape,\n confirmToken: z\n .string()\n .optional()\n .describe('Round-2 only: the token returned by the round-1 gate, proving the arguments are unchanged.'),\n }\n}\n\nconst wrapForRegistration = (handle: ReturnType<typeof createToolHandler>) => {\n return (params: unknown, ctx: ToolCallContext) => {\n return handle(params, ctx)\n }\n}\n", "import { createRequestStateCodec } from '@modelcontextprotocol/server'\nimport { randomBytes } from 'node:crypto'\n\n/**\n * @fileoverview\n *\n * Round-1 gate payload \u2192 round-2 binding for the destructive-op confirm gate.\n *\n * Round 1 mints a `confirmToken` over the tool name and the CANONICAL form of the\n * arguments; round 2 must present that token together with the same arguments, or\n * the tool does not run.\n *\n * Without the binding an agent could obtain a gate for `env:dev` and re-call with\n * `confirm:true` for `env:prod` \u2014 the substitution hole\n * `docs/infra-kit-slash-commands-plan.md` \u00A70.12 records.\n */\n\n/** Keys the gate itself owns; they are never part of the signed argument set. */\nconst GATE_KEYS = new Set(['confirm', 'confirmToken'])\n\n/** Default token lifetime. Long enough for a human to read the gate, short enough to bound replay. */\nexport const CONFIRM_TOKEN_TTL_SECONDS = 600\n\n/** What a round-1 gate signs. `args` is the canonical JSON of the tool arguments. */\nexport interface ConfirmPayload {\n args: string\n}\n\n/** The subset of the SDK codec the gate uses; `ctx` is the tool name the token is bound to. */\nexport interface ConfirmCodec {\n mint: (payload: ConfirmPayload, toolName: string) => Promise<string>\n verify: (token: string, toolName: string) => Promise<ConfirmPayload>\n}\n\nexport type ConfirmRefusal = 'absent' | 'malformed' | 'mac' | 'expired' | 'bind' | 'mismatch'\n\ninterface BindContext {\n toolName: string\n}\n\ntype SdkBindContext = NonNullable<Parameters<ReturnType<typeof createRequestStateCodec>['mint']>[1]>\n\nconst asBindContext = (toolName: string): SdkBindContext => {\n return { toolName } as unknown as SdkBindContext\n}\n\nexport type ConfirmVerdict = { ok: true } | { ok: false; reason: ConfirmRefusal }\n\nconst sortKeysDeep = (value: unknown): unknown => {\n if (Array.isArray(value)) {\n return value.map(sortKeysDeep)\n }\n\n if (value !== null && typeof value === 'object') {\n const record = value as Record<string, unknown>\n\n return Object.fromEntries(\n Object.keys(record)\n .sort()\n .map((key) => {\n return [key, sortKeysDeep(record[key])]\n }),\n )\n }\n\n return value\n}\n\n/**\n * Stable JSON for an argument object: keys sorted recursively, arrays kept in order.\n * `JSON.stringify` is key-order-sensitive and round-2 arguments are re-serialized by\n * the client in whatever order the host emits, so signing or verifying the raw form\n * would refuse legitimate confirmations intermittently, per client.\n *\n * @example\n * canonicalArgs({ b: 2, a: { d: 1, c: [2, 1] } }) // => '{\"a\":{\"c\":[2,1],\"d\":1},\"b\":2}'\n */\nexport const canonicalArgs = (args: unknown): string => {\n return JSON.stringify(sortKeysDeep(args))\n}\n\n/**\n * The tool's own arguments: the incoming params with the gate's keys removed. This is\n * the ONE filter both rounds share, so round 2 (which carries `confirm` and\n * `confirmToken` on top of round 1's arguments) verifies against exactly what round 1 signed.\n *\n * @example\n * stripGateKeys({ env: 'dev', confirm: true, confirmToken: 'v1.x.y' }) // => { env: 'dev' }\n */\nexport const stripGateKeys = (params: unknown): Record<string, unknown> => {\n if (typeof params !== 'object' || params === null) return {}\n\n return Object.fromEntries(\n Object.entries(params as Record<string, unknown>).filter(([key]) => {\n return !GATE_KEYS.has(key)\n }),\n )\n}\n\n/**\n * Build the gate's codec. The key is per-process and random by default: one stdio\n * server process serves both rounds of every confirmation, so a key that dies with\n * the process is sufficient \u2014 that stops being true on any HTTP transport, where the\n * key would have to be shared across workers. Tests inject a short `ttlSeconds` or a\n * second key to exercise the expiry and tamper refusals.\n *\n * @example\n * const codec = createConfirmCodec()\n * const token = await codec.mint({ args: '{\"env\":\"dev\"}' }, 'env-clear')\n * await codec.verify(token, 'env-clear') // => { args: '{\"env\":\"dev\"}' }\n */\nexport const createConfirmCodec = (options: { key?: Uint8Array; ttlSeconds?: number } = {}): ConfirmCodec => {\n // The SDK types the bind context as its own `ServerContext`; the gate only ever binds to the\n // tool name, so a one-field object stands in for it on both mint and verify.\n const codec = createRequestStateCodec<ConfirmPayload>({\n key: options.key ?? randomBytes(32),\n ttlSeconds: options.ttlSeconds ?? CONFIRM_TOKEN_TTL_SECONDS,\n bind: (ctx) => {\n return (ctx as unknown as BindContext).toolName\n },\n })\n\n return {\n mint: (payload, toolName) => {\n return codec.mint(payload, asBindContext(toolName))\n },\n verify: (token, toolName) => {\n return codec.verify(token, asBindContext(toolName))\n },\n }\n}\n\nlet defaultCodec: ConfirmCodec | undefined\n\n/** The process-wide codec every registered tool shares, created on first use. */\nexport const getDefaultConfirmCodec = (): ConfirmCodec => {\n defaultCodec ??= createConfirmCodec()\n\n return defaultCodec\n}\n\nconst readConfirmToken = (params: unknown): string | undefined => {\n if (typeof params !== 'object' || params === null) return undefined\n\n const { confirmToken } = params as { confirmToken?: unknown }\n\n return typeof confirmToken === 'string' ? confirmToken : undefined\n}\n\nconst REFUSALS_FROM_CODEC: ReadonlySet<ConfirmRefusal> = new Set(['malformed', 'mac', 'expired', 'bind'])\n\nconst codecRefusal = (error: unknown): ConfirmRefusal => {\n const message = error instanceof Error ? error.message : ''\n\n return REFUSALS_FROM_CODEC.has(message as ConfirmRefusal) ? (message as ConfirmRefusal) : 'malformed'\n}\n\n/**\n * Mint the round-1 token for `params` (gate keys excluded), bound to `toolName`.\n *\n * @example\n * await mintConfirmToken(codec, 'env-clear', { env: 'dev' }) // => 'v1.<payload>.<mac>'\n */\nexport const mintConfirmToken = async (codec: ConfirmCodec, toolName: string, params: unknown): Promise<string> => {\n return codec.mint({ args: canonicalArgs(stripGateKeys(params)) }, toolName)\n}\n\n/**\n * Decide whether a round-2 call may run. Absence is a refusal in its own right \u2014\n * \"verify only when a token is present\" would let an agent omit the token and\n * substitute arguments freely, reopening the hole this module closes.\n *\n * @example\n * await verifyConfirmToken(codec, 'env-clear', { env: 'prod', confirm: true, confirmToken })\n * // => { ok: false, reason: 'mismatch' } (the token was minted for env:dev)\n */\nexport const verifyConfirmToken = async (\n codec: ConfirmCodec,\n toolName: string,\n params: unknown,\n): Promise<ConfirmVerdict> => {\n const token = readConfirmToken(params)\n\n if (token === undefined) return { ok: false, reason: 'absent' }\n\n let payload: ConfirmPayload\n\n try {\n payload = await codec.verify(token, toolName)\n } catch (error) {\n return { ok: false, reason: codecRefusal(error) }\n }\n\n if (payload.args !== canonicalArgs(stripGateKeys(params))) return { ok: false, reason: 'mismatch' }\n\n return { ok: true }\n}\n", "import { commandEcho } from 'src/lib/command-echo'\nimport { ensureUserProjectConfig } from 'src/lib/config-bootstrap'\nimport { logger } from 'src/lib/logger'\nimport { textContent } from 'src/types'\nimport type { ToolsExecutionResult } from 'src/types'\n\nimport { getDefaultConfirmCodec, mintConfirmToken, stripGateKeys, verifyConfirmToken } from './confirm-token'\nimport type { ConfirmCodec, ConfirmRefusal } from './confirm-token'\n\ninterface ToolHandlerArgs {\n toolName: string\n handler: (params: any) => Promise<ToolsExecutionResult>\n /**\n * When true, this tool is gated by the destructive-op confirm gate below. Sourced from the\n * catalog tool's {@link CatalogMcpTool.requiresHumanConfirm} at registration (`mcp/tools/index.ts`).\n */\n requiresHumanConfirm?: boolean\n /** Token codec for the gate. Defaults to the process-wide one; tests inject short-TTL or foreign-key codecs. */\n confirmCodec?: ConfirmCodec\n}\n\n/**\n * The SDK hands every tool callback a second argument (session, request state, abort signal).\n * Threaded through untouched so the signature lands once; the gate itself needs none of it yet.\n */\nexport interface ToolCallContext {\n sessionId?: string\n}\n\ntype GateState = 'run' | 'gate' | 'verify'\n\n/** True when the incoming MCP params carry an explicit `confirm:true` (the call-2 execute signal). */\nconst isConfirmed = (params: unknown): boolean => {\n return typeof params === 'object' && params !== null && (params as { confirm?: unknown }).confirm === true\n}\n\n/**\n * First-match-wins, every condition complete: an ungated tool runs; a gated tool without\n * `confirm:true` gets the gate; a gated tool WITH it goes to verification \u2014 never back to the\n * gate, because a token-less round 2 that slid back to the gate would reopen the argument-\n * substitution hole (the refusal lives INSIDE `verify`, not in this discrimination).\n */\nconst resolveGateState = (requiresHumanConfirm: boolean | undefined, params: unknown): GateState => {\n // Spelled `=== true` on purpose: `mcp-confirm-gate-mutation.test.ts` neuters exactly this\n // predicate at build time to prove the gate's e2e assertions are load-bearing.\n if (requiresHumanConfirm === true) return isConfirmed(params) ? 'verify' : 'gate'\n\n return 'run'\n}\n\n/**\n * Every gate and refusal payload sets `isError: true`: the MCP SDK validates a result's\n * `structuredContent` against the tool's `outputSchema` UNLESS `isError` is set, and no gated\n * tool's schema matches these payloads. A soft stop, not a failure \u2014 the tool did NOT run.\n */\nconst softStop = (structuredContent: Record<string, unknown>): ToolsExecutionResult => {\n return {\n content: textContent(JSON.stringify(structuredContent, null, 2)),\n structuredContent,\n isError: true,\n }\n}\n\n/**\n * The round-1 gate: names the tool, echoes the arguments it will bind to, and carries the\n * `confirmToken` round 2 must return alongside `confirm:true` and the SAME arguments.\n */\nconst buildConfirmGate = async (\n codec: ConfirmCodec,\n toolName: string,\n params: unknown,\n): Promise<ToolsExecutionResult> => {\n const resolvedArgs = stripGateKeys(params)\n const confirmToken = await mintConfirmToken(codec, toolName, params)\n\n return softStop({\n status: 'confirmation_required',\n tool: toolName,\n resolvedArgs,\n confirmToken,\n message: `${toolName} mutates external state and is gated. It was NOT executed. Re-call ${toolName} with the same arguments plus \"confirm\": true and this \"confirmToken\" to execute.`,\n })\n}\n\nconst REFUSAL_TEXT: Record<ConfirmRefusal, string> = {\n absent: 'no \"confirmToken\" was supplied',\n malformed: 'the \"confirmToken\" is malformed',\n mac: 'the \"confirmToken\" was not issued by this server',\n expired: 'the \"confirmToken\" has expired',\n bind: 'the \"confirmToken\" was issued for a different tool',\n mismatch: 'the arguments differ from the ones the \"confirmToken\" was issued for',\n}\n\n/** The round-2 refusal: a terminal stop, deliberately NOT a second gate. */\nconst buildConfirmRefusal = (toolName: string, reason: ConfirmRefusal): ToolsExecutionResult => {\n return softStop({\n status: 'confirmation_refused',\n tool: toolName,\n reason,\n message: `${toolName} was NOT executed: ${REFUSAL_TEXT[reason]}. Call ${toolName} again WITHOUT \"confirm\" to receive a fresh gate, then re-call with the same arguments plus \"confirm\": true and the returned \"confirmToken\".`,\n })\n}\n\nexport const createToolHandler = ({\n toolName,\n handler,\n requiresHumanConfirm,\n confirmCodec,\n}: ToolHandlerArgs): ((params: unknown, ctx?: ToolCallContext) => Promise<ToolsExecutionResult>) => {\n const codec = confirmCodec ?? getDefaultConfirmCodec()\n\n return async (params: unknown) => {\n logger.info({ msg: `Tool execution started: ${toolName}`, params })\n try {\n // MCP entry-boundary seed. This wrapper is the sole chokepoint for every exposed tool\n // (`src/mcp/tools/index.ts` holds the only `registerTool` call), and the `mcp` command itself is\n // in program.ts's SEED_EXCLUDED \u2014 so an MCP server seeds lazily, on its FIRST tool invocation,\n // and a server that never receives one never writes to $HOME. The once-per-process guard inside\n // ensureUserProjectConfig makes calls 2..N free on this long-lived process.\n //\n // Deliberately INSIDE the try, not before it: defense-in-depth. ensureUserProjectConfig is\n // contractually never-throw, but if that contract ever broke, an unhandled rejection here would\n // take down the long-lived server. Inside the try it degrades to an ordinary tool error.\n await ensureUserProjectConfig()\n\n // The command handlers record their resolved flags into the `commandEcho` singleton. On the CLI,\n // Commander's `preAction` clears it before every command; this long-lived server never runs that\n // hook, so without a reset here one tool call's flags would leak into the next one's snapshot and\n // the options array would grow for the life of the process.\n commandEcho.reset()\n\n // Orthogonal destructive-op confirm gate. It sits BEFORE the handler and is INDEPENDENT of the\n // `confirmedCommand:true` injected below \u2014 that flag is a prompt-skip / behavior discriminator\n // (e.g. worktrees-remove keys `allowEditorRelaunch` off it) and MUST keep being injected on the\n // real call, or the non-TTY server would hang on an inquirer prompt and the Zed relaunch would\n // re-enable. Round 1 returns the gate with a token bound to the tool and its arguments; round 2\n // runs only when `confirm:true` comes with that token AND the same arguments.\n const state = resolveGateState(requiresHumanConfirm, params)\n\n if (state === 'gate') {\n logger.info({ msg: `Tool execution gated (awaiting confirm): ${toolName}` })\n\n return await buildConfirmGate(codec, toolName, params)\n }\n\n if (state === 'verify') {\n const verdict = await verifyConfirmToken(codec, toolName, params)\n\n if (!verdict.ok) {\n logger.info({ msg: `Tool execution refused (${verdict.reason}): ${toolName}` })\n\n return buildConfirmRefusal(toolName, verdict.reason)\n }\n }\n\n const payload = await handler({ ...(params as object), confirmedCommand: true })\n\n logger.info({ msg: `Tool execution successful: ${toolName}` })\n\n return payload\n } catch (error) {\n logger.error({\n err: error,\n params,\n msg: `Tool execution failed: ${toolName}`,\n })\n\n throw error\n }\n }\n}\n"],
5
- "mappings": "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",
6
- "names": ["serveStdio", "process", "delay", "process", "setupErrorHandlers", "logger", "process", "error", "LOG_FILE_PATH", "reason", "promise", "McpServer", "initializePrompts", "_server", "CONFIG_RESOURCE_URI", "DEV_CONTEXT_RESOURCE_URI", "defaultDeps", "getInfraKitConfig", "readDevContext", "jsonResource", "uri", "value", "initializeResources", "server", "deps", "error", "z", "createRequestStateCodec", "randomBytes", "GATE_KEYS", "CONFIRM_TOKEN_TTL_SECONDS", "asBindContext", "toolName", "sortKeysDeep", "value", "record", "key", "canonicalArgs", "args", "stripGateKeys", "params", "createConfirmCodec", "options", "codec", "ctx", "payload", "token", "defaultCodec", "getDefaultConfirmCodec", "readConfirmToken", "confirmToken", "REFUSALS_FROM_CODEC", "codecRefusal", "error", "message", "mintConfirmToken", "verifyConfirmToken", "isConfirmed", "params", "resolveGateState", "requiresHumanConfirm", "softStop", "structuredContent", "textContent", "buildConfirmGate", "codec", "toolName", "resolvedArgs", "stripGateKeys", "confirmToken", "mintConfirmToken", "REFUSAL_TEXT", "buildConfirmRefusal", "reason", "createToolHandler", "handler", "confirmCodec", "getDefaultConfirmCodec", "logger", "ensureUserProjectConfig", "commandEcho", "state", "verdict", "verifyConfirmToken", "payload", "error", "initializeTools", "server", "tool", "getExposedMcpTools", "z", "withConfirmToken", "wrapForRegistration", "createToolHandler", "shape", "handle", "params", "ctx", "createMcpServer", "mcpMode", "server", "McpServer", "package_default", "initializePrompts", "initializeResources", "initializeTools", "suppressTypelessPackageJsonWarning", "logger", "initLoggerMcp", "buildOrDie", "createMcpServer", "error", "process", "setupErrorHandlers", "handle", "serveStdio", "isShuttingDown", "shutdown", "signal", "delay"]
3
+ "sources": ["../src/entry/mcp.ts", "../src/lib/error-handlers/index.ts", "../src/mcp/server.ts", "../resources/workflow/release-create.md", "../resources/workflow/setup.md", "../src/mcp/workflow-bodies.ts", "../src/mcp/prompts/index.ts", "../src/mcp/resources/index.ts", "../src/mcp/tools/index.ts", "../src/lib/tool-handler/confirm-token.ts", "../src/lib/tool-handler/argument-form.ts", "../src/lib/tool-handler/tool-handler.ts"],
4
+ "sourcesContent": ["import { serveStdio } from '@modelcontextprotocol/server/stdio'\nimport process from 'node:process'\nimport { setTimeout as delay } from 'node:timers/promises'\n\nimport { setupErrorHandlers } from 'src/lib/error-handlers'\nimport { initLoggerMcp } from 'src/lib/logger'\nimport { suppressTypelessPackageJsonWarning } from 'src/lib/node-warnings'\n\nimport { createMcpServer } from '../mcp/server'\n\n// The MCP tools run the same commands as the CLI, so they hit the same consumer `.ts` configs.\n// This keeps the client's log pane clean; it cannot affect protocol framing either way, since\n// the stdio transport frames JSON-RPC on stdout and `emitWarning` only ever writes to stderr.\nsuppressTypelessPackageJsonWarning()\n\nconst logger = initLoggerMcp()\n\n/**\n * `serveStdio` calls this LAZILY, on the first inbound message, and may call it TWICE on one\n * connection via the probe-then-legacy discard path. The factory must therefore stay free of\n * non-idempotent process-scope side effects.\n */\nconst buildOrDie = async () => {\n try {\n return await createMcpServer()\n } catch (error) {\n logger.error({ err: error, msg: 'Failed to create MCP server' })\n logger.flush()\n\n process.exit(1)\n }\n}\n\nsetupErrorHandlers(logger)\n\nconst handle = serveStdio(buildOrDie, {\n onerror: (error) => {\n logger.error({ err: error, msg: 'MCP stdio entry error' })\n logger.flush()\n },\n})\n\n// NOT \"listening\": `serveStdio` returns while the transport start is still pending, so nothing is\n// established at this point beyond the entry having been wired up. Claiming readiness here would\n// print \"listening\" on a transport that failed to start.\nlogger.info({ msg: 'MCP stdio entry started.' })\n\nlet isShuttingDown = false\n\nconst shutdown = async (signal: NodeJS.Signals) => {\n // A second signal inside the 1500 ms window must not start a second teardown.\n if (isShuttingDown) {\n return\n }\n\n isShuttingDown = true\n\n logger.info({ msg: `Received ${signal}. Shutting down...` })\n\n // Exit 0 is unconditional: a rejecting close() must not escape to `unhandledRejection` and turn a\n // clean shutdown into exit 1. The SDK already routes close errors to `onerror`; this is the belt.\n try {\n await Promise.race([handle.close(), delay(1500)])\n } catch (error) {\n logger.error({ err: error, msg: 'MCP stdio close failed during shutdown' })\n }\n\n logger.flush()\n\n process.exit(0)\n}\n\nprocess.on('SIGINT', () => {\n void shutdown('SIGINT')\n})\n\nprocess.on('SIGTERM', () => {\n void shutdown('SIGTERM')\n})\n", "import process from 'node:process'\nimport type { Logger } from 'pino'\n\nimport { LOG_FILE_PATH } from '../logger/index'\n\n/**\n * Setup handlers for fatal, non-signal process events.\n *\n * @param logger - The logger instance\n *\n * ONLY FOR SERVER!\n *\n * Signal handling deliberately does NOT live here: a bounded teardown needs the\n * `StdioServerHandle` returned by `serveStdio`, which only the MCP entry holds, so\n * `SIGINT`/`SIGTERM` are registered in `src/entry/mcp.ts` instead. The arms removed from\n * here called `process.exit(0)` without flushing the logger, discarding the final log\n * lines on every clean shutdown; the entry's `shutdown` flushes before exiting.\n */\nexport const setupErrorHandlers = (logger: Logger) => {\n process.on('uncaughtException', (error) => {\n logger.fatal({ err: error, msg: 'Uncaught Exception' })\n logger.error(`Uncaught Exception! Check ${LOG_FILE_PATH}. Shutting down...`)\n logger.flush()\n process.exit(1)\n })\n\n process.on('unhandledRejection', (reason, promise) => {\n logger.fatal({ reason, promise, msg: 'Unhandled Rejection' })\n logger.error(`Unhandled Rejection! Check ${LOG_FILE_PATH}. Shutting down...`)\n logger.flush()\n process.exit(1)\n })\n}\n", "import { McpServer } from '@modelcontextprotocol/server'\n\nimport { mcpMode } from 'src/lib/mcp-mode'\n\nimport packageJson from '../../package.json' with { type: 'json' }\nimport { initializePrompts } from './prompts'\nimport { initializeResources } from './resources'\nimport { initializeTools } from './tools'\n\nexport async function createMcpServer() {\n // Marks `process.stdin` as the JSON-RPC transport for every prompt guard in the\n // process. Set HERE rather than in `entry/mcp.ts` (the sole caller) because that\n // module starts a server at import time and so can never be unit-tested \u2014 a test\n // there could only stub the flag, which proves nothing about it ever being set.\n // This runs before `server.connect`, so no tool handler can outrun it.\n mcpMode.enabled = true\n\n const server = new McpServer(\n {\n name: 'infra-kit',\n version: packageJson.version,\n },\n {\n capabilities: {\n // `initializeResources` registers read-only resources below; the SDK's\n // `registerResource` also declares `resources.listChanged` on top of this.\n resources: { listChanged: true },\n tools: {},\n prompts: {},\n },\n },\n )\n\n await initializePrompts(server)\n await initializeResources(server)\n await initializeTools(server)\n\n return server\n}\n", "# release-create \u2014 cutting a release through infra-kit\n\nThe tool is `mcp__infra-kit__release-create`. Everything below is about calling that tool.\n\nDo not shell out. A `Bash` call running `git switch`, `git push` or `gh pr create` reproduces none\nof the preconditions below and bypasses the confirm gate in section 2 \u2014 which is the only place a\nhuman approves the release.\n\n## 1. Preconditions\n\nCheck these before the first call; each one is a refusal the human has to clear, not something to\nwork around.\n\n- **The main repository checkout, not a linked worktree.** The tool refuses outright from inside a\n linked worktree.\n- **A clean working tree.** Uncommitted changes are refused; the human commits or stashes.\n- **No other worktree holding the base branch.** Regular releases branch off `dev`, hotfixes off\n `main`. If a linked worktree has that branch checked out, the tool refuses and names the path.\n- **Jira configured.** Every release gets a matching fix version, so `JIRA_BASE_URL`,\n `JIRA_EMAIL`, `JIRA_PROJECT_ID` and `JIRA_TOKEN` (or `JIRA_API_TOKEN`) must be in the\n environment \u2014 load them with `ik env-load` and source the file it returns. The check runs before\n anything is cut.\n\n**You do not have to already be on the base branch.** The tool runs `git fetch origin`,\n`git switch <base>` and `git pull --ff-only` itself. That is a real side effect on the human's\ncheckout: say so before call 2.\n\n## 2. The two-call confirm protocol\n\n`release-create` is gated. **The first call never executes anything.**\n\n**Call 1** \u2014 send the real arguments, with no `confirm` and no `confirmToken`. The result is a gate\npayload, `{\"status\": \"confirmation_required\", ...}`, and it carries `\"isError\": true`.\n\n**That `isError` does not mean the call failed.** It is set because the payload is a gate rather\nthan the tool's declared output. Nothing was created, nothing was pushed, nothing was switched. An\nagent that reads it as a failure \u2014 and gives up, or retries, or falls back to `Bash` \u2014 has skipped\nthe human approval this protocol exists for. Do none of those.\n\nThe gate payload carries two things you need:\n\n- `resolvedArgs` \u2014 exactly the arguments the server bound. **Show these to the human.** This is the\n approval moment; there is no other one.\n- `confirmToken` \u2014 an HMAC bound to the tool name and to those exact arguments. It expires 600\n seconds after it is minted.\n\n**Call 2** \u2014 repeat the **same arguments**, unchanged, plus `\"confirm\": true` and the\n`confirmToken` from call 1.\n\nChange any argument between the two calls and round 2 comes back as\n`{\"status\": \"confirmation_refused\", \"reason\": \"mismatch\"}`. That is terminal \u2014 it is not a second\ngate. The other reasons are `absent`, `malformed`, `mac`, `expired` and `bind`, and every one of\nthem recovers the same way: call again **without** `confirm` to mint a fresh gate, then re-call\nwith those arguments and the new token. Never retry call 2 with the old token.\n\nIf the human wants different arguments, go back to call 1 with the new arguments. Do not edit the\narguments and reuse the token \u2014 that is exactly what `mismatch` refuses.\n\n## 3. What goes in `releases`\n\n`releases` is an array with at least one entry. Every entry carries **exactly one** of:\n\n- `version` \u2014 a semver string such as `\"1.64.0\"`, or the literal token `\"next\"`.\n- `name` \u2014 a free-form kebab-case identifier such as `\"checkout-redesign\"`.\n\nThey are mutually exclusive and one is required. An entry with both, or with neither, is rejected\nby the schema before the tool runs.\n\nEach entry also carries `type` (`\"regular\"` or `\"hotfix\"`, default `\"regular\"`) and an optional\n`description`, which becomes the Jira fix version's description and feeds the PR body.\n\n### The `\"next\"` token\n\n`\"next\"` is version-only \u2014 a named release never auto-bumps. It resolves against the union of the\nremote `release/v*` branches on `origin` and the project's Jira fix versions:\n\n- `\"regular\"` bumps the minor and resets the patch: `1.63.2` becomes `1.64.0`.\n- `\"hotfix\"` bumps the patch on the highest minor: `1.63.2` becomes `1.63.3`.\n\nSeveral `\"next\"` entries in one call advance sequentially rather than all resolving to the same\nversion.\n\n**Be honest about what `\"next\"` could see.** The two sources are queried in parallel and a source\nthat fails is logged and dropped, not raised \u2014 if the Jira call fails, `\"next\"` is computed from\nthe remote branches alone and can land on a version Jira already knows about. If neither source\nyields a prior version the tool refuses and asks for an explicit one. When the exact number\nmatters, pass the semver instead of the token.\n\n### Reading `$ARGUMENTS`\n\nThe `/infra-kit:release-create` command hands you `$ARGUMENTS` verbatim, and its argument hint is\n`[--hotfix] [--desc <text>] [<version|name>]`. **Those two flags are conventions of this command, not\nCLI flags** \u2014 `infra-kit release create` accepts neither, and the tool takes neither. They exist so a\nhuman can type the whole request on one line, and it is your job to translate them:\n\n- `--hotfix` \u2192 `type: \"hotfix\"` on every entry you build. Its absence means `\"regular\"`.\n- `--desc <text>` \u2192 `description` on the entry. The text runs to the end of the argument string.\n- The bare token \u2192 `version` when it is a semver or the literal `next`, `name` when it is kebab-case.\n\nSo `--hotfix --desc \"Card expiry fix\" 1.63.3` is one entry:\n`{version: \"1.63.3\", type: \"hotfix\", description: \"Card expiry fix\"}`.\n\nIf `$ARGUMENTS` is empty, ask the human what to cut rather than guessing a version \u2014 and read the\n`\"next\"` caveats above before offering it.\n\n**Precedence, when a form is also involved.** If the server answers with an argument form and the\nhuman edits it, **the form wins field by field wherever the human supplied a value, and the values\nyou parsed from `$ARGUMENTS` win everywhere else.** A human who typed `--hotfix` and then picked\n`regular` in the form gets `regular` \u2014 they saw the field and changed it. A human who typed\n`--hotfix` and left `type` untouched gets `hotfix`. Never rebuild the entry from the form alone: that\nconverts every untouched field into a silent overwrite by a value the human never saw.\n\n### Batches\n\nOne call may create several releases, but **all entries must share the same `type`**. Regular and\nhotfix branch off different bases, so a mixed batch is rejected \u2014 cut them in separate\ninvocations.\n\nA batch does not stop at the first failure. Each entry is attempted and the result reports\n`successCount`, `failureCount`, `createdBranches` and `failedReleases`. Read all four before\ntelling the human the release was created: partial success is a normal outcome here.\n\n## 4. What one release does\n\nPer entry, in order: fetch and switch to the base branch, cut `release/v<semver>` (or\n`release/<name>`), open a GitHub release PR, and create or reuse the Jira fix version (`v<semver>`\nor `<name>`).\n\nTwo consequences worth stating before call 2:\n\n- An existing fix version is **reused**, and a `description` that differs is written through to it,\n so the PR and the fix version cannot disagree.\n- A fix version that is already released or archived is **refused**, not reused. The human either\n picks a different version or un-releases it in Jira.\n\n## 5. What not to do\n\n- Do not work around a refusal with `git` or `gh`. Every refusal in section 1 is a state only the\n human can clear.\n- Do not invent a list of candidate versions for the human. Pass `\"next\"` and let the server\n resolve it from the real branches and fix versions, or ask for the exact semver.\n- Do not read `isError: true` on a `confirmation_required` payload as a failure. See section 2.\n", "# setup \u2014 bringing a machine to a working infra-kit\n\nThe tool is `mcp__infra-kit__setup`. Everything below is about calling that tool.\n\nThe same code runs behind `infra-kit setup` in a terminal, so this body describes both spellings: the\nCLI flag first, the tool field it sets second. They are one implementation, not two.\n\nDo not shell out. A `Bash` call running `brew install`, `curl \u2026 | bash` or the writers in section 1\nreproduces none of the refusals in section 3 and bypasses the confirm gate in section 4 \u2014 which is the\nonly place a human approves an install.\n\n**If all you want is to know what this machine looks like, call `doctor` instead.** It reports the same\nfive tools plus the rest of the setup, mutates nothing, and raises no confirmation prompt. `infra-kit setup` is\nthe write path; `doctor` is the read path, and they are separate tools precisely so that asking a\nquestion does not cost an approval.\n\n## 1. What one call does, in order\n\nTwo halves, and **both always run**. Neither short-circuits the other: an init-half failure is recorded\nand the dependency half still runs.\n\n### Step 1 \u2014 the init half: local, offline, additive, near-instant\n\nIn this order:\n\n1. the managed block in `~/.zshrc` \u2014 the shell integration\n2. the four config migrations, in their recorded order\n3. the user-global config seed\n4. the repo's agent-instruction files (`CLAUDE.md` guidance blocks) \u2014 **non-fatal**; a repo it cannot\n resolve is warned about, not failed on\n5. the git-root resolution for writes, warning when the two root gates disagree\n6. the Claude Code plugin pointer \u2014 `.claude/settings.json`, `.mcp.json`, and the plugin install\n7. the per-project config reseed\n8. a warning when `$SHELL` is not zsh\n\nEvery writer here is additive and never overwrites. Nothing in this half installs software and nothing\nreaches the network.\n\n**It runs first deliberately.** Step 1.6 is what makes the MCP surface usable at all, so it must not sit\nbehind a network converge that can be slow or fail.\n\n### Step 2 \u2014 the dependency converge\n\nFive tools, serially, in registry order: **brew, aws, gh, doppler, portless**. Serial and ordered\nbecause the recipes have prerequisites \u2014 `gh` and `doppler` both need `brew`, and doppler's own two\nsteps (gnupg, then the tap) must not interleave with another tool's.\n\nPer tool: install it when it is absent, update it when it is present, skip it when its manager is not\none infra-kit manages. A recipe the risk predicate refuses is **printed, not run** \u2014 section 3.\n\n### Step 3 \u2014 one combined summary\n\nOne line per tool, then the exact argv for every refused recipe, then the `source ~/.zshrc` reminder\nlast of all.\n\n### How to read the result\n\n- `init` \u2014 one entry per step above, each with an `outcome` of `written`, `unchanged`, `skipped` or\n `warned`, and the same message a human would have read.\n- `tools` \u2014 one entry per requested dependency: `action` (`installed`, `updated`, `skipped`, `refused`\n or `failed`), the `before` state, the `commands` that were run or would have been, and a one-line\n `detail`.\n- `converged` \u2014 whether the dependency step could act at all. `false` under `skipTools`.\n- `changed` \u2014 whether anything was installed or updated.\n- `allSucceeded` \u2014 whether no tool **failed**. **A refusal is not a failure**, so this stays `true` when\n a recipe was printed instead of run.\n\nThe process exits non-zero when either half hard-failed. **Do not read a success as \"everything is\ninstalled\"** \u2014 read `tools[].action`, and tell the human about every `refused` entry.\n\n## 2. The flags, and what each one narrows\n\nThe default \u2014 no flag, no field \u2014 converges all five tools.\n\n- `--tools <ids...>` \u2192 `tools: [\"gh\", \"doppler\"]`. Converge **only those ids**. Same behaviour per\n tool, smaller set. The ids are `brew`, `aws`, `gh`, `doppler` and `portless`.\n- `--update [ids...]` \u2192 `mode: \"update\"`. **Never installs.** A tool that is present is updated; a tool\n that is absent is reported `skipped` with the reason, and its install recipe is not run. Given ids, it\n also narrows the set, so `--update gh` is \"update gh, and nothing else, and only if it is there\".\n- `--skip-tools` \u2192 `skipTools: true`. A **read-only probe**. The init half still runs \u2014 it is local and\n additive \u2014 and then each tool is reported with what it needs and the exact argv that would fix it.\n Nothing is installed and nothing is updated.\n\n**`--skip-tools` with `--tools` or `--update` is a usage error, not a precedence rule.** Every\nprecedence answer is wrong: honouring `--skip-tools` would ignore a set the caller chose, and honouring\nthe other would install software the caller asked not to install. The call is refused instead.\n\n## 3. Recipes that are printed rather than run\n\nWhether a recipe may run unattended is **computed**, not a per-recipe flag someone set. Two of the four\nconjuncts are static and are applied to every recipe unconditionally:\n\n- **`needs-sudo`** \u2014 the recipe escalates privilege.\n- **`fetches-network-script`** \u2014 the recipe pipes a script fetched at run time into a shell.\n\nTwo are detection-based: **`manager-absent`** (the package manager the recipe drives is not on this\nhost) and **`manager-mismatch`** (a different manager owns the binary \u2014 running `brew upgrade` against\nan npm install, or the vendor's self-updater against a Homebrew keg, is the split-brain infra-kit\nrefuses for itself).\n\n**Two recipes fail the static conjuncts, and they are the two bootstraps:**\n\n- **the Homebrew bootstrap** \u2014 `/bin/bash -c \"$(curl -fsSL \u2026/install.sh)\"` \u2014 fails **both**: it pipes a\n network-fetched script **and** needs sudo on macOS.\n- **the first AWS CLI install** \u2014 `curl -fsSL https://awscli.amazonaws.com/v2/install.sh | bash` \u2014\n needs no sudo (it installs under `$HOME`), but it is still a network-fetched script.\n\nBoth are the tools' own documented installers, and neither is a bug to route around. They are refused\n**by computation, applied before any detection runs**, which is what makes the refusal trustworthy: it\ncannot be widened by a probe getting something wrong, only narrowed.\n\nOnce Homebrew exists, `gh` and `doppler` install through it and run unattended; once the AWS CLI exists,\n`aws update` is a plain no-sudo recipe and runs. The refusals are a first-install cost, not permanent.\n\nWhat to do with one: the entry's `commands` array is the exact argv, one string per step. **Show it to\nthe human and let them run it themselves.** Do not reconstruct it as a `Bash` call \u2014 that is the same\nunattended `sudo` and the same piped script, with the control removed.\n\n## 4. The confirm gate\n\n`mcp__infra-kit__setup` is gated, and **both gates fire on every call \u2014 `skipTools` included**.\n\n**Call 1** \u2014 send the real arguments, with no `confirm` and no `confirmToken`. The result is a gate\npayload, `{\"status\": \"confirmation_required\", \u2026}`, carrying `\"isError\": true`.\n\n**That `isError` does not mean the call failed.** Nothing was written and nothing was installed. An\nagent that reads it as a failure \u2014 and gives up, or retries, or falls back to `Bash` \u2014 has skipped the\nhuman approval this protocol exists for. Do none of those. Show the human `resolvedArgs`; that is the\napproval moment.\n\n**Call 2** \u2014 repeat the **same arguments**, unchanged, plus `\"confirm\": true` and the `confirmToken`\nfrom call 1. Change any argument between the two and round 2 comes back\n`{\"status\": \"confirmation_refused\", \"reason\": \"mismatch\"}`, which is terminal, not a second gate. Every\nrefusal reason recovers the same way: call again **without** `confirm` for a fresh gate, then re-call\nwith the new token. Never retry with the old one.\n\nThe tool also carries `anthropic/requiresUserInteraction`, so the host prompts a human even where an\nallow rule would otherwise skip it. One install therefore costs two prompts. That is intended: neither\ngate substitutes for the other, and neither substitutes for the computed refusals in section 3, which\nare the only control that ships inside the CLI itself.\n\n## 5. There is no `init` command\n\nIt was removed outright. The binary rejects the name \u2014 an instruction that still says to run it fails\nat the parser rather than quietly doing something else.\n\nRepos can still say it. A consumer's committed CLAUDE.md block is rewritten only from inside that repo,\nso a repo upgrades the global CLI without its own text changing and can sit arbitrarily far behind. So\nwhen a human asks for \"init\", or a repo's instructions still name it:\n\n- Run **`infra-kit setup`** if they want the tools installed or updated too.\n- Run **`infra-kit setup --skip-tools`** (`skipTools: true`) for the additive local writes with nothing\n installed. That is the whole reason the flag exists: without it, removing `init` would have deleted a\n capability rather than renamed one.\n\nSay which one you chose. Running `infra-kit audit --fix` inside that repo rewrites the stale block.\n\n## 6. What not to do\n\n- Do not work around a refusal in section 3 with `Bash`. The refusal is the control.\n- Do not read `isError: true` on a `confirmation_required` payload as a failure. See section 4.\n- Do not report success from the exit status alone. Read every `tools[].action` and name the refusals.\n- Do not call this tool to answer a question. Call `doctor` \u2014 it changes nothing and prompts no one.\n", "// This import list must stay flat, static and explicit. Esbuild bundles the CLI\n// into a single file and only inlines specifiers it can see statically \u2014 a dynamic\n// `import()`, a glob or a computed path resolves to nothing it can follow, the\n// specifier survives into `dist`, and node throws `ERR_UNKNOWN_FILE_EXTENSION` on a\n// consumer's machine. Every test in this package runs from `src/`, where such a\n// refactor still works, so nothing but the bundle guard would catch it.\n//\n// The `?raw` suffix \u2014 not a bare `.md` \u2014 is the one spelling correct in all three\n// toolchains this package builds with (see src/md.d.ts).\nimport releaseCreate from '../../resources/workflow/release-create.md?raw'\nimport setup from '../../resources/workflow/setup.md?raw'\n\n/** Name of a workflow procedure, matching the exposed MCP tool it is the procedure for. */\nexport type WorkflowKey = 'release-create' | 'setup'\n\n/**\n * The workflow procedures, inlined at build time.\n *\n * Every body is registered as an MCP resource under `infra-kit://workflow/<key>`, which is the\n * channel an AGENT can reach: an agent cannot fetch a prompt, because a prompt is a host UI\n * affordance a human picks out of the `/` menu.\n *\n * `release-create` is additionally registered as a prompt of that name, so the human half exists\n * too \u2014 one constant for both, so the prose cannot drift between them without two edits. `setup`\n * deliberately has no prompt: its human surface is the `/infra-kit:setup` plugin command, and a\n * second `/` entry carrying the same text would be a duplicate of it rather than a second reader.\n *\n * `trimEnd()` because prettier gives every `.md` a trailing newline and every wire form carries the\n * body verbatim; normalizing here keeps the channels byte-identical by construction.\n */\nexport const WORKFLOW_BODIES: Readonly<Record<WorkflowKey, string>> = {\n 'release-create': releaseCreate.trimEnd(),\n setup: setup.trimEnd(),\n}\n", "import type { McpServer } from '@modelcontextprotocol/server'\n\nimport { WORKFLOW_BODIES } from '../workflow-bodies'\n\n/**\n * Register infra-kit's MCP prompts \u2014 the human half of the two-channel pair.\n *\n * A prompt is a host UI affordance: a human picks it out of the `/` menu, and an agent cannot\n * fetch one on its own initiative. So every body registered here is ALSO registered as a resource\n * (`src/mcp/resources`), from the same `WORKFLOW_BODIES` constant, which is what an agent reads.\n * Two channels, one constant \u2014 the prose cannot drift between them without two edits.\n */\nexport const initializePrompts = async (server: McpServer) => {\n server.registerPrompt(\n 'release-create',\n {\n title: 'Cut a release',\n description:\n 'The procedure for cutting a release with infra-kit: preconditions, the two-call confirm ' +\n 'protocol, and what \"next\" resolves against.',\n // `argsSchema` is OMITTED, deliberately. The SDK types it optional, and this is the one shape\n // on which a `prompts/get` carrying no `arguments` cannot throw: a bare `z.object({\u2026})`\n // argsSchema rejects an omitted `arguments`, and the obvious `.default({})` repair voids\n // `.shape`. Giving this prompt a `version` argument means solving that first \u2014 a named\n // follow-up, not a silent gap. The consequence is that the prompt takes nothing and the\n // human types the version in chat afterwards.\n },\n // No-args callback: the SDK's prompt-callback types degrade to `(ctx) => \u2026` when no argsSchema\n // is declared, and a zero-parameter function satisfies that.\n () => {\n return {\n messages: [\n {\n role: 'user' as const,\n content: { type: 'text' as const, text: WORKFLOW_BODIES['release-create'] },\n },\n ],\n }\n },\n )\n}\n", "import type { McpServer } from '@modelcontextprotocol/server'\n\nimport type { InfraKitConfig } from 'src/lib/infra-kit-config'\nimport { getInfraKitConfig } from 'src/lib/infra-kit-config'\n\nimport type { WorkflowKey } from '../workflow-bodies'\nimport { WORKFLOW_BODIES } from '../workflow-bodies'\nimport type { DevContextSnapshot } from './dev-context'\nimport { readDevContext } from './dev-context'\n\n/** Stable URI of the merged-config resource. */\nexport const CONFIG_RESOURCE_URI = 'infra-kit://config'\n\n/** Stable URI of the dev-context resource. */\nexport const DEV_CONTEXT_RESOURCE_URI = 'infra-kit://dev-context'\n\n/**\n * Stable URI of the `release-create` procedure.\n *\n * This is the agent-reachable half of the pair: the same body is also registered as a prompt\n * (`src/mcp/prompts`), because an agent can read a resource but cannot fetch a prompt.\n */\nexport const RELEASE_CREATE_WORKFLOW_URI = 'infra-kit://workflow/release-create'\n\n/**\n * Stable URI of the `setup` procedure.\n *\n * Resource-only, unlike `release-create`: `setup`'s human channel is the `/infra-kit:setup` plugin\n * command, so the prompt half would duplicate it rather than reach a second reader. It ships in the\n * CLI and not in the plugin because `scripts/check-workflow-resource-published.mjs` refuses to let a\n * plugin command merge until the PUBLISHED CLI answers `resources/list` with the URI its body names.\n */\nexport const SETUP_WORKFLOW_URI = 'infra-kit://workflow/setup'\n\n/**\n * The two disk reads the resources need, injected so the registration is unit-testable without touching\n * the filesystem or the config-loader's mtime cache. Production uses the real readers by default.\n */\nexport interface ResourceDeps {\n /** Loads the merged `infra-kit.json` (all override layers applied). May throw when no config exists. */\n loadConfig: () => Promise<InfraKitConfig>\n /** Reads the dev-context fragments `infra-kit dev` last wrote. Never throws; absent session \u21D2 empty. */\n readDevContext: () => DevContextSnapshot\n}\n\nconst defaultDeps: ResourceDeps = {\n loadConfig: getInfraKitConfig,\n readDevContext: () => {\n return readDevContext()\n },\n}\n\n/** Serialize a resource body as pretty JSON text \u2014 the wire form every MCP client can read. */\nconst jsonResource = (uri: string, value: unknown): { contents: { uri: string; mimeType: string; text: string }[] } => {\n return { contents: [{ uri, mimeType: 'application/json', text: JSON.stringify(value, null, 2) }] }\n}\n\n/**\n * Register one workflow procedure at its URI.\n *\n * A helper rather than a second copy of the registration block: the two bodies differ only in their\n * key, URI and blurb, and a hand-copied handler is exactly where a second workflow would quietly get\n * the FIRST one's text \u2014 a drift no `resources/list` assertion would catch, because the URI would\n * still be listed.\n *\n * No dep injection and no `async`: the body is a build-time constant, so there is nothing to read,\n * nothing to fail, and nothing a test would need to stub.\n */\nconst registerWorkflow = (\n server: McpServer,\n workflow: { key: WorkflowKey; uri: string; title: string; description: string },\n): void => {\n server.registerResource(\n `infra-kit-workflow-${workflow.key}`,\n workflow.uri,\n { title: workflow.title, description: workflow.description, mimeType: 'text/markdown' },\n (uri) => {\n return { contents: [{ uri: uri.toString(), mimeType: 'text/markdown', text: WORKFLOW_BODIES[workflow.key] }] }\n },\n )\n}\n\n/**\n * Register infra-kit's READ-ONLY MCP resources so an agent can inspect the repo's config and dev state\n * without calling a tool (or mutating anything):\n *\n * - `infra-kit://config` \u2014 the merged `infra-kit.json` an agent would run against.\n * - `infra-kit://dev-context` \u2014 what `infra-kit dev` last wrote (backends up, ports, freshness); an\n * absent dev session resolves to an empty `session: 'none'` payload, NOT\n * an error.\n *\n * Both handlers are side-effect free and swallow their reader's failure into an `{ error }` payload rather\n * than throwing, so a bad on-disk config can never crash the long-lived MCP server.\n */\nexport const initializeResources = async (server: McpServer, deps: ResourceDeps = defaultDeps) => {\n server.registerResource(\n 'infra-kit-config',\n CONFIG_RESOURCE_URI,\n {\n title: 'infra-kit config',\n description: 'The merged infra-kit.json configuration (all override layers applied). Read-only.',\n mimeType: 'application/json',\n },\n async (uri) => {\n try {\n return jsonResource(uri.toString(), await deps.loadConfig())\n } catch (error) {\n return jsonResource(uri.toString(), { error: error instanceof Error ? error.message : String(error) })\n }\n },\n )\n\n server.registerResource(\n 'infra-kit-dev-context',\n DEV_CONTEXT_RESOURCE_URI,\n {\n title: 'infra-kit dev context',\n description:\n 'What `infra-kit dev` last wrote: backends currently up, their ports/origins, and fragment ' +\n 'freshness. Read-only and advisory (no liveness probe). No active session resolves to an empty payload.',\n mimeType: 'application/json',\n },\n // The SDK's `ReadResourceCallback` accepts a synchronous return; the disk read is sync, so no `async`.\n (uri) => {\n return jsonResource(uri.toString(), deps.readDevContext())\n },\n )\n\n registerWorkflow(server, {\n key: 'release-create',\n uri: RELEASE_CREATE_WORKFLOW_URI,\n title: 'release-create procedure',\n description:\n 'How to cut a release with the release-create tool: the preconditions, the two-call confirm ' +\n 'protocol, and what the \"next\" token actually resolves against. Read this before calling ' +\n 'mcp__infra-kit__release-create.',\n })\n\n registerWorkflow(server, {\n key: 'setup',\n uri: SETUP_WORKFLOW_URI,\n title: 'setup procedure',\n description:\n 'How to set a machine up with the setup tool: the ordered local writes, then the dependency ' +\n 'converge; what tools/mode/skipTools each narrow; which recipes are printed instead of run and ' +\n 'why; and what to run when a repo still tells you to set it up some older way. Read this before ' +\n 'calling mcp__infra-kit__setup.',\n })\n}\n", "import type { McpServer } from '@modelcontextprotocol/server'\nimport { z } from 'zod'\n\nimport { getExposedMcpTools } from 'src/lib/command-catalog'\nimport { createToolHandler } from 'src/lib/tool-handler'\nimport type { ToolCallContext } from 'src/lib/tool-handler'\n\nexport const initializeTools = async (server: McpServer) => {\n // The registered tool set is derived from the single command catalog, filtered\n // by its explicit `mcpExposed` allowlist. Nothing is added or subtracted here \u2014\n // an exposure decision that is not visible in the catalog is invisible to the\n // catalog's own gates, so this loop stays a plain projection of it.\n //\n // `doctor` IS exposed, and it is the one entry whose safety does not come from\n // those gates: it is `mutating: false` (read-only, ungated) purely because\n // `doctorMcpTool` is nullary and forwards nothing, so the CLI's `--fix` write path\n // is unreachable from MCP by construction. The P1 fail-closed gate only inspects\n // `mutating && mcpExposed` entries, so it cannot see that claim go false \u2014 the\n // guard is `src/commands/doctor/__tests__/doctor-mcp-surface.test.ts`, not this list.\n for (const tool of getExposedMcpTools()) {\n server.registerTool(\n tool.name,\n {\n // Display label; hosts fall back to `name` when absent. TOP-LEVEL `title` only \u2014\n // `annotations.title` is deliberately left unset, because some hosts prefer the latter when\n // present and two titles are a divergence waiting to happen.\n title: tool.title,\n description: tool.description,\n // Wrapped HERE, at the single `registerTool` call site, rather than in the 27\n // `defineMcpTool` definitions or in `CatalogMcpTool`. SDK v2 offers a deprecated\n // raw-shape overload that would auto-wrap, but it types the shape as its own\n // `ZodRawShape` (`Record<string, ZodType>`) while zod 4's `z.ZodRawShape` is the\n // looser `Readonly<{ [k: string]: $ZodType }>` \u2014 `$ZodType` lacks `def`/`type`/`_def`\n // and ~42 other members, so the raw shape is NOT assignable and the overload is\n // unreachable for us. `z.object()` closes that gap and lands on v2's PREFERRED\n // (non-deprecated) Standard Schema overload, since a `ZodObject` carries `~standard`.\n //\n // Wrapping here keeps the authoring shape in all 27 `src/commands/**` definitions\n // unchanged and leaves `defineMcpTool`'s `z.infer<z.ZodObject<TIn>>` handler typing\n // untouched \u2014 the migration needs zero edits under `src/commands/`.\n //\n // Gated tools additionally accept `confirmToken` here, at the boundary: `z.object` strips\n // undeclared keys, so without this the round-2 token the gate hands out would never reach\n // `createToolHandler`'s verification and every confirmation would be refused as absent.\n inputSchema: z.object(\n tool.requiresHumanConfirm === true ? withConfirmToken(tool.inputSchema) : tool.inputSchema,\n ),\n outputSchema: z.object(tool.outputSchema),\n // ADVISORY protocol hints, derived in the catalog from `mutating`. The authority for\n // destructive operations remains `requiresHumanConfirm` + `lib/tool-handler`'s confirm gate;\n // nothing in that gate reads these, and the spec forbids a client treating them as security.\n annotations: tool.annotations,\n // `anthropic/requiresUserInteraction` rides here on `setup`, the one tool that carries it. It is the\n // only human gate on the MCP path: the host prompts on EVERY call, in `bypassPermissions` too,\n // and an allow rule cannot skip it. Undefined for every other tool, which is the same as absent.\n _meta: tool.meta,\n },\n // The SDK's second callback argument (session, request state, signal) is threaded through so\n // the handler signature lands once; the gate binds tokens by tool name and needs none of it.\n wrapForRegistration(\n createToolHandler({\n toolName: tool.name,\n handler: tool.handler,\n requiresHumanConfirm: tool.requiresHumanConfirm,\n formProvider: tool.formProvider,\n // Capabilities are only knowable after initialize, and this handler is built at\n // registration \u2014 so the probe is injected as a CLOSURE over the live server rather than as\n // a snapshot taken here, which would be `undefined` for the life of the process.\n // `server.server` is the underlying protocol instance the McpServer facade wraps; the\n // integration test in `mcp/__tests__/server.test.ts` reaches through it the same way.\n //\n // `getClientCapabilities()` is deprecated in favour of `ctx.mcpReq.envelope`, which is\n // backfilled per request on 2026-era instances. Kept deliberately: the accessor remains\n // functional, and the migration is a one-line change HERE rather than inside the handler.\n getClientCapabilities: () => {\n return server.server.getClientCapabilities()\n },\n }),\n ),\n )\n }\n}\n\nconst withConfirmToken = (shape: z.ZodRawShape): z.ZodRawShape => {\n return {\n ...shape,\n confirmToken: z\n .string()\n .optional()\n .describe('Round-2 only: the token returned by the round-1 gate, proving the arguments are unchanged.'),\n }\n}\n\nconst wrapForRegistration = (handle: ReturnType<typeof createToolHandler>) => {\n return (params: unknown, ctx: ToolCallContext) => {\n return handle(params, ctx)\n }\n}\n", "import { createRequestStateCodec } from '@modelcontextprotocol/server'\nimport { randomBytes } from 'node:crypto'\n\n/**\n * @fileoverview\n *\n * Round-1 gate payload \u2192 round-2 binding for the destructive-op confirm gate.\n *\n * Round 1 mints a `confirmToken` over the tool name and the CANONICAL form of the\n * arguments; round 2 must present that token together with the same arguments, or\n * the tool does not run.\n *\n * Without the binding an agent could obtain a gate for `env:dev` and re-call with\n * `confirm:true` for `env:prod` \u2014 the substitution hole\n * `docs/infra-kit-slash-commands-plan.md` \u00A70.12 records.\n */\n\n/** Keys the gate itself owns; they are never part of the signed argument set. */\nconst GATE_KEYS = new Set(['confirm', 'confirmToken'])\n\n/** Default token lifetime. Long enough for a human to read the gate, short enough to bound replay. */\nexport const CONFIRM_TOKEN_TTL_SECONDS = 600\n\n/** What a round-1 gate signs. `args` is the canonical JSON of the tool arguments. */\nexport interface ConfirmPayload {\n args: string\n}\n\n/** The subset of the SDK codec the gate uses; `ctx` is the tool name the token is bound to. */\nexport interface ConfirmCodec {\n mint: (payload: ConfirmPayload, toolName: string) => Promise<string>\n verify: (token: string, toolName: string) => Promise<ConfirmPayload>\n}\n\nexport type ConfirmRefusal = 'absent' | 'malformed' | 'mac' | 'expired' | 'bind' | 'mismatch'\n\ninterface BindContext {\n toolName: string\n}\n\ntype SdkBindContext = NonNullable<Parameters<ReturnType<typeof createRequestStateCodec>['mint']>[1]>\n\nconst asBindContext = (toolName: string): SdkBindContext => {\n return { toolName } as unknown as SdkBindContext\n}\n\nexport type ConfirmVerdict = { ok: true } | { ok: false; reason: ConfirmRefusal }\n\nconst sortKeysDeep = (value: unknown): unknown => {\n if (Array.isArray(value)) {\n return value.map(sortKeysDeep)\n }\n\n if (value !== null && typeof value === 'object') {\n const record = value as Record<string, unknown>\n\n return Object.fromEntries(\n Object.keys(record)\n .sort()\n .map((key) => {\n return [key, sortKeysDeep(record[key])]\n }),\n )\n }\n\n return value\n}\n\n/**\n * Stable JSON for an argument object: keys sorted recursively, arrays kept in order.\n * `JSON.stringify` is key-order-sensitive and round-2 arguments are re-serialized by\n * the client in whatever order the host emits, so signing or verifying the raw form\n * would refuse legitimate confirmations intermittently, per client.\n *\n * @example\n * canonicalArgs({ b: 2, a: { d: 1, c: [2, 1] } }) // => '{\"a\":{\"c\":[2,1],\"d\":1},\"b\":2}'\n */\nexport const canonicalArgs = (args: unknown): string => {\n return JSON.stringify(sortKeysDeep(args))\n}\n\n/**\n * The tool's own arguments: the incoming params with the gate's keys removed. This is\n * the ONE filter both rounds share, so round 2 (which carries `confirm` and\n * `confirmToken` on top of round 1's arguments) verifies against exactly what round 1 signed.\n *\n * @example\n * stripGateKeys({ env: 'dev', confirm: true, confirmToken: 'v1.x.y' }) // => { env: 'dev' }\n */\nexport const stripGateKeys = (params: unknown): Record<string, unknown> => {\n if (typeof params !== 'object' || params === null) return {}\n\n return Object.fromEntries(\n Object.entries(params as Record<string, unknown>).filter(([key]) => {\n return !GATE_KEYS.has(key)\n }),\n )\n}\n\n/**\n * Build the gate's codec. The key is per-process and random by default: one stdio\n * server process serves both rounds of every confirmation, so a key that dies with\n * the process is sufficient \u2014 that stops being true on any HTTP transport, where the\n * key would have to be shared across workers. Tests inject a short `ttlSeconds` or a\n * second key to exercise the expiry and tamper refusals.\n *\n * @example\n * const codec = createConfirmCodec()\n * const token = await codec.mint({ args: '{\"env\":\"dev\"}' }, 'env-clear')\n * await codec.verify(token, 'env-clear') // => { args: '{\"env\":\"dev\"}' }\n */\nexport const createConfirmCodec = (options: { key?: Uint8Array; ttlSeconds?: number } = {}): ConfirmCodec => {\n // The SDK types the bind context as its own `ServerContext`; the gate only ever binds to the\n // tool name, so a one-field object stands in for it on both mint and verify.\n const codec = createRequestStateCodec<ConfirmPayload>({\n key: options.key ?? randomBytes(32),\n ttlSeconds: options.ttlSeconds ?? CONFIRM_TOKEN_TTL_SECONDS,\n bind: (ctx) => {\n return (ctx as unknown as BindContext).toolName\n },\n })\n\n return {\n mint: (payload, toolName) => {\n return codec.mint(payload, asBindContext(toolName))\n },\n verify: (token, toolName) => {\n return codec.verify(token, asBindContext(toolName))\n },\n }\n}\n\nlet defaultCodec: ConfirmCodec | undefined\n\n/** The process-wide codec every registered tool shares, created on first use. */\nexport const getDefaultConfirmCodec = (): ConfirmCodec => {\n defaultCodec ??= createConfirmCodec()\n\n return defaultCodec\n}\n\nconst readConfirmToken = (params: unknown): string | undefined => {\n if (typeof params !== 'object' || params === null) return undefined\n\n const { confirmToken } = params as { confirmToken?: unknown }\n\n return typeof confirmToken === 'string' ? confirmToken : undefined\n}\n\nconst REFUSALS_FROM_CODEC: ReadonlySet<ConfirmRefusal> = new Set(['malformed', 'mac', 'expired', 'bind'])\n\nconst codecRefusal = (error: unknown): ConfirmRefusal => {\n const message = error instanceof Error ? error.message : ''\n\n return REFUSALS_FROM_CODEC.has(message as ConfirmRefusal) ? (message as ConfirmRefusal) : 'malformed'\n}\n\n/**\n * Mint the round-1 token for `params` (gate keys excluded), bound to `toolName`.\n *\n * @example\n * await mintConfirmToken(codec, 'env-clear', { env: 'dev' }) // => 'v1.<payload>.<mac>'\n */\nexport const mintConfirmToken = async (codec: ConfirmCodec, toolName: string, params: unknown): Promise<string> => {\n return codec.mint({ args: canonicalArgs(stripGateKeys(params)) }, toolName)\n}\n\n/**\n * Decide whether a round-2 call may run. Absence is a refusal in its own right \u2014\n * \"verify only when a token is present\" would let an agent omit the token and\n * substitute arguments freely, reopening the hole this module closes.\n *\n * @example\n * await verifyConfirmToken(codec, 'env-clear', { env: 'prod', confirm: true, confirmToken })\n * // => { ok: false, reason: 'mismatch' } (the token was minted for env:dev)\n */\nexport const verifyConfirmToken = async (\n codec: ConfirmCodec,\n toolName: string,\n params: unknown,\n): Promise<ConfirmVerdict> => {\n const token = readConfirmToken(params)\n\n if (token === undefined) return { ok: false, reason: 'absent' }\n\n let payload: ConfirmPayload\n\n try {\n payload = await codec.verify(token, toolName)\n } catch (error) {\n return { ok: false, reason: codecRefusal(error) }\n }\n\n if (payload.args !== canonicalArgs(stripGateKeys(params))) return { ok: false, reason: 'mismatch' }\n\n return { ok: true }\n}\n", "import { acceptedContent, inputRequired, inputResponse } from '@modelcontextprotocol/server'\nimport type { InputRequiredResult } from '@modelcontextprotocol/server'\nimport type { z } from 'zod'\n\nimport type { ArgumentFormProvider } from 'src/types'\n\n/**\n * @fileoverview\n *\n * The argument-form half of the MCP chokepoint: build the form, read what came back, and refuse\n * anything that came back narrower than it went out.\n *\n * Every function here is TOTAL. The form path runs on two things the handler does not control \u2014 a\n * provider written by someone else, and content the client sent back \u2014 and the SDK validates\n * neither (\"treat them as untrusted input\", `\u00ABd.mts\u00BB:1406`, `:1500`, `:2114`). A throw escaping\n * from here would leave the chokepoint's catch returning a TOOL ERROR on a call that was owed a\n * confirm gate, which is the failure this module exists to prevent. So every provider call and\n * every SDK call below is wrapped, and every failure mode \u2014 rejection, deadline, `null`, a thrown\n * `TypeError` \u2014 reads as the same single answer: `null`, meaning \"no form; fall to the gate\".\n */\n\n/**\n * The identifier the form is registered under in `inputRequests`, and the key the client's reply\n * comes back under in `ctx.mcpReq.inputResponses`. One key, one form, one round trip.\n */\n// Module-private on purpose. Both sides of the round trip are in this file \u2014 `buildArgumentForm`\n// registers under it, `readFormAction` and `readAcceptedArgs` read back under it \u2014 so the key is an\n// internal convention, not a contract anyone outside gets to depend on. Exporting it would invite a\n// caller to reach into `inputResponses` themselves and re-derive an action without the totality the\n// functions below are built to guarantee.\nconst FORM_KEY = 'args'\n\n/**\n * How long the chokepoint waits for a provider to produce a schema.\n *\n * A provider may reach the network (the release provider runs `git ls-remote` and a Jira fetch),\n * and those are `Promise.allSettled`-wrapped inside it \u2014 so they never FAIL, they only take as long\n * as they take. Without a ceiling a slow remote or an SSH key prompt turns `tools/call` into a call\n * that returns nothing at all: no form, no gate, no error. The deadline converts that into today's\n * behaviour instead.\n */\nexport const FORM_DEADLINE_MS = 3_000\n\n/**\n * The action a retried request carried for the form, with every non-elicitation response kind\n * folded into `missing`.\n *\n * `inputResponse` is documented TOTAL \u2014 a malformed or absent entry reads as `{kind:'missing'}` \u2014\n * and that is also where a key the SDK DROPPED lands: a dropped entry is removed from\n * `inputResponses` and only its name is kept in `droppedInputResponseKeys`, so it reaches us as an\n * absent key. That is why nothing here reads `droppedInputResponseKeys`: the drop is already\n * covered, as a `missing` that is not an accept.\n */\nexport type FormAction = 'accept' | 'decline' | 'cancel' | 'missing'\n\n/**\n * Read the form's action out of a retried request's responses.\n *\n * `InputResponseView` is a union and `action` exists only on its `kind:'elicit'` member, so the\n * narrowing is load-bearing rather than cosmetic.\n *\n * @example\n * readFormAction({ args: { action: 'decline' } }) // => 'decline'\n * readFormAction(undefined) // => 'missing'\n */\nexport const readFormAction = (responses: Record<string, unknown> | undefined): FormAction => {\n const view = inputResponse(responses, FORM_KEY)\n\n return view.kind === 'elicit' ? view.action : 'missing'\n}\n\n/**\n * Resolve `work`, or `null` if it rejects or outruns `ms`.\n *\n * Deliberately collapses rejection and timeout into the same value: the caller treats a provider\n * that failed and a provider that was too slow identically, because from the human's side both mean\n * \"no usable candidates were offered\".\n */\nconst withDeadline = async <T>(work: Promise<T>, ms: number): Promise<T | null> => {\n return new Promise<T | null>((resolve) => {\n const timer = setTimeout(() => {\n resolve(null)\n }, ms)\n\n work.then(\n (value) => {\n clearTimeout(timer)\n resolve(value)\n },\n () => {\n clearTimeout(timer)\n resolve(null)\n },\n )\n })\n}\n\n/**\n * The provider's schema for these arguments, or `null` on ANY failure: a rejection, a deadline\n * overrun, a synchronous throw before the promise is even returned, or the provider's own `null`.\n *\n * Called on BOTH rounds. Round 1 needs it to build the form; round 2 needs a schema to validate what\n * came back (\u00ABd.mts\u00BB:1406), and the round-2 `params` are the round-1 arguments the client echoed\n * back, so re-asking the provider reproduces it.\n */\n// The REBUILD on round 2 is deliberate, and it is not free. The two rounds are two separate\n// `tools/call` requests \u2014 round 2 is a re-entry, discriminated by `responses !== undefined` \u2014 so this\n// runs at most once per request and never twice within one. Across an accepted form that is two\n// builds, and for a provider that reaches the network (the release provider runs `git ls-remote` and\n// a Jira fetch) four calls where a single build would cost two. Both alternatives are worse.\n//\n// Reusing round 1's schema is not \"threading a value through\": nothing in this chokepoint survives a\n// request. The gate is stateless BY CONSTRUCTION \u2014 round 2 is trusted because it returns an HMAC the\n// server minted, not because the server remembered anything \u2014 so a reused schema needs either a\n// keyed server-side cache with its own eviction, or transport through the client. The second hands\n// the client the validator that is the ONLY thing checking the content it just sent. The first\n// re-introduces on a long-lived process exactly the per-request state `confirm-token.ts` was built\n// to avoid, to save two network calls per human decision.\n//\n// The rebuild does mean round 2 validates against a MOVED world: a choice that was legal when the\n// human made it can fail now, and they lose it. That is the correct direction to fail. A provider\n// enumerates candidates precisely because the current world constrains them \u2014 the release provider\n// lists the versions that already exist so a cut cannot collide \u2014 so honouring a stale selection\n// executes the collision the enumeration was there to prevent. The loss is not silent either: it\n// lands on the `formDiscarded` gate, which names the discard in a field AND in prose. Telling a\n// drift-caused failure apart from ordinary invalid content would need round 1's schema to compare\n// against, which is the cross-request state above \u2014 bought to reword a message, not to change an\n// outcome. `f14` in the tests pins the drifted round trip.\nconst trySchema = async (\n provider: ArgumentFormProvider,\n params: unknown,\n deadlineMs: number,\n): Promise<z.ZodObject<z.ZodRawShape> | null> => {\n try {\n return await withDeadline(provider.buildRequestedSchema(params), deadlineMs)\n } catch {\n return null\n }\n}\n\n/**\n * The form to send, or `null` meaning \"offer no form; fall to the gate\".\n *\n * The `elicit()` call is wrapped for a documented reason, not defensively: a Standard Schema it\n * cannot express as the restricted wire shape throws a `TypeError` BEFORE anything is sent\n * (\u00ABd.mts\u00BB:1404-1407) \u2014 a nested object property is enough \u2014 and a provider can arm that without\n * writing anything the type system objects to.\n *\n * @example\n * await buildArgumentForm(provider, { releases: [] }, 3000)\n * // => { resultType: 'input_required', inputRequests: { args: { method: 'elicitation/create', \u2026 } } }\n */\nexport const buildArgumentForm = async (\n provider: ArgumentFormProvider,\n params: unknown,\n deadlineMs: number,\n): Promise<InputRequiredResult | null> => {\n const schema = await trySchema(provider, params, deadlineMs)\n\n if (schema === null) return null\n\n try {\n return inputRequired({\n inputRequests: {\n [FORM_KEY]: inputRequired.elicit({ message: provider.message, requestedSchema: schema }),\n },\n })\n } catch {\n return null\n }\n}\n\n/**\n * The accepted form content, VALIDATED and merged over the round-1 arguments \u2014 or `null` when it\n * cannot be trusted, which the caller reads as \"gate with the round-1 arguments\".\n *\n * Three untrusted steps, each wrapped: the provider's schema, the SDK's schema-aware validation of\n * the content, and the provider's own merge.\n */\n// `acceptedContent`'s schema-aware overload is used because nothing between the client and here has\n// checked the content. It throws a `TypeError` on an asynchronously validating schema\n// (\u00ABd.mts\u00BB:1468-1470), and `z.ZodObject<z.ZodRawShape>` freely permits `.refine(async \u2026)` \u2014 so the\n// wrap is the enforcement of a contract the types cannot express. It returns `undefined` both for\n// \"not an accept\" and for \"failed validation\"; the caller has already discriminated the action, so\n// here `undefined` means only the latter. `toArgs` is the one member called on client-supplied\n// input: its contract says it never throws, and the wrap is what makes that true rather than promised.\nexport const readAcceptedArgs = async (\n provider: ArgumentFormProvider,\n params: unknown,\n responses: Record<string, unknown> | undefined,\n deadlineMs: number,\n): Promise<Record<string, unknown> | null> => {\n const schema = await trySchema(provider, params, deadlineMs)\n\n if (schema === null) return null\n\n let content: Record<string, unknown> | undefined\n\n try {\n content = acceptedContent(responses, FORM_KEY, schema) as Record<string, unknown> | undefined\n } catch {\n return null\n }\n\n if (content === undefined) return null\n\n try {\n return provider.toArgs(content, params)\n } catch {\n return null\n }\n}\n\n/** A plain object \u2014 not an array, not `null`, not a primitive. */\nconst isRecord = (value: unknown): value is Record<string, unknown> => {\n return typeof value === 'object' && value !== null && !Array.isArray(value)\n}\n\n/**\n * True when `after` removed a top-level key of `before`, or CHANGED the length of one of its\n * arrays. The chokepoint refuses such a merge and gates on the round-1 arguments instead.\n *\n * Both array directions are violations, and the growth half is the silent one: an injected entry\n * the caller never supplied stays schema-valid and EXECUTES. Deliberately TOP-LEVEL ONLY \u2014 nested\n * removal and equal-shape substitution are OUT of scope and are guarded only by the provider's merge.\n *\n * @example\n * narrowsArgs({ releases: [1, 2] }, { releases: [1] }) // => true (truncated)\n * narrowsArgs({ releases: [1] }, { releases: [1, 2] }) // => true (grown)\n * narrowsArgs({ a: 1, b: 2 }, { a: 9, b: 2 }) // => false (substitution is the feature)\n */\n// A form narrows a CHOICE; it never withdraws or invents an item the caller listed. So the length\n// rule is a ceiling rather than a heuristic \u2014 no legitimate provider, present or future, needs it\n// relaxed, and one that trips it is wrong by construction. The depth limit is the opposite kind of\n// decision: a recursive walk would forbid a human blanking an optional field they no longer want,\n// and nothing generic separates \"the human cleared this\" from \"the provider dropped this\".\nexport const narrowsArgs = (before: unknown, after: unknown): boolean => {\n if (!isRecord(before)) return false\n // `before` is a record and `after` is not: EVERY argument was narrowed away.\n if (!isRecord(after)) return true\n\n for (const [key, value] of Object.entries(before)) {\n if (!(key in after)) return true\n\n const replacement = after[key]\n\n if (Array.isArray(value) && (!Array.isArray(replacement) || replacement.length !== value.length)) return true\n }\n\n return false\n}\n", "import type { ClientCapabilities, InputRequiredResult } from '@modelcontextprotocol/server'\n\nimport { commandEcho } from 'src/lib/command-echo'\nimport { ensureUserProjectConfig } from 'src/lib/config-bootstrap'\nimport { logger } from 'src/lib/logger'\nimport { textContent } from 'src/types'\nimport type { ArgumentFormProvider, ToolsExecutionResult } from 'src/types'\n\nimport { FORM_DEADLINE_MS, buildArgumentForm, narrowsArgs, readAcceptedArgs, readFormAction } from './argument-form'\nimport type { FormAction } from './argument-form'\nimport { getDefaultConfirmCodec, mintConfirmToken, stripGateKeys, verifyConfirmToken } from './confirm-token'\nimport type { ConfirmCodec, ConfirmRefusal } from './confirm-token'\n\ninterface ToolHandlerArgs {\n toolName: string\n handler: (params: any) => Promise<ToolsExecutionResult>\n /**\n * When true, this tool is gated by the destructive-op confirm gate below. Sourced from the\n * catalog tool's {@link CatalogMcpTool.requiresHumanConfirm} at registration (`mcp/tools/index.ts`).\n */\n requiresHumanConfirm?: boolean\n /**\n * Optional per-tool argument-form seam, sourced from the catalog tool's\n * {@link CatalogMcpTool.formProvider} at registration. Absent on every tool today, and nothing here\n * reads it yet: it is declared so registration can forward it while the state machine that consumes\n * it lands separately. Absent must always mean \"the gate behaves exactly as it always has\".\n */\n formProvider?: ArgumentFormProvider\n /**\n * Reads the connected client's declared capabilities, so the chokepoint can ask whether this client\n * can render a form at all before offering one. Injected as a CLOSURE rather than a value because\n * capabilities are only known after initialize, and this handler is built at registration time \u2014\n * and because the accessor behind it is deprecated in favour of the per-request envelope, so the\n * migration is a one-line change at the injection site rather than an edit in here.\n */\n getClientCapabilities?: () => ClientCapabilities | undefined\n /** Token codec for the gate. Defaults to the process-wide one; tests inject short-TTL or foreign-key codecs. */\n confirmCodec?: ConfirmCodec\n /**\n * How long the form path waits for {@link ArgumentFormProvider.buildRequestedSchema}. Defaults to\n * {@link FORM_DEADLINE_MS}; tests inject a few milliseconds so the deadline lane asserts a bound\n * instead of hanging until the runner's own timeout.\n */\n formDeadlineMs?: number\n}\n\n/**\n * The SDK hands every tool callback a second argument (session, request state, abort signal).\n * Threaded through untouched so the signature lands once.\n */\nexport interface ToolCallContext {\n sessionId?: string\n /**\n * The per-request MCP envelope. Only the multi-round-trip carrier is declared: `inputResponses`\n * is how a client's answer to the argument form comes BACK, and its presence \u2014 not its content \u2014\n * is what separates a first call from a re-entry.\n */\n mcpReq?: {\n inputResponses?: Record<string, unknown>\n /**\n * Keys the SDK dropped because they were not bare response objects. Declared for completeness\n * and deliberately UNREAD: a dropped key is absent from `inputResponses`, so it already reads\n * as `{kind:'missing'}` \u2014 an action that is not an accept, which is the decline path.\n */\n droppedInputResponseKeys?: string[]\n }\n}\n\nexport type GateState = 'run' | 'form' | 'declined' | 'gate' | 'verify'\n\n/** Everything `resolveGateState` needs, all of it knowable BEFORE any work is done. */\nexport interface GateInputs {\n /** `requiresHumanConfirm === true` \u2014 spelled at the call site, not re-derived here. */\n gated: boolean\n /** The round-2 execute signal. */\n confirmed: boolean\n /** Present iff the client came BACK with an answer to the form. Content is irrelevant here. */\n responses: Record<string, unknown> | undefined\n /** The client declared form-mode elicitation. */\n canForm: boolean\n /** This tool carries an {@link ArgumentFormProvider}. */\n hasProvider: boolean\n /** The provider wants a form for THESE arguments. */\n formable: boolean\n /** The client came back with an ACCEPT specifically \u2014 decline, cancel and absence are all `false`. */\n accepted: boolean\n}\n\n/** True when the incoming MCP params carry an explicit `confirm:true` (the call-2 execute signal). */\nconst isConfirmed = (params: unknown): boolean => {\n return typeof params === 'object' && params !== null && (params as { confirm?: unknown }).confirm === true\n}\n\n/**\n * The CANDIDATE state for a call. First-match-wins, in the order written, **every condition stated\n * in full** \u2014 no row relies on an earlier row having excluded anything.\n *\n * It is a candidate, not a verdict: `form` is the one outcome that can decline itself, because\n * whether a form can actually be built is knowable only after the provider has run under a\n * deadline. The caller falls through to `gate` when it does. Every other outcome is terminal in\n * the caller's own body.\n */\n// `!confirmed` is spelled on rows 1, 2 AND 3 for one reason: a non-elicitation round 2 carries\n// `confirm:true` with no `inputResponses`, so without it row 3 matches, the call re-gates forever\n// and `verify` is unreachable. `responses !== undefined` is likewise the ONLY first-call/came-back\n// discriminator \u2014 `acceptedContent(...)` being falsy reads the same for decline, cancel and absence\n// alike, which would send a decline back to the form and re-prompt forever.\n//\n// Exported for its OWN tests, and not re-exported from `index.ts`, so the package's public surface\n// is unchanged. Some conjuncts are backed a second time downstream \u2014 `hasProvider` is also enforced\n// by the provider narrowing in `buildFormOrGate` \u2014 so an end-to-end assertion cannot tell which\n// guard held. Testing the predicate directly is what makes deleting one of them observable.\nexport const resolveGateState = (input: GateInputs): GateState => {\n // Spelled `=== true` at the call site that fills `gated`: `mcp-confirm-gate-mutation.test.ts`\n // neuters exactly that predicate at build time to prove the gate's e2e assertions are load-bearing.\n if (!input.gated) return 'run'\n\n if (\n input.gated &&\n !input.confirmed &&\n input.responses === undefined &&\n input.canForm &&\n input.hasProvider &&\n input.formable\n ) {\n return 'form'\n }\n\n if (input.gated && !input.confirmed && input.responses !== undefined && !input.accepted) return 'declined'\n\n if (input.gated && !input.confirmed && (input.responses === undefined || input.accepted)) return 'gate'\n\n // Row 4, condition complete: `gated \u2227 confirmed`. Rows 2 and 3 partition `gated \u2227 !confirmed`\n // between them \u2014 row 2 is `responses !== undefined \u2227 !accepted`, row 3 its exact complement \u2014 so\n // this is reached on that condition and no other.\n return 'verify'\n}\n\n/**\n * Every gate and refusal payload sets `isError: true`: the MCP SDK validates a result's\n * `structuredContent` against the tool's `outputSchema` UNLESS `isError` is set, and no gated\n * tool's schema matches these payloads. A soft stop, not a failure \u2014 the tool did NOT run.\n */\nconst softStop = (structuredContent: Record<string, unknown>): ToolsExecutionResult => {\n return {\n content: textContent(JSON.stringify(structuredContent, null, 2)),\n structuredContent,\n isError: true,\n }\n}\n\n/** What the gate must SAY beyond its standing instruction, and to whom the arguments belong. */\ninterface GateNotices {\n /**\n * The human answered the form and their answer was thrown away \u2014 it failed validation, or it\n * narrowed the arguments. The gate then presents the AGENT'S values for approval in place of the\n * ones the human just chose, so saying so is not decoration: an unlabelled swap of the whole\n * selection is a worse substitution than the single-field one the merge exists to prevent.\n */\n formDiscarded: boolean\n /** This tool carries a form provider at all. */\n hasProvider: boolean\n}\n\nconst FORM_DISCARDED_CLAUSE =\n 'The values you submitted in the form could NOT be applied and were DISCARDED \u2014 they failed validation or narrowed the arguments \u2014 so the arguments shown above are the ORIGINAL ones, not your selection.'\n\n// PM-D: only one gated tool in eight carries a provider, so a user who learns \"infra-kit asks me\n// before it does this\" from the one that does will read the other seven's silence as safety. The\n// true rule is \"no dialog \u21D2 this tool has no provider\", and nothing else in the payload says so.\nconst NO_FORM_CLAUSE =\n 'This tool does not prompt for its arguments; you are being asked to approve the values shown above.'\n\n/**\n * The round-1 gate: names the tool, echoes the arguments it will bind to, and carries the\n * `confirmToken` round 2 must return alongside `confirm:true` and the SAME arguments.\n */\nconst buildConfirmGate = async (\n codec: ConfirmCodec,\n toolName: string,\n params: unknown,\n notices: GateNotices,\n): Promise<ToolsExecutionResult> => {\n const resolvedArgs = stripGateKeys(params)\n const confirmToken = await mintConfirmToken(codec, toolName, params)\n const message = [\n notices.formDiscarded ? FORM_DISCARDED_CLAUSE : undefined,\n `${toolName} mutates external state and is gated. It was NOT executed. Re-call ${toolName} with the same arguments plus \"confirm\": true and this \"confirmToken\" to execute.`,\n notices.hasProvider ? undefined : NO_FORM_CLAUSE,\n ]\n .filter((clause) => {\n return clause !== undefined\n })\n .join(' ')\n\n return softStop({\n status: 'confirmation_required',\n tool: toolName,\n resolvedArgs,\n confirmToken,\n // A field and not only prose: an agent reacts to `formDiscarded` programmatically, for the same\n // reason `status` and `reason` are fields here rather than sentences.\n formDiscarded: notices.formDiscarded,\n message,\n })\n}\n\n/**\n * The form was answered with anything other than an accept. Terminal, and deliberately NOT a second\n * form: re-issuing one on a decline is an infinite prompt loop with the human's \"no\" as its engine.\n */\nconst buildFormDeclined = (toolName: string, action: FormAction): ToolsExecutionResult => {\n return softStop({\n status: 'form_declined',\n tool: toolName,\n action,\n message: `${toolName} was NOT executed: the argument form came back as \"${action}\". No confirmation is pending \u2014 call ${toolName} again to start over.`,\n })\n}\n\nconst REFUSAL_TEXT: Record<ConfirmRefusal, string> = {\n absent: 'no \"confirmToken\" was supplied',\n malformed: 'the \"confirmToken\" is malformed',\n mac: 'the \"confirmToken\" was not issued by this server',\n expired: 'the \"confirmToken\" has expired',\n bind: 'the \"confirmToken\" was issued for a different tool',\n mismatch: 'the arguments differ from the ones the \"confirmToken\" was issued for',\n}\n\n/** The round-2 refusal: a terminal stop, deliberately NOT a second gate. */\nconst buildConfirmRefusal = (toolName: string, reason: ConfirmRefusal): ToolsExecutionResult => {\n return softStop({\n status: 'confirmation_refused',\n tool: toolName,\n reason,\n message: `${toolName} was NOT executed: ${REFUSAL_TEXT[reason]}. Call ${toolName} again WITHOUT \"confirm\" to receive a fresh gate, then re-call with the same arguments plus \"confirm\": true and the returned \"confirmToken\".`,\n })\n}\n\n/** Everything the stop resolution below needs from the tool's registration. */\ninterface StopDeps {\n toolName: string\n codec: ConfirmCodec\n requiresHumanConfirm: boolean | undefined\n formProvider: ArgumentFormProvider | undefined\n getClientCapabilities: (() => ClientCapabilities | undefined) | undefined\n formDeadlineMs: number\n}\n\n/** The arguments the gate will bind its token to, and whether they replaced a discarded selection. */\ninterface GateArgs {\n params: unknown\n formDiscarded: boolean\n}\n\n/**\n * The arguments a gate reached from an ACCEPTED form should carry. The round-1 arguments on every\n * path except one: a validated, merged, non-narrowing result.\n *\n * A failure here never goes back to the form \u2014 that is the re-prompt loop through a second door \u2014\n * and it never proceeds with the client's values either. It gates on what the agent originally\n * asked for, and says so.\n */\n/**\n * Runs one form PREDICATE, treating any throw as `false`.\n *\n * `isFormable` and the capability probe are the two calls that decide whether a form is even\n * offered, and both run OUTSIDE the wraps `argument-form.ts` puts around `buildRequestedSchema`\n * and `toArgs`. Unwrapped, a provider that throws here escapes through the handler's outer catch\n * as a TOOL ERROR on a call that was owed a GATE \u2014 the exact failure those wraps exist to prevent,\n * reached one step earlier.\n *\n * `types.ts` states a never-throws contract for `isFormable`, but a contract is not a mechanism:\n * it is enforced per provider, which is the residual class the chokepoint's non-narrowing check was\n * adopted to remove. Failing to `false` here means the worst a broken predicate can do is decline a\n * form the human would have seen \u2014 never skip the gate, never reach the handler.\n */\nconst tryPredicate = (read: () => boolean): boolean => {\n try {\n return read()\n } catch {\n return false\n }\n}\n\nconst resolveGateArgs = async (\n deps: StopDeps,\n params: unknown,\n responses: Record<string, unknown> | undefined,\n formAction: FormAction,\n): Promise<GateArgs> => {\n const provider = deps.formProvider\n\n if (provider === undefined || formAction !== 'accept') return { params, formDiscarded: false }\n\n const merged = await readAcceptedArgs(provider, params, responses, deps.formDeadlineMs)\n\n if (merged === null) {\n logger.info({ msg: `Tool execution form discarded (validation): ${deps.toolName}` })\n\n return { params, formDiscarded: true }\n }\n\n // Compared against `stripGateKeys(params)` \u2014 the same normalization the token is minted over, so\n // the check and the signature are looking at exactly one thing.\n if (narrowsArgs(stripGateKeys(params), merged)) {\n logger.info({ msg: `Tool execution form discarded (narrowed): ${deps.toolName}` })\n\n return { params, formDiscarded: true }\n }\n\n return { params: merged, formDiscarded: false }\n}\n\n/**\n * States `form` and `gate`. `form` is the one candidate that can decline itself: a provider that\n * rejects, outruns its deadline, resolves `null`, or hands `elicit()` a shape it cannot express\n * falls through to the gate here, with today's behaviour and no thrown error.\n */\nconst buildFormOrGate = async (\n deps: StopDeps,\n params: unknown,\n responses: Record<string, unknown> | undefined,\n state: GateState,\n formAction: FormAction,\n): Promise<ToolsExecutionResult | InputRequiredResult> => {\n if (state === 'form' && deps.formProvider !== undefined) {\n const form = await buildArgumentForm(deps.formProvider, params, deps.formDeadlineMs)\n\n if (form !== null) {\n logger.info({ msg: `Tool execution form requested: ${deps.toolName}` })\n\n return form\n }\n\n logger.info({ msg: `Tool execution form unavailable: ${deps.toolName}` })\n }\n\n const gateArgs = await resolveGateArgs(deps, params, responses, formAction)\n\n logger.info({ msg: `Tool execution gated (awaiting confirm): ${deps.toolName}` })\n\n return await buildConfirmGate(deps.codec, deps.toolName, gateArgs.params, {\n formDiscarded: gateArgs.formDiscarded,\n hasProvider: deps.formProvider !== undefined,\n })\n}\n\n/**\n * The chokepoint's answer for a call, or `null` meaning \"nothing stops this; run the handler\".\n *\n * @example\n * await resolveStop(deps, { env: 'prod' }, undefined) // => the round-1 confirm gate\n */\nconst resolveStop = async (\n deps: StopDeps,\n params: unknown,\n ctx: ToolCallContext | undefined,\n): Promise<ToolsExecutionResult | InputRequiredResult | null> => {\n const responses = ctx?.mcpReq?.inputResponses\n const formAction = readFormAction(responses)\n const state = resolveGateState({\n // Spelled `=== true` on purpose: `mcp-confirm-gate-mutation.test.ts` neuters exactly this\n // predicate at build time to prove the gate's e2e assertions are load-bearing.\n gated: deps.requiresHumanConfirm === true,\n confirmed: isConfirmed(params),\n responses,\n // `caps?.elicitation?.form`, NEVER `caps?.elicitation`: the SDK normalizes a bare\n // `{elicitation:{}}` to `{elicitation:{form:{}}}`, so both spellings agree on every fixture\n // except a url-only client \u2014 which is exactly the client that must NOT be offered a form.\n canForm: tryPredicate(() => {\n return deps.getClientCapabilities?.()?.elicitation?.form !== undefined\n }),\n hasProvider: deps.formProvider !== undefined,\n formable: tryPredicate(() => {\n return deps.formProvider?.isFormable(params) === true\n }),\n accepted: formAction === 'accept',\n })\n\n if (state === 'run') return null\n\n if (state === 'declined') {\n logger.info({ msg: `Tool execution form declined (${formAction}): ${deps.toolName}` })\n\n return buildFormDeclined(deps.toolName, formAction)\n }\n\n if (state === 'verify') {\n const verdict = await verifyConfirmToken(deps.codec, deps.toolName, params)\n\n if (verdict.ok) return null\n\n logger.info({ msg: `Tool execution refused (${verdict.reason}): ${deps.toolName}` })\n\n return buildConfirmRefusal(deps.toolName, verdict.reason)\n }\n\n return await buildFormOrGate(deps, params, responses, state, formAction)\n}\n\n// The return is a union because the chokepoint can now answer a call with an `InputRequiredResult` \u2014\n// the SDK's \"I need input from the human before I can run\" reply \u2014 as well as with a tool result.\n// It typechecks at the registration site because `ToolCallback` already returns\n// `CallToolResult | InputRequiredResult`.\n//\n// Deliberately UNCONDITIONAL, though nothing returns the second member yet. Narrowing it back for\n// callers that pass no `formProvider` would be asserting \"no provider means a form is unreachable\" \u2014\n// true only while the state machine keeps requiring a provider, and enforced by nothing if that ever\n// changes. Every caller narrows the union explicitly instead.\nexport const createToolHandler = ({\n toolName,\n handler,\n requiresHumanConfirm,\n formProvider,\n getClientCapabilities,\n confirmCodec,\n formDeadlineMs,\n}: ToolHandlerArgs): ((\n params: unknown,\n ctx?: ToolCallContext,\n) => Promise<ToolsExecutionResult | InputRequiredResult>) => {\n const deps: StopDeps = {\n toolName,\n codec: confirmCodec ?? getDefaultConfirmCodec(),\n requiresHumanConfirm,\n formProvider,\n getClientCapabilities,\n formDeadlineMs: formDeadlineMs ?? FORM_DEADLINE_MS,\n }\n\n // `ctx` is bound HERE, in the returned closure, and not merely declared on the exported type. The\n // SDK passes it on every call, so a closure that omits the parameter discards it silently while\n // the signature keeps advertising it \u2014 the gap survived PR 1 unnoticed for exactly that reason.\n return async (params: unknown, ctx?: ToolCallContext) => {\n logger.info({ msg: `Tool execution started: ${toolName}`, params, sessionId: ctx?.sessionId })\n try {\n // MCP entry-boundary seed. This wrapper is the sole chokepoint for every exposed tool\n // (`src/mcp/tools/index.ts` holds the only `registerTool` call), and the `mcp` command itself is\n // in program.ts's SEED_EXCLUDED \u2014 so an MCP server seeds lazily, on its FIRST tool invocation,\n // and a server that never receives one never writes to $HOME. The once-per-process guard inside\n // ensureUserProjectConfig makes calls 2..N free on this long-lived process.\n //\n // Deliberately INSIDE the try, not before it: defense-in-depth. ensureUserProjectConfig is\n // contractually never-throw, but if that contract ever broke, an unhandled rejection here would\n // take down the long-lived server. Inside the try it degrades to an ordinary tool error.\n await ensureUserProjectConfig()\n\n // The command handlers record their resolved flags into the `commandEcho` singleton. On the CLI,\n // Commander's `preAction` clears it before every command; this long-lived server never runs that\n // hook, so without a reset here one tool call's flags would leak into the next one's snapshot and\n // the options array would grow for the life of the process.\n commandEcho.reset()\n\n // Orthogonal destructive-op confirm gate, and the argument form that feeds it. Both sit BEFORE\n // the handler and are INDEPENDENT of the `confirmedCommand:true` injected below \u2014 that flag is a\n // prompt-skip / behavior discriminator (e.g. worktrees-remove keys `allowEditorRelaunch` off it)\n // and MUST keep being injected on the real call, or the non-TTY server would hang on an inquirer\n // prompt and the Zed relaunch would re-enable. The form is collected on the way INTO the gate,\n // never instead of it: round 1 returns a form or the gate, and round 2 runs only when\n // `confirm:true` comes with that gate's token AND the same arguments.\n const stop = await resolveStop(deps, params, ctx)\n\n if (stop !== null) return stop\n\n const payload = await handler({ ...(params as object), confirmedCommand: true })\n\n logger.info({ msg: `Tool execution successful: ${toolName}` })\n\n return payload\n } catch (error) {\n logger.error({\n err: error,\n params,\n msg: `Tool execution failed: ${toolName}`,\n })\n\n throw error\n }\n }\n}\n"],
5
+ "mappings": "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",
6
+ "names": ["serveStdio", "process", "delay", "process", "setupErrorHandlers", "logger", "process", "error", "LOG_FILE_PATH", "reason", "promise", "McpServer", "release_create_default", "setup_default", "WORKFLOW_BODIES", "release_create_default", "setup_default", "initializePrompts", "server", "WORKFLOW_BODIES", "CONFIG_RESOURCE_URI", "DEV_CONTEXT_RESOURCE_URI", "RELEASE_CREATE_WORKFLOW_URI", "SETUP_WORKFLOW_URI", "defaultDeps", "getInfraKitConfig", "readDevContext", "jsonResource", "uri", "value", "registerWorkflow", "server", "workflow", "WORKFLOW_BODIES", "initializeResources", "deps", "error", "z", "createRequestStateCodec", "randomBytes", "GATE_KEYS", "CONFIRM_TOKEN_TTL_SECONDS", "asBindContext", "toolName", "sortKeysDeep", "value", "record", "key", "canonicalArgs", "args", "stripGateKeys", "params", "createConfirmCodec", "options", "codec", "ctx", "payload", "token", "defaultCodec", "getDefaultConfirmCodec", "readConfirmToken", "confirmToken", "REFUSALS_FROM_CODEC", "codecRefusal", "error", "message", "mintConfirmToken", "verifyConfirmToken", "acceptedContent", "inputRequired", "inputResponse", "FORM_KEY", "FORM_DEADLINE_MS", "readFormAction", "responses", "view", "withDeadline", "work", "ms", "resolve", "timer", "value", "trySchema", "provider", "params", "deadlineMs", "buildArgumentForm", "schema", "readAcceptedArgs", "content", "isRecord", "narrowsArgs", "before", "after", "key", "replacement", "isConfirmed", "params", "resolveGateState", "input", "softStop", "structuredContent", "textContent", "FORM_DISCARDED_CLAUSE", "NO_FORM_CLAUSE", "buildConfirmGate", "codec", "toolName", "notices", "resolvedArgs", "stripGateKeys", "confirmToken", "mintConfirmToken", "message", "clause", "buildFormDeclined", "action", "REFUSAL_TEXT", "buildConfirmRefusal", "reason", "tryPredicate", "read", "resolveGateArgs", "deps", "responses", "formAction", "provider", "merged", "readAcceptedArgs", "logger", "narrowsArgs", "buildFormOrGate", "state", "form", "buildArgumentForm", "gateArgs", "resolveStop", "ctx", "readFormAction", "verdict", "verifyConfirmToken", "createToolHandler", "handler", "requiresHumanConfirm", "formProvider", "getClientCapabilities", "confirmCodec", "formDeadlineMs", "getDefaultConfirmCodec", "FORM_DEADLINE_MS", "ensureUserProjectConfig", "commandEcho", "stop", "payload", "error", "initializeTools", "server", "tool", "getExposedMcpTools", "z", "withConfirmToken", "wrapForRegistration", "createToolHandler", "shape", "handle", "params", "ctx", "createMcpServer", "mcpMode", "server", "McpServer", "package_default", "initializePrompts", "initializeResources", "initializeTools", "suppressTypelessPackageJsonWarning", "logger", "initLoggerMcp", "buildOrDie", "createMcpServer", "error", "process", "setupErrorHandlers", "handle", "serveStdio", "isShuttingDown", "shutdown", "signal", "delay"]
7
7
  }
@@ -1,2 +1,2 @@
1
- import{i}from"./chunk-NKXTDKWV.js";import{a as t}from"./chunk-NS5KIBXS.js";import"./chunk-7POMR4QK.js";import"./chunk-CHETBZ6M.js";import a from"node:fs";import o from"node:process";import{fileURLToPath as s}from"node:url";var m=r=>{let n=r.indexOf("--parent-pid");if(n===-1)return;let e=Number(r[n+1]);return Number.isInteger(e)&&e>0?e:void 0},p=()=>a.realpathSync(s(new URL("./cli.js",import.meta.url))),d=async()=>{await i(t.version,{selfRealPath:p(),parentPid:m(o.argv)})};d().catch(()=>{o.exitCode=0});
1
+ import{d as i}from"./chunk-Y3FV2BRI.js";import{a as t}from"./chunk-6GWRKIZ7.js";import"./chunk-XJOY7J3G.js";import"./chunk-D4UYDBN5.js";import a from"node:fs";import o from"node:process";import{fileURLToPath as s}from"node:url";var m=r=>{let n=r.indexOf("--parent-pid");if(n===-1)return;let e=Number(r[n+1]);return Number.isInteger(e)&&e>0?e:void 0},p=()=>a.realpathSync(s(new URL("./cli.js",import.meta.url))),d=async()=>{await i(t.version,{selfRealPath:p(),parentPid:m(o.argv)})};d().catch(()=>{o.exitCode=0});
2
2
  //# sourceMappingURL=update-check.js.map
@@ -2,6 +2,6 @@
2
2
  "version": 3,
3
3
  "sources": ["../src/entry/update-check.ts"],
4
4
  "sourcesContent": ["/**\n * Background auto-update worker. Spawned detached by `maybeAutoUpdate` as\n * `node dist/update-check.js --parent-pid <pid>`, and NEVER run by a human.\n *\n * This is a dedicated bundle, a sibling of `dist/cli.js`, for the same reason `dist/mcp.js` is: booting\n * `cli.js` here would re-run commander, the command catalog, `warnIfLocalInstall()`, and\n * `maybeAutoUpdate()` itself as top-level side effects \u2014 the last of which would spawn another child,\n * and another. Importing nothing from `src/entry/cli.ts` is a hard invariant, guarded by a test.\n *\n * It carries NO hashbang: nothing execs it directly, and `dist-shebang.test.ts` asserts its absence.\n * It writes nothing to stdout (it is spawned with `stdio: 'ignore'`, so there is nowhere to write).\n */\nimport fs from 'node:fs'\nimport process from 'node:process'\nimport { fileURLToPath } from 'node:url'\n\n// Deep import, not the `src/lib/update-check` barrel: the barrel re-exports `maybeAutoUpdate`, which would\n// drag the parent-side spawn path (and pino) into this worker's bundle for no reason.\nimport { runUpdateCheck } from 'src/lib/update-check/run-update-check'\n\nimport packageJson from '../../package.json' with { type: 'json' }\n\n/** `--parent-pid <pid>`, or undefined when absent/malformed (then we install without waiting). */\nconst parseParentPid = (argv: string[]): number | undefined => {\n const index = argv.indexOf('--parent-pid')\n\n if (index === -1) return undefined\n\n const pid = Number(argv[index + 1])\n\n return Number.isInteger(pid) && pid > 0 ? pid : undefined\n}\n\n/** The installed `dist/cli.js` beside this bundle \u2014 the path whose owning package manager we detect. */\nconst selfRealPath = (): string => {\n return fs.realpathSync(fileURLToPath(new URL('./cli.js', import.meta.url)))\n}\n\nconst main = async (): Promise<void> => {\n await runUpdateCheck(packageJson.version, {\n selfRealPath: selfRealPath(),\n parentPid: parseParentPid(process.argv),\n })\n}\n\n// A detached background worker must never surface a crash, a non-zero exit, or an unhandled rejection \u2014\n// there is no one to see it and nothing depends on it. `runUpdateCheck` already swallows its own\n// failures; this is the last line of defence around path resolution and JSON import.\nmain().catch(() => {\n process.exitCode = 0\n})\n"],
5
- "mappings": "mIAYA,OAAOA,MAAQ,UACf,OAAOC,MAAa,eACpB,OAAS,iBAAAC,MAAqB,WAS9B,IAAMC,EAAkBC,GAAuC,CAC7D,IAAMC,EAAQD,EAAK,QAAQ,cAAc,EAEzC,GAAIC,IAAU,GAAI,OAElB,IAAMC,EAAM,OAAOF,EAAKC,EAAQ,CAAC,CAAC,EAElC,OAAO,OAAO,UAAUC,CAAG,GAAKA,EAAM,EAAIA,EAAM,MAClD,EAGMC,EAAe,IACZC,EAAG,aAAaC,EAAc,IAAI,IAAI,WAAY,YAAY,GAAG,CAAC,CAAC,EAGtEC,EAAO,SAA2B,CACtC,MAAMC,EAAeC,EAAY,QAAS,CACxC,aAAcL,EAAa,EAC3B,UAAWJ,EAAeU,EAAQ,IAAI,CACxC,CAAC,CACH,EAKAH,EAAK,EAAE,MAAM,IAAM,CACjBG,EAAQ,SAAW,CACrB,CAAC",
5
+ "mappings": "wIAYA,OAAOA,MAAQ,UACf,OAAOC,MAAa,eACpB,OAAS,iBAAAC,MAAqB,WAS9B,IAAMC,EAAkBC,GAAuC,CAC7D,IAAMC,EAAQD,EAAK,QAAQ,cAAc,EAEzC,GAAIC,IAAU,GAAI,OAElB,IAAMC,EAAM,OAAOF,EAAKC,EAAQ,CAAC,CAAC,EAElC,OAAO,OAAO,UAAUC,CAAG,GAAKA,EAAM,EAAIA,EAAM,MAClD,EAGMC,EAAe,IACZC,EAAG,aAAaC,EAAc,IAAI,IAAI,WAAY,YAAY,GAAG,CAAC,CAAC,EAGtEC,EAAO,SAA2B,CACtC,MAAMC,EAAeC,EAAY,QAAS,CACxC,aAAcL,EAAa,EAC3B,UAAWJ,EAAeU,EAAQ,IAAI,CACxC,CAAC,CACH,EAKAH,EAAK,EAAE,MAAM,IAAM,CACjBG,EAAQ,SAAW,CACrB,CAAC",
6
6
  "names": ["fs", "process", "fileURLToPath", "parseParentPid", "argv", "index", "pid", "selfRealPath", "fs", "fileURLToPath", "main", "runUpdateCheck", "package_default", "process"]
7
7
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "infra-kit",
3
3
  "type": "module",
4
- "version": "0.4.0",
4
+ "version": "0.5.1",
5
5
  "files": [
6
6
  "dist"
7
7
  ],
@@ -15,37 +15,37 @@
15
15
  },
16
16
  "dependencies": {
17
17
  "@aws-lambda-powertools/logger": "^2.35.0",
18
- "@inquirer/checkbox": "^5.2.3",
19
- "@inquirer/confirm": "^6.3.0",
20
- "@inquirer/input": "^5.1.4",
21
- "@inquirer/password": "^5.2.0",
22
- "@inquirer/select": "^5.2.3",
18
+ "@inquirer/checkbox": "^5.2.5",
19
+ "@inquirer/confirm": "^6.3.2",
20
+ "@inquirer/input": "^5.1.6",
21
+ "@inquirer/password": "^5.2.2",
22
+ "@inquirer/select": "^5.2.5",
23
23
  "@modelcontextprotocol/server": "^2.0.0",
24
24
  "@slip-stream-kit/config": "^0.4.0",
25
25
  "chalk": "^6.0.0",
26
26
  "chokidar": "^5.0.0",
27
27
  "commander": "^15.0.0",
28
- "fastify": "^5.12.1",
28
+ "fastify": "^5.12.3",
29
29
  "ink": "^7.1.1",
30
30
  "pino": "^10.3.1",
31
31
  "pino-pretty": "^13.1.3",
32
32
  "portless": "^0.15.6",
33
33
  "react": "19.2.8",
34
34
  "yaml": "^2.9.0",
35
- "zod": "^4.5.2",
35
+ "zod": "^4.5.4",
36
36
  "zx": "^8.8.5"
37
37
  },
38
38
  "devDependencies": {
39
39
  "@modelcontextprotocol/client": "^2.0.0",
40
40
  "@modelcontextprotocol/sdk": "^1.30.0",
41
- "@types/aws-lambda": "^8.10.162",
41
+ "@types/aws-lambda": "^8.10.163",
42
42
  "@types/react": "19.2.18",
43
43
  "esbuild": "^0.28.2",
44
44
  "ink-testing-library": "^4.0.0",
45
45
  "typescript": "^6.0.3",
46
- "@wl/vitest-config": "0.1.0",
47
46
  "@wl/eslint-config": "0.1.0",
48
- "@slip-stream-kit/eslint-plugin": "0.4.0"
47
+ "@slip-stream-kit/eslint-plugin": "0.5.0",
48
+ "@wl/vitest-config": "0.1.0"
49
49
  },
50
50
  "scripts": {
51
51
  "inspector": "npx @modelcontextprotocol/inspector node ./dist/mcp.js --debug",