inscope 0.8.2 → 0.8.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -12,7 +12,7 @@
12
12
  > #### `cd` into a project and you are the right person: the right GitHub token, the right MCP servers, the right git commit email, all resolved live from `$PWD`. No toggles, no profile switching, and it holds up with several Claude Code sessions open at once.
13
13
 
14
14
  <p align="center">
15
- <img src="https://raw.githubusercontent.com/nrjdalal/demo-kit/main/inscope/demo.gif" alt="inscope demo: interactive add, list, and doctor" width="900" />
15
+ <img src="https://raw.githubusercontent.com/nrjdalal/inscope/main/.github/assets/demo.gif" alt="inscope demo: interactive add, list, and doctor" width="900" />
16
16
  </p>
17
17
 
18
18
  Concurrent sessions in different projects should never bleed work and personal accounts into each other. You describe each workspace once; `inscope` owns the moving parts and keeps them in sync:
@@ -63,7 +63,7 @@ inscope rm acme
63
63
  `cd ~/acme/api` and you are the work account, with work MCP servers and your work commit email. `cd ~/personal/blog` and you are you.
64
64
 
65
65
  <p align="center">
66
- <img src="https://raw.githubusercontent.com/nrjdalal/demo-kit/main/inscope/demo-switch.gif" alt="inscope switching git identity and tokens on cd" width="900" />
66
+ <img src="https://raw.githubusercontent.com/nrjdalal/inscope/main/.github/assets/demo-switch.gif" alt="inscope switching git identity and tokens on cd" width="900" />
67
67
  </p>
68
68
 
69
69
  ---
@@ -141,11 +141,7 @@ inscope doctor Verify tokens, identities, and the hook resolve correctly
141
141
  Run any command with `-h` for its options.
142
142
 
143
143
  <p align="center">
144
- <img src="https://raw.githubusercontent.com/nrjdalal/demo-kit/main/inscope/demo-diff.gif" alt="inscope diff: preview drift as a colored diff, then --adopt back-syncs an on-disk setting into the config" width="900" />
145
- </p>
146
-
147
- <p align="center">
148
- <img src="https://raw.githubusercontent.com/nrjdalal/demo-kit/main/inscope/demo-manage.gif" alt="inscope edit and rm with type-to-confirm" width="900" />
144
+ <img src="https://raw.githubusercontent.com/nrjdalal/inscope/main/.github/assets/demo-manage.gif" alt="inscope edit and rm with type-to-confirm" width="900" />
149
145
  </p>
150
146
 
151
147
  ### `inscope add`
@@ -168,6 +164,14 @@ Run it bare and it walks you through everything: pick the GitHub account from yo
168
164
  -y, --yes accept defaults, skip all prompts (non-interactive)
