infra-kit 0.7.0 → 0.7.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/chunk-4KWVFLSU.js +443 -0
- package/dist/chunk-4KWVFLSU.js.map +7 -0
- package/dist/chunk-HHWP3YXW.js +2 -0
- package/dist/chunk-HHWP3YXW.js.map +7 -0
- package/dist/{chunk-UI2MUEVR.js → chunk-LDF2KOGN.js} +2 -2
- package/dist/chunk-TNPZPZJJ.js +3 -0
- package/dist/chunk-TNPZPZJJ.js.map +7 -0
- package/dist/cli.js +13 -13
- package/dist/cli.js.map +4 -4
- package/dist/dev-server.js +1 -1
- package/dist/mcp-proxy.js +1 -1
- package/dist/mcp.js +1 -110
- package/dist/mcp.js.map +3 -3
- package/dist/update-check.js +1 -1
- package/dist/update-check.js.map +1 -1
- package/package.json +3 -3
- package/readme.md +8 -0
- package/dist/chunk-7YE5QKKB.js +0 -444
- package/dist/chunk-7YE5QKKB.js.map +0 -7
- package/dist/chunk-GYPVEWUV.js +0 -2
- package/dist/chunk-GYPVEWUV.js.map +0 -7
- package/dist/chunk-MFLYORUJ.js +0 -2
- package/dist/chunk-MFLYORUJ.js.map +0 -7
- package/dist/chunk-QS4JZKF2.js +0 -2
- package/dist/chunk-QS4JZKF2.js.map +0 -7
- /package/dist/{chunk-UI2MUEVR.js.map → chunk-LDF2KOGN.js.map} +0 -0
package/dist/dev-server.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import{a as Ue,b as L,c as M,d as Fe,e as H,k as Me,l as He,m as Be,p as je}from"./chunk-KXQQUBGV.js";import{$ as Ce,K as T,L as Ae,R as ne,U as C,V as U,W as Re,X as Ee,Y as F,Z as Te,_ as $,aa as $e,ba as Le,ca as Ne,da as Ie,ea as Oe,fa as _e}from"./chunk-63VNUKUS.js";import{c as te,k as re,o as ke,r as xe}from"./chunk-
|
|
1
|
+
import{a as Ue,b as L,c as M,d as Fe,e as H,k as Me,l as He,m as Be,p as je}from"./chunk-KXQQUBGV.js";import{$ as Ce,K as T,L as Ae,R as ne,U as C,V as U,W as Re,X as Ee,Y as F,Z as Te,_ as $,aa as $e,ba as Le,ca as Ne,da as Ie,ea as Oe,fa as _e}from"./chunk-63VNUKUS.js";import{c as te,k as re,o as ke,r as xe}from"./chunk-LDF2KOGN.js";import"./chunk-MFAFQVTW.js";import"./chunk-HHWP3YXW.js";import{d as Pe,f as ee,o as De}from"./chunk-NFIFWIO6.js";import{a as N,b as We}from"./chunk-VDBJ73LS.js";import{Command as Fn}from"commander";import P from"node:process";import{pathToFileURL as Mn}from"node:url";import*as Ve from"node:path";import qt from"node:process";var Jt=r=>({pane:{surfaces:[{type:"terminal",command:r}]}}),ie=(r,e)=>{if(r.length===1)return Jt(r[0]);let t=Math.ceil(r.length/2),n=r.slice(0,t),i=r.slice(t);return{direction:e%2===0?"horizontal":"vertical",split:Math.round(t/r.length*100)/100,children:[ie(n,e+1),ie(i,e+1)]}},Ge=r=>{if(r.length===0)throw new Error("buildCmuxLayout: at least one command is required");return ie(r,0)};var zt=(r,e)=>r.map(({app:t,targets:n})=>`pnpm exec infra-kit dev ${n&&n.length>0?`--target=${n.join(",")}`:`--app=${t}`}${e?" --watch":""}`),Yt=r=>{let e=new Map;for(let t of Object.keys(r?.apps??{})){let n=t.split("/")[0];n===void 0||n==="*"||e.set(n,[...e.get(n)??[],t])}return e},Xt=(r,e)=>{let t=U(r);return e?t.filter(n=>e.includes(n.name)):t},Qt=(r,e)=>{T.info(`\u{1F9E9} Opened cmux dev workspace ${e} with ${r.length} pane(s):`);for(let t of r)T.info(` \u2022 ${t.name} (infra-kit dev --app=${t.name})`)},Zt=r=>{H({onSignal:async e=>{T.info(`
|
|
2
2
|
Received ${e}, closing cmux dev workspace ${r}...`),await Be(r)}})},Ke=async r=>{let e=C(qt.cwd()),t=Xt(e,F(r.include));if(t.length===0){T.warn("No API apps found to run");return}let n=Yt(r.presetDef),i=zt(t.map(u=>({app:u.name,targets:n.get(u.name)})),r.watch??!1),s=Ge(i),o=`${Ve.basename(e)} dev`,a=await He({cwd:e,title:o,layout:s});Qt(t,a),Zt(a);let l=setInterval(()=>{l.refresh()},2**30);await new Promise(()=>{})};import se from"node:process";var oe=r=>{try{se.stderr.write(r)}catch{}},I=(r,e,t=!0)=>{let n=e instanceof Error?`${e.message}
|
|
3
3
|
${e.stack??"(no stack)"}`:String(e);return`
|
|
4
4
|
\u26A0\uFE0F ${r}: ${n}
|
package/dist/mcp-proxy.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import{u as G}from"./chunk-
|
|
2
|
+
import{u as G}from"./chunk-LDF2KOGN.js";import{a as M}from"./chunk-MFAFQVTW.js";import"./chunk-HHWP3YXW.js";import{a as W,d as H,n as T,o as E,s as K}from"./chunk-NFIFWIO6.js";import m from"node:process";var ve=/^[a-z][a-z0-9-]*$/,ye=/^[A-Z_]\w*$/i,Pe=(e,n,r)=>n==="--name"?e.name!==null?"--name given twice":ve.test(r)?(e.name=r,null):`--name "${r}" must be lowercase letters, digits and hyphens`:n==="--env"||n==="--unset"?ye.test(r)?M.has(r)?`${n} "${r}" is process plumbing, not a credential`:(n==="--env"?e.env.push(r):e.unset.push(r),null):`${n} "${r}" is not an environment-variable name`:`unknown flag ${n}`,Se=(e,n)=>{let r=0;for(;r<e.length;){let o=e[r];if(o==="--")return{separator:r};if(!o.startsWith("--"))return{problem:`missing "--" before the upstream command (saw "${o}")`};let a=e[r+1];if(a===void 0||a.startsWith("--"))return{problem:`${o} needs a value`};let p=Pe(n,o,a);if(p!==null)return{problem:p};r+=2}return{problem:'missing "--" before the upstream command'}},Q=e=>{let n={name:null,env:[],unset:[]},r=Se(e,n);if("problem"in r)return{ok:!1,problem:r.problem};if(n.name===null)return{ok:!1,problem:"missing --name"};if(n.env.length===0)return{ok:!1,problem:"missing --env (at least one variable to read)"};let o=e[r.separator+1];return o===void 0||o.length===0?{ok:!1,problem:'missing upstream command after "--"'}:{ok:!0,spec:{name:n.name,command:o,args:e.slice(r.separator+2),env:n.env,unset:n.unset}}};import{spawnSync as xe}from"node:child_process";import C from"node:fs";import x from"node:path";import O from"node:process";var Y=e=>x.join(E(),"mcp-proxy",e),be=e=>e.replaceAll(/[^\w.-]/g,"_").slice(0,64),Z=(e,n)=>x.join(n,`profile-${be(e)}.json`),A=(e,n)=>{let r=x.isAbsolute(e)?[e]:(n??O.env.PATH??"").split(x.delimiter).map(o=>x.join(o,e));for(let o of r)try{let a=C.statSync(o);return`stat-${a.size}-${Math.trunc(a.mtimeMs)}`}catch{}return"unknown"},X=(e,n=["--version"],r)=>{try{let o=xe(e,[...n],{encoding:"utf8",timeout:5e3,env:r==null?O.env:{...O.env,PATH:r}});if(o.status!==0)return A(e,r);let a=`${o.stdout}`.trim().split(`
|
|
3
3
|
`)[0]?.trim()??"";return a.length>0?a:A(e,r)}catch{return A(e,r)}},ee=(e,n)=>{try{let r=JSON.parse(C.readFileSync(Z(e,n),"utf8"));return r.version!==e||!Array.isArray(r.tools)?null:{version:e,protocolVersion:typeof r.protocolVersion=="string"?r.protocolVersion:"",capabilities:r.capabilities??{},tools:r.tools}}catch{return null}},re=(e,n)=>{try{return C.mkdirSync(n,{recursive:!0}),K(Z(e.version,n),`${JSON.stringify(e)}
|
|
4
4
|
`,384),!0}catch{return!1}};import we from"node:path";import L from"node:process";var Ee="no-session",Me="IK_MCP_ENV_FILE",Re=()=>{let e=L.env[H];return e!=null&&e.length>0?e:Ee},R=()=>{let e=L.env[Me];return e!=null&&e.length>0?e:we.join(E(),Re(),W)},$e=(e,n)=>{let r=e[n];if(r!=null&&r.length>0)return r;let o=L.env[n];return o!=null&&o.length>0?o:""},ne=(e,n=R())=>{let r=T(n),o={};for(let a of e){let p=$e(r,a);if(p.length===0)return null;o[a]=p}return o},te=(e,n=R())=>{let r=new Set(e);return Object.keys(T(n)).filter(o=>!r.has(o))};var $=(e,n)=>{let r="";e.setEncoding("utf8"),e.on("data",o=>{r+=o;let a=r.indexOf(`
|
|
5
5
|
`);for(;a!==-1;){let p=r.slice(0,a);r=r.slice(a+1);let f=p.endsWith("\r")?p.slice(0,-1):p;f.trim().length>0&&n(f),a=r.indexOf(`
|
package/dist/mcp.js
CHANGED
|
@@ -1,111 +1,2 @@
|
|
|
1
|
-
import{a as A,d as g,fa as O,ia as N,s as P}from"./chunk-7YE5QKKB.js";import{i as I}from"./chunk-YKCY657C.js";import"./chunk-KXQQUBGV.js";import{I as p,J as _,K as i,R as D,c as S}from"./chunk-63VNUKUS.js";import"./chunk-UI2MUEVR.js";import"./chunk-MFAFQVTW.js";import{a as E}from"./chunk-MFLYORUJ.js";import"./chunk-GYPVEWUV.js";import"./chunk-NFIFWIO6.js";import"./chunk-QWVLAZ6N.js";import{serveStdio as Le}from"@modelcontextprotocol/server/stdio";import m from"node:process";import{setTimeout as Ge}from"node:timers/promises";import u from"node:process";var M=e=>{u.on("uncaughtException",t=>{e.fatal({err:t,msg:"Uncaught Exception"}),e.error(`Uncaught Exception! Check ${p}. Shutting down...`),e.flush(),u.exit(1)}),u.on("unhandledRejection",(t,o)=>{e.fatal({reason:t,promise:o,msg:"Unhandled Rejection"}),e.error(`Unhandled Rejection! Check ${p}. Shutting down...`),e.flush(),u.exit(1)})};import{McpServer as $e}from"@modelcontextprotocol/server";var F='# 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=`# session \u2014 switching a terminal's context through infra-kit
|
|
2
|
-
|
|
3
|
-
Three tools do the work, and this body is only the procedure that composes them:
|
|
4
|
-
\`mcp__infra-kit__env-list\`, \`mcp__infra-kit__env-load\` and \`mcp__infra-kit__env-clear\`. None of them
|
|
5
|
-
changes here.
|
|
6
|
-
|
|
7
|
-
## 1. What a session is
|
|
8
|
-
|
|
9
|
-
A named context. Today it resolves to exactly one activation \u2014 the Doppler environment \u2014 and the
|
|
10
|
-
name **is** the Doppler config name. There is no mapping table to consult.
|
|
11
|
-
|
|
12
|
-
The contract a second provider would implement, and the recipe for adding one, live in
|
|
13
|
-
\`docs/session-context-orchestrator.md\`. They are written for whoever adds that provider, not for you.
|
|
14
|
-
|
|
15
|
-
## 2. How this reaches the user's shell, and how it fails
|
|
16
|
-
|
|
17
|
-
\`env-load\` mutates no process. It downloads the config's variables, writes them to
|
|
18
|
-
\`\${XDG_CACHE_HOME:-$HOME/.cache}/infra-kit/$INFRA_KIT_SESSION/env-load.sh\` and returns that path as
|
|
19
|
-
\`filePath\`. The zsh block \`infra-kit setup\` installs registers a \`precmd\` hook that sources the file
|
|
20
|
-
when its mtime beats the last one sourced, the shell's start time, and the clear file.
|
|
21
|
-
|
|
22
|
-
State all three of the following.
|
|
23
|
-
|
|
24
|
-
**Timing.** \`precmd\` runs before a prompt is drawn and cannot run while a foreground process holds
|
|
25
|
-
the shell. The variables appear at its next prompt \u2014 after Claude Code exits or is backgrounded, not
|
|
26
|
-
when the tool returns.
|
|
27
|
-
|
|
28
|
-
**Destination.** The session id is the one the MCP server inherited when Claude Code launched, so the
|
|
29
|
-
file lands in the terminal that launched Claude Code and no other. A server that has outlived its
|
|
30
|
-
shell writes into a directory nothing is watching and still returns success \u2014 no error, no other
|
|
31
|
-
signal. So report the session id from the returned filePath, and tell the human to
|
|
32
|
-
compare it with INFRA_KIT_SESSION at their own prompt. That comparison is the only check there is.
|
|
33
|
-
|
|
34
|
-
**Sourcing it yourself is not a substitute.** Claude Code's \`Bash\` tool
|
|
35
|
-
does not persist shell state between calls, so a \`source\` call changes nothing durable, and
|
|
36
|
-
reporting success from it hides the real failure.
|
|
37
|
-
|
|
38
|
-
The loud failure is \`INFRA_KIT_SESSION is not set\`: the shell block was never installed, or this
|
|
39
|
-
shell predates it. Tell the human to run \`infra-kit setup --skip-tools\` and then \`source ~/.zshrc\`.
|
|
40
|
-
Do not retry \u2014 nothing about a second call will differ.
|
|
41
|
-
|
|
42
|
-
The authoritative reading of what a terminal holds is \`infra-kit env-status\`
|
|
43
|
-
typed in the terminal, not over MCP. Over MCP that tool reads the long-lived server's own
|
|
44
|
-
environment, frozen when Claude Code launched, so it can flatly contradict a load you made moments
|
|
45
|
-
ago. Never use it to verify one.
|
|
46
|
-
|
|
47
|
-
## 3. Resolving \`$ARGUMENTS\`
|
|
48
|
-
|
|
49
|
-
**A bare token is the environment name.** Call \`mcp__infra-kit__env-load\` with \`config: <token>\`.
|
|
50
|
-
|
|
51
|
-
**No token means the human has not chosen yet.** Call \`mcp__infra-kit__env-list\`, then ask with
|
|
52
|
-
\`AskUserQuestion\` \u2014 one option per environment, and **no table**. You receive \`structuredContent\`,
|
|
53
|
-
not the aligned table \`env-list\` prints on the CLI path, so any table here is one you hand-built from
|
|
54
|
-
JSON.
|
|
55
|
-
|
|
56
|
-
Carry exactly one field into the options: an environment whose \`hasToken\` is \`false\` cannot succeed,
|
|
57
|
-
so annotate that option with \`infra-kit env-token-set <env>\` as its fix. Do not surface \`source\` \u2014 it
|
|
58
|
-
records how we learned the environment exists, which helps nobody choose.
|
|
59
|
-
|
|
60
|
-
More than four environments: offer the four most likely and let a typed "Other" carry the rest. Then
|
|
61
|
-
load what they picked. Never invent a name, and never load without an explicit choice.
|
|
62
|
-
|
|
63
|
-
## 4. The list is local and may be wrong
|
|
64
|
-
|
|
65
|
-
\`env-list\` is the union of two local sources: every environment declared in a workflow's
|
|
66
|
-
\`workflow_dispatch\` \`environment.options\`, and every environment the local token store holds a token
|
|
67
|
-
for. It is not a live Doppler enumeration \u2014 a Doppler service token is config-scoped and cannot
|
|
68
|
-
enumerate its siblings.
|
|
69
|
-
|
|
70
|
-
Two consequences. First, an empty list is a legitimate result rather than an error: say so, and ask
|
|
71
|
-
for a name in prose. Second, a name absent from the list must still be passed to \`env-load\`, because
|
|
72
|
-
the list is not authoritative about what exists. Only \`hasToken\` is authoritative about what loads.
|
|
73
|
-
|
|
74
|
-
## 5. The flag
|
|
75
|
-
|
|
76
|
-
- \`--clear\` \u2192 \`mcp__infra-kit__env-clear\`, through the two-call confirm protocol in section 6.
|
|
77
|
-
|
|
78
|
-
\`--clear\` together with a bare environment token is a usage error and is refused, not resolved by
|
|
79
|
-
precedence. Both precedence answers are wrong: loading is not what was asked for, and clearing
|
|
80
|
-
discards the name that was typed.
|
|
81
|
-
|
|
82
|
-
## 6. \`--clear\`'s confirm gate, and the tie hazard
|
|
83
|
-
|
|
84
|
-
\`env-clear\` is gated. **Call 1** \u2014 the real arguments, no \`confirm\` and no \`confirmToken\` \u2014 returns
|
|
85
|
-
\`{"status": "confirmation_required", \u2026}\` carrying \`"isError": true\`. That is not a failure: nothing
|
|
86
|
-
was cleared. Show the human what it resolved, because that is the approval moment. **Call 2** repeats
|
|
87
|
-
those arguments unchanged plus \`"confirm": true\` and the \`confirmToken\` from call 1. A mismatch comes
|
|
88
|
-
back \`confirmation_refused\`, which is terminal \u2014 mint a fresh gate, never reuse a token.
|
|
89
|
-
|
|
90
|
-
\`env-load\` is not gated. Say so if the human expects a prompt, so nobody waits for one that never comes.
|
|
91
|
-
|
|
92
|
-
**The tie hazard.** The shell's clear gate compares mtimes in whole seconds and strictly, while its
|
|
93
|
-
load gate does not. A clear whose file lands in the same wall-clock second as the load it follows
|
|
94
|
-
therefore loses: the terminal prints \`infra-kit: auto-loaded vars for <config>\` after the human asked
|
|
95
|
-
to clear, or prints nothing and stays loaded. Running \`--clear\` once more a second later is the
|
|
96
|
-
recovery. Tell the human to confirm at their own prompt rather than trusting this tool's return.
|
|
97
|
-
|
|
98
|
-
**How often this matters.** On this path, rarely \u2014 the confirm gate puts a human approval between the
|
|
99
|
-
load and the clear, and that latency is usually enough. It is common in scripted or back-to-back use,
|
|
100
|
-
where nothing interposes. Raise it when a clear closely follows a load, not on every clear.
|
|
101
|
-
|
|
102
|
-
## 7. What not to do
|
|
103
|
-
|
|
104
|
-
- Do not shell out to \`doppler\`. The tools own the token resolution and the credential filtering.
|
|
105
|
-
- Do not \`export\` anything in a shell, and do not present a file you sourced as a loaded environment.
|
|
106
|
-
- Never echo a variable's value. \`env-list\` reports token presence only, and \`env-load.sh\` holds
|
|
107
|
-
single-quoted secrets \u2014 printing one puts it in the transcript.
|
|
108
|
-
- Do not read \`isError: true\` on a \`confirmation_required\` payload as a failure. See section 6.
|
|
109
|
-
- Do not verify a load with \`env-status\` over MCP. See section 2.
|
|
110
|
-
`;var j='# 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 $={"release-create":F.trimEnd(),session:q.trimEnd(),setup:j.trimEnd()};var de="infra-kit://config",he="infra-kit://dev-context",ue="infra-kit://workflow/release-create",me="infra-kit://workflow/setup",fe="infra-kit://workflow/session",pe={loadConfig:D,readDevContext:()=>g()},w=(e,t)=>({contents:[{uri:e,mimeType:"application/json",text:JSON.stringify(t,null,2)}]}),y=(e,t)=>{e.registerResource(`infra-kit-workflow-${t.key}`,t.uri,{title:t.title,description:t.description,mimeType:"text/markdown"},o=>({contents:[{uri:o.toString(),mimeType:"text/markdown",text:$[t.key]}]}))},L=async(e,t=pe)=>{e.registerResource("infra-kit-config",de,{title:"infra-kit config",description:"The merged infra-kit.json configuration (all override layers applied). Read-only.",mimeType:"application/json"},async o=>{try{return w(o.toString(),await t.loadConfig())}catch(n){return w(o.toString(),{error:n instanceof Error?n.message:String(n)})}}),e.registerResource("infra-kit-dev-context",he,{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"},o=>w(o.toString(),t.readDevContext())),y(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.'}),y(e,{key:"setup",uri:me,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."}),y(e,{key:"session",uri:fe,title:"session procedure",description:"How to switch a terminal to a named environment by composing env-list, env-load and env-clear: which tool to call when the human names no environment, how the loaded vars reach the terminal that launched Claude Code and when they appear there, and the two silent failures \u2014 a load that lands in a session nothing is watching, and a clear that loses a same-second tie to the load before it. Read this before loading an environment for someone."})};import{z as x}from"zod";import{createRequestStateCodec as ge}from"@modelcontextprotocol/server";import{randomBytes as we}from"node:crypto";var ye=new Set(["confirm","confirmToken"]),ve=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(o=>[o,v(t[o])]))}return e},k=e=>JSON.stringify(v(e)),c=e=>typeof e!="object"||e===null?{}:Object.fromEntries(Object.entries(e).filter(([t])=>!ye.has(t))),U=(e={})=>{let t=ge({key:e.key??we(32),ttlSeconds:e.ttlSeconds??ve,bind:o=>o.toolName});return{mint:(o,n)=>t.mint(o,G(n)),verify:(o,n)=>t.verify(o,G(n))}},H,W=()=>(H??=U(),H),ke=e=>{if(typeof e!="object"||e===null)return;let{confirmToken:t}=e;return typeof t=="string"?t:void 0},be=new Set(["malformed","mac","expired","bind"]),Ce=e=>{let t=e instanceof Error?e.message:"";return be.has(t)?t:"malformed"},K=async(e,t,o)=>e.mint({args:k(c(o))},t),B=async(e,t,o)=>{let n=ke(o);if(n===void 0)return{ok:!1,reason:"absent"};let r;try{r=await e.verify(n,t)}catch(a){return{ok:!1,reason:Ce(a)}}return r.args!==k(c(o))?{ok:!1,reason:"mismatch"}:{ok:!0}};import{acceptedContent as Te,inputRequired as z,inputResponse as xe}from"@modelcontextprotocol/server";var b="args",Y=3e3,Z=e=>{let t=xe(e,b);return t.kind==="elicit"?t.action:"missing"},Re=async(e,t)=>new Promise(o=>{let n=setTimeout(()=>{o(null)},t);e.then(r=>{clearTimeout(n),o(r)},()=>{clearTimeout(n),o(null)})}),V=async(e,t,o)=>{try{return await Re(e.buildRequestedSchema(t),o)}catch{return null}},X=async(e,t,o)=>{let n=await V(e,t,o);if(n===null)return null;try{return z({inputRequests:{[b]:z.elicit({message:e.message,requestedSchema:n})}})}catch{return null}},Q=async(e,t,o,n)=>{let r=await V(e,t,n);if(r===null)return null;let a;try{a=Te(o,b,r)}catch{return null}if(a===void 0)return null;try{return e.toArgs(a,t)}catch{return null}},J=e=>typeof e=="object"&&e!==null&&!Array.isArray(e),ee=(e,t)=>{if(!J(e))return!1;if(!J(t))return!0;for(let[o,n]of Object.entries(e)){if(!(o in t))return!0;let r=t[o];if(Array.isArray(n)&&(!Array.isArray(r)||r.length!==n.length))return!0}return!1};var Se=e=>typeof e=="object"&&e!==null&&e.confirm===!0,_e=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",C=e=>({content:A(JSON.stringify(e,null,2)),structuredContent:e,isError:!0}),Ae="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.",Ee="This tool does not prompt for its arguments; you are being asked to approve the values shown above.",De=async(e,t,o,n)=>{let r=c(o),a=await K(e,t,o),s=[n.formDiscarded?Ae: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.`,n.hasProvider?void 0:Ee].filter(f=>f!==void 0).join(" ");return C({status:"confirmation_required",tool:t,resolvedArgs:r,confirmToken:a,formDiscarded:n.formDiscarded,message:s})},Ie=(e,t)=>C({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'},Oe=(e,t)=>C({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}},Ne=async(e,t,o,n)=>{let r=e.formProvider;if(r===void 0||n!=="accept")return{params:t,formDiscarded:!1};let a=await Q(r,t,o,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}},Me=async(e,t,o,n,r)=>{if(n==="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 Ne(e,t,o,r);return i.info({msg:`Tool execution gated (awaiting confirm): ${e.toolName}`}),await De(e.codec,e.toolName,a.params,{formDiscarded:a.formDiscarded,hasProvider:e.formProvider!==void 0})},Fe=async(e,t,o)=>{let n=o?.mcpReq?.inputResponses,r=Z(n),a=_e({gated:e.requiresHumanConfirm===!0,confirmed:Se(t),responses:n,canForm:te(()=>e.getClientCapabilities?.()?.elicitation?.form!==void 0),hasProvider:e.formProvider!==void 0,formable:te(()=>e.formProvider?.isFormable(t)===!0),accepted:r==="accept"});if(a==="run")return null;if(a==="declined")return i.info({msg:`Tool execution form declined (${r}): ${e.toolName}`}),Ie(e.toolName,r);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}`}),Oe(e.toolName,s.reason))}return await Me(e,t,n,a,r)},T=({toolName:e,handler:t,requiresHumanConfirm:o,formProvider:n,getClientCapabilities:r,confirmCodec:a,formDeadlineMs:s})=>{let f={toolName:e,codec:a??W(),requiresHumanConfirm:o,formProvider:n,getClientCapabilities:r,formDeadlineMs:s??Y};return async(h,R)=>{i.info({msg:`Tool execution started: ${e}`,params:h,sessionId:R?.sessionId});try{await P(),I.reset();let d=await Fe(f,h,R);if(d!==null)return d;let ie=await t({...h,confirmedCommand:!0});return i.info({msg:`Tool execution successful: ${e}`}),ie}catch(d){throw i.error({err:d,params:h,msg:`Tool execution failed: ${e}`}),d}}};var oe=async e=>{for(let t of O())e.registerTool(t.name,{title:t.title,description:t.description,inputSchema:x.object(t.requiresHumanConfirm===!0?qe(t.inputSchema):t.inputSchema),outputSchema:x.object(t.outputSchema),annotations:t.annotations,_meta:t.meta},je(T({toolName:t.name,handler:t.handler,requiresHumanConfirm:t.requiresHumanConfirm,formProvider:t.formProvider,getClientCapabilities:()=>e.server.getClientCapabilities()})))},qe=e=>({...e,confirmToken:x.string().optional().describe("Round-2 only: the token returned by the round-1 gate, proving the arguments are unchanged.")}),je=e=>(t,o)=>e(t,o);async function ne(){S.enabled=!0;let e=new $e({name:"infra-kit",version:E.version},{capabilities:{resources:{listChanged:!0},tools:{}}});return await L(e),await oe(e),e}N();var l=_(),He=async()=>{try{return await ne()}catch(e){l.error({err:e,msg:"Failed to create MCP server"}),l.flush(),m.exit(1)}};M(l);var Ue=Le(He,{onerror:e=>{l.error({err:e,msg:"MCP stdio entry error"}),l.flush()}});l.info({msg:"MCP stdio entry started."});var re=!1,ae=async e=>{if(!re){re=!0,l.info({msg:`Received ${e}. Shutting down...`});try{await Promise.race([Ue.close(),Ge(1500)])}catch(t){l.error({err:t,msg:"MCP stdio close failed during shutdown"})}l.flush(),m.exit(0)}};m.on("SIGINT",()=>{ae("SIGINT")});m.on("SIGTERM",()=>{ae("SIGTERM")});
|
|
1
|
+
import{Z as j,a as I,b as y,c as P,d as F,g as k,ka as H,na as W,w as L}from"./chunk-4KWVFLSU.js";import{i as q}from"./chunk-YKCY657C.js";import"./chunk-KXQQUBGV.js";import{I as v,J as O,K as i,R as $,c as N}from"./chunk-63VNUKUS.js";import"./chunk-LDF2KOGN.js";import"./chunk-MFAFQVTW.js";import{l as M}from"./chunk-TNPZPZJJ.js";import"./chunk-HHWP3YXW.js";import"./chunk-NFIFWIO6.js";import"./chunk-QWVLAZ6N.js";import{serveStdio as Qe}from"@modelcontextprotocol/server/stdio";import p from"node:process";import{setTimeout as et}from"node:timers/promises";import m from"node:process";var G=e=>{m.on("uncaughtException",t=>{e.fatal({err:t,msg:"Uncaught Exception"}),e.error(`Uncaught Exception! Check ${v}. Shutting down...`),e.flush(),m.exit(1)}),m.on("unhandledRejection",(t,o)=>{e.fatal({reason:t,promise:o,msg:"Unhandled Rejection"}),e.error(`Unhandled Rejection! Check ${v}. Shutting down...`),e.flush(),m.exit(1)})};import{McpServer as Ye}from"@modelcontextprotocol/server";import Ze from"node:process";var U='# release-create \u2014 cutting a release through infra-kit\n\nThe tool is `mcp__plugin_infra-kit_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 z="# session \u2014 switching a terminal's context through infra-kit\n\nThree tools do the work, and this body is only the procedure that composes them:\n`mcp__plugin_infra-kit_infra-kit__env-list`, `mcp__plugin_infra-kit_infra-kit__env-load` and `mcp__plugin_infra-kit_infra-kit__env-clear`. None of them\nchanges here.\n\n## 1. What a session is\n\nA named context. Today it resolves to exactly one activation \u2014 the Doppler environment \u2014 and the\nname **is** the Doppler config name. There is no mapping table to consult.\n\nThe contract a second provider would implement, and the recipe for adding one, live in\n`docs/session-context-orchestrator.md`. They are written for whoever adds that provider, not for you.\n\n## 2. How this reaches the user's shell, and how it fails\n\n`env-load` mutates no process. It downloads the config's variables, writes them to\n`${XDG_CACHE_HOME:-$HOME/.cache}/infra-kit/$INFRA_KIT_SESSION/env-load.sh` and returns that path as\n`filePath`. The zsh block `infra-kit setup` installs registers a `precmd` hook that sources the file\nwhen its mtime beats the last one sourced, the shell's start time, and the clear file.\n\nState all three of the following.\n\n**Timing.** `precmd` runs before a prompt is drawn and cannot run while a foreground process holds\nthe shell. The variables appear at its next prompt \u2014 after Claude Code exits or is backgrounded, not\nwhen the tool returns.\n\nEvery zsh spawned from that terminal after the file lands sees it immediately \u2014 the `Bash` tool\nincluded \u2014 because a fresh shell sources `~/.zshenv` on its own at startup, not through `precmd`.\nThat holds only when `~/.zshenv` carries the infra-kit session-env block; `infra-kit doctor` reports\nthe row `zshenv session block` for it, and a machine set up before that row existed needs\n`infra-kit setup --skip-tools` once to gain it.\nSo `infra-kit env-status` run through Bash is a truthful reading of what the agent's own commands\nsee \u2014 it reads that child shell's inherited environment, not the terminal's. `env-status` over MCP\nhas not changed: it is still the long-lived server's frozen environment, never a verification.\n\n**Destination.** The session id is the one the MCP server inherited when Claude Code launched, so the\nfile lands in the terminal that launched Claude Code and no other. A server that has outlived its\nshell writes into a directory nothing is watching and still returns success \u2014 no error, no other\nsignal. So report the session id from the returned filePath, and tell the human to\ncompare it with INFRA_KIT_SESSION at their own prompt. That comparison is the only check there is.\n\n**Sourcing it yourself is not a substitute.** Claude Code's `Bash` tool\ndoes not persist shell state between calls, so a `source` call changes nothing durable, and\nreporting success from it hides the real failure. A shell spawned fresh after the block lands is\ndifferent: it sources `~/.zshenv` on its own at startup, needing no `source` call from you at all.\n\nThe loud failure is `INFRA_KIT_SESSION is not set`: the shell block was never installed, or this\nshell predates it. Tell the human to run `infra-kit setup --skip-tools` and then `source ~/.zshrc`.\nDo not retry \u2014 nothing about a second call will differ.\n\nThe authoritative reading of what a terminal holds is `infra-kit env-status`\ntyped in the terminal, not over MCP. Over MCP that tool reads the long-lived server's own\nenvironment, frozen when Claude Code launched, so it can flatly contradict a load you made moments\nago. Never use it to verify one.\n\n## 3. Resolving `$ARGUMENTS`\n\n**A bare token is the environment name.** Call `mcp__plugin_infra-kit_infra-kit__env-load` with `config: <token>`.\n\n**No token means the human has not chosen yet.** Call `mcp__plugin_infra-kit_infra-kit__env-load` **without `config`**:\nthe server offers the human a form listing every environment, and the human's pick IS the load \u2014 there\nis no second prompt. If the call comes back as a tool error or a refused result naming `config`, this client cannot\nrender forms: call `mcp__plugin_infra-kit_infra-kit__env-list` and show **every** entry as a numbered prose list,\n`hasToken: false` annotated with `infra-kit env-token-set <env>`, then ask in prose which one. Never\n`AskUserQuestion` \u2014 it caps the list at four and drops the rest \u2014 never invent a name, and never load\nwithout an explicit choice.\n\n## 4. The list is local and may be wrong\n\n`env-list` is the union of two local sources: every environment declared in a workflow's\n`workflow_dispatch` `environment.options`, and every environment the local token store holds a token\nfor. It is not a live Doppler enumeration \u2014 a Doppler service token is config-scoped and cannot\nenumerate its siblings.\n\nTwo consequences. First, an empty list is a legitimate result rather than an error: say so, and ask\nfor a name in prose. Second, a name absent from the list must still be passed to `env-load`, because\nthe list is not authoritative about what exists. Only `hasToken` is authoritative about what loads.\n\n## 5. The flag\n\n- `--clear` \u2192 `mcp__plugin_infra-kit_infra-kit__env-clear`, through the two-call confirm protocol in section 6.\n\n`--clear` together with a bare environment token is a usage error and is refused, not resolved by\nprecedence. Both precedence answers are wrong: loading is not what was asked for, and clearing\ndiscards the name that was typed.\n\n## 6. `--clear`'s confirm gate, and the tie hazard\n\n`env-clear` is gated. **Call 1** \u2014 the real arguments, no `confirm` and no `confirmToken` \u2014 returns\n`{\"status\": \"confirmation_required\", \u2026}` carrying `\"isError\": true`. That is not a failure: nothing\nwas cleared. Show the human what it resolved, because that is the approval moment. **Call 2** repeats\nthose arguments unchanged plus `\"confirm\": true` and the `confirmToken` from call 1. A mismatch comes\nback `confirmation_refused`, which is terminal \u2014 mint a fresh gate, never reuse a token.\n\n`env-load` is not gated, but it can PROMPT: called without `config` it offers an argument form, not a confirm\ngate, and the human's pick is the load. Say so if the human expects a second prompt, so nobody waits for one.\n\n**The tie hazard.** The shell's clear gate compares mtimes in whole seconds and strictly, while its\nload gate does not. A clear whose file lands in the same wall-clock second as the load it follows\ntherefore loses: the terminal prints `infra-kit: auto-loaded vars for <config>` after the human asked\nto clear, or prints nothing and stays loaded. Running `--clear` once more a second later is the\nrecovery. Tell the human to confirm at their own prompt rather than trusting this tool's return.\n\n**How often this matters.** On this path, rarely \u2014 the confirm gate puts a human approval between the\nload and the clear, and that latency is usually enough. It is common in scripted or back-to-back use,\nwhere nothing interposes. Raise it when a clear closely follows a load, not on every clear.\n\n## 7. What not to do\n\n- Do not shell out to `doppler`. The tools own the token resolution and the credential filtering.\n- Do not `export` anything in a shell, and do not present a file you sourced as a loaded environment.\n- Never echo a variable's value. `env-list` reports token presence only, and `env-load.sh` holds\n single-quoted secrets \u2014 printing one puts it in the transcript.\n- Do not read `isError: true` on a `confirmation_required` payload as a failure. See section 6.\n- Do not verify a load with `env-status` over MCP. See section 2.\n- Do not supply a `config` the human did not name in order to skip the form.\n- Never send `inputResponses` yourself \u2014 that field is the human's answer, and the server cannot tell yours from theirs.\n";var K='# setup \u2014 bringing a machine to a working infra-kit\n\nThe tool is `mcp__plugin_infra-kit_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 managed block in `~/.zshenv` \u2014 the session-env inheritance\n3. the four config migrations, in their recorded order\n4. the user-global config seed\n5. 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\n6. the git-root resolution for writes, warning when the two root gates disagree\n7. the Claude Code plugin pointer \u2014 `.claude/settings.json`, the plugin install or update (the plugin serves the MCP server), and a read-only report of the repo\'s `.mcp.json`\n8. the per-project config reseed\n9. 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__plugin_infra-kit_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 B={"release-create":U.trimEnd(),session:z.trimEnd(),setup:K.trimEnd()};var ye="infra-kit://config",ve="infra-kit://dev-context",ke="infra-kit://workflow/release-create",be="infra-kit://workflow/setup",Te="infra-kit://workflow/session",Ce={loadConfig:$,readDevContext:()=>k()},b=(e,t)=>({contents:[{uri:e,mimeType:"application/json",text:JSON.stringify(t,null,2)}]}),T=(e,t,o)=>{e.registerResource(`infra-kit-workflow-${o.key}`,o.uri,{title:o.title,description:o.description,mimeType:"text/markdown"},n=>({contents:[{uri:n.toString(),mimeType:"text/markdown",text:P(B[o.key],t)}]}))},J=async(e,t,o=Ce)=>{e.registerResource("infra-kit-config",ye,{title:"infra-kit config",description:"The merged infra-kit.json configuration (all override layers applied). Read-only.",mimeType:"application/json"},async n=>{try{return b(n.toString(),await o.loadConfig())}catch(r){return b(n.toString(),{error:r instanceof Error?r.message:String(r)})}}),e.registerResource("infra-kit-dev-context",ve,{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"},n=>b(n.toString(),o.readDevContext())),T(e,t,{key:"release-create",uri:ke,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 ${y("release-create",t)}.`}),T(e,t,{key:"setup",uri:be,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 ${y("setup",t)}.`}),T(e,t,{key:"session",uri:Te,title:"session procedure",description:"How to switch a terminal to a named environment by composing env-list, env-load and env-clear: which tool to call when the human names no environment, how the loaded vars reach the terminal that launched Claude Code and when they appear there, and the two silent failures \u2014 a load that lands in a session nothing is watching, and a clear that loses a same-second tie to the load before it. Read this before loading an environment for someone."})};import{CLIENT_CAPABILITIES_META_KEY as Je}from"@modelcontextprotocol/server";import{z as E}from"zod";import{createRequestStateCodec as xe}from"@modelcontextprotocol/server";import{randomBytes as Re}from"node:crypto";var Se=new Set(["confirm","confirmToken"]),_e=600,V=e=>({toolName:e}),C=e=>{if(Array.isArray(e))return e.map(C);if(e!==null&&typeof e=="object"){let t=e;return Object.fromEntries(Object.keys(t).sort().map(o=>[o,C(t[o])]))}return e},x=e=>JSON.stringify(C(e)),l=e=>typeof e!="object"||e===null?{}:Object.fromEntries(Object.entries(e).filter(([t])=>!Se.has(t))),Y=(e={})=>{let t=xe({key:e.key??Re(32),ttlSeconds:e.ttlSeconds??_e,bind:o=>o.toolName});return{mint:(o,n)=>t.mint(o,V(n)),verify:(o,n)=>t.verify(o,V(n))}},X,Z=()=>(X??=Y(),X),Ae=e=>{if(typeof e!="object"||e===null)return;let{confirmToken:t}=e;return typeof t=="string"?t:void 0},Ee=new Set(["malformed","mac","expired","bind"]),De=e=>{let t=e instanceof Error?e.message:"";return Ee.has(t)?t:"malformed"},Q=async(e,t,o)=>e.mint({args:x(l(o))},t),ee=async(e,t,o)=>{let n=Ae(o);if(n===void 0)return{ok:!1,reason:"absent"};let r;try{r=await e.verify(n,t)}catch(a){return{ok:!1,reason:De(a)}}return r.args!==x(l(o))?{ok:!1,reason:"mismatch"}:{ok:!0}};import{acceptedContent as Ie,inputRequired as te,inputResponse as Pe}from"@modelcontextprotocol/server";var R="args",ne=3e3,re=e=>{let t=Pe(e,R);return t.kind==="elicit"?t.action:"missing"},Ne=async(e,t)=>new Promise(o=>{let n=setTimeout(()=>{o(null)},t);e.then(r=>{clearTimeout(n),o(r)},()=>{clearTimeout(n),o(null)})}),ae=async(e,t,o)=>{try{return await Ne(e.buildRequestedSchema(t),o)}catch{return null}},ie=async(e,t,o)=>{let n=await ae(e,t,o);if(n===null)return null;try{return te({inputRequests:{[R]:te.elicit({message:e.message,requestedSchema:n})}})}catch{return null}},S=async(e,t,o,n)=>{let r=await ae(e,t,n);if(r===null)return null;let a;try{a=Ie(o,R,r)}catch{return null}if(a===void 0)return null;try{return e.toArgs(a,t)}catch{return null}},oe=e=>typeof e=="object"&&e!==null&&!Array.isArray(e),_=(e,t)=>{if(!oe(e))return!1;if(!oe(t))return!0;for(let[o,n]of Object.entries(e)){if(!(o in t))return!0;let r=t[o];if(Array.isArray(n)&&(!Array.isArray(r)||r.length!==n.length))return!0}return!1};var Oe=e=>typeof e=="object"&&e!==null&&e.confirm===!0,Fe=e=>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":e.gated&&e.confirmed?"verify":!e.gated&&e.responses===void 0&&e.canForm&&e.hasProvider&&e.formable?"form":!e.gated&&e.responses!==void 0&&e.hasProvider&&!e.accepted?"declined":!e.gated&&e.responses!==void 0&&e.hasProvider&&e.accepted?"run-form":"run",f=e=>({content:F(JSON.stringify(e,null,2)),structuredContent:e,isError:!0}),Me="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.",$e="This tool does not prompt for its arguments; you are being asked to approve the values shown above.",qe=async(e,t,o,n)=>{let r=l(o),a=await Q(e,t,o),d=[n.formDiscarded?Me: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.`,n.hasProvider?void 0:$e].filter(g=>g!==void 0).join(" ");return f({status:"confirmation_required",tool:t,resolvedArgs:r,confirmToken:a,formDiscarded:n.formDiscarded,message:d})},Le=(e,t)=>f({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.`}),je={validation:"failed validation",narrowed:"narrowed the arguments"},se=(e,t)=>f({status:"form_discarded",tool:e,reason:t,message:`${e} was NOT executed: the values you submitted in the form ${je[t]} and were DISCARDED. No confirmation is pending \u2014 call ${e} again to start over.`}),He={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'},We=(e,t)=>f({status:"confirmation_refused",tool:e,reason:t,message:`${e} was NOT executed: ${He[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".`}),le=e=>{try{return e()}catch{return!1}},Ge=async(e,t,o,n)=>{let r=e.formProvider;if(r===void 0||n!=="accept")return{params:t,formDiscarded:!1};let a=await S(r,t,o,e.formDeadlineMs);return a===null?(i.info({msg:`Tool execution form discarded (validation): ${e.toolName}`}),{params:t,formDiscarded:!0}):_(l(t),a)?(i.info({msg:`Tool execution form discarded (narrowed): ${e.toolName}`}),{params:t,formDiscarded:!0}):{params:a,formDiscarded:!1}},c=e=>({kind:"stop",result:e}),u=e=>({kind:"run",params:e}),ce=async(e,t,o,n)=>{let r=await Ge(e,t,o,n);return i.info({msg:`Tool execution gated (awaiting confirm): ${e.toolName}`}),await qe(e.codec,e.toolName,r.params,{formDiscarded:r.formDiscarded,hasProvider:e.formProvider!==void 0})},Ue=async(e,t,o,n,r)=>{let a=e.formProvider===void 0?null:await ie(e.formProvider,t,e.formDeadlineMs);return a!==null?(i.info({msg:`Tool execution form requested: ${e.toolName}`}),c(a)):(i.info({msg:`Tool execution form unavailable: ${e.toolName}`}),r?c(await ce(e,t,o,n)):u(t))},ze=async(e,t,o)=>{let n=e.formProvider;if(n===void 0)return u(t);let r=await S(n,t,o,e.formDeadlineMs);return r===null?(i.info({msg:`Tool execution form discarded (validation): ${e.toolName}`}),c(se(e.toolName,"validation"))):_(l(t),r)?(i.info({msg:`Tool execution form discarded (narrowed): ${e.toolName}`}),c(se(e.toolName,"narrowed"))):(i.info({msg:`Tool execution form accepted: ${e.toolName}`}),u(r))},Ke=async(e,t)=>{let o=await ee(e.codec,e.toolName,t);return o.ok?u(t):(i.info({msg:`Tool execution refused (${o.reason}): ${e.toolName}`}),c(We(e.toolName,o.reason)))},Be=async(e,t,o)=>{let n=o?.mcpReq?.inputResponses,r=re(n),a=e.requiresHumanConfirm===!0,d=Fe({gated:a,confirmed:Oe(t),responses:n,canForm:le(()=>e.getClientCapabilities?.(o)?.elicitation?.form!==void 0),hasProvider:e.formProvider!==void 0,formable:le(()=>e.formProvider?.isFormable(t)===!0),accepted:r==="accept"});switch(d){case"run":return u(t);case"declined":return i.info({msg:`Tool execution form declined (${r}): ${e.toolName}`}),c(Le(e.toolName,r));case"verify":return await Ke(e,t);case"run-form":return await ze(e,t,n);case"form":return await Ue(e,t,n,r,a);case"gate":return c(await ce(e,t,n,r));default:return j(d)}},A=({toolName:e,handler:t,requiresHumanConfirm:o,formProvider:n,getClientCapabilities:r,confirmCodec:a,formDeadlineMs:d})=>{let g={toolName:e,codec:a??Z(),requiresHumanConfirm:o,formProvider:n,getClientCapabilities:r,formDeadlineMs:d??ne};return async(w,D)=>{i.info({msg:`Tool execution started: ${e}`,params:w,sessionId:D?.sessionId});try{await L(),q.reset();let h=await Be(g,w,D);if(h.kind==="stop")return h.result;let fe=await t({...h.params,confirmedCommand:!0});return i.info({msg:`Tool execution successful: ${e}`}),fe}catch(h){throw i.error({err:h,params:w,msg:`Tool execution failed: ${e}`}),h}}};var de=async e=>{for(let t of H())e.registerTool(t.name,{title:t.title,description:t.description,inputSchema:E.object(t.requiresHumanConfirm===!0?Ve(t.inputSchema):t.inputSchema),outputSchema:E.object(t.outputSchema),annotations:t.annotations,_meta:t.meta},Xe(A({toolName:t.name,handler:t.handler,requiresHumanConfirm:t.requiresHumanConfirm,formProvider:t.formProvider,getClientCapabilities:o=>o?.mcpReq?.envelope?.[Je]??e.server.getClientCapabilities()})))},Ve=e=>({...e,confirmToken:E.string().optional().describe("Round-2 only: the token returned by the round-1 gate, proving the arguments are unchanged.")}),Xe=e=>(t,o)=>e(t,o);async function he(){N.enabled=!0;let e=new Ye({name:"infra-kit",version:M.version},{capabilities:{resources:{listChanged:!0},tools:{}}}),t=I(Ze.env);return await J(e,t),await de(e),e}W();var s=O(),tt=async()=>{try{return await he()}catch(e){s.error({err:e,msg:"Failed to create MCP server"}),s.flush(),p.exit(1)}};G(s);var ot=Qe(tt,{onerror:e=>{s.error({err:e,msg:"MCP stdio entry error"}),s.flush()}});s.info({msg:"MCP stdio entry started."});var ue=!1,me=async e=>{if(!ue){ue=!0,s.info({msg:`Received ${e}. Shutting down...`});try{await Promise.race([ot.close(),et(1500)])}catch(t){s.error({err:t,msg:"MCP stdio close failed during shutdown"})}s.flush(),p.exit(0)}};p.on("SIGINT",()=>{me("SIGINT")});p.on("SIGTERM",()=>{me("SIGTERM")});
|
|
111
2
|
//# sourceMappingURL=mcp.js.map
|