169
165
  ```
170
166
 
167
+ ### `inscope diff`
168
+
169
+ Preview exactly what `apply` would write: a colored diff of the hook, git includes, and each `.mcp.json` against your config. `--adopt` pulls config-expressible on-disk settings (a Slack add-message tool, a custom server URL) back into the config, so the next apply keeps them instead of dropping them.
170
+
171
+ <p align="center">
172
+ <img src="https://raw.githubusercontent.com/nrjdalal/inscope/main/.github/assets/demo-diff.gif" alt="inscope diff: preview drift as a colored diff, then --adopt back-syncs an on-disk setting into the config" width="900" />
173
+ </p>
174
+
171
175
  ---
172
176
 
173
177
  ## 🧩 What It Manages
@@ -216,7 +220,7 @@ inscope add ~/acme --gh neeraj-acme-org --servers github,slack --seed-slack
216
220
  You need a Slack app with a user OAuth (`xoxp`) token first. If you don't have one, follow the [slack-mcp-server authentication guide](https://github.com/korotovsky/slack-mcp-server/blob/HEAD/docs/01-authentication-setup.md#option-2-using-slack_mcp_xoxp_token-user-oauth). inscope points you there during `add` when Slack is enabled.
217
221
 
218
222
  <p align="center">
219
- <img src="https://raw.githubusercontent.com/nrjdalal/demo-kit/main/inscope/demo-slack.gif" alt="inscope adding Slack: keychain prompt and Yes/No selector confirms" width="900" />
223
+ <img src="https://raw.githubusercontent.com/nrjdalal/inscope/main/.github/assets/demo-slack.gif" alt="inscope adding Slack: keychain prompt and Yes/No selector confirms" width="900" />
220
224
  </p>
221
225
 
222
226
  ---
@@ -1,20 +1,19 @@
1
1
  #!/usr/bin/env node
2
- import e from"node:fs";import{parseArgs as t}from"node:util";import n from"node:path";import r from"node:os";import{spawnSync as i}from"node:child_process";import a from"node:readline";const o=()=>r.homedir(),s=()=>process.env.XDG_CONFIG_HOME?.trim()||n.join(o(),`.config`),c=e=>e===`~`?o():e.startsWith(`~/`)?n.join(o(),e.slice(2)):e,l=e=>{let t=c(e),r=o();return t===r?`~`:t.startsWith(r+n.sep)?`~/`+t.slice(r.length+1):t},u=e=>n.resolve(c(e)),d=()=>n.join(s(),`inscope`),f=()=>n.join(d(),`inscope.json`),p=()=>n.join(d(),`inscope.zsh`),m=()=>n.join(d(),`git`),h=()=>n.join(o(),`.gitconfig`),g=()=>n.join(o(),`.zshrc`),_=()=>({version:1,workspaces:[]}),v=()=>e.existsSync(f()),y=()=>{let t=f(),n=e.readFileSync(t,`utf8`),r=JSON.parse(n),i=te(r);if(i)throw Error(i);try{E(r)}catch(e){let n=e instanceof Error?e.message:String(e);throw Error(`${n}\nFix it in ${l(t)}, then re-run.`)}return r},b=t=>{E(t);let r=f();e.mkdirSync(n.dirname(r),{recursive:!0}),e.writeFileSync(r,JSON.stringify(t,null,2)+`
3
- `)},x=/^[A-Za-z0-9._-]+$/,S=e=>e?x.test(e)?null:`use only letters, digits, dot (.), dash (-), or underscore (_)`:`must not be empty`,C=/[\\"`$\n]/,w=e=>C.test(e)?'must not contain a backslash (\\), quote ("), backtick (`), $, or newline':null,ee=e=>e?w(e):`must not be empty`,T=e=>/[\n\r]/.test(e)?`must not contain a newline`:null,te=e=>typeof e.version==`number`&&e.version>1?`config version ${e.version} is newer than this inscope supports (max 1); upgrade inscope`:null,ne=e=>e.toLowerCase().replace(/[^a-z0-9._-]+/g,`-`).replace(/^[-.]+|[-.]+$/g,``),E=e=>{if(!e||typeof e!=`object`)throw Error(`config is not an object`);let t=te(e);if(t)throw Error(t);if(!Array.isArray(e.workspaces))throw Error(`config.workspaces must be an array`);let n=new Set;for(let t of e.workspaces){if(!t.name)throw Error(`a workspace is missing a name`);let e=S(t.name);if(e)throw Error(`workspace name "${t.name}" is invalid: ${e}`);if(!t.path)throw Error(`workspace "${t.name}" is missing a path`);let r=ee(t.path);if(r)throw Error(`workspace "${t.name}" path "${t.path}" is invalid: ${r}`);if(t.gh){let e=w(t.gh);if(e)throw Error(`workspace "${t.name}" gh account "${t.gh}" is invalid: ${e}`)}if(t.git?.email){let e=T(t.git.email);if(e)throw Error(`workspace "${t.name}" git email "${t.git.email}" is invalid: ${e}`)}if(t.git?.name){let e=T(t.git.name);if(e)throw Error(`workspace "${t.name}" git name "${t.git.name}" is invalid: ${e}`)}let i=t.servers?.slack;if(i&&i.keychain){let e=w(i.keychain);if(e)throw Error(`workspace "${t.name}" Slack keychain "${i.keychain}" is invalid: ${e}`)}if(n.has(t.name))throw Error(`duplicate workspace name "${t.name}"`);n.add(t.name)}},re=e=>ne(n.basename(u(e)))||`workspace`,D=(e,t)=>{let n=e.workspaces.find(e=>e.name===t);if(n)return n;let r=u(t);return e.workspaces.find(e=>u(e.path)===r)},ie=(e,t)=>{let n=e.workspaces.filter(e=>e.name!==t.name);return n.push({...t,path:l(t.path)}),n.sort((e,t)=>e.name.localeCompare(t.name)),{...e,workspaces:n}},ae=(e,t)=>{let n=D(e,t);return n?{cfg:{...e,workspaces:e.workspaces.filter(e=>e.name!==n.name)},removed:n}:{cfg:e}},O={atlassian:`https://mcp.atlassian.com/v1/mcp`,canva:`https://mcp.canva.com/mcp`,clickup:`https://mcp.clickup.com/mcp`,hubspot:`https://mcp.hubspot.com`,intercom:`https://mcp.intercom.com/mcp`,linear:`https://mcp.linear.app/mcp`,monday:`https://mcp.monday.com/mcp`,notion:`https://mcp.notion.com/mcp`,plane:`https://mcp.plane.so/http/mcp`,sentry:`https://mcp.sentry.dev/mcp`,stripe:`https://mcp.stripe.com`,vercel:`https://mcp.vercel.com`,webflow:`https://mcp.webflow.com/`},k=[`github`,`atlassian`,`canva`,`clickup`,`hubspot`,`intercom`,`linear`,`monday`,`notion`,`plane`,`sentry`,`slack`,`stripe`,`vercel`,`webflow`],A=e=>k.map(t=>`${t}-${e}`),j=e=>n.join(u(e.path),`.mcp.json`),oe=(e,t)=>e&&typeof e==`object`&&e.url?e.url:t,se=e=>{let t=e.servers,n={};for(let r of k){let i=t[r];if(!i)continue;let a=`${r}-${e.name}`;if(r===`github`)n[a]={type:`http`,url:`https://api.githubcopilot.com/mcp/`,headers:{Authorization:"Bearer ${GITHUB_TOKEN}"}};else if(r===`slack`){let e=i,t={SLACK_MCP_XOXP_TOKEN:"${SLACK_MCP_XOXP_TOKEN}"};e.addMessageTool&&(t.SLACK_MCP_ADD_MESSAGE_TOOL=`true`),n[a]={type:`stdio`,command:`npx`,args:[`-y`,`slack-mcp-server@1.3.0`,`--transport`,`stdio`],env:t}}else n[a]={type:`http`,url:oe(i,O[r])}}return n},ce=t=>{if(!e.existsSync(t))return{};try{return JSON.parse(e.readFileSync(t,`utf8`))}catch{return{}}},le=t=>{if(!e.existsSync(t))return{};try{return JSON.parse(e.readFileSync(t,`utf8`))}catch{throw Error(`${t} is not valid JSON; fix or remove it, then re-run inscope (left it untouched)`)}},ue=t=>{let n=j(t);return e.existsSync(n)?ce(n):null},de=t=>{let r=j(t);e.mkdirSync(n.dirname(r),{recursive:!0});let i=le(r),a=i.mcpServers&&typeof i.mcpServers==`object`?{...i.mcpServers}:{};for(let e of A(t.name))delete a[e];Object.assign(a,se(t)),i.mcpServers=a,e.writeFileSync(r,JSON.stringify(i,null,2)+`
4
- `)},fe=t=>{let n=j(t);if(!e.existsSync(n))return;let r=le(n);if(r.mcpServers&&typeof r.mcpServers==`object`)for(let e of A(t.name))delete r.mcpServers[e];e.writeFileSync(n,JSON.stringify(r,null,2)+`
5
- `)},M=(e,t,n)=>{let r=i(e,t,{encoding:`utf8`,input:n?.input});return{status:r.status??(r.error?127:1),stdout:r.stdout??``,stderr:r.stderr??``}},pe=()=>process.platform===`darwin`,N=()=>process.env.USER||``,me=(e,t=M)=>{let n=t(`gh`,[`auth`,`token`,`-u`,e]),r=n.stdout.trim();return n.status===0&&r?r:null},he=(e=M)=>{let t=e(`gh`,[`auth`,`status`]);return(t.stdout+t.stderr).trim()},ge=(e=M)=>{let t=[];for(let n of he(e).matchAll(/account (\S+) \(/g))t.includes(n[1])||t.push(n[1]);return t},_e=(e,t=M)=>{let n=t(`git`,[`config`,`--global`,e]),r=n.stdout.trim();return n.status===0&&r?r:null},P=(e,t=M)=>{let n=t(`security`,[`find-generic-password`,`-a`,N(),`-s`,e,`-w`]);return n.status===0&&n.stdout.trim().length>0},ve=(e,t,n=M)=>{let r=n(`security`,[`add-generic-password`,`-U`,`-a`,N(),`-s`,e,`-w`,t]);if(r.status!==0)throw Error(`security add-generic-password failed: ${r.stderr.trim()||`unknown error`}`)},ye=e=>`security add-generic-password -U -a "${N()||`$USER`}" -s ${e} -w 'xoxp-...'`,be=(e,t=M)=>{let n=t(`git`,[`config`,`--file`,e,`user.email`]);return n.status===0?n.stdout.trim():null},F=()=>!!(process.stdin.isTTY&&process.stdout.isTTY),xe=(e,t=e)=>process.stdout.isTTY?`\x1b]8;;${e}\x07${t}\x1b]8;;\x07`:e,I=e=>t=>process.stdout.isTTY?`\x1b[${e}m${t}\x1b[0m`:t,L=I(`38;5;208`),Se=I(`38;2;63;185;80`),R=I(`38;2;210;153;34`),z=I(`38;2;248;81;73`),B=e=>{let t=process.stdin;t.isTTY&&typeof t.setRawMode==`function`&&t.setRawMode(e)};let V=``;const Ce=e=>new Promise(t=>{process.stdout.write(e);let n=()=>{let e=V.indexOf(`
6
- `);if(e<0)return!1;let n=V.slice(0,e).replace(/\r$/,``);return V=V.slice(e+1),t(n),!0};if(n())return;let r=e=>{V+=e.toString(`utf8`),V.includes(`
7
- `)&&(process.stdin.off(`data`,r),process.stdin.off(`end`,i),process.stdin.pause(),n())},i=()=>{process.stdin.off(`data`,r),process.stdin.off(`end`,i);let e=V.replace(/\r$/,``);V=``,t(e)};process.stdin.on(`data`,r),process.stdin.on(`end`,i),process.stdin.resume()}),H=async(e,t=``)=>(await Ce(`${e}${t?` [${t}]`:``}: `)).trim()||t,U=async(e,t=!1)=>{if(!F()){let n=(await Ce(`${e} [${t?`Y/n`:`y/N`}]: `)).trim().toLowerCase();return n?n===`y`||n===`yes`:t}return W(e,[{label:`Yes`,value:!0},{label:`No`,value:!1}],t?0:1)},we=e=>new Promise(t=>{let n=a.createInterface({input:process.stdin,output:process.stdout}),r=n.output,i=!1;n._writeToOutput=t=>{if(!i){r.write(t),t.includes(e)&&(i=!0);return}},n.question(e,e=>{r.write(`
8
- `),n.close(),t(e.trim())})}),Te=`\x1B[36m`,Ee=`\x1B[0m`,W=(e,t,n=0)=>new Promise(r=>{if(!F()||t.length===0){r(t[Math.min(n,t.length-1)]?.value);return}let i=Math.max(0,Math.min(n,t.length-1)),o=process.stdout;o.write(e+`
9
- `);let s=e=>{e||o.write(`\x1b[${t.length}A`);for(let e=0;e<t.length;e++){let n=e===i,r=`${n?`❯`:` `} ${t[e].label}`;o.write(`\x1b[2K ${n?Te+r+Ee:r}\n`)}};s(!0),a.emitKeypressEvents(process.stdin),B(!0),process.stdin.resume();let c=()=>{process.stdin.off(`keypress`,l),B(!1),process.stdin.pause()},l=(e,n)=>{n.name===`up`||n.name===`k`?(i=(i-1+t.length)%t.length,s(!1)):n.name===`down`||n.name===`j`?(i=(i+1)%t.length,s(!1)):n.name===`return`||n.name===`enter`?(c(),r(t[i].value)):n.ctrl&&n.name===`c`&&(c(),o.write(`
10
- `),process.exit(130))};process.stdin.on(`keypress`,l)}),De=(e,t)=>new Promise(n=>{let r=t.map(e=>!!e.checked),i=()=>t.filter((e,t)=>r[t]).map(e=>e.value);if(!F()||t.length===0){n(i());return}let o=0,s=process.stdout;s.write(e+`
11
- `);let c=e=>{e||s.write(`\x1b[${t.length}A`);for(let e=0;e<t.length;e++){let n=e===o,i=`${n?`❯`:` `} ${r[e]?`◉`:`◯`} ${t[e].label}`;s.write(`\x1b[2K ${n?Te+i+Ee:i}\n`)}};c(!0),a.emitKeypressEvents(process.stdin),B(!0),process.stdin.resume();let l=()=>{process.stdin.off(`keypress`,u),B(!1),process.stdin.pause()},u=(e,a)=>{a.name===`up`||a.name===`k`?(o=(o-1+t.length)%t.length,c(!1)):a.name===`down`||a.name===`j`?(o=(o+1)%t.length,c(!1)):a.name===`space`||e===` `?(r[o]=!r[o],c(!1)):a.name===`return`||a.name===`enter`?(l(),n(i())):a.ctrl&&a.name===`c`&&(l(),s.write(`
12
- `),process.exit(130))};process.stdin.on(`keypress`,u)}),G=e=>`# >>> inscope:${e} >>>`,K=e=>`# <<< inscope:${e} <<<`,q=e=>e.replace(/[.*+?^${}()|[\]\\]/g,`\\$&`),Oe=e=>RegExp(`${q(G(e))}\\n[\\s\\S]*?\\n${q(K(e))}\\n?`),ke=(e,t)=>{let n=t.replace(/\n+$/,``);return`${G(e)}\n${n}\n${K(e)}\n`},Ae=t=>{try{return e.readFileSync(t,`utf8`)}catch{return``}},je=(t,r,i)=>{e.mkdirSync(n.dirname(t),{recursive:!0});let a=Ae(t),o=ke(r,i),s=Oe(r),c;if(s.test(a))c=a.replace(s,o);else{let e=a.replace(/\n*$/,``);c=e.length?`${e}\n\n${o}`:o}e.writeFileSync(t,c)},Me=(t,n)=>{let r=Ae(t);if(!r)return;let i=r.replace(Oe(n),``).replace(/\n{3,}/g,`
13
-
14
- `).replace(/^\n+/,``);e.writeFileSync(t,i)},Ne=(e,t)=>{let n=Ae(e).match(RegExp(`${q(G(t))}\\n([\\s\\S]*?)\\n${q(K(t))}`));return n?n[1]:null},J=`gitconfig`,Y=e=>!!(e.git&&(e.git.email||e.git.name)),X=e=>n.join(m(),`${e}.gitconfig`),Pe=e=>l(e).replace(/\/+$/,``)+`/`,Fe=e=>e.workspaces.filter(Y).map(e=>`[includeIf "gitdir:${Pe(e.path)}"]\n\tpath = ${l(X(e.name))}`).join(`
15
- `),Ie=e=>{let t=[`# Managed by inscope. Do not edit by hand.`,`[user]`];return e.git?.email&&t.push(`\temail = ${e.git.email}`),e.git?.name&&t.push(`\tname = ${e.git.name}`),t.join(`
2
+ import e from"node:fs";import{parseArgs as t}from"node:util";import n from"node:path";import r from"node:os";import{spawnSync as i}from"node:child_process";import a from"node:readline";const o=()=>r.homedir(),s=()=>process.env.XDG_CONFIG_HOME?.trim()||n.join(o(),`.config`),c=e=>e===`~`?o():e.startsWith(`~/`)?n.join(o(),e.slice(2)):e,l=e=>{let t=c(e),r=o();return t===r?`~`:t.startsWith(r+n.sep)?`~/`+t.slice(r.length+1):t},u=e=>n.resolve(c(e)),d=()=>n.join(s(),`inscope`),f=()=>n.join(d(),`inscope.json`),p=()=>n.join(d(),`inscope.zsh`),m=()=>n.join(d(),`git`),h=()=>n.join(o(),`.gitconfig`),g=()=>n.join(o(),`.zshrc`),_=t=>{try{return e.readFileSync(t,`utf8`)}catch{return null}},v=e=>_(e)??``,y=(t,r)=>{let i=t;try{i=e.realpathSync(t)}catch{}let a=n.dirname(i);e.mkdirSync(a,{recursive:!0});let o;try{o=e.statSync(i).mode}catch{}let s=n.join(a,`.${n.basename(i)}.inscope-${process.pid}.tmp`);e.writeFileSync(s,r),o!==void 0&&e.chmodSync(s,o&4095);try{e.renameSync(s,i)}catch(t){try{e.rmSync(s,{force:!0})}catch{}throw t}},b=()=>({version:1,workspaces:[]}),x=()=>e.existsSync(f()),S=()=>{let t=f(),n=e.readFileSync(t,`utf8`),r;try{r=JSON.parse(n)}catch{throw Error(`${l(t)} is not valid JSON; fix it, then re-run.`)}let i=ae(r);if(i)throw Error(i);try{T(r)}catch(e){let n=e instanceof Error?e.message:String(e);throw Error(`${n}\nFix it in ${l(t)}, then re-run.`)}return r},C=e=>{T(e),y(f(),JSON.stringify(e,null,2)+`
3
+ `)},ee=/^[A-Za-z0-9._-]+$/,te=e=>e?ee.test(e)?null:`use only letters, digits, dot (.), dash (-), or underscore (_)`:`must not be empty`,ne=/[\\"`$\n]/,w=e=>ne.test(e)?'must not contain a backslash (\\), quote ("), backtick (`), $, or newline':null,re=e=>e?w(e):`must not be empty`,ie=e=>/[\n\r]/.test(e)?`must not contain a newline`:null,ae=e=>typeof e.version==`number`&&e.version>1?`config version ${e.version} is newer than this inscope supports (max 1); upgrade inscope`:null,oe=e=>e.toLowerCase().replace(/[^a-z0-9._-]+/g,`-`).replace(/^[-.]+|[-.]+$/g,``),T=e=>{if(!e||typeof e!=`object`)throw Error(`config is not an object`);let t=ae(e);if(t)throw Error(t);if(!Array.isArray(e.workspaces))throw Error(`config.workspaces must be an array`);let n=new Set;for(let t of e.workspaces){if(!t.name)throw Error(`a workspace is missing a name`);let e=te(t.name);if(e)throw Error(`workspace name "${t.name}" is invalid: ${e}`);if(!t.path)throw Error(`workspace "${t.name}" is missing a path`);let r=re(t.path);if(r)throw Error(`workspace "${t.name}" path "${t.path}" is invalid: ${r}`);if(t.gh){let e=w(t.gh);if(e)throw Error(`workspace "${t.name}" gh account "${t.gh}" is invalid: ${e}`)}if(t.git?.email){let e=ie(t.git.email);if(e)throw Error(`workspace "${t.name}" git email "${t.git.email}" is invalid: ${e}`)}if(t.git?.name){let e=ie(t.git.name);if(e)throw Error(`workspace "${t.name}" git name "${t.git.name}" is invalid: ${e}`)}let i=t.servers?.slack;if(i&&i.keychain){let e=w(i.keychain);if(e)throw Error(`workspace "${t.name}" Slack keychain "${i.keychain}" is invalid: ${e}`)}if(n.has(t.name))throw Error(`duplicate workspace name "${t.name}"`);n.add(t.name)}},se=e=>oe(n.basename(u(e)))||`workspace`,E=(e,t)=>{let n=e.workspaces.find(e=>e.name===t);if(n)return n;let r=u(t);return e.workspaces.find(e=>u(e.path)===r)},ce=(e,t,n)=>{let r=u(t);return e.workspaces.find(e=>e.name!==n&&u(e.path)===r)},le=(e,t)=>{let n=e.workspaces.filter(e=>e.name!==t.name);return n.push({...t,path:l(t.path)}),n.sort((e,t)=>e.name.localeCompare(t.name)),{...e,workspaces:n}},ue=(e,t)=>{let n=E(e,t);return n?{cfg:{...e,workspaces:e.workspaces.filter(e=>e.name!==n.name)},removed:n}:{cfg:e}},D={atlassian:`https://mcp.atlassian.com/v1/mcp`,canva:`https://mcp.canva.com/mcp`,clickup:`https://mcp.clickup.com/mcp`,hubspot:`https://mcp.hubspot.com`,intercom:`https://mcp.intercom.com/mcp`,linear:`https://mcp.linear.app/mcp`,monday:`https://mcp.monday.com/mcp`,notion:`https://mcp.notion.com/mcp`,plane:`https://mcp.plane.so/http/mcp`,sentry:`https://mcp.sentry.dev/mcp`,stripe:`https://mcp.stripe.com`,vercel:`https://mcp.vercel.com`,webflow:`https://mcp.webflow.com/`},O=[`github`,`atlassian`,`canva`,`clickup`,`hubspot`,`intercom`,`linear`,`monday`,`notion`,`plane`,`sentry`,`slack`,`stripe`,`vercel`,`webflow`],k=e=>O.map(t=>`${t}-${e}`),A=e=>n.join(u(e.path),`.mcp.json`),de=(e,t)=>e&&typeof e==`object`&&e.url?e.url:t,fe=e=>{let t=e.servers,n={};for(let r of O){let i=t[r];if(!i)continue;let a=`${r}-${e.name}`;if(r===`github`)n[a]={type:`http`,url:`https://api.githubcopilot.com/mcp/`,headers:{Authorization:"Bearer ${GITHUB_TOKEN}"}};else if(r===`slack`){let e=i,t={SLACK_MCP_XOXP_TOKEN:"${SLACK_MCP_XOXP_TOKEN}"};e.addMessageTool&&(t.SLACK_MCP_ADD_MESSAGE_TOOL=`true`),n[a]={type:`stdio`,command:`npx`,args:[`-y`,`slack-mcp-server@1.3.0`,`--transport`,`stdio`],env:t}}else n[a]={type:`http`,url:de(i,D[r])}}return n},pe=t=>{if(!e.existsSync(t))return{};try{return JSON.parse(e.readFileSync(t,`utf8`))}catch{return{}}},j=t=>{if(!e.existsSync(t))return{};try{return JSON.parse(e.readFileSync(t,`utf8`))}catch{throw Error(`${t} is not valid JSON; fix or remove it, then re-run inscope (left it untouched)`)}},me=t=>{let n=A(t);return e.existsSync(n)?pe(n):null},he=(e,t)=>{let n=e.mcpServers&&typeof e.mcpServers==`object`?{...e.mcpServers}:{};for(let e of k(t.name))delete n[e];return Object.assign(n,fe(t)),{...e,mcpServers:n}},M=e=>JSON.stringify(e,null,2)+`
4
+ `,ge=e=>{for(let t of e)j(A(t))},_e=e=>{let t=A(e);y(t,M(he(j(t),e)))},ve=t=>{let n=A(t);if(!e.existsSync(n))return;let r=j(n);if(r.mcpServers&&typeof r.mcpServers==`object`)for(let e of k(t.name))delete r.mcpServers[e];y(n,M(r))},N=(e,t,n)=>{let r=i(e,t,{encoding:`utf8`,input:n?.input});return{status:r.status??(r.error?127:1),stdout:r.stdout??``,stderr:r.stderr??``}},ye=()=>process.platform===`darwin`,P=()=>process.env.USER||``,be=(e,t=N)=>{let n=t(`gh`,[`auth`,`token`,`-u`,e]),r=n.stdout.trim();return n.status===0&&r?r:null},xe=(e=N)=>{let t=e(`gh`,[`auth`,`status`]);return(t.stdout+t.stderr).trim()},Se=(e=N)=>{let t=[];for(let n of xe(e).matchAll(/account (\S+) \(/g))t.includes(n[1])||t.push(n[1]);return t},Ce=(e,t=N)=>{let n=t(`git`,[`config`,`--global`,e]),r=n.stdout.trim();return n.status===0&&r?r:null},F=(e,t=N)=>{let n=t(`security`,[`find-generic-password`,`-a`,P(),`-s`,e,`-w`]);return n.status===0&&n.stdout.trim().length>0},we=(e,t,n=N)=>{let r=n(`security`,[`add-generic-password`,`-U`,`-a`,P(),`-s`,e,`-w`,t]);if(r.status!==0)throw Error(`security add-generic-password failed: ${r.stderr.trim()||`unknown error`}`)},Te=e=>`security add-generic-password -U -a "${P()||`$USER`}" -s ${e} -w 'xoxp-...'`,Ee=(e,t=N)=>{let n=t(`git`,[`config`,`--file`,e,`user.email`]);return n.status===0?n.stdout.trim():null},I=()=>!!(process.stdin.isTTY&&process.stdout.isTTY),De=(e,t=e)=>process.stdout.isTTY?`\x1b]8;;${e}\x07${t}\x1b]8;;\x07`:e,L=e=>t=>process.stdout.isTTY?`\x1b[${e}m${t}\x1b[0m`:t,R=L(`38;5;208`),Oe=L(`38;2;63;185;80`),z=L(`38;2;210;153;34`),B=L(`38;2;248;81;73`),V=e=>{let t=process.stdin;t.isTTY&&typeof t.setRawMode==`function`&&t.setRawMode(e)},ke=()=>{V(!0),process.stdin.resume();let e=()=>V(!1);return process.once(`exit`,e),()=>{process.removeListener(`exit`,e),V(!1),process.stdin.pause()}};let H=``;const Ae=e=>new Promise(t=>{process.stdout.write(e);let n=()=>{let e=H.indexOf(`
5
+ `);if(e<0)return!1;let n=H.slice(0,e).replace(/\r$/,``);return H=H.slice(e+1),t(n),!0};if(n())return;let r=e=>{H+=e.toString(`utf8`),H.includes(`
6
+ `)&&(process.stdin.off(`data`,r),process.stdin.off(`end`,i),process.stdin.pause(),n())},i=()=>{process.stdin.off(`data`,r),process.stdin.off(`end`,i);let e=H.replace(/\r$/,``);H=``,t(e)};process.stdin.on(`data`,r),process.stdin.on(`end`,i),process.stdin.resume()}),U=async(e,t=``)=>(await Ae(`${e}${t?` [${t}]`:``}: `)).trim()||t,W=async(e,t=!1)=>{if(!I()){let n=(await Ae(`${e} [${t?`Y/n`:`y/N`}]: `)).trim().toLowerCase();return n?n===`y`||n===`yes`:t}return G(e,[{label:`Yes`,value:!0},{label:`No`,value:!1}],t?0:1)},je=e=>new Promise(t=>{let n=a.createInterface({input:process.stdin,output:process.stdout}),r=n.output,i=!1;n._writeToOutput=t=>{if(!i){r.write(t),t.includes(e)&&(i=!0);return}},n.question(e,e=>{r.write(`
7
+ `),n.close(),t(e.trim())})}),Me=`\x1B[36m`,Ne=`\x1B[0m`,G=(e,t,n=0)=>new Promise(r=>{if(!I()||t.length===0){r(t[Math.min(n,t.length-1)]?.value);return}let i=Math.max(0,Math.min(n,t.length-1)),o=process.stdout;o.write(e+`
8
+ `);let s=e=>{e||o.write(`\x1b[${t.length}A`);for(let e=0;e<t.length;e++){let n=e===i,r=`${n?`❯`:` `} ${t[e].label}`;o.write(`\x1b[2K ${n?Me+r+Ne:r}\n`)}};s(!0),a.emitKeypressEvents(process.stdin);let c=ke(),l=()=>{process.stdin.off(`keypress`,u),c()},u=(e,n)=>{n.name===`up`||n.name===`k`?(i=(i-1+t.length)%t.length,s(!1)):n.name===`down`||n.name===`j`?(i=(i+1)%t.length,s(!1)):n.name===`return`||n.name===`enter`?(l(),r(t[i].value)):n.ctrl&&n.name===`c`&&(l(),o.write(`
9
+ `),process.exit(130))};process.stdin.on(`keypress`,u)}),Pe=(e,t)=>new Promise(n=>{let r=t.map(e=>!!e.checked),i=()=>t.filter((e,t)=>r[t]).map(e=>e.value);if(!I()||t.length===0){n(i());return}let o=0,s=process.stdout;s.write(e+`
10
+ `);let c=e=>{e||s.write(`\x1b[${t.length}A`);for(let e=0;e<t.length;e++){let n=e===o,i=`${n?`❯`:` `} ${r[e]?`◉`:`◯`} ${t[e].label}`;s.write(`\x1b[2K ${n?Me+i+Ne:i}\n`)}};c(!0),a.emitKeypressEvents(process.stdin);let l=ke(),u=()=>{process.stdin.off(`keypress`,d),l()},d=(e,a)=>{a.name===`up`||a.name===`k`?(o=(o-1+t.length)%t.length,c(!1)):a.name===`down`||a.name===`j`?(o=(o+1)%t.length,c(!1)):a.name===`space`||e===` `?(r[o]=!r[o],c(!1)):a.name===`return`||a.name===`enter`?(u(),n(i())):a.ctrl&&a.name===`c`&&(u(),s.write(`
11
+ `),process.exit(130))};process.stdin.on(`keypress`,d)}),K=e=>`# >>> inscope:${e} >>>`,q=e=>`# <<< inscope:${e} <<<`,J=e=>e.replace(/[.*+?^${}()|[\]\\]/g,`\\$&`),Fe=e=>RegExp(`${J(K(e))}\\n[\\s\\S]*?\\n${J(q(e))}\\n?`),Ie=(e,t)=>{let n=t.replace(/\n+$/,``);return`${K(e)}\n${n}\n${q(e)}\n`},Le=(e,t,n)=>{let r=v(e),i=Ie(t,n),a=Fe(t),o;if(a.test(r))o=r.replace(a,i);else{let e=r.replace(/\n*$/,``);o=e.length?`${e}\n\n${i}`:i}y(e,o)},Re=(e,t)=>{let n=v(e);n&&y(e,n.replace(Fe(t),``).replace(/\n{3,}/g,`
12
+
13
+ `).replace(/^\n+/,``))},ze=(e,t)=>{let n=v(e).match(RegExp(`${J(K(t))}\\n([\\s\\S]*?)\\n${J(q(t))}`));return n?n[1]:null},Y=`gitconfig`,X=e=>!!(e.git&&(e.git.email||e.git.name)),Z=e=>n.join(m(),`${e}.gitconfig`),Be=e=>l(e).replace(/\/+$/,``)+`/`,Ve=e=>e.workspaces.filter(X).map(e=>`[includeIf "gitdir:${Be(e.path)}"]\n\tpath = ${l(Z(e.name))}`).join(`
14
+ `),He=e=>{let t=[`# Managed by inscope. Do not edit by hand.`,`[user]`];return e.git?.email&&t.push(`\temail = ${e.git.email}`),e.git?.name&&t.push(`\tname = ${e.git.name}`),t.join(`
16
15
  `)+`
17
- `},Le=t=>{e.mkdirSync(m(),{recursive:!0});for(let n of t.workspaces)Y(n)&&e.writeFileSync(X(n.name),Ie(n));let n=Fe(t);n?je(h(),J,n):Me(h(),J)},Re=t=>{let n=X(t);e.existsSync(n)&&e.rmSync(n)},ze=e=>{let t=l(e);return t===`~`?`"$HOME/"*`:t.startsWith(`~/`)?`"$HOME/${t.slice(2)}/"*`:`"${t}/"*`},Be=e=>e.servers.slack?e.servers.slack.keychain:``,Ve=e=>{let t=[...e.workspaces].sort((e,t)=>e.name.localeCompare(t.name));return`# Managed by inscope. Do not edit by hand.
16
+ `},Ue=t=>{e.mkdirSync(m(),{recursive:!0});for(let e of t.workspaces)X(e)?y(Z(e.name),He(e)):We(e.name);let n=Ve(t);n?Le(h(),Y,n):Re(h(),Y)},We=t=>{let n=Z(t);e.existsSync(n)&&e.rmSync(n)},Ge=e=>{let t=l(e);return t===`~`?`"$HOME/"*`:t.startsWith(`~/`)?`"$HOME/${t.slice(2)}/"*`:`"${t}/"*`},Ke=e=>e.servers.slack?e.servers.slack.keychain:``,qe=e=>{let t=[...e.workspaces].sort((e,t)=>e.name.localeCompare(t.name));return`# Managed by inscope. Do not edit by hand.
18
17
  # Source of truth: ~/.config/inscope/inscope.json
19
18
  # Edit there, then run \`inscope apply\` to regenerate this file.
20
19
  #
@@ -25,7 +24,7 @@ import e from"node:fs";import{parseArgs as t}from"node:util";import n from"node:
25
24
  __inscope_resolve_identity() {
26
25
  local ws
27
26
  case "\${PWD}/" in
28
- ${[...t].sort((e,t)=>l(t.path).length-l(e.path).length).map(e=>` ${ze(e.path)}) ws="${e.name}" ;;`).join(`
27
+ ${[...t].sort((e,t)=>l(t.path).length-l(e.path).length).map(e=>` ${Ge(e.path)}) ws="${e.name}" ;;`).join(`
29
28
  `)||` # no workspaces configured`}
30
29
  *) ws="" ;;
31
30
  esac
@@ -35,7 +34,7 @@ ${[...t].sort((e,t)=>l(t.path).length-l(e.path).length).map(e=>` ${ze(e.path)
35
34
 
36
35
  local gh_user="" slack_svc=""
37
36
  case "$ws" in
38
- ${t.map(e=>{let t=[];e.gh&&t.push(`gh_user="${e.gh}"`);let n=Be(e);return n&&t.push(`slack_svc="${n}"`),` ${e.name}) ${t.length?t.join(`; `):`:`} ;;`}).join(`
37
+ ${t.map(e=>{let t=[];e.gh&&t.push(`gh_user="${e.gh}"`);let n=Ke(e);return n&&t.push(`slack_svc="${n}"`),` ${e.name}) ${t.length?t.join(`; `):`:`} ;;`}).join(`
39
38
  `)||` # no workspaces configured`}
40
39
  *) return ;; # outside a mapped workspace: nothing set
41
40
  esac
@@ -61,13 +60,13 @@ autoload -Uz add-zsh-hook
61
60
  add-zsh-hook chpwd __inscope_resolve_identity
62
61
  __inscope_ws="__init__" # force the first resolve, clearing any inherited token
63
62
  __inscope_resolve_identity
64
- `},He=e=>{let t=o();return e===t?`$HOME`:e.startsWith(t+n.sep)?`$HOME/${e.slice(t.length+1)}`:e},Ue=()=>{let e=He(p());return`[ -r "${e}" ] && source "${e}"`},We=e=>{let t=Ue();if(e.includes(t))return e;let n=e.replace(/\n*$/,``),r=`# inscope: load each workspace's tokens (GitHub, Slack) from \$PWD on every cd\n${t}`;return n.length?`${n}\n\n${r}\n`:`${r}\n`},Ge=()=>{let t=g(),n=``;try{n=e.readFileSync(t,`utf8`)}catch{}let r=We(n);r!==n&&e.writeFileSync(t,r)},Ke=()=>{try{return e.readFileSync(g(),`utf8`).includes(Ue())}catch{return!1}},Z=t=>{let r=p();e.mkdirSync(n.dirname(r),{recursive:!0}),e.writeFileSync(r,Ve(t)),Le(t),Ge();let i=[];for(let e of t.workspaces)de(e),i.push(j(e));return{hook:r,gitconfig:t.workspaces.some(e=>e.git?.email||e.git?.name),mcp:i}},qe=k,Je=e=>`SLACK_MCP_XOXP_TOKEN_${e.toUpperCase().replace(/[^A-Z0-9]+/g,`_`)}`,Ye=e=>k.filter(t=>!!e[t]),Xe=(e,t)=>{let n={};for(let r of k)n[r]=r===`slack`?t?{keychain:t.keychain,addMessageTool:t.addMessageTool}:!1:e.includes(r);return n},Ze=e=>{let t=ie(v()?y():_(),e);b(t),Z(t)},Qe=async(e,t)=>{if(!e.servers.slack)return;let n=e.servers.slack.keychain;if(t){let e=await we(`Paste the Slack xoxp token for ${n}: `);e?(ve(n,e),console.log(`\n✓ stored ${n} in the macOS keychain`)):console.error(`
65
- No token entered; skipped keychain write.`)}else P(n)||console.log(`\nSlack token not in the keychain yet. Store it once with:\n${L(ye(n))}\n\nSetup guide: ${L(xe(`https://github.com/korotovsky/slack-mcp-server/blob/HEAD/docs/01-authentication-setup.md#option-2-using-slack_mcp_xoxp_token-user-oauth`))}`)};var Q=`inscope`,$e=`0.8.2`,et={name:`Neeraj Dalal`,email:`admin@nrjdalal.com`,url:`https://nrjdalal.com`};const tt=`Map a directory to a GitHub account, git email, and MCP servers.
63
+ `},Je=e=>{let t=o();return e===t?`$HOME`:e.startsWith(t+n.sep)?`$HOME/${e.slice(t.length+1)}`:e},Ye=()=>{let e=Je(p());return`[ -r "${e}" ] && source "${e}"`},Xe=e=>{let t=Ye();if(e.includes(t))return e;let n=e.replace(/\n*$/,``),r=`# inscope: load each workspace's tokens (GitHub, Slack) from \$PWD on every cd\n${t}`;return n.length?`${n}\n\n${r}\n`:`${r}\n`},Ze=()=>{let e=g(),t=v(e),n=Xe(t);n!==t&&y(e,n)},Qe=()=>v(g()).includes(Ye()),Q=e=>{ge(e.workspaces);let t=p();y(t,qe(e)),Ue(e),Ze();let n=[];for(let t of e.workspaces)_e(t),n.push(A(t));return{hook:t,gitconfig:e.workspaces.some(e=>e.git?.email||e.git?.name),mcp:n}},$e=O,et=e=>`SLACK_MCP_XOXP_TOKEN_${e.toUpperCase().replace(/[^A-Z0-9]+/g,`_`)}`,tt=e=>O.filter(t=>!!e[t]),nt=(e,t)=>{let n={};for(let r of O)n[r]=r===`slack`?t?{keychain:t.keychain,addMessageTool:t.addMessageTool}:!1:e.includes(r);return n},rt=e=>e?`global: ${e}`:`no global set`,it=e=>{let t=x()?S():b(),n=t.workspaces.find(t=>t.name===e.name),r=le(t,e);C(r),Q(r),n&&u(n.path)!==u(e.path)&&ve(n)},at=async(e,t)=>{if(!e.servers.slack)return;let n=e.servers.slack.keychain;if(t){let e=await je(`Paste the Slack xoxp token for ${n}: `);e?(we(n,e),console.log(`\n✓ stored ${n} in the macOS keychain`)):console.error(`
64
+ No token entered; skipped keychain write.`)}else F(n)||console.log(`\nSlack token not in the keychain yet. Store it once with:\n${R(Te(n))}\n\nSetup guide: ${R(De(`https://github.com/korotovsky/slack-mcp-server/blob/HEAD/docs/01-authentication-setup.md#option-2-using-slack_mcp_xoxp_token-user-oauth`))}`)};var $=`inscope`,ot=`0.8.4`,st={name:`Neeraj Dalal`,email:`admin@nrjdalal.com`,url:`https://nrjdalal.com`};const ct=`Map a directory to a GitHub account, git email, and MCP servers.
66
65
  Runs interactively in a terminal; pass flags or -y to skip the prompts. Re-running
67
66
  with the same label updates that workspace; each directory maps to one workspace.
68
67
 
69
68
  Usage:
70
- $ ${Q} add [path] [options]
69
+ $ ${$} add [path] [options]
71
70
 
72
71
  Options:
73
72
  --gh <account> gh account whose token this workspace uses
@@ -83,22 +82,22 @@ Options:
83
82
  --slack-message allow the Slack MCP server to post messages
84
83
  --seed-slack prompt for the Slack token and store it in the keychain
85
84
  -y, --yes accept defaults, skip all prompts (non-interactive)
86
- -h, --help Display help message`,nt=k.map(e=>({label:e,value:e,checked:e===`github`})),rt=async n=>{let{positionals:r,values:i}=t({allowPositionals:!0,options:{help:{type:`boolean`,short:`h`},yes:{type:`boolean`,short:`y`},gh:{type:`string`},email:{type:`string`},"git-name":{type:`string`},label:{type:`string`},servers:{type:`string`},"slack-keychain":{type:`string`},"slack-message":{type:`boolean`},"seed-slack":{type:`boolean`}},args:n});i.help&&(console.log(tt),process.exit(0));let a=F()&&!i.yes;a&&console.log();let o=r[0];if(!o)if(a)o=await H(`Workspace directory`,process.cwd());else throw Error(tt);let s=ee(o);s&&(console.error(`\nInvalid workspace path "${o}": ${s}`),process.exit(1)),e.existsSync(u(o))||console.error(R(`\nWarning: ${l(o)} does not exist yet; it will be created.`));let c=i.label||re(o);a&&!i.label&&(c=await H(`Label`,c));let d=S(c);if(d&&(console.error(`\nInvalid label "${c}": ${d}`),process.exit(1)),v()){let e=D(y(),o);e&&e.name!==c&&(console.error(`\n${l(o)} is already mapped to workspace "${e.name}". Run \`${Q} edit ${e.name}\` to change it, or \`${Q} rm ${e.name}\` first.`),process.exit(1))}let f=i.gh;f===void 0&&a&&(f=await W(`
87
- GitHub account for this workspace`,[...ge().map(e=>({label:e,value:e})),{label:`(none)`,value:``}])||void 0);let p=i.email,m=i[`git-name`];if(a){if(p===void 0){let e=_e(`user.email`);p=await H(`Git email${e?` [${e} · global]`:``} (enter to inherit global)`)||void 0}if(m===void 0){let e=_e(`user.name`);m=await H(`Git name${e?` [${e} · global]`:``} (enter to inherit global)`)||void 0}}let h;if(i.servers!==void 0){h=i.servers.split(`,`).map(e=>e.trim()).filter(Boolean);let e=new Set(k),t=h.filter(t=>!e.has(t));t.length&&console.error(R(`\nIgnoring unknown server(s): ${t.join(`, `)}`))}else h=a?await De(`MCP servers (space toggles, enter confirms)`,nt):[`github`];let g=h.includes(`slack`)||!!i[`slack-keychain`]||!!i[`seed-slack`],_=i[`slack-keychain`]||Je(c),b=!!i[`slack-message`],x=!!i[`seed-slack`];g&&a&&(console.log(`
88
- Slack uses a user OAuth (xoxp) token.`),i[`slack-keychain`]||(_=await H(`Slack keychain service`,_)),i[`slack-message`]||(b=await U(`Allow Slack to post messages?`,!0)),i[`seed-slack`]||(x=await U(`Store the Slack token now?`,!0)));let C=f?w(f):null;if(C&&(console.error(`\nInvalid gh account "${f}": ${C}`),process.exit(1)),g){let e=w(_);e&&(console.error(`\nInvalid Slack keychain service "${_}": ${e}`),process.exit(1))}let T={name:c,path:l(o),gh:f,git:p||m?{email:p,name:m}:void 0,servers:Xe(h,g?{keychain:_,addMessageTool:b}:null)};Ze(T),console.log(`\n✓ workspace "${c}" -> ${T.path}`),console.log(`✓ regenerated the hook, git includes, and ${T.path}/.mcp.json`),await Qe(T,x),console.log(`\nLaunch \`claude\` from ${T.path} (or relaunch) to pick up the new identity.`),process.exit(0)},it=`Regenerate the chpwd hook, git includes, and every .mcp.json
85
+ -h, --help Display help message`,lt=O.map(e=>({label:e,value:e,checked:e===`github`})),ut=async n=>{let{positionals:r,values:i}=t({allowPositionals:!0,options:{help:{type:`boolean`,short:`h`},yes:{type:`boolean`,short:`y`},gh:{type:`string`},email:{type:`string`},"git-name":{type:`string`},label:{type:`string`},servers:{type:`string`},"slack-keychain":{type:`string`},"slack-message":{type:`boolean`},"seed-slack":{type:`boolean`}},args:n});i.help&&(console.log(ct),process.exit(0));let a=I()&&!i.yes;a&&console.log();let o=r[0];if(!o)if(a)o=await U(`Workspace directory`,process.cwd());else throw Error(ct);let s=re(o);s&&(console.error(`\nInvalid workspace path "${o}": ${s}`),process.exit(1)),e.existsSync(u(o))||console.error(z(`Warning: ${l(o)} does not exist yet; it will be created.`)+`
86
+ `);let c=i.label||se(o);a&&!i.label&&(c=await U(`Label`,c));let d=te(c);if(d&&(console.error(`\nInvalid label "${c}": ${d}`),process.exit(1)),x()){let e=ce(S(),o,c);e&&(console.error(`\n${l(o)} is already mapped to workspace "${e.name}". Run \`${$} edit ${e.name}\` to change it, or \`${$} rm ${e.name}\` first.`),process.exit(1))}let f=i.gh;f===void 0&&a&&(f=await G(`
87
+ GitHub account for this workspace`,[...Se().map(e=>({label:e,value:e})),{label:`(none)`,value:``}])||void 0);let p=i.email,m=i[`git-name`];a&&(p===void 0&&(p=await U(`Git email (${rt(Ce(`user.email`))})`)||void 0),m===void 0&&(m=await U(`Git name (${rt(Ce(`user.name`))})`)||void 0));let h;if(i.servers!==void 0){h=i.servers.split(`,`).map(e=>e.trim()).filter(Boolean);let e=new Set(O),t=h.filter(t=>!e.has(t));t.length&&console.error(z(`\nIgnoring unknown server(s): ${t.join(`, `)}`))}else h=a?await Pe(`MCP servers (space toggles, enter confirms)`,lt):[`github`];let g=h.includes(`slack`)||!!i[`slack-keychain`]||!!i[`seed-slack`],_=i[`slack-keychain`]||et(c),v=!!i[`slack-message`],y=!!i[`seed-slack`];g&&a&&(console.log(`
88
+ Slack uses a user OAuth (xoxp) token.`),i[`slack-keychain`]||(_=await U(`Slack keychain service`,_)),i[`slack-message`]||(v=await W(`Allow Slack to post messages?`,!0)),i[`seed-slack`]||(y=await W(`Store the Slack token now?`,!0)));let b=f?w(f):null;if(b&&(console.error(`\nInvalid gh account "${f}": ${b}`),process.exit(1)),g){let e=w(_);e&&(console.error(`\nInvalid Slack keychain service "${_}": ${e}`),process.exit(1))}let C={name:c,path:l(o),gh:f,git:p||m?{email:p,name:m}:void 0,servers:nt(h,g?{keychain:_,addMessageTool:v}:null)};it(C),console.log(`\n✓ workspace "${c}" -> ${C.path}`),console.log(`✓ regenerated the hook, git includes, and ${C.path}/.mcp.json`),await at(C,y),console.log(`\nLaunch \`claude\` from ${C.path} (or relaunch) to pick up the new identity.`),process.exit(0)},dt=`Regenerate the chpwd hook, git includes, and every .mcp.json
89
89
  from your config. Idempotent: run it any time the config changes.
90
90
 
91
91
  Usage:
92
- $ ${Q} apply
92
+ $ ${$} apply
93
93
 
94
94
  Options:
95
- -h, --help Display help message`,at=e=>{let{values:n}=t({allowPositionals:!0,options:{help:{type:`boolean`,short:`h`}},args:e});n.help&&(console.log(it),process.exit(0)),v()||(console.error(`No config found. Run \`${Q} init\` first.`),process.exit(1));let r=y(),i=Z(r);console.log(`\n✓ hook ${i.hook}`),i.gitconfig&&console.log(`✓ gitconfig ~/.gitconfig (includeIf block)`);for(let e of i.mcp)console.log(`✓ mcp ${e}`);console.log(`\nApplied ${r.workspaces.length} workspace(s).`),process.exit(0)},$=t=>{try{return e.readFileSync(t,`utf8`)}catch{return``}},ot=e=>{let t=$(e);if(!t)return null;try{return JSON.parse(t)}catch{return null}},st=e=>{let t=ot(j(e))??{},n=t.mcpServers&&typeof t.mcpServers==`object`?{...t.mcpServers}:{};for(let t of A(e.name))delete n[t];return Object.assign(n,se(e)),t.mcpServers=n,JSON.stringify(t,null,2)+`
96
- `},ct=t=>{let n=j(t);if(!e.existsSync(n))return null;try{return JSON.parse(e.readFileSync(n,`utf8`)),null}catch{return"invalid JSON; `apply` will not touch it until you fix it"}},lt=e=>{let t=[],n=p();t.push({label:`hook`,path:n,current:$(n),next:Ve(e)}),t.push({label:`gitconfig`,path:h(),current:Ne(h(),J)??``,next:Fe(e)});for(let n of e.workspaces){if(!Y(n))continue;let e=X(n.name);t.push({label:`gitconfig:${n.name}`,path:e,current:$(e),next:Ie(n)})}for(let n of e.workspaces){let e=j(n),r=ct(n);if(r){t.push({label:`mcp:${n.name}`,path:e,current:``,next:``,error:r});continue}t.push({label:`mcp:${n.name}`,path:e,current:$(e),next:st(n)})}return t.filter(e=>e.error!=null||e.current!==e.next)},ut=(e,t)=>{let n=e.length?e.split(`
95
+ -h, --help Display help message`,ft=e=>{let{values:n}=t({allowPositionals:!0,options:{help:{type:`boolean`,short:`h`}},args:e});n.help&&(console.log(dt),process.exit(0)),x()||(console.error(`No config found. Run \`${$} init\` first.`),process.exit(1));let r=S(),i=Q(r);console.log(`\n✓ hook ${i.hook}`),i.gitconfig&&console.log(`✓ gitconfig ~/.gitconfig (includeIf block)`);for(let e of i.mcp)console.log(`✓ mcp ${e}`);console.log(`\nApplied ${r.workspaces.length} workspace(s).`),process.exit(0)},pt=e=>{let t=v(e);if(!t)return null;try{return JSON.parse(t)}catch{return null}},mt=e=>M(he(pt(A(e))??{},e)),ht=t=>{let n=A(t);if(!e.existsSync(n))return null;try{return JSON.parse(e.readFileSync(n,`utf8`)),null}catch{return"invalid JSON; `apply` will not touch it until you fix it"}},gt=e=>{let t=[],n=p();t.push({label:`hook`,path:n,current:v(n),next:qe(e)}),t.push({label:`gitconfig`,path:h(),current:ze(h(),Y)??``,next:Ve(e)});for(let n of e.workspaces){if(!X(n))continue;let e=Z(n.name);t.push({label:`gitconfig:${n.name}`,path:e,current:v(e),next:He(n)})}for(let n of e.workspaces){let e=A(n),r=ht(n);if(r){t.push({label:`mcp:${n.name}`,path:e,current:``,next:``,error:r});continue}t.push({label:`mcp:${n.name}`,path:e,current:v(e),next:mt(n)})}return t.filter(e=>e.error!=null||e.current!==e.next)},_t=(e,t)=>{let n=e.length?e.split(`
97
96
  `):[],r=t.length?t.split(`
98
97
  `):[],i=n.length,a=r.length,o=Array.from({length:i+1},()=>Array.from({length:a+1},()=>0));for(let e=i-1;e>=0;e--)for(let t=a-1;t>=0;t--)o[e][t]=n[e]===r[t]?o[e+1][t+1]+1:Math.max(o[e+1][t],o[e][t+1]);let s=[],c=0,l=0;for(;c<i&&l<a;)n[c]===r[l]?(s.push(` ${n[c]}`),c++,l++):o[c+1][l]>=o[c][l+1]?(s.push(`- ${n[c]}`),c++):(s.push(`+ ${r[l]}`),l++);for(;c<i;)s.push(`- ${n[c++]}`);for(;l<a;)s.push(`+ ${r[l++]}`);return s.join(`
99
- `)},dt=e=>{let t=[],n=e.workspaces.map(e=>{let n=ot(j(e))?.mcpServers;if(!n||typeof n!=`object`)return e;let r=e.servers,i=e.servers.slack;i&&!i.addMessageTool&&n[`slack-${e.name}`]?.env?.SLACK_MCP_ADD_MESSAGE_TOOL===`true`&&(r={...r,slack:{...i,addMessageTool:!0}},t.push(`${e.name}: slack.addMessageTool = true`));for(let i of k){if(i===`github`||i===`slack`)continue;let a=n[`${i}-${e.name}`]?.url;if(typeof a!=`string`)continue;let o=e.servers[i];if(!o){r={...r,[i]:a===O[i]?!0:{url:a}},t.push(`${e.name}: ${i} = ${a===O[i]?`enabled`:a}`);continue}a!==((typeof o==`object`?o.url:void 0)??O[i])&&(r={...r,[i]:{url:a}},t.push(`${e.name}: ${i}.url = ${a}`))}return r===e.servers?e:{...e,servers:r}});return{cfg:{...e,workspaces:n},changes:t}},ft=e=>{if(!process.stdout.isTTY)return e;let t=process.stdout.columns||80;return e.split(`
98
+ `)},vt=e=>{let t=[],n=e.workspaces.map(e=>{let n=pt(A(e))?.mcpServers;if(!n||typeof n!=`object`)return e;let r=e.servers,i=e.servers.slack;i&&!i.addMessageTool&&n[`slack-${e.name}`]?.env?.SLACK_MCP_ADD_MESSAGE_TOOL===`true`&&(r={...r,slack:{...i,addMessageTool:!0}},t.push(`${e.name}: slack.addMessageTool = true`));for(let i of O){if(i===`github`||i===`slack`)continue;let a=n[`${i}-${e.name}`]?.url;if(typeof a!=`string`)continue;let o=e.servers[i];if(!o){r={...r,[i]:a===D[i]?!0:{url:a}},t.push(`${e.name}: ${i} = ${a===D[i]?`enabled`:a}`);continue}a!==((typeof o==`object`?o.url:void 0)??D[i])&&(r={...r,[i]:{url:a}},t.push(`${e.name}: ${i}.url = ${a}`))}return r===e.servers?e:{...e,servers:r}});return{cfg:{...e,workspaces:n},changes:t}},yt=e=>{if(!process.stdout.isTTY)return e;let t=process.stdout.columns||80;return e.split(`
100
99
  `).map(e=>{if(!e.startsWith(`- `)&&!e.startsWith(`+ `))return e;let n=` `.repeat(Math.max(0,t-e.length));return`\x1b[${e.startsWith(`- `)?`48;2;48;27;31;38;2;248;81;73`:`48;2;18;38;30;38;2;63;185;80`}m${e}${n}\x1b[0m`}).join(`
101
- `)},pt=`Show what \`${Q} apply\` would change: a diff of each managed
100
+ `)},bt=`Show what \`${$} apply\` would change: a diff of each managed
102
101
  artifact (the zsh hook, git includes, and .mcp.json files) against what your
103
102
  config would generate. Read-only.
104
103
 
@@ -107,62 +106,65 @@ With --adopt, pull settings that exist in your .mcp.json but not your config
107
106
  next apply keeps them instead of dropping them.
108
107
 
109
108
  Usage:
110
- $ ${Q} diff [--adopt]
109
+ $ ${$} diff [--adopt] [--exit-code]
111
110
 
112
111
  Options:
113
- --adopt Write config-expressible on-disk settings back into the config
114
- -h, --help Display help message`,mt=e=>{let{values:n}=t({allowPositionals:!0,options:{help:{type:`boolean`,short:`h`},adopt:{type:`boolean`}},args:e});n.help&&(console.log(pt),process.exit(0)),v()||(console.error(`No config found. Run \`${Q} init\` first.`),process.exit(1));let r=y();if(n.adopt){let{cfg:e,changes:t}=dt(r);t.length||(console.log(`Nothing to adopt: the config already covers your .mcp.json settings.`),process.exit(0)),E(e),b(e),console.log(`Adopted into config:`);for(let e of t)console.log(` ${e}`);console.log(`\nRun ${L(`${Q} apply`)} to regenerate from the updated config.`),process.exit(0)}let i=lt(r);i.length||(console.log("In sync. `apply` would change nothing."),process.exit(0));for(let e of i)console.log(`\n${L(`${l(e.path)} (${e.label})`)}`),e.error?console.log(z(` ${e.error}`)):console.log(ft(ut(e.current,e.next)));let{changes:a}=dt(r);if(a.length){console.log("\nThese .mcp.json settings aren't in your config, so `apply` would drop them:");for(let e of a)console.log(` ${e}`);console.log(`\nRun ${L(`${Q} diff --adopt`)} to keep them by writing them into the config.`)}process.exit(0)},ht=t=>{try{return e.readFileSync(t,`utf8`)}catch{return null}},gt=e=>{let t=[],n=e?.mcpServers;if(!n||typeof n!=`object`)return t;for(let[e,r]of Object.entries(n)){let n=Array.isArray(r?.args)?r.args:[];if(n.some(e=>typeof e==`string`&&e.endsWith(`@latest`)))t.push(e);else if(r?.command===`npx`){let r=n.find(e=>typeof e==`string`&&!e.startsWith(`-`));r&&!r.includes(`@`)&&t.push(e)}}return t},_t=(e,t=process.cwd())=>{let r=n.resolve(t),i,a=-1;for(let t of e.workspaces){let e=u(t.path);(r===e||r.startsWith(e+n.sep))&&e.length>a&&(i=t,a=e.length)}return i},vt=(e=M)=>{let t=e(`gh`,[`api`,`user`,`--jq`,`.login`]),n=e(`git`,[`config`,`user.email`]);return{pwd:process.cwd(),gh:t.status===0&&t.stdout.trim()?t.stdout.trim():`none`,gitEmail:n.status===0?n.stdout.trim():`none`,tokenSet:!!process.env.GITHUB_TOKEN}},yt=(t,n=M)=>{let r=[];pe()||r.push({status:`warn`,label:`platform`,detail:`inscope's secret resolution targets macOS (gh keyring + Keychain)`});let i=p(),a=ht(i);a===null?r.push({status:`fail`,label:`hook`,detail:`missing ${i}; run \`inscope init\``}):a===Ve(t)?r.push({status:`ok`,label:`hook`,detail:i}):r.push({status:`warn`,label:`hook`,detail:"out of date; run `inscope apply`"}),r.push(Ke()?{status:`ok`,label:`zshrc`,detail:`sources the hook`}:{status:`warn`,label:`zshrc`,detail:"does not source the hook; run `inscope init`"}),t.workspaces.some(Y)&&r.push(Ne(h(),J)===null?{status:`fail`,label:`gitconfig`,detail:"missing includeIf block; run `inscope apply`"}:{status:`ok`,label:`gitconfig`,detail:`includeIf block present`});for(let i of t.workspaces){let t=`[${i.name}]`;if(i.gh&&r.push(me(i.gh,n)?{status:`ok`,label:`${t} gh`,detail:`token for ${i.gh}`}:{status:`fail`,label:`${t} gh`,detail:`no token for ${i.gh}; run \`gh auth login\``}),i.servers.slack){let e=i.servers.slack.keychain;r.push(P(e,n)?{status:`ok`,label:`${t} slack`,detail:e}:{status:`fail`,label:`${t} slack`,detail:`${e} not in keychain; run \`${ye(e)}\``})}if(Y(i)){let a=X(i.name);if(!e.existsSync(a))r.push({status:`fail`,label:`${t} git`,detail:`missing ${a}; run \`inscope apply\``});else if(i.git?.email){let e=be(a,n);r.push(e===i.git.email?{status:`ok`,label:`${t} git`,detail:i.git.email}:{status:`fail`,label:`${t} git`,detail:`email is ${e??`unset`}, expected ${i.git.email}`})}}let a=ct(i);if(a)r.push({status:`fail`,label:`${t} mcp`,detail:a});else{let e=ue(i);if(e===null)r.push({status:`warn`,label:`${t} mcp`,detail:"no .mcp.json; run `inscope apply`"});else{let n=A(i.name).filter(t=>e.mcpServers?.[t]);r.push({status:`ok`,label:`${t} mcp`,detail:`${n.length} server(s)`}),ht(j(i))!==st(i)&&r.push({status:`warn`,label:`${t} mcp`,detail:"out of date; run `inscope diff`"});let a=gt(e);a.length&&r.push({status:`warn`,label:`${t} mcp`,detail:`unpinned: ${a.join(`, `)}`})}}}return r},bt=`Verify the setup: gh tokens resolve, keychain entries exist,
112
+ --adopt Write config-expressible on-disk settings back into the config
113
+ --exit-code Exit 1 if anything is out of sync (for CI / pre-commit gates)
114
+ -h, --help Display help message`,xt=e=>{let{values:n}=t({allowPositionals:!0,options:{help:{type:`boolean`,short:`h`},adopt:{type:`boolean`},"exit-code":{type:`boolean`}},args:e});n.help&&(console.log(bt),process.exit(0)),x()||(console.error(`No config found. Run \`${$} init\` first.`),process.exit(1));let r=S();if(n.adopt){let{cfg:e,changes:t}=vt(r);t.length||(console.log(`
115
+ Nothing to adopt: the config already covers your .mcp.json settings.`),process.exit(0)),T(e),C(e),console.log(`
116
+ Adopted into config:`);for(let e of t)console.log(` ${e}`);console.log(`\nRun ${R(`${$} apply`)} to regenerate from the updated config.`),process.exit(0)}let i=gt(r);i.length||(console.log("\nIn sync. `apply` would change nothing."),process.exit(0));let a=n[`exit-code`]?1:0;for(let e of i)console.log(`\n${R(`${l(e.path)} (${e.label})`)}`),e.error?console.log(B(` ${e.error}`)):console.log(yt(_t(e.current,e.next)));let{changes:o}=vt(r);if(o.length){console.log("\nThese .mcp.json settings aren't in your config, so `apply` would drop them:");for(let e of o)console.log(` ${e}`);console.log(`\nRun ${R(`${$} diff --adopt`)} to keep them by writing them into the config.`)}process.exit(a)},St=e=>{let t=[],n=e?.mcpServers;if(!n||typeof n!=`object`)return t;for(let[e,r]of Object.entries(n)){let n=Array.isArray(r?.args)?r.args:[];if(n.some(e=>typeof e==`string`&&e.endsWith(`@latest`)))t.push(e);else if(r?.command===`npx`){let r=n.find(e=>typeof e==`string`&&!e.startsWith(`-`));r&&!r.includes(`@`)&&t.push(e)}}return t},Ct=(e,t=process.cwd())=>{let r=n.resolve(t),i,a=-1;for(let t of e.workspaces){let e=u(t.path);(r===e||r.startsWith(e+n.sep))&&e.length>a&&(i=t,a=e.length)}return i},wt=(e=N)=>{let t=e(`gh`,[`api`,`user`,`--jq`,`.login`]),n=e(`git`,[`config`,`user.email`]);return{pwd:process.cwd(),gh:t.status===0&&t.stdout.trim()?t.stdout.trim():`none`,gitEmail:n.status===0?n.stdout.trim():`none`,tokenSet:!!process.env.GITHUB_TOKEN}},Tt=(t,r=N)=>{let i=[];ye()||i.push({status:`warn`,label:`platform`,detail:`inscope's secret resolution targets macOS (gh keyring + Keychain)`});let a=process.env.SHELL??``;a&&!/(^|\/)zsh$/.test(a)&&i.push({status:`warn`,label:`shell`,detail:`login shell is ${n.basename(a)}; inscope targets zsh (the hook is written to ~/.zshrc)`});let o=p(),s=_(o);s===null?i.push({status:`fail`,label:`hook`,detail:`missing ${o}; run \`inscope init\``}):s===qe(t)?i.push({status:`ok`,label:`hook`,detail:o}):i.push({status:`warn`,label:`hook`,detail:"out of date; run `inscope apply`"}),i.push(Qe()?{status:`ok`,label:`zshrc`,detail:`sources the hook`}:{status:`warn`,label:`zshrc`,detail:"does not source the hook; run `inscope init`"}),t.workspaces.some(X)&&i.push(ze(h(),Y)===null?{status:`fail`,label:`gitconfig`,detail:"missing includeIf block; run `inscope apply`"}:{status:`ok`,label:`gitconfig`,detail:`includeIf block present`});for(let n of t.workspaces){let t=`[${n.name}]`;if(n.gh&&i.push(be(n.gh,r)?{status:`ok`,label:`${t} gh`,detail:`token for ${n.gh}`}:{status:`fail`,label:`${t} gh`,detail:`no token for ${n.gh}; run \`gh auth login\``}),n.servers.slack){let e=n.servers.slack.keychain;i.push(F(e,r)?{status:`ok`,label:`${t} slack`,detail:e}:{status:`fail`,label:`${t} slack`,detail:`${e} not in keychain; run \`${Te(e)}\``})}if(X(n)){let a=Z(n.name);if(!e.existsSync(a))i.push({status:`fail`,label:`${t} git`,detail:`missing ${a}; run \`inscope apply\``});else if(n.git?.email){let e=Ee(a,r);i.push(e===n.git.email?{status:`ok`,label:`${t} git`,detail:n.git.email}:{status:`fail`,label:`${t} git`,detail:`email is ${e??`unset`}, expected ${n.git.email}`})}}let a=ht(n);if(a)i.push({status:`fail`,label:`${t} mcp`,detail:a});else{let e=me(n);if(e===null)i.push({status:`warn`,label:`${t} mcp`,detail:"no .mcp.json; run `inscope apply`"});else{let r=k(n.name).filter(t=>e.mcpServers?.[t]);i.push({status:`ok`,label:`${t} mcp`,detail:`${r.length} server(s)`}),_(A(n))!==mt(n)&&i.push({status:`warn`,label:`${t} mcp`,detail:"out of date; run `inscope diff`"});let a=St(e);a.length&&i.push({status:`warn`,label:`${t} mcp`,detail:`unpinned: ${a.join(`, `)}`})}}}return i},Et=`Verify the setup: gh tokens resolve, keychain entries exist,
115
117
  git emails match per path, the hook is current, and no MCP server is unpinned.
116
118
  Exits non-zero if any check fails.
117
119
 
118
120
  Usage:
119
- $ ${Q} doctor
121
+ $ ${$} doctor
120
122
 
121
123
  Options:
122
- -h, --help Display help message`,xt={ok:`✓`,warn:`!`,fail:`✗`},St={ok:Se,warn:R,fail:z},Ct=e=>{let{values:n}=t({allowPositionals:!0,options:{help:{type:`boolean`,short:`h`}},args:e});n.help&&(console.log(bt),process.exit(0)),v()||(console.error(`No config found. Run \`${Q} init\` first.`),process.exit(1));let r=y(),i=yt(r),a=i.map(e=>`${St[e.status](xt[e.status])} ${e.label}${e.detail?` ${e.detail}`:``}`).join(`
123
- `);console.log(`\n${a}`);let o=_t(r);if(o){let e=vt();console.log(`\nThis shell (${o.name}):`),console.log(` pwd ${e.pwd}`),console.log(` gh ${e.gh}`),console.log(` git ${e.gitEmail}`),console.log(` token ${e.tokenSet?`set`:`unset`}`)}let s=i.filter(e=>e.status===`fail`).length;s&&(console.log(`\n${z(`${s} check(s) failed.`)}`),process.exit(1)),console.log(`\n${Se(`All checks passed.`)}`),process.exit(0)},wt=`Edit a configured workspace interactively, then re-apply.
124
+ -h, --help Display help message`,Dt={ok:`✓`,warn:`!`,fail:`✗`},Ot={ok:Oe,warn:z,fail:B},kt=e=>{let{values:n}=t({allowPositionals:!0,options:{help:{type:`boolean`,short:`h`}},args:e});n.help&&(console.log(Et),process.exit(0)),x()||(console.error(`No config found. Run \`${$} init\` first.`),process.exit(1));let r=S(),i=Tt(r),a=i.map(e=>`${Ot[e.status](Dt[e.status])} ${e.label}${e.detail?` ${e.detail}`:``}`).join(`
125
+ `);console.log(`\n${a}`);let o=Ct(r);if(o){let e=wt();console.log(`\nThis shell (${o.name}):`),console.log(` pwd ${e.pwd}`),console.log(` gh ${e.gh}`),console.log(` git ${e.gitEmail}`),console.log(` token ${e.tokenSet?`set`:`unset`}`)}let s=i.filter(e=>e.status===`fail`).length;s&&(console.log(`\n${B(`${s} check(s) failed.`)}`),process.exit(1)),console.log(`\n${Oe(`All checks passed.`)}`),process.exit(0)},At=`Edit a configured workspace interactively, then re-apply.
124
126
  Pick a workspace (or pass its path/label), step through the prompts pre-filled
125
127
  with its current values, and inscope regenerates everything on save.
126
128
 
127
129
  Usage:
128
- $ ${Q} edit [path|label]
130
+ $ ${$} edit [path|label]
129
131
 
130
132
  Options:
131
- -h, --help Display help message`,Tt=async e=>{let{positionals:n,values:r}=t({allowPositionals:!0,options:{help:{type:`boolean`,short:`h`}},args:e});r.help&&(console.log(wt),process.exit(0)),v()||(console.error(`No config found. Run \`${Q} init\` first.`),process.exit(1));let i=y();i.workspaces.length||(console.error(`No workspaces yet. Add one with \`${Q} add <path>\`.`),process.exit(1));let a=n[0],o=await(async()=>{if(a){let e=D(i,a);return e||(console.error(`No workspace matching "${a}".`),process.exit(1)),e}if(i.workspaces.length===1)return i.workspaces[0];if(F())return W(`Edit which workspace?`,i.workspaces.map(e=>({label:`${e.name} (${e.path})`,value:e})));console.error(`Specify a workspace, e.g. \`${Q} edit <label>\`.`),process.exit(1)})();console.log(`\nEditing "${o.name}" (${o.path})\n`);let s=[...ge().map(e=>({label:e,value:e})),{label:`(none)`,value:``}],c=await W(`GitHub account`,s,Math.max(0,s.findIndex(e=>e.value===(o.gh??``))))||void 0,l=o.git?.email,u=await H(l?`Git email (enter keeps ${l}, "-" to inherit global)`:`Git email (enter to inherit global)`,l??``),d=u===`-`?void 0:u||void 0,f=o.git?.name,p=await H(f?`Git name (enter keeps ${f}, "-" to inherit global)`:`Git name (enter to inherit global)`,f??``),m=p===`-`?void 0:p||void 0,h=Ye(o.servers),g=await De(`MCP servers (space toggles, enter confirms)`,qe.map(e=>({label:e,value:e,checked:h.includes(e)}))),_=g.includes(`slack`),b=o.servers.slack?o.servers.slack.keychain:Je(o.name),x=o.servers.slack?!!o.servers.slack.addMessageTool:!1,S=!1;if(_&&(console.log(`
132
- Slack uses a user OAuth (xoxp) token.`),b=await H(`Slack keychain service`,b),x=await U(`Allow Slack to post messages?`,x),P(b)||(S=await U(`Store the Slack token now?`,!0))),_){let e=w(b);e&&(console.error(`\nInvalid Slack keychain service "${b}": ${e}`),process.exit(1))}let C={name:o.name,path:o.path,gh:c,git:d||m?{email:d,name:m}:void 0,servers:Xe(g,_?{keychain:b,addMessageTool:x}:null)};Ze(C),console.log(`\n✓ updated "${C.name}" -> ${C.path}`),await Qe(C,S),console.log(`\nRelaunch \`claude\` from ${C.path} to pick up the changes.`),process.exit(0)},Et=`Set up inscope: create the config, generate the chpwd hook, and
133
+ -h, --help Display help message`,jt=async e=>{let{positionals:n,values:r}=t({allowPositionals:!0,options:{help:{type:`boolean`,short:`h`}},args:e});r.help&&(console.log(At),process.exit(0)),x()||(console.error(`No config found. Run \`${$} init\` first.`),process.exit(1));let i=S();i.workspaces.length||(console.error(`No workspaces yet. Add one with \`${$} add <path>\`.`),process.exit(1));let a=n[0],o=await(async()=>{if(a){let e=E(i,a);return e||(console.error(`No workspace matching "${a}".`),process.exit(1)),e}if(i.workspaces.length===1)return i.workspaces[0];if(I())return G(`Edit which workspace?`,i.workspaces.map(e=>({label:`${e.name} (${e.path})`,value:e})));console.error(`Specify a workspace, e.g. \`${$} edit <label>\`.`),process.exit(1)})();console.log(`\nEditing "${o.name}" (${o.path})\n`);let s=[...Se().map(e=>({label:e,value:e})),{label:`(none)`,value:``}],c=await G(`GitHub account`,s,Math.max(0,s.findIndex(e=>e.value===(o.gh??``))))||void 0,l=o.git?.email,u=await U(l?`Git email (enter keeps ${l}, "-" to inherit global)`:`Git email (enter to inherit global)`,l??``),d=u===`-`?void 0:u||void 0,f=o.git?.name,p=await U(f?`Git name (enter keeps ${f}, "-" to inherit global)`:`Git name (enter to inherit global)`,f??``),m=p===`-`?void 0:p||void 0,h=tt(o.servers),g=await Pe(`MCP servers (space toggles, enter confirms)`,$e.map(e=>({label:e,value:e,checked:h.includes(e)}))),_=g.includes(`slack`),v=o.servers.slack?o.servers.slack.keychain:et(o.name),y=o.servers.slack?!!o.servers.slack.addMessageTool:!1,b=!1;if(_&&(console.log(`
134
+ Slack uses a user OAuth (xoxp) token.`),v=await U(`Slack keychain service`,v),y=await W(`Allow Slack to post messages?`,y),F(v)||(b=await W(`Store the Slack token now?`,!0))),_){let e=w(v);e&&(console.error(`\nInvalid Slack keychain service "${v}": ${e}`),process.exit(1))}let C={name:o.name,path:o.path,gh:c,git:d||m?{email:d,name:m}:void 0,servers:nt(g,_?{keychain:v,addMessageTool:y}:null)};it(C),console.log(`\n✓ updated "${C.name}" -> ${C.path}`),await at(C,b),console.log(`\nRelaunch \`claude\` from ${C.path} to pick up the changes.`),process.exit(0)},Mt=`Set up inscope: create the config, generate the chpwd hook, and
133
135
  source it from ~/.zshrc. Safe to run again; it never overwrites your config.
134
136
 
135
137
  Usage:
136
- $ ${Q} init
138
+ $ ${$} init
137
139
 
138
140
  Options:
139
- -h, --help Display help message`,Dt=e=>{let{values:n}=t({allowPositionals:!0,options:{help:{type:`boolean`,short:`h`}},args:e});n.help&&(console.log(Et),process.exit(0));let r;v()?(r=y(),console.log(`\nUsing existing config at ${f()}`)):(r=_(),b(r),console.log(`\nCreated ${f()}`)),Z(r),console.log(`Generated the chpwd hook and added a source line to ~/.zshrc.`),console.log(`
141
+ -h, --help Display help message`,Nt=e=>{let{values:n}=t({allowPositionals:!0,options:{help:{type:`boolean`,short:`h`}},args:e});n.help&&(console.log(Mt),process.exit(0));let r;x()?(r=S(),console.log(`\nUsing existing config at ${f()}`)):(r=b(),C(r),console.log(`\nCreated ${f()}`)),Q(r),console.log(`Generated the chpwd hook and added a source line to ~/.zshrc.`),console.log(`
140
142
  Next steps:
141
143
  1. Reload your shell: source ~/.zshrc (or open a new terminal)
142
- 2. Map a workspace: ${Q} add ~/acme`),process.exit(0)},Ot=`List the configured workspaces. Run \`${Q} doctor\` to verify
144
+ 2. Map a workspace: ${$} add ~/acme`),process.exit(0)},Pt=`List the configured workspaces. Run \`${$} doctor\` to verify
143
145
  that their tokens and identities actually resolve.
144
146
 
145
147
  Usage:
146
- $ ${Q} list
148
+ $ ${$} list
147
149
 
148
150
  Options:
149
- -h, --help Display help message`,kt=e=>k.filter(t=>!!e[t]).join(`, `)||`none`,At=e=>{let{values:n}=t({allowPositionals:!0,options:{help:{type:`boolean`,short:`h`}},args:e});n.help&&(console.log(Ot),process.exit(0)),v()||(console.error(`No config found. Run \`${Q} init\` first.`),process.exit(1));let r=y();r.workspaces.length||(console.log(`No workspaces yet. Add one with \`${Q} add <path> --gh <account>\`.`),process.exit(0));for(let e of r.workspaces)console.log(`\n${e.name}`),console.log(` path ${e.path}`),console.log(` gh ${e.gh??`(none)`}`),console.log(` git ${e.git?.email??`(default)`}`),console.log(` servers ${kt(e.servers)}`),e.servers.slack&&console.log(` slack keychain: ${e.servers.slack.keychain}`);process.exit(0)},jt=`Remove a workspace mapping. Drops its git include and the MCP
151
+ -h, --help Display help message`,Ft=e=>{let{values:n}=t({allowPositionals:!0,options:{help:{type:`boolean`,short:`h`}},args:e});n.help&&(console.log(Pt),process.exit(0)),x()||(console.error(`No config found. Run \`${$} init\` first.`),process.exit(1));let r=S();r.workspaces.length||(console.log(`No workspaces yet. Add one with \`${$} add <path> --gh <account>\`.`),process.exit(0));for(let e of r.workspaces)console.log(`\n${e.name}`),console.log(` path ${e.path}`),console.log(` gh ${e.gh??`(none)`}`),console.log(` git ${e.git?.email??`(default)`}`),console.log(` servers ${tt(e.servers).join(`, `)||`none`}`),e.servers.slack&&console.log(` slack keychain: ${e.servers.slack.keychain}`);process.exit(0)},It=`Remove a workspace mapping. Drops its git include and the MCP
150
152
  servers inscope manages; leaves your keychain and gh accounts untouched. Pick a
151
153
  workspace, or pass its path/label.
152
154
 
153
155
  Usage:
154
- $ ${Q} rm [path|label]
156
+ $ ${$} rm [path|label]
155
157
 
156
158
  Options:
157
159
  -y, --yes Skip the type-the-label confirmation
158
- -h, --help Display help message`,Mt=async e=>{let{positionals:n,values:r}=t({allowPositionals:!0,options:{help:{type:`boolean`,short:`h`},yes:{type:`boolean`,short:`y`}},args:e});r.help&&(console.log(jt),process.exit(0)),v()||(console.error(`No config found. Run \`${Q} init\` first.`),process.exit(1));let i=y();i.workspaces.length||(console.error(`No workspaces to remove.`),process.exit(1));let a=n[0],o;if(a){let e=D(i,a);e||(console.error(`No workspace matching "${a}".`),process.exit(1)),o=e}else F()?o=await W(`Remove which workspace?`,i.workspaces.map(e=>({label:`${e.name} (${e.path})`,value:e}))):(console.error(`Specify a workspace, e.g. \`${Q} rm <label>\`.`),process.exit(1));if(!r.yes){console.log(`\n⚠ Removing "${o.name}" (${o.path}) unmaps it from inscope.`);let e=await H(`Type "${o.name}" to confirm`);e!==o.name&&(console.error(`Aborted: "${e}" does not match "${o.name}".`),process.exit(1))}let{cfg:s}=ae(i,o.name);fe(o),Re(o.name),b(s),Z(s),console.log(`\n✓ removed workspace "${o.name}"`),o.servers.slack&&console.log(`\nNote: the keychain entry ${o.servers.slack.keychain} was left in place.\nDelete it with: ${L(`security delete-generic-password -s ${o.servers.slack.keychain}`)}`),process.exit(0)},Nt=`Version:
159
- ${Q}@${$e}
160
+ -h, --help Display help message`,Lt=async e=>{let{positionals:n,values:r}=t({allowPositionals:!0,options:{help:{type:`boolean`,short:`h`},yes:{type:`boolean`,short:`y`}},args:e});r.help&&(console.log(It),process.exit(0)),x()||(console.error(`No config found. Run \`${$} init\` first.`),process.exit(1));let i=S();i.workspaces.length||(console.error(`No workspaces to remove.`),process.exit(1));let a=n[0],o;if(a){let e=E(i,a);e||(console.error(`No workspace matching "${a}".`),process.exit(1)),o=e}else I()?o=await G(`Remove which workspace?`,i.workspaces.map(e=>({label:`${e.name} (${e.path})`,value:e}))):(console.error(`Specify a workspace, e.g. \`${$} rm <label>\`.`),process.exit(1));if(!r.yes){console.log(`\n⚠ Removing "${o.name}" (${o.path}) unmaps it from inscope.`);let e=await U(`Type "${o.name}" to confirm`);e!==o.name&&(console.error(`Aborted: "${e}" does not match "${o.name}".`),process.exit(1))}let{cfg:s}=ue(i,o.name);ve(o),We(o.name),C(s),Q(s),console.log(`\n✓ removed workspace "${o.name}"`),o.servers.slack&&console.log(`\nNote: the keychain entry ${o.servers.slack.keychain} was left in place.\nDelete it with: ${R(`security delete-generic-password -s ${o.servers.slack.keychain}`)}`),process.exit(0)},Rt=`Version:
161
+ ${$}@${ot}
160
162
 
161
163
  Per-workspace identity for Claude Code: scope MCP servers, GitHub auth, and git
162
164
  commit identity to the directory you are in, so concurrent sessions never clash.
163
165
 
164
166
  Usage:
165
- $ ${Q} <command> [options]
167
+ $ ${$} <command> [options]
166
168
 
167
169
  Commands:
168
170
  init Create the config, generate the hook, source it from ~/.zshrc
@@ -179,4 +181,4 @@ Options:
179
181
  -h, --help Display help
180
182
 
181
183
  Author:
182
- ${et.name} <${et.email}> (${et.url})`;(async()=>{try{let e=process.argv.slice(2),t=e[0],n=e.slice(1);switch(t){case`init`:return Dt(n);case`add`:return await rt(n);case`edit`:return Tt(n);case`rm`:case`remove`:return await Mt(n);case`ls`:case`list`:return At(n);case`diff`:return mt(n);case`apply`:case`sync`:return at(n);case`doctor`:return Ct(n)}(t===`-v`||t===`--version`)&&(console.log(`${Q}@${$e}`),process.exit(0)),(!t||t===`-h`||t===`--help`)&&(console.log(Nt),process.exit(0)),console.error(`unknown command: ${e.join(` `)}\n`),console.error(Nt),process.exit(1)}catch(e){console.error(e.message),process.exit(1)}})();export{};
184
+ ${st.name} <${st.email}> (${st.url})`;(async()=>{try{let e=process.argv.slice(2),t=e[0],n=e.slice(1);switch(t){case`init`:return Nt(n);case`add`:return await ut(n);case`edit`:return jt(n);case`rm`:case`remove`:return await Lt(n);case`ls`:case`list`:return Ft(n);case`diff`:return xt(n);case`apply`:case`sync`:return ft(n);case`doctor`:return kt(n)}(t===`-v`||t===`--version`)&&(console.log(`${$}@${ot}`),process.exit(0)),(!t||t===`-h`||t===`--help`)&&(console.log(Rt),process.exit(0)),console.error(`unknown command: ${e.join(` `)}\n`),console.error(Rt),process.exit(1)}catch(e){console.error(e.message),process.exit(1)}})();export{};
package/dist/index.d.mts CHANGED
@@ -52,6 +52,7 @@ declare const slugify: (s: string) => string;
52
52
  declare const validateConfig: (cfg: Config) => void;
53
53
  declare const labelFromPath: (p: string) => string;
54
54
  declare const findWorkspace: (cfg: Config, key: string) => Workspace | undefined;
55
+ declare const pathConflict: (cfg: Config, target: string, label: string) => Workspace | undefined;
55
56
  declare const upsertWorkspace: (cfg: Config, ws: Workspace) => Config;
56
57
  declare const removeWorkspace: (cfg: Config, key: string) => {
57
58
  cfg: Config;
@@ -142,4 +143,4 @@ declare const readMcp: (ws: Workspace) => Record<string, any> | null;
142
143
  declare const applyMcp: (ws: Workspace) => void;
143
144
  declare const removeMcp: (ws: Workspace) => void;
144
145
  //#endregion
145
- export { ApplyResult, CONFIG_VERSION, Check, CheckStatus, Config, Drift, HttpServer, RunResult, Runner, SLACK_MCP_VERSION, Servers, SlackServer, WORKSPACE_NAME_RE, Workspace, adoptable, applyAll, applyGitconfig, applyMcp, computeDrift, configExists, configVersionError, currentWorkspace, defaultConfig, defaultRunner, diffLines, ensureZshrcSource, findWorkspace, ghAccounts, ghStatus, ghToken, gitEmailForFile, gitGlobal, gitValueError, hookValueError, isMacOS, keychainHas, keychainSet, keychainSetCommand, labelFromPath, liveSnapshot, loadConfig, managedKeys, mcpError, mcpFilePath, mcpTarget, readMcp, removeMcp, removeWorkspace, renderGitInclude, renderHook, renderMcp, renderPerWorkspaceGitconfig, renderServers, renderZshrcSource, runDoctor, saveConfig, slugify, upsertWorkspace, validateConfig, workspaceNameError, workspacePathError, zshrcSourcesHook };
146
+ export { ApplyResult, CONFIG_VERSION, Check, CheckStatus, Config, Drift, HttpServer, RunResult, Runner, SLACK_MCP_VERSION, Servers, SlackServer, WORKSPACE_NAME_RE, Workspace, adoptable, applyAll, applyGitconfig, applyMcp, computeDrift, configExists, configVersionError, currentWorkspace, defaultConfig, defaultRunner, diffLines, ensureZshrcSource, findWorkspace, ghAccounts, ghStatus, ghToken, gitEmailForFile, gitGlobal, gitValueError, hookValueError, isMacOS, keychainHas, keychainSet, keychainSetCommand, labelFromPath, liveSnapshot, loadConfig, managedKeys, mcpError, mcpFilePath, mcpTarget, pathConflict, readMcp, removeMcp, removeWorkspace, renderGitInclude, renderHook, renderMcp, renderPerWorkspaceGitconfig, renderServers, renderZshrcSource, runDoctor, saveConfig, slugify, upsertWorkspace, validateConfig, workspaceNameError, workspacePathError, zshrcSourcesHook };
package/dist/index.mjs CHANGED
@@ -1,10 +1,10 @@
1
- import e from"node:fs";import t from"node:path";import n from"node:os";import{spawnSync as r}from"node:child_process";const i=()=>n.homedir(),a=()=>process.env.XDG_CONFIG_HOME?.trim()||t.join(i(),`.config`),o=e=>e===`~`?i():e.startsWith(`~/`)?t.join(i(),e.slice(2)):e,s=e=>{let n=o(e),r=i();return n===r?`~`:n.startsWith(r+t.sep)?`~/`+n.slice(r.length+1):n},c=e=>t.resolve(o(e)),l=()=>t.join(a(),`inscope`),u=()=>t.join(l(),`inscope.json`),d=()=>t.join(l(),`inscope.zsh`),f=()=>t.join(l(),`git`),p=()=>t.join(i(),`.gitconfig`),m=()=>t.join(i(),`.zshrc`),ee=1,te=()=>({version:1,workspaces:[]}),ne=()=>e.existsSync(u()),re=()=>{let t=u(),n=e.readFileSync(t,`utf8`),r=JSON.parse(n),i=b(r);if(i)throw Error(i);try{S(r)}catch(e){let n=e instanceof Error?e.message:String(e);throw Error(`${n}\nFix it in ${s(t)}, then re-run.`)}return r},ie=n=>{S(n);let r=u();e.mkdirSync(t.dirname(r),{recursive:!0}),e.writeFileSync(r,JSON.stringify(n,null,2)+`
2
- `)},h=/^[A-Za-z0-9._-]+$/,g=e=>e?h.test(e)?null:`use only letters, digits, dot (.), dash (-), or underscore (_)`:`must not be empty`,ae=/[\\"`$\n]/,_=e=>ae.test(e)?'must not contain a backslash (\\), quote ("), backtick (`), $, or newline':null,v=e=>e?_(e):`must not be empty`,y=e=>/[\n\r]/.test(e)?`must not contain a newline`:null,b=e=>typeof e.version==`number`&&e.version>1?`config version ${e.version} is newer than this inscope supports (max 1); upgrade inscope`:null,x=e=>e.toLowerCase().replace(/[^a-z0-9._-]+/g,`-`).replace(/^[-.]+|[-.]+$/g,``),S=e=>{if(!e||typeof e!=`object`)throw Error(`config is not an object`);let t=b(e);if(t)throw Error(t);if(!Array.isArray(e.workspaces))throw Error(`config.workspaces must be an array`);let n=new Set;for(let t of e.workspaces){if(!t.name)throw Error(`a workspace is missing a name`);let e=g(t.name);if(e)throw Error(`workspace name "${t.name}" is invalid: ${e}`);if(!t.path)throw Error(`workspace "${t.name}" is missing a path`);let r=v(t.path);if(r)throw Error(`workspace "${t.name}" path "${t.path}" is invalid: ${r}`);if(t.gh){let e=_(t.gh);if(e)throw Error(`workspace "${t.name}" gh account "${t.gh}" is invalid: ${e}`)}if(t.git?.email){let e=y(t.git.email);if(e)throw Error(`workspace "${t.name}" git email "${t.git.email}" is invalid: ${e}`)}if(t.git?.name){let e=y(t.git.name);if(e)throw Error(`workspace "${t.name}" git name "${t.git.name}" is invalid: ${e}`)}let i=t.servers?.slack;if(i&&i.keychain){let e=_(i.keychain);if(e)throw Error(`workspace "${t.name}" Slack keychain "${i.keychain}" is invalid: ${e}`)}if(n.has(t.name))throw Error(`duplicate workspace name "${t.name}"`);n.add(t.name)}},oe=e=>x(t.basename(c(e)))||`workspace`,C=(e,t)=>{let n=e.workspaces.find(e=>e.name===t);if(n)return n;let r=c(t);return e.workspaces.find(e=>c(e.path)===r)},se=(e,t)=>{let n=e.workspaces.filter(e=>e.name!==t.name);return n.push({...t,path:s(t.path)}),n.sort((e,t)=>e.name.localeCompare(t.name)),{...e,workspaces:n}},ce=(e,t)=>{let n=C(e,t);return n?{cfg:{...e,workspaces:e.workspaces.filter(e=>e.name!==n.name)},removed:n}:{cfg:e}},w=e=>`# >>> inscope:${e} >>>`,T=e=>`# <<< inscope:${e} <<<`,E=e=>e.replace(/[.*+?^${}()|[\]\\]/g,`\\$&`),le=e=>RegExp(`${E(w(e))}\\n[\\s\\S]*?\\n${E(T(e))}\\n?`),ue=(e,t)=>{let n=t.replace(/\n+$/,``);return`${w(e)}\n${n}\n${T(e)}\n`},D=t=>{try{return e.readFileSync(t,`utf8`)}catch{return``}},de=(n,r,i)=>{e.mkdirSync(t.dirname(n),{recursive:!0});let a=D(n),o=ue(r,i),s=le(r),c;if(s.test(a))c=a.replace(s,o);else{let e=a.replace(/\n*$/,``);c=e.length?`${e}\n\n${o}`:o}e.writeFileSync(n,c)},fe=(t,n)=>{let r=D(t);if(!r)return;let i=r.replace(le(n),``).replace(/\n{3,}/g,`
1
+ import e from"node:fs";import t from"node:path";import n from"node:os";import{spawnSync as r}from"node:child_process";const i=()=>n.homedir(),a=()=>process.env.XDG_CONFIG_HOME?.trim()||t.join(i(),`.config`),o=e=>e===`~`?i():e.startsWith(`~/`)?t.join(i(),e.slice(2)):e,s=e=>{let n=o(e),r=i();return n===r?`~`:n.startsWith(r+t.sep)?`~/`+n.slice(r.length+1):n},c=e=>t.resolve(o(e)),l=()=>t.join(a(),`inscope`),u=()=>t.join(l(),`inscope.json`),d=()=>t.join(l(),`inscope.zsh`),f=()=>t.join(l(),`git`),p=()=>t.join(i(),`.gitconfig`),m=()=>t.join(i(),`.zshrc`),h=t=>{try{return e.readFileSync(t,`utf8`)}catch{return null}},g=e=>h(e)??``,_=(n,r)=>{let i=n;try{i=e.realpathSync(n)}catch{}let a=t.dirname(i);e.mkdirSync(a,{recursive:!0});let o;try{o=e.statSync(i).mode}catch{}let s=t.join(a,`.${t.basename(i)}.inscope-${process.pid}.tmp`);e.writeFileSync(s,r),o!==void 0&&e.chmodSync(s,o&4095);try{e.renameSync(s,i)}catch(t){try{e.rmSync(s,{force:!0})}catch{}throw t}},ee=1,te=()=>({version:1,workspaces:[]}),ne=()=>e.existsSync(u()),re=()=>{let t=u(),n=e.readFileSync(t,`utf8`),r;try{r=JSON.parse(n)}catch{throw Error(`${s(t)} is not valid JSON; fix it, then re-run.`)}let i=C(r);if(i)throw Error(i);try{T(r)}catch(e){let n=e instanceof Error?e.message:String(e);throw Error(`${n}\nFix it in ${s(t)}, then re-run.`)}return r},ie=e=>{T(e),_(u(),JSON.stringify(e,null,2)+`
2
+ `)},v=/^[A-Za-z0-9._-]+$/,y=e=>e?v.test(e)?null:`use only letters, digits, dot (.), dash (-), or underscore (_)`:`must not be empty`,ae=/[\\"`$\n]/,b=e=>ae.test(e)?'must not contain a backslash (\\), quote ("), backtick (`), $, or newline':null,x=e=>e?b(e):`must not be empty`,S=e=>/[\n\r]/.test(e)?`must not contain a newline`:null,C=e=>typeof e.version==`number`&&e.version>1?`config version ${e.version} is newer than this inscope supports (max 1); upgrade inscope`:null,w=e=>e.toLowerCase().replace(/[^a-z0-9._-]+/g,`-`).replace(/^[-.]+|[-.]+$/g,``),T=e=>{if(!e||typeof e!=`object`)throw Error(`config is not an object`);let t=C(e);if(t)throw Error(t);if(!Array.isArray(e.workspaces))throw Error(`config.workspaces must be an array`);let n=new Set;for(let t of e.workspaces){if(!t.name)throw Error(`a workspace is missing a name`);let e=y(t.name);if(e)throw Error(`workspace name "${t.name}" is invalid: ${e}`);if(!t.path)throw Error(`workspace "${t.name}" is missing a path`);let r=x(t.path);if(r)throw Error(`workspace "${t.name}" path "${t.path}" is invalid: ${r}`);if(t.gh){let e=b(t.gh);if(e)throw Error(`workspace "${t.name}" gh account "${t.gh}" is invalid: ${e}`)}if(t.git?.email){let e=S(t.git.email);if(e)throw Error(`workspace "${t.name}" git email "${t.git.email}" is invalid: ${e}`)}if(t.git?.name){let e=S(t.git.name);if(e)throw Error(`workspace "${t.name}" git name "${t.git.name}" is invalid: ${e}`)}let i=t.servers?.slack;if(i&&i.keychain){let e=b(i.keychain);if(e)throw Error(`workspace "${t.name}" Slack keychain "${i.keychain}" is invalid: ${e}`)}if(n.has(t.name))throw Error(`duplicate workspace name "${t.name}"`);n.add(t.name)}},oe=e=>w(t.basename(c(e)))||`workspace`,E=(e,t)=>{let n=e.workspaces.find(e=>e.name===t);if(n)return n;let r=c(t);return e.workspaces.find(e=>c(e.path)===r)},se=(e,t,n)=>{let r=c(t);return e.workspaces.find(e=>e.name!==n&&c(e.path)===r)},ce=(e,t)=>{let n=e.workspaces.filter(e=>e.name!==t.name);return n.push({...t,path:s(t.path)}),n.sort((e,t)=>e.name.localeCompare(t.name)),{...e,workspaces:n}},le=(e,t)=>{let n=E(e,t);return n?{cfg:{...e,workspaces:e.workspaces.filter(e=>e.name!==n.name)},removed:n}:{cfg:e}},D=e=>`# >>> inscope:${e} >>>`,O=e=>`# <<< inscope:${e} <<<`,k=e=>e.replace(/[.*+?^${}()|[\]\\]/g,`\\$&`),ue=e=>RegExp(`${k(D(e))}\\n[\\s\\S]*?\\n${k(O(e))}\\n?`),de=(e,t)=>{let n=t.replace(/\n+$/,``);return`${D(e)}\n${n}\n${O(e)}\n`},fe=(e,t,n)=>{let r=g(e),i=de(t,n),a=ue(t),o;if(a.test(r))o=r.replace(a,i);else{let e=r.replace(/\n*$/,``);o=e.length?`${e}\n\n${i}`:i}_(e,o)},pe=(e,t)=>{let n=g(e);n&&_(e,n.replace(ue(t),``).replace(/\n{3,}/g,`
3
3
 
4
- `).replace(/^\n+/,``);e.writeFileSync(t,i)},O=(e,t)=>{let n=D(e).match(RegExp(`${E(w(t))}\\n([\\s\\S]*?)\\n${E(T(t))}`));return n?n[1]:null},k=`gitconfig`,A=e=>!!(e.git&&(e.git.email||e.git.name)),j=e=>t.join(f(),`${e}.gitconfig`),pe=e=>s(e).replace(/\/+$/,``)+`/`,M=e=>e.workspaces.filter(A).map(e=>`[includeIf "gitdir:${pe(e.path)}"]\n\tpath = ${s(j(e.name))}`).join(`
5
- `),N=e=>{let t=[`# Managed by inscope. Do not edit by hand.`,`[user]`];return e.git?.email&&t.push(`\temail = ${e.git.email}`),e.git?.name&&t.push(`\tname = ${e.git.name}`),t.join(`
4
+ `).replace(/^\n+/,``))},A=(e,t)=>{let n=g(e).match(RegExp(`${k(D(t))}\\n([\\s\\S]*?)\\n${k(O(t))}`));return n?n[1]:null},j=`gitconfig`,M=e=>!!(e.git&&(e.git.email||e.git.name)),N=e=>t.join(f(),`${e}.gitconfig`),me=e=>s(e).replace(/\/+$/,``)+`/`,P=e=>e.workspaces.filter(M).map(e=>`[includeIf "gitdir:${me(e.path)}"]\n\tpath = ${s(N(e.name))}`).join(`
5
+ `),F=e=>{let t=[`# Managed by inscope. Do not edit by hand.`,`[user]`];return e.git?.email&&t.push(`\temail = ${e.git.email}`),e.git?.name&&t.push(`\tname = ${e.git.name}`),t.join(`
6
6
  `)+`
7
- `},P=t=>{e.mkdirSync(f(),{recursive:!0});for(let n of t.workspaces)A(n)&&e.writeFileSync(j(n.name),N(n));let n=M(t);n?de(p(),k,n):fe(p(),k)},me=e=>{let t=s(e);return t===`~`?`"$HOME/"*`:t.startsWith(`~/`)?`"$HOME/${t.slice(2)}/"*`:`"${t}/"*`},he=e=>e.servers.slack?e.servers.slack.keychain:``,F=e=>{let t=[...e.workspaces].sort((e,t)=>e.name.localeCompare(t.name));return`# Managed by inscope. Do not edit by hand.
7
+ `},I=t=>{e.mkdirSync(f(),{recursive:!0});for(let e of t.workspaces)M(e)?_(N(e.name),F(e)):he(e.name);let n=P(t);n?fe(p(),j,n):pe(p(),j)},he=t=>{let n=N(t);e.existsSync(n)&&e.rmSync(n)},ge=e=>{let t=s(e);return t===`~`?`"$HOME/"*`:t.startsWith(`~/`)?`"$HOME/${t.slice(2)}/"*`:`"${t}/"*`},_e=e=>e.servers.slack?e.servers.slack.keychain:``,L=e=>{let t=[...e.workspaces].sort((e,t)=>e.name.localeCompare(t.name));return`# Managed by inscope. Do not edit by hand.
8
8
  # Source of truth: ~/.config/inscope/inscope.json
9
9
  # Edit there, then run \`inscope apply\` to regenerate this file.
10
10
  #
@@ -15,7 +15,7 @@ import e from"node:fs";import t from"node:path";import n from"node:os";import{sp
15
15
  __inscope_resolve_identity() {
16
16
  local ws
17
17
  case "\${PWD}/" in
18
- ${[...t].sort((e,t)=>s(t.path).length-s(e.path).length).map(e=>` ${me(e.path)}) ws="${e.name}" ;;`).join(`
18
+ ${[...t].sort((e,t)=>s(t.path).length-s(e.path).length).map(e=>` ${ge(e.path)}) ws="${e.name}" ;;`).join(`
19
19
  `)||` # no workspaces configured`}
20
20
  *) ws="" ;;
21
21
  esac
@@ -25,7 +25,7 @@ ${[...t].sort((e,t)=>s(t.path).length-s(e.path).length).map(e=>` ${me(e.path)
25
25
 
26
26
  local gh_user="" slack_svc=""
27
27
  case "$ws" in
28
- ${t.map(e=>{let t=[];e.gh&&t.push(`gh_user="${e.gh}"`);let n=he(e);return n&&t.push(`slack_svc="${n}"`),` ${e.name}) ${t.length?t.join(`; `):`:`} ;;`}).join(`
28
+ ${t.map(e=>{let t=[];e.gh&&t.push(`gh_user="${e.gh}"`);let n=_e(e);return n&&t.push(`slack_svc="${n}"`),` ${e.name}) ${t.length?t.join(`; `):`:`} ;;`}).join(`
29
29
  `)||` # no workspaces configured`}
30
30
  *) return ;; # outside a mapped workspace: nothing set
31
31
  esac
@@ -51,10 +51,8 @@ autoload -Uz add-zsh-hook
51
51
  add-zsh-hook chpwd __inscope_resolve_identity
52
52
  __inscope_ws="__init__" # force the first resolve, clearing any inherited token
53
53
  __inscope_resolve_identity
54
- `},I=`1.3.0`,L={atlassian:`https://mcp.atlassian.com/v1/mcp`,canva:`https://mcp.canva.com/mcp`,clickup:`https://mcp.clickup.com/mcp`,hubspot:`https://mcp.hubspot.com`,intercom:`https://mcp.intercom.com/mcp`,linear:`https://mcp.linear.app/mcp`,monday:`https://mcp.monday.com/mcp`,notion:`https://mcp.notion.com/mcp`,plane:`https://mcp.plane.so/http/mcp`,sentry:`https://mcp.sentry.dev/mcp`,stripe:`https://mcp.stripe.com`,vercel:`https://mcp.vercel.com`,webflow:`https://mcp.webflow.com/`},R=[`github`,`atlassian`,`canva`,`clickup`,`hubspot`,`intercom`,`linear`,`monday`,`notion`,`plane`,`sentry`,`slack`,`stripe`,`vercel`,`webflow`],z=e=>R.map(t=>`${t}-${e}`),B=e=>t.join(c(e.path),`.mcp.json`),ge=(e,t)=>e&&typeof e==`object`&&e.url?e.url:t,V=e=>{let t=e.servers,n={};for(let r of R){let i=t[r];if(!i)continue;let a=`${r}-${e.name}`;if(r===`github`)n[a]={type:`http`,url:`https://api.githubcopilot.com/mcp/`,headers:{Authorization:"Bearer ${GITHUB_TOKEN}"}};else if(r===`slack`){let e=i,t={SLACK_MCP_XOXP_TOKEN:"${SLACK_MCP_XOXP_TOKEN}"};e.addMessageTool&&(t.SLACK_MCP_ADD_MESSAGE_TOOL=`true`),n[a]={type:`stdio`,command:`npx`,args:[`-y`,`slack-mcp-server@${I}`,`--transport`,`stdio`],env:t}}else n[a]={type:`http`,url:ge(i,L[r])}}return n},_e=e=>({mcpServers:V(e)}),ve=t=>{if(!e.existsSync(t))return{};try{return JSON.parse(e.readFileSync(t,`utf8`))}catch{return{}}},H=t=>{if(!e.existsSync(t))return{};try{return JSON.parse(e.readFileSync(t,`utf8`))}catch{throw Error(`${t} is not valid JSON; fix or remove it, then re-run inscope (left it untouched)`)}},U=t=>{let n=B(t);return e.existsSync(n)?ve(n):null},W=n=>{let r=B(n);e.mkdirSync(t.dirname(r),{recursive:!0});let i=H(r),a=i.mcpServers&&typeof i.mcpServers==`object`?{...i.mcpServers}:{};for(let e of z(n.name))delete a[e];Object.assign(a,V(n)),i.mcpServers=a,e.writeFileSync(r,JSON.stringify(i,null,2)+`
55
- `)},ye=t=>{let n=B(t);if(!e.existsSync(n))return;let r=H(n);if(r.mcpServers&&typeof r.mcpServers==`object`)for(let e of z(t.name))delete r.mcpServers[e];e.writeFileSync(n,JSON.stringify(r,null,2)+`
56
- `)},be=e=>{let n=i();return e===n?`$HOME`:e.startsWith(n+t.sep)?`$HOME/${e.slice(n.length+1)}`:e},xe=()=>{let e=be(d());return`[ -r "${e}" ] && source "${e}"`},G=e=>{let t=xe();if(e.includes(t))return e;let n=e.replace(/\n*$/,``),r=`# inscope: load each workspace's tokens (GitHub, Slack) from \$PWD on every cd\n${t}`;return n.length?`${n}\n\n${r}\n`:`${r}\n`},K=()=>{let t=m(),n=``;try{n=e.readFileSync(t,`utf8`)}catch{}let r=G(n);r!==n&&e.writeFileSync(t,r)},q=()=>{try{return e.readFileSync(m(),`utf8`).includes(xe())}catch{return!1}},Se=n=>{let r=d();e.mkdirSync(t.dirname(r),{recursive:!0}),e.writeFileSync(r,F(n)),P(n),K();let i=[];for(let e of n.workspaces)W(e),i.push(B(e));return{hook:r,gitconfig:n.workspaces.some(e=>e.git?.email||e.git?.name),mcp:i}},J=t=>{try{return e.readFileSync(t,`utf8`)}catch{return``}},Ce=e=>{let t=J(e);if(!t)return null;try{return JSON.parse(t)}catch{return null}},Y=e=>{let t=Ce(B(e))??{},n=t.mcpServers&&typeof t.mcpServers==`object`?{...t.mcpServers}:{};for(let t of z(e.name))delete n[t];return Object.assign(n,V(e)),t.mcpServers=n,JSON.stringify(t,null,2)+`
57
- `},X=t=>{let n=B(t);if(!e.existsSync(n))return null;try{return JSON.parse(e.readFileSync(n,`utf8`)),null}catch{return"invalid JSON; `apply` will not touch it until you fix it"}},we=e=>{let t=[],n=d();t.push({label:`hook`,path:n,current:J(n),next:F(e)}),t.push({label:`gitconfig`,path:p(),current:O(p(),k)??``,next:M(e)});for(let n of e.workspaces){if(!A(n))continue;let e=j(n.name);t.push({label:`gitconfig:${n.name}`,path:e,current:J(e),next:N(n)})}for(let n of e.workspaces){let e=B(n),r=X(n);if(r){t.push({label:`mcp:${n.name}`,path:e,current:``,next:``,error:r});continue}t.push({label:`mcp:${n.name}`,path:e,current:J(e),next:Y(n)})}return t.filter(e=>e.error!=null||e.current!==e.next)},Te=(e,t)=>{let n=e.length?e.split(`
54
+ `},R=`1.3.0`,z={atlassian:`https://mcp.atlassian.com/v1/mcp`,canva:`https://mcp.canva.com/mcp`,clickup:`https://mcp.clickup.com/mcp`,hubspot:`https://mcp.hubspot.com`,intercom:`https://mcp.intercom.com/mcp`,linear:`https://mcp.linear.app/mcp`,monday:`https://mcp.monday.com/mcp`,notion:`https://mcp.notion.com/mcp`,plane:`https://mcp.plane.so/http/mcp`,sentry:`https://mcp.sentry.dev/mcp`,stripe:`https://mcp.stripe.com`,vercel:`https://mcp.vercel.com`,webflow:`https://mcp.webflow.com/`},B=[`github`,`atlassian`,`canva`,`clickup`,`hubspot`,`intercom`,`linear`,`monday`,`notion`,`plane`,`sentry`,`slack`,`stripe`,`vercel`,`webflow`],V=e=>B.map(t=>`${t}-${e}`),H=e=>t.join(c(e.path),`.mcp.json`),ve=(e,t)=>e&&typeof e==`object`&&e.url?e.url:t,U=e=>{let t=e.servers,n={};for(let r of B){let i=t[r];if(!i)continue;let a=`${r}-${e.name}`;if(r===`github`)n[a]={type:`http`,url:`https://api.githubcopilot.com/mcp/`,headers:{Authorization:"Bearer ${GITHUB_TOKEN}"}};else if(r===`slack`){let e=i,t={SLACK_MCP_XOXP_TOKEN:"${SLACK_MCP_XOXP_TOKEN}"};e.addMessageTool&&(t.SLACK_MCP_ADD_MESSAGE_TOOL=`true`),n[a]={type:`stdio`,command:`npx`,args:[`-y`,`slack-mcp-server@${R}`,`--transport`,`stdio`],env:t}}else n[a]={type:`http`,url:ve(i,z[r])}}return n},ye=e=>({mcpServers:U(e)}),be=t=>{if(!e.existsSync(t))return{};try{return JSON.parse(e.readFileSync(t,`utf8`))}catch{return{}}},W=t=>{if(!e.existsSync(t))return{};try{return JSON.parse(e.readFileSync(t,`utf8`))}catch{throw Error(`${t} is not valid JSON; fix or remove it, then re-run inscope (left it untouched)`)}},G=t=>{let n=H(t);return e.existsSync(n)?be(n):null},K=(e,t)=>{let n=e.mcpServers&&typeof e.mcpServers==`object`?{...e.mcpServers}:{};for(let e of V(t.name))delete n[e];return Object.assign(n,U(t)),{...e,mcpServers:n}},q=e=>JSON.stringify(e,null,2)+`
55
+ `,xe=e=>{for(let t of e)W(H(t))},Se=e=>{let t=H(e);_(t,q(K(W(t),e)))},Ce=t=>{let n=H(t);if(!e.existsSync(n))return;let r=W(n);if(r.mcpServers&&typeof r.mcpServers==`object`)for(let e of V(t.name))delete r.mcpServers[e];_(n,q(r))},we=e=>{let n=i();return e===n?`$HOME`:e.startsWith(n+t.sep)?`$HOME/${e.slice(n.length+1)}`:e},Te=()=>{let e=we(d());return`[ -r "${e}" ] && source "${e}"`},Ee=e=>{let t=Te();if(e.includes(t))return e;let n=e.replace(/\n*$/,``),r=`# inscope: load each workspace's tokens (GitHub, Slack) from \$PWD on every cd\n${t}`;return n.length?`${n}\n\n${r}\n`:`${r}\n`},De=()=>{let e=m(),t=g(e),n=Ee(t);n!==t&&_(e,n)},Oe=()=>g(m()).includes(Te()),ke=e=>{xe(e.workspaces);let t=d();_(t,L(e)),I(e),De();let n=[];for(let t of e.workspaces)Se(t),n.push(H(t));return{hook:t,gitconfig:e.workspaces.some(e=>e.git?.email||e.git?.name),mcp:n}},Ae=e=>{let t=g(e);if(!t)return null;try{return JSON.parse(t)}catch{return null}},J=e=>q(K(Ae(H(e))??{},e)),Y=t=>{let n=H(t);if(!e.existsSync(n))return null;try{return JSON.parse(e.readFileSync(n,`utf8`)),null}catch{return"invalid JSON; `apply` will not touch it until you fix it"}},je=e=>{let t=[],n=d();t.push({label:`hook`,path:n,current:g(n),next:L(e)}),t.push({label:`gitconfig`,path:p(),current:A(p(),j)??``,next:P(e)});for(let n of e.workspaces){if(!M(n))continue;let e=N(n.name);t.push({label:`gitconfig:${n.name}`,path:e,current:g(e),next:F(n)})}for(let n of e.workspaces){let e=H(n),r=Y(n);if(r){t.push({label:`mcp:${n.name}`,path:e,current:``,next:``,error:r});continue}t.push({label:`mcp:${n.name}`,path:e,current:g(e),next:J(n)})}return t.filter(e=>e.error!=null||e.current!==e.next)},Me=(e,t)=>{let n=e.length?e.split(`
58
56
  `):[],r=t.length?t.split(`
59
57
  `):[],i=n.length,a=r.length,o=Array.from({length:i+1},()=>Array.from({length:a+1},()=>0));for(let e=i-1;e>=0;e--)for(let t=a-1;t>=0;t--)o[e][t]=n[e]===r[t]?o[e+1][t+1]+1:Math.max(o[e+1][t],o[e][t+1]);let s=[],c=0,l=0;for(;c<i&&l<a;)n[c]===r[l]?(s.push(` ${n[c]}`),c++,l++):o[c+1][l]>=o[c][l+1]?(s.push(`- ${n[c]}`),c++):(s.push(`+ ${r[l]}`),l++);for(;c<i;)s.push(`- ${n[c++]}`);for(;l<a;)s.push(`+ ${r[l++]}`);return s.join(`
60
- `)},Ee=e=>{let t=[],n=e.workspaces.map(e=>{let n=Ce(B(e))?.mcpServers;if(!n||typeof n!=`object`)return e;let r=e.servers,i=e.servers.slack;i&&!i.addMessageTool&&n[`slack-${e.name}`]?.env?.SLACK_MCP_ADD_MESSAGE_TOOL===`true`&&(r={...r,slack:{...i,addMessageTool:!0}},t.push(`${e.name}: slack.addMessageTool = true`));for(let i of R){if(i===`github`||i===`slack`)continue;let a=n[`${i}-${e.name}`]?.url;if(typeof a!=`string`)continue;let o=e.servers[i];if(!o){r={...r,[i]:a===L[i]?!0:{url:a}},t.push(`${e.name}: ${i} = ${a===L[i]?`enabled`:a}`);continue}a!==((typeof o==`object`?o.url:void 0)??L[i])&&(r={...r,[i]:{url:a}},t.push(`${e.name}: ${i}.url = ${a}`))}return r===e.servers?e:{...e,servers:r}});return{cfg:{...e,workspaces:n},changes:t}},Z=(e,t,n)=>{let i=r(e,t,{encoding:`utf8`,input:n?.input});return{status:i.status??(i.error?127:1),stdout:i.stdout??``,stderr:i.stderr??``}},De=()=>process.platform===`darwin`,Q=()=>process.env.USER||``,Oe=(e,t=Z)=>{let n=t(`gh`,[`auth`,`token`,`-u`,e]),r=n.stdout.trim();return n.status===0&&r?r:null},ke=(e=Z)=>{let t=e(`gh`,[`auth`,`status`]);return(t.stdout+t.stderr).trim()},Ae=(e=Z)=>{let t=[];for(let n of ke(e).matchAll(/account (\S+) \(/g))t.includes(n[1])||t.push(n[1]);return t},je=(e,t=Z)=>{let n=t(`git`,[`config`,`--global`,e]),r=n.stdout.trim();return n.status===0&&r?r:null},Me=(e,t=Z)=>{let n=t(`security`,[`find-generic-password`,`-a`,Q(),`-s`,e,`-w`]);return n.status===0&&n.stdout.trim().length>0},Ne=(e,t,n=Z)=>{let r=n(`security`,[`add-generic-password`,`-U`,`-a`,Q(),`-s`,e,`-w`,t]);if(r.status!==0)throw Error(`security add-generic-password failed: ${r.stderr.trim()||`unknown error`}`)},Pe=e=>`security add-generic-password -U -a "${Q()||`$USER`}" -s ${e} -w 'xoxp-...'`,Fe=(e,t=Z)=>{let n=t(`git`,[`config`,`--file`,e,`user.email`]);return n.status===0?n.stdout.trim():null},$=t=>{try{return e.readFileSync(t,`utf8`)}catch{return null}},Ie=e=>{let t=[],n=e?.mcpServers;if(!n||typeof n!=`object`)return t;for(let[e,r]of Object.entries(n)){let n=Array.isArray(r?.args)?r.args:[];if(n.some(e=>typeof e==`string`&&e.endsWith(`@latest`)))t.push(e);else if(r?.command===`npx`){let r=n.find(e=>typeof e==`string`&&!e.startsWith(`-`));r&&!r.includes(`@`)&&t.push(e)}}return t},Le=(e,n=process.cwd())=>{let r=t.resolve(n),i,a=-1;for(let n of e.workspaces){let e=c(n.path);(r===e||r.startsWith(e+t.sep))&&e.length>a&&(i=n,a=e.length)}return i},Re=(e=Z)=>{let t=e(`gh`,[`api`,`user`,`--jq`,`.login`]),n=e(`git`,[`config`,`user.email`]);return{pwd:process.cwd(),gh:t.status===0&&t.stdout.trim()?t.stdout.trim():`none`,gitEmail:n.status===0?n.stdout.trim():`none`,tokenSet:!!process.env.GITHUB_TOKEN}},ze=(t,n=Z)=>{let r=[];De()||r.push({status:`warn`,label:`platform`,detail:`inscope's secret resolution targets macOS (gh keyring + Keychain)`});let i=d(),a=$(i);a===null?r.push({status:`fail`,label:`hook`,detail:`missing ${i}; run \`inscope init\``}):a===F(t)?r.push({status:`ok`,label:`hook`,detail:i}):r.push({status:`warn`,label:`hook`,detail:"out of date; run `inscope apply`"}),r.push(q()?{status:`ok`,label:`zshrc`,detail:`sources the hook`}:{status:`warn`,label:`zshrc`,detail:"does not source the hook; run `inscope init`"}),t.workspaces.some(A)&&r.push(O(p(),k)===null?{status:`fail`,label:`gitconfig`,detail:"missing includeIf block; run `inscope apply`"}:{status:`ok`,label:`gitconfig`,detail:`includeIf block present`});for(let i of t.workspaces){let t=`[${i.name}]`;if(i.gh&&r.push(Oe(i.gh,n)?{status:`ok`,label:`${t} gh`,detail:`token for ${i.gh}`}:{status:`fail`,label:`${t} gh`,detail:`no token for ${i.gh}; run \`gh auth login\``}),i.servers.slack){let e=i.servers.slack.keychain;r.push(Me(e,n)?{status:`ok`,label:`${t} slack`,detail:e}:{status:`fail`,label:`${t} slack`,detail:`${e} not in keychain; run \`${Pe(e)}\``})}if(A(i)){let a=j(i.name);if(!e.existsSync(a))r.push({status:`fail`,label:`${t} git`,detail:`missing ${a}; run \`inscope apply\``});else if(i.git?.email){let e=Fe(a,n);r.push(e===i.git.email?{status:`ok`,label:`${t} git`,detail:i.git.email}:{status:`fail`,label:`${t} git`,detail:`email is ${e??`unset`}, expected ${i.git.email}`})}}let a=X(i);if(a)r.push({status:`fail`,label:`${t} mcp`,detail:a});else{let e=U(i);if(e===null)r.push({status:`warn`,label:`${t} mcp`,detail:"no .mcp.json; run `inscope apply`"});else{let n=z(i.name).filter(t=>e.mcpServers?.[t]);r.push({status:`ok`,label:`${t} mcp`,detail:`${n.length} server(s)`}),$(B(i))!==Y(i)&&r.push({status:`warn`,label:`${t} mcp`,detail:"out of date; run `inscope diff`"});let a=Ie(e);a.length&&r.push({status:`warn`,label:`${t} mcp`,detail:`unpinned: ${a.join(`, `)}`})}}}return r};export{ee as CONFIG_VERSION,I as SLACK_MCP_VERSION,h as WORKSPACE_NAME_RE,Ee as adoptable,Se as applyAll,P as applyGitconfig,W as applyMcp,we as computeDrift,ne as configExists,b as configVersionError,Le as currentWorkspace,te as defaultConfig,Z as defaultRunner,Te as diffLines,K as ensureZshrcSource,C as findWorkspace,Ae as ghAccounts,ke as ghStatus,Oe as ghToken,Fe as gitEmailForFile,je as gitGlobal,y as gitValueError,_ as hookValueError,De as isMacOS,Me as keychainHas,Ne as keychainSet,Pe as keychainSetCommand,oe as labelFromPath,Re as liveSnapshot,re as loadConfig,z as managedKeys,X as mcpError,B as mcpFilePath,Y as mcpTarget,U as readMcp,ye as removeMcp,ce as removeWorkspace,M as renderGitInclude,F as renderHook,_e as renderMcp,N as renderPerWorkspaceGitconfig,V as renderServers,G as renderZshrcSource,ze as runDoctor,ie as saveConfig,x as slugify,se as upsertWorkspace,S as validateConfig,g as workspaceNameError,v as workspacePathError,q as zshrcSourcesHook};
58
+ `)},Ne=e=>{let t=[],n=e.workspaces.map(e=>{let n=Ae(H(e))?.mcpServers;if(!n||typeof n!=`object`)return e;let r=e.servers,i=e.servers.slack;i&&!i.addMessageTool&&n[`slack-${e.name}`]?.env?.SLACK_MCP_ADD_MESSAGE_TOOL===`true`&&(r={...r,slack:{...i,addMessageTool:!0}},t.push(`${e.name}: slack.addMessageTool = true`));for(let i of B){if(i===`github`||i===`slack`)continue;let a=n[`${i}-${e.name}`]?.url;if(typeof a!=`string`)continue;let o=e.servers[i];if(!o){r={...r,[i]:a===z[i]?!0:{url:a}},t.push(`${e.name}: ${i} = ${a===z[i]?`enabled`:a}`);continue}a!==((typeof o==`object`?o.url:void 0)??z[i])&&(r={...r,[i]:{url:a}},t.push(`${e.name}: ${i}.url = ${a}`))}return r===e.servers?e:{...e,servers:r}});return{cfg:{...e,workspaces:n},changes:t}},X=(e,t,n)=>{let i=r(e,t,{encoding:`utf8`,input:n?.input});return{status:i.status??(i.error?127:1),stdout:i.stdout??``,stderr:i.stderr??``}},Pe=()=>process.platform===`darwin`,Z=()=>process.env.USER||``,Fe=(e,t=X)=>{let n=t(`gh`,[`auth`,`token`,`-u`,e]),r=n.stdout.trim();return n.status===0&&r?r:null},Ie=(e=X)=>{let t=e(`gh`,[`auth`,`status`]);return(t.stdout+t.stderr).trim()},Le=(e=X)=>{let t=[];for(let n of Ie(e).matchAll(/account (\S+) \(/g))t.includes(n[1])||t.push(n[1]);return t},Re=(e,t=X)=>{let n=t(`git`,[`config`,`--global`,e]),r=n.stdout.trim();return n.status===0&&r?r:null},Q=(e,t=X)=>{let n=t(`security`,[`find-generic-password`,`-a`,Z(),`-s`,e,`-w`]);return n.status===0&&n.stdout.trim().length>0},ze=(e,t,n=X)=>{let r=n(`security`,[`add-generic-password`,`-U`,`-a`,Z(),`-s`,e,`-w`,t]);if(r.status!==0)throw Error(`security add-generic-password failed: ${r.stderr.trim()||`unknown error`}`)},Be=e=>`security add-generic-password -U -a "${Z()||`$USER`}" -s ${e} -w 'xoxp-...'`,$=(e,t=X)=>{let n=t(`git`,[`config`,`--file`,e,`user.email`]);return n.status===0?n.stdout.trim():null},Ve=e=>{let t=[],n=e?.mcpServers;if(!n||typeof n!=`object`)return t;for(let[e,r]of Object.entries(n)){let n=Array.isArray(r?.args)?r.args:[];if(n.some(e=>typeof e==`string`&&e.endsWith(`@latest`)))t.push(e);else if(r?.command===`npx`){let r=n.find(e=>typeof e==`string`&&!e.startsWith(`-`));r&&!r.includes(`@`)&&t.push(e)}}return t},He=(e,n=process.cwd())=>{let r=t.resolve(n),i,a=-1;for(let n of e.workspaces){let e=c(n.path);(r===e||r.startsWith(e+t.sep))&&e.length>a&&(i=n,a=e.length)}return i},Ue=(e=X)=>{let t=e(`gh`,[`api`,`user`,`--jq`,`.login`]),n=e(`git`,[`config`,`user.email`]);return{pwd:process.cwd(),gh:t.status===0&&t.stdout.trim()?t.stdout.trim():`none`,gitEmail:n.status===0?n.stdout.trim():`none`,tokenSet:!!process.env.GITHUB_TOKEN}},We=(n,r=X)=>{let i=[];Pe()||i.push({status:`warn`,label:`platform`,detail:`inscope's secret resolution targets macOS (gh keyring + Keychain)`});let a=process.env.SHELL??``;a&&!/(^|\/)zsh$/.test(a)&&i.push({status:`warn`,label:`shell`,detail:`login shell is ${t.basename(a)}; inscope targets zsh (the hook is written to ~/.zshrc)`});let o=d(),s=h(o);s===null?i.push({status:`fail`,label:`hook`,detail:`missing ${o}; run \`inscope init\``}):s===L(n)?i.push({status:`ok`,label:`hook`,detail:o}):i.push({status:`warn`,label:`hook`,detail:"out of date; run `inscope apply`"}),i.push(Oe()?{status:`ok`,label:`zshrc`,detail:`sources the hook`}:{status:`warn`,label:`zshrc`,detail:"does not source the hook; run `inscope init`"}),n.workspaces.some(M)&&i.push(A(p(),j)===null?{status:`fail`,label:`gitconfig`,detail:"missing includeIf block; run `inscope apply`"}:{status:`ok`,label:`gitconfig`,detail:`includeIf block present`});for(let t of n.workspaces){let n=`[${t.name}]`;if(t.gh&&i.push(Fe(t.gh,r)?{status:`ok`,label:`${n} gh`,detail:`token for ${t.gh}`}:{status:`fail`,label:`${n} gh`,detail:`no token for ${t.gh}; run \`gh auth login\``}),t.servers.slack){let e=t.servers.slack.keychain;i.push(Q(e,r)?{status:`ok`,label:`${n} slack`,detail:e}:{status:`fail`,label:`${n} slack`,detail:`${e} not in keychain; run \`${Be(e)}\``})}if(M(t)){let a=N(t.name);if(!e.existsSync(a))i.push({status:`fail`,label:`${n} git`,detail:`missing ${a}; run \`inscope apply\``});else if(t.git?.email){let e=$(a,r);i.push(e===t.git.email?{status:`ok`,label:`${n} git`,detail:t.git.email}:{status:`fail`,label:`${n} git`,detail:`email is ${e??`unset`}, expected ${t.git.email}`})}}let a=Y(t);if(a)i.push({status:`fail`,label:`${n} mcp`,detail:a});else{let e=G(t);if(e===null)i.push({status:`warn`,label:`${n} mcp`,detail:"no .mcp.json; run `inscope apply`"});else{let r=V(t.name).filter(t=>e.mcpServers?.[t]);i.push({status:`ok`,label:`${n} mcp`,detail:`${r.length} server(s)`}),h(H(t))!==J(t)&&i.push({status:`warn`,label:`${n} mcp`,detail:"out of date; run `inscope diff`"});let a=Ve(e);a.length&&i.push({status:`warn`,label:`${n} mcp`,detail:`unpinned: ${a.join(`, `)}`})}}}return i};export{ee as CONFIG_VERSION,R as SLACK_MCP_VERSION,v as WORKSPACE_NAME_RE,Ne as adoptable,ke as applyAll,I as applyGitconfig,Se as applyMcp,je as computeDrift,ne as configExists,C as configVersionError,He as currentWorkspace,te as defaultConfig,X as defaultRunner,Me as diffLines,De as ensureZshrcSource,E as findWorkspace,Le as ghAccounts,Ie as ghStatus,Fe as ghToken,$ as gitEmailForFile,Re as gitGlobal,S as gitValueError,b as hookValueError,Pe as isMacOS,Q as keychainHas,ze as keychainSet,Be as keychainSetCommand,oe as labelFromPath,Ue as liveSnapshot,re as loadConfig,V as managedKeys,Y as mcpError,H as mcpFilePath,J as mcpTarget,se as pathConflict,G as readMcp,Ce as removeMcp,le as removeWorkspace,P as renderGitInclude,L as renderHook,ye as renderMcp,F as renderPerWorkspaceGitconfig,U as renderServers,Ee as renderZshrcSource,We as runDoctor,ie as saveConfig,w as slugify,ce as upsertWorkspace,T as validateConfig,y as workspaceNameError,x as workspacePathError,Oe as zshrcSourcesHook};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "inscope",
3
- "version": "0.8.2",
3
+ "version": "0.8.4",
4
4
  "description": "Per-workspace identity for Claude Code: scope MCP servers, GitHub auth, and git commit identity to the directory you are in.",
5
5
  "keywords": [
6
6
  "chpwd",