inscope 0.5.2 โ 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -4
- package/dist/bin/index.mjs +39 -35
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -7,12 +7,12 @@
|
|
|
7
7
|
[](https://www.npmjs.com/package/inscope)
|
|
8
8
|
[](https://github.com/nrjdalal/inscope)
|
|
9
9
|
|
|
10
|
-
๐ **The why behind the design:** [Race-Free Identity in Claude Code](https://zerostarter.dev/blog/mcp-per-workspace)
|
|
10
|
+
๐ **The why behind the design:** [Race-Free Identity in Claude Code](https://zerostarter.dev/blog/mcp-per-workspace) aka multiple gh, linear, notion, slack and other accounts.
|
|
11
11
|
|
|
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/
|
|
15
|
+
<img src="https://raw.githubusercontent.com/nrjdalal/demo-kit/main/inscope/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/
|
|
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" />
|
|
67
67
|
</p>
|
|
68
68
|
|
|
69
69
|
---
|
|
@@ -138,7 +138,7 @@ inscope doctor Verify tokens, identities, and the hook resolve correctly
|
|
|
138
138
|
Run any command with `-h` for its options.
|
|
139
139
|
|
|
140
140
|
<p align="center">
|
|
141
|
-
<img src="https://raw.githubusercontent.com/nrjdalal/
|
|
141
|
+
<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" />
|
|
142
142
|
</p>
|
|
143
143
|
|
|
144
144
|
### `inscope add`
|
|
@@ -208,6 +208,10 @@ inscope add ~/acme --gh neeraj-acme-org --servers github,slack --seed-slack
|
|
|
208
208
|
|
|
209
209
|
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.
|
|
210
210
|
|
|
211
|
+
<p align="center">
|
|
212
|
+
<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" />
|
|
213
|
+
</p>
|
|
214
|
+
|
|
211
215
|
---
|
|
212
216
|
|
|
213
217
|
## ๐ Config File
|
package/dist/bin/index.mjs
CHANGED
|
@@ -2,19 +2,19 @@
|
|
|
2
2
|
import{parseArgs as e}from"node:util";import t from"node:fs";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=()=>t.existsSync(f()),y=()=>{let e=f(),n=t.readFileSync(e,`utf8`),r=JSON.parse(n);return x(r),r},b=e=>{let r=f();t.mkdirSync(n.dirname(r),{recursive:!0}),t.writeFileSync(r,JSON.stringify(e,null,2)+`
|
|
3
3
|
`)},x=e=>{if(!e||typeof e!=`object`)throw Error(`config is not an object`);if(!Array.isArray(e.workspaces))throw Error(`config.workspaces must be an array`);let t=new Set;for(let n of e.workspaces){if(!n.name)throw Error(`a workspace is missing a name`);if(!n.path)throw Error(`workspace "${n.name}" is missing a path`);if(t.has(n.name))throw Error(`duplicate workspace name "${n.name}"`);t.add(n.name)}},S=e=>n.basename(u(e)),C=(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)},w=(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}},ee=(e,t)=>{let n=C(e,t);return n?{cfg:{...e,workspaces:e.workspaces.filter(e=>e.name!==n.name)},removed:n}:{cfg:e}},te={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/`},T=[`github`,`atlassian`,`canva`,`clickup`,`hubspot`,`intercom`,`linear`,`monday`,`notion`,`plane`,`sentry`,`slack`,`stripe`,`vercel`,`webflow`],E=e=>T.map(t=>`${t}-${e}`),D=e=>n.join(u(e.path),`.mcp.json`),ne=(e,t)=>e&&typeof e==`object`&&e.url?e.url:t,re=e=>{let t=e.servers,n={};for(let r of T){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:ne(i,te[r])}}return n},ie=e=>{if(!t.existsSync(e))return{};try{return JSON.parse(t.readFileSync(e,`utf8`))}catch{return{}}},O=e=>{if(!t.existsSync(e))return{};try{return JSON.parse(t.readFileSync(e,`utf8`))}catch{throw Error(`${e} is not valid JSON; fix or remove it, then re-run inscope (left it untouched)`)}},ae=e=>{let n=D(e);return t.existsSync(n)?ie(n):null},oe=e=>{let r=D(e);t.mkdirSync(n.dirname(r),{recursive:!0});let i=O(r),a=i.mcpServers&&typeof i.mcpServers==`object`?{...i.mcpServers}:{};for(let t of E(e.name))delete a[t];Object.assign(a,re(e)),i.mcpServers=a,t.writeFileSync(r,JSON.stringify(i,null,2)+`
|
|
4
4
|
`)},se=e=>{let n=D(e);if(!t.existsSync(n))return;let r=O(n);if(r.mcpServers&&typeof r.mcpServers==`object`)for(let t of E(e.name))delete r.mcpServers[t];t.writeFileSync(n,JSON.stringify(r,null,2)+`
|
|
5
|
-
`)},k=(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??``}},ce=()=>process.platform===`darwin`,A=()=>process.env.USER||``,le=(e,t=k)=>{let n=t(`gh`,[`auth`,`token`,`-u`,e]),r=n.stdout.trim();return n.status===0&&r?r:null},ue=(e=k)=>{let t=e(`gh`,[`auth`,`status`]);return(t.stdout+t.stderr).trim()},de=(e=k)=>{let t=[];for(let n of ue(e).matchAll(/account (\S+) \(/g))t.includes(n[1])||t.push(n[1]);return t},fe=(e,t=k)=>{let n=t(`git`,[`config`,`--global`,e]),r=n.stdout.trim();return n.status===0&&r?r:null},j=(e,t=k)=>{let n=t(`security`,[`find-generic-password`,`-a`,A(),`-s`,e,`-w`]);return n.status===0&&n.stdout.trim().length>0},pe=(e,t,n=k)=>{let r=n(`security`,[`add-generic-password`,`-U`,`-a`,A(),`-s`,e,`-w`,t]);if(r.status!==0)throw Error(`security add-generic-password failed: ${r.stderr.trim()||`unknown error`}`)},me=e=>`security add-generic-password -U -a "${A()||`$USER`}" -s ${e} -w 'xoxp-...'`,he=(e,t=k)=>{let n=t(`git`,[`config`,`--file`,e,`user.email`]);return n.status===0?n.stdout.trim():null},M=()=>!!(process.stdin.isTTY&&process.stdout.isTTY),N=e=>{let t=process.stdin;t.isTTY&&typeof t.setRawMode==`function`&&t.setRawMode(e)};let
|
|
6
|
-
`);if(e<0)return!1;let n=
|
|
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=
|
|
8
|
-
`),n.close(),t(e.trim())})}),
|
|
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?
|
|
10
|
-
`),process.exit(130))};process.stdin.on(`keypress`,l)}),
|
|
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?
|
|
12
|
-
`),process.exit(130))};process.stdin.on(`keypress`,u)}),
|
|
13
|
-
|
|
14
|
-
`).replace(/^\n+/,``);t.writeFileSync(e,i)},
|
|
15
|
-
`),
|
|
5
|
+
`)},k=(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??``}},ce=()=>process.platform===`darwin`,A=()=>process.env.USER||``,le=(e,t=k)=>{let n=t(`gh`,[`auth`,`token`,`-u`,e]),r=n.stdout.trim();return n.status===0&&r?r:null},ue=(e=k)=>{let t=e(`gh`,[`auth`,`status`]);return(t.stdout+t.stderr).trim()},de=(e=k)=>{let t=[];for(let n of ue(e).matchAll(/account (\S+) \(/g))t.includes(n[1])||t.push(n[1]);return t},fe=(e,t=k)=>{let n=t(`git`,[`config`,`--global`,e]),r=n.stdout.trim();return n.status===0&&r?r:null},j=(e,t=k)=>{let n=t(`security`,[`find-generic-password`,`-a`,A(),`-s`,e,`-w`]);return n.status===0&&n.stdout.trim().length>0},pe=(e,t,n=k)=>{let r=n(`security`,[`add-generic-password`,`-U`,`-a`,A(),`-s`,e,`-w`,t]);if(r.status!==0)throw Error(`security add-generic-password failed: ${r.stderr.trim()||`unknown error`}`)},me=e=>`security add-generic-password -U -a "${A()||`$USER`}" -s ${e} -w 'xoxp-...'`,he=(e,t=k)=>{let n=t(`git`,[`config`,`--file`,e,`user.email`]);return n.status===0?n.stdout.trim():null},M=()=>!!(process.stdin.isTTY&&process.stdout.isTTY),ge=(e,t=e)=>process.stdout.isTTY?`\x1b]8;;${e}\x07${t}\x1b]8;;\x07`:e,N=e=>process.stdout.isTTY?`\x1b[38;5;208m${e}\x1b[0m`:e,P=e=>{let t=process.stdin;t.isTTY&&typeof t.setRawMode==`function`&&t.setRawMode(e)};let F=``;const I=e=>new Promise(t=>{process.stdout.write(e);let n=()=>{let e=F.indexOf(`
|
|
6
|
+
`);if(e<0)return!1;let n=F.slice(0,e).replace(/\r$/,``);return F=F.slice(e+1),t(n),!0};if(n())return;let r=e=>{F+=e.toString(`utf8`),F.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=F.replace(/\r$/,``);F=``,t(e)};process.stdin.on(`data`,r),process.stdin.on(`end`,i),process.stdin.resume()}),L=async(e,t=``)=>(await I(`${e}${t?` [${t}]`:``}: `)).trim()||t,R=async(e,t=!1)=>{if(!M()){let n=(await I(`${e} [${t?`Y/n`:`y/N`}]: `)).trim().toLowerCase();return n?n===`y`||n===`yes`:t}return V(e,[{label:`Yes`,value:!0},{label:`No`,value:!1}],t?0:1)},_e=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())})}),z=`\x1B[36m`,B=`\x1B[0m`,V=(e,t,n=0)=>new Promise(r=>{if(!M()||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?z+r+B:r}\n`)}};s(!0),a.emitKeypressEvents(process.stdin),P(!0),process.stdin.resume();let c=()=>{process.stdin.off(`keypress`,l),P(!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)}),H=(e,t)=>new Promise(n=>{let r=t.map(e=>!!e.checked),i=()=>t.filter((e,t)=>r[t]).map(e=>e.value);if(!M()||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?z+i+B:i}\n`)}};c(!0),a.emitKeypressEvents(process.stdin),P(!0),process.stdin.resume();let l=()=>{process.stdin.off(`keypress`,u),P(!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)}),U=e=>`# >>> inscope:${e} >>>`,W=e=>`# <<< inscope:${e} <<<`,G=e=>e.replace(/[.*+?^${}()|[\]\\]/g,`\\$&`),ve=e=>RegExp(`${G(U(e))}\\n[\\s\\S]*?\\n${G(W(e))}\\n?`),ye=(e,t)=>{let n=t.replace(/\n+$/,``);return`${U(e)}\n${n}\n${W(e)}\n`},K=e=>{try{return t.readFileSync(e,`utf8`)}catch{return``}},be=(e,r,i)=>{t.mkdirSync(n.dirname(e),{recursive:!0});let a=K(e),o=ye(r,i),s=ve(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}t.writeFileSync(e,c)},xe=(e,n)=>{let r=K(e);if(!r)return;let i=r.replace(ve(n),``).replace(/\n{3,}/g,`
|
|
13
|
+
|
|
14
|
+
`).replace(/^\n+/,``);t.writeFileSync(e,i)},Se=(e,t)=>{let n=K(e).match(RegExp(`${G(U(t))}\\n([\\s\\S]*?)\\n${G(W(t))}`));return n?n[1]:null},q=`gitconfig`,J=e=>!!(e.git&&(e.git.email||e.git.name)),Y=e=>n.join(m(),`${e}.gitconfig`),Ce=e=>l(e).replace(/\/+$/,``)+`/`,we=e=>e.workspaces.filter(J).map(e=>`[includeIf "gitdir:${Ce(e.path)}"]\n\tpath = ${l(Y(e.name))}`).join(`
|
|
15
|
+
`),Te=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
16
|
`)+`
|
|
17
|
-
`},
|
|
17
|
+
`},Ee=e=>{t.mkdirSync(m(),{recursive:!0});for(let n of e.workspaces)J(n)&&t.writeFileSync(Y(n.name),Te(n));let n=we(e);n?be(h(),q,n):xe(h(),q)},De=e=>{let n=Y(e);t.existsSync(n)&&t.rmSync(n)},Oe=e=>{let t=l(e);return t===`~`?`"$HOME/"*`:t.startsWith(`~/`)?`"$HOME/${t.slice(2)}/"*`:`"${t}/"*`},ke=e=>e.servers.slack?e.servers.slack.keychain:``,Ae=e=>{let t=[...e.workspaces].sort((e,t)=>e.name.localeCompare(t.name));return`# Managed by inscope. Do not edit by hand.
|
|
18
18
|
# Source of truth: ~/.config/inscope/inscope.json
|
|
19
19
|
# Edit there, then run \`inscope apply\` to regenerate this file.
|
|
20
20
|
#
|
|
@@ -25,7 +25,7 @@ import{parseArgs as e}from"node:util";import t from"node:fs";import n from"node:
|
|
|
25
25
|
__inscope_resolve_identity() {
|
|
26
26
|
local ws
|
|
27
27
|
case "\${PWD}/" in
|
|
28
|
-
${t.map(e=>` ${
|
|
28
|
+
${t.map(e=>` ${Oe(e.path)}) ws=${e.name} ;;`).join(`
|
|
29
29
|
`)||` # no workspaces configured`}
|
|
30
30
|
*) ws="" ;;
|
|
31
31
|
esac
|
|
@@ -35,7 +35,7 @@ ${t.map(e=>` ${Ee(e.path)}) ws=${e.name} ;;`).join(`
|
|
|
35
35
|
|
|
36
36
|
local gh_user="" slack_svc=""
|
|
37
37
|
case "$ws" in
|
|
38
|
-
${t.map(e=>{let t=[];e.gh&&t.push(`gh_user=${e.gh}`);let n=
|
|
38
|
+
${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
39
|
`)||` # no workspaces configured`}
|
|
40
40
|
*) return ;; # outside a mapped workspace: nothing set
|
|
41
41
|
esac
|
|
@@ -61,12 +61,13 @@ autoload -Uz add-zsh-hook
|
|
|
61
61
|
add-zsh-hook chpwd __inscope_resolve_identity
|
|
62
62
|
__inscope_ws="__init__" # force the first resolve, clearing any inherited token
|
|
63
63
|
__inscope_resolve_identity
|
|
64
|
-
`},
|
|
64
|
+
`},je=e=>{let t=o();return e===t?`$HOME`:e.startsWith(t+n.sep)?`$HOME/${e.slice(t.length+1)}`:e},Me=()=>{let e=je(p());return`[ -r "${e}" ] && source "${e}"`},Ne=e=>{let t=Me();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`},Pe=()=>{let e=g(),n=``;try{n=t.readFileSync(e,`utf8`)}catch{}let r=Ne(n);r!==n&&t.writeFileSync(e,r)},Fe=()=>{try{return t.readFileSync(g(),`utf8`).includes(Me())}catch{return!1}},X=e=>{let r=p();t.mkdirSync(n.dirname(r),{recursive:!0}),t.writeFileSync(r,Ae(e)),Ee(e),Pe();let i=[];for(let t of e.workspaces)oe(t),i.push(D(t));return{hook:r,gitconfig:e.workspaces.some(e=>e.git?.email||e.git?.name),mcp:i}},Ie=T,Le=e=>`SLACK_MCP_XOXP_TOKEN_${e.toUpperCase().replace(/[^A-Z0-9]+/g,`_`)}`,Re=e=>T.filter(t=>!!e[t]),ze=(e,t)=>{let n={};for(let r of T)n[r]=r===`slack`?t?{keychain:t.keychain,addMessageTool:t.addMessageTool}:!1:e.includes(r);return n},Be=e=>{let t=w(v()?y():_(),e);b(t),X(t)},Ve=async(e,t)=>{if(!e.servers.slack)return;let n=e.servers.slack.keychain;if(t){let e=await _e(`Paste the Slack xoxp token for ${n}: `);e?(pe(n,e),console.log(`\nโ stored ${n} in the macOS keychain`)):console.error(`
|
|
65
|
+
No token entered; skipped keychain write.`)}else j(n)||console.log(`\nSlack token not in the keychain yet. Store it once with:\n${N(me(n))}\n\nSetup guide: ${N(ge(`https://github.com/korotovsky/slack-mcp-server/blob/HEAD/docs/01-authentication-setup.md#option-2-using-slack_mcp_xoxp_token-user-oauth`))}`)};var Z=`inscope`,He=`0.6.0`,Q={name:`Neeraj Dalal`,email:`admin@nrjdalal.com`,url:`https://nrjdalal.com`};const Ue=`Map a directory to a GitHub account, git email, and MCP servers.
|
|
65
66
|
Runs interactively in a terminal; pass flags or -y to skip the prompts. Re-running
|
|
66
67
|
with the same path or label updates that workspace.
|
|
67
68
|
|
|
68
69
|
Usage:
|
|
69
|
-
$ ${
|
|
70
|
+
$ ${Z} add [path] [options]
|
|
70
71
|
|
|
71
72
|
Options:
|
|
72
73
|
--gh <account> gh account whose token this workspace uses
|
|
@@ -82,65 +83,68 @@ Options:
|
|
|
82
83
|
--slack-message allow the Slack MCP server to post messages
|
|
83
84
|
--seed-slack prompt for the Slack token and store it in the keychain
|
|
84
85
|
-y, --yes accept defaults, skip all prompts (non-interactive)
|
|
85
|
-
-h, --help Display help message`,
|
|
86
|
+
-h, --help Display help message`,We=T.map(e=>({label:e,value:e,checked:e===`github`})),Ge=async t=>{let{positionals:n,values:r}=e({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:t});r.help&&(console.log(Ue),process.exit(0));let i=M()&&!r.yes;i&&console.log();let a=n[0];if(!a)if(i)a=await L(`Workspace directory`,process.cwd());else throw Error(Ue);let o=r.label||S(a);i&&!r.label&&(o=await L(`Label`,o));let s=r.gh;s===void 0&&i&&(s=await V(`
|
|
87
|
+
GitHub account for this workspace`,[...de().map(e=>({label:e,value:e})),{label:`(none)`,value:``}])||void 0);let c=r.email,u=r[`git-name`];if(i){if(c===void 0){let e=fe(`user.email`);c=await L(`Git email${e?` [${e} ยท global]`:``} (enter to inherit global)`)||void 0}if(u===void 0){let e=fe(`user.name`);u=await L(`Git name${e?` [${e} ยท global]`:``} (enter to inherit global)`)||void 0}}let d;d=r.servers===void 0?i?await H(`MCP servers (space toggles, enter confirms)`,We):[`github`]:r.servers.split(`,`).map(e=>e.trim()).filter(Boolean);let f=d.includes(`slack`)||!!r[`slack-keychain`]||!!r[`seed-slack`],p=r[`slack-keychain`]||Le(o),m=!!r[`slack-message`],h=!!r[`seed-slack`];f&&i&&(console.log(`
|
|
88
|
+
Slack uses a user OAuth (xoxp) token.`),r[`slack-keychain`]||(p=await L(`Slack keychain service`,p)),r[`slack-message`]||(m=await R(`Allow Slack to post messages?`,!0)),r[`seed-slack`]||(h=await R(`Store the Slack token now?`,!0)));let g={name:o,path:l(a),gh:s,git:c||u?{email:c,name:u}:void 0,servers:ze(d,f?{keychain:p,addMessageTool:m}:null)};Be(g),console.log(`\nโ workspace "${o}" -> ${g.path}`),console.log(`โ regenerated the hook, git includes, and ${g.path}/.mcp.json`),await Ve(g,h),console.log(`\nLaunch \`claude\` from ${g.path} (or relaunch) to pick up the new identity.`),process.exit(0)},Ke=`Regenerate the chpwd hook, git includes, and every .mcp.json
|
|
86
89
|
from your config. Idempotent: run it any time the config changes.
|
|
87
90
|
|
|
88
91
|
Usage:
|
|
89
|
-
$ ${
|
|
92
|
+
$ ${Z} apply
|
|
90
93
|
|
|
91
94
|
Options:
|
|
92
|
-
-h, --help Display help message`,
|
|
95
|
+
-h, --help Display help message`,qe=t=>{let{values:n}=e({allowPositionals:!0,options:{help:{type:`boolean`,short:`h`}},args:t});n.help&&(console.log(Ke),process.exit(0)),v()||(console.error(`No config found. Run \`${Z} init\` first.`),process.exit(1));let r=y(),i=X(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)},Je=e=>{try{return t.readFileSync(e,`utf8`)}catch{return null}},Ye=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`&&/@latest$/.test(e)))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},Xe=(e,t=process.cwd())=>{let r=n.resolve(t);return e.workspaces.find(e=>{let t=u(e.path);return r===t||r.startsWith(t+n.sep)})},Ze=(e=k)=>{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}},Qe=(e,n=k)=>{let r=[];ce()||r.push({status:`warn`,label:`platform`,detail:`inscope's secret resolution targets macOS (gh keyring + Keychain)`});let i=p(),a=Je(i);a===null?r.push({status:`fail`,label:`hook`,detail:`missing ${i}; run \`inscope init\``}):a===Ae(e)?r.push({status:`ok`,label:`hook`,detail:i}):r.push({status:`warn`,label:`hook`,detail:"out of date; run `inscope apply`"}),r.push(Fe()?{status:`ok`,label:`zshrc`,detail:`sources the hook`}:{status:`warn`,label:`zshrc`,detail:"does not source the hook; run `inscope init`"}),e.workspaces.some(J)&&r.push(Se(h(),q)===null?{status:`fail`,label:`gitconfig`,detail:"missing includeIf block; run `inscope apply`"}:{status:`ok`,label:`gitconfig`,detail:`includeIf block present`});for(let i of e.workspaces){let e=`[${i.name}]`;if(i.gh&&r.push(le(i.gh,n)?{status:`ok`,label:`${e} gh`,detail:`token for ${i.gh}`}:{status:`fail`,label:`${e} gh`,detail:`no token for ${i.gh}; run \`gh auth login\``}),i.servers.slack){let t=i.servers.slack.keychain;r.push(j(t,n)?{status:`ok`,label:`${e} slack`,detail:t}:{status:`fail`,label:`${e} slack`,detail:`${t} not in keychain; run \`${me(t)}\``})}if(J(i)){let a=Y(i.name);if(!t.existsSync(a))r.push({status:`fail`,label:`${e} git`,detail:`missing ${a}; run \`inscope apply\``});else if(i.git?.email){let t=he(a,n);r.push(t===i.git.email?{status:`ok`,label:`${e} git`,detail:i.git.email}:{status:`fail`,label:`${e} git`,detail:`email is ${t??`unset`}, expected ${i.git.email}`})}}let a=ae(i);if(a===null)r.push({status:`warn`,label:`${e} mcp`,detail:"no .mcp.json; run `inscope apply`"});else{let t=E(i.name).filter(e=>a.mcpServers?.[e]);r.push({status:`ok`,label:`${e} mcp`,detail:`${t.length} server(s)`});let n=Ye(a);n.length&&r.push({status:`warn`,label:`${e} mcp`,detail:`unpinned: ${n.join(`, `)}`})}}return r},$e=`Verify the setup: gh tokens resolve, keychain entries exist,
|
|
93
96
|
git emails match per path, the hook is current, and no MCP server is unpinned.
|
|
94
97
|
Exits non-zero if any check fails.
|
|
95
98
|
|
|
96
99
|
Usage:
|
|
97
|
-
$ ${
|
|
100
|
+
$ ${Z} doctor
|
|
98
101
|
|
|
99
102
|
Options:
|
|
100
|
-
-h, --help Display help message`,
|
|
101
|
-
|
|
103
|
+
-h, --help Display help message`,et={ok:`โ`,warn:`!`,fail:`โ`},tt=t=>{let{values:n}=e({allowPositionals:!0,options:{help:{type:`boolean`,short:`h`}},args:t});n.help&&(console.log($e),process.exit(0)),v()||(console.error(`No config found. Run \`${Z} init\` first.`),process.exit(1));let r=y(),i=Qe(r),a=i.map(e=>`${et[e.status]} ${e.label}${e.detail?` ${e.detail}`:``}`).join(`
|
|
104
|
+
`);console.log(`\n${a}`);let o=Xe(r);if(o){let e=Ze();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${s} check(s) failed.`),process.exit(1)),console.log(`
|
|
105
|
+
All checks passed.`),process.exit(0)},nt=`Edit a configured workspace interactively, then re-apply.
|
|
102
106
|
Pick a workspace (or pass its path/label), step through the prompts pre-filled
|
|
103
107
|
with its current values, and inscope regenerates everything on save.
|
|
104
108
|
|
|
105
109
|
Usage:
|
|
106
|
-
$ ${
|
|
110
|
+
$ ${Z} edit [path|label]
|
|
107
111
|
|
|
108
112
|
Options:
|
|
109
|
-
-h, --help Display help message`,
|
|
113
|
+
-h, --help Display help message`,rt=async t=>{let{positionals:n,values:r}=e({allowPositionals:!0,options:{help:{type:`boolean`,short:`h`}},args:t});r.help&&(console.log(nt),process.exit(0)),v()||(console.error(`No config found. Run \`${Z} init\` first.`),process.exit(1));let i=y();i.workspaces.length||(console.error(`No workspaces yet. Add one with \`${Z} add <path>\`.`),process.exit(1));let a=n[0],o=await(async()=>{if(a){let e=C(i,a);return e||(console.error(`No workspace matching "${a}".`),process.exit(1)),e}if(i.workspaces.length===1)return i.workspaces[0];if(M())return V(`Edit which workspace?`,i.workspaces.map(e=>({label:`${e.name} (${e.path})`,value:e})));console.error(`Specify a workspace, e.g. \`${Z} edit <label>\`.`),process.exit(1)})();console.log(`\nEditing "${o.name}" (${o.path})\n`);let s=[...de().map(e=>({label:e,value:e})),{label:`(none)`,value:``}],c=await V(`GitHub account`,s,Math.max(0,s.findIndex(e=>e.value===(o.gh??``))))||void 0,l=o.git?.email,u=await L(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 L(f?`Git name (enter keeps ${f}, "-" to inherit global)`:`Git name (enter to inherit global)`,f??``),m=p===`-`?void 0:p||void 0,h=Re(o.servers),g=await H(`MCP servers (space toggles, enter confirms)`,Ie.map(e=>({label:e,value:e,checked:h.includes(e)}))),_=g.includes(`slack`),b=o.servers.slack?o.servers.slack.keychain:Le(o.name),x=o.servers.slack?!!o.servers.slack.addMessageTool:!1,S=!1;_&&(console.log(`
|
|
114
|
+
Slack uses a user OAuth (xoxp) token.`),b=await L(`Slack keychain service`,b),x=await R(`Allow Slack to post messages?`,x),j(b)||(S=await R(`Store the Slack token now?`,!0)));let w={name:o.name,path:o.path,gh:c,git:d||m?{email:d,name:m}:void 0,servers:ze(g,_?{keychain:b,addMessageTool:x}:null)};Be(w),console.log(`\nโ updated "${w.name}" -> ${w.path}`),await Ve(w,S),console.log(`\nRelaunch \`claude\` from ${w.path} to pick up the changes.`),process.exit(0)},it=`Set up inscope: create the config, generate the chpwd hook, and
|
|
110
115
|
source it from ~/.zshrc. Safe to run again; it never overwrites your config.
|
|
111
116
|
|
|
112
117
|
Usage:
|
|
113
|
-
$ ${
|
|
118
|
+
$ ${Z} init
|
|
114
119
|
|
|
115
120
|
Options:
|
|
116
|
-
-h, --help Display help message`,
|
|
121
|
+
-h, --help Display help message`,at=t=>{let{values:n}=e({allowPositionals:!0,options:{help:{type:`boolean`,short:`h`}},args:t});n.help&&(console.log(it),process.exit(0));let r;v()?(r=y(),console.log(`\nUsing existing config at ${f()}`)):(r=_(),b(r),console.log(`\nCreated ${f()}`)),X(r),console.log(`Generated the chpwd hook and added a source line to ~/.zshrc.`),console.log(`
|
|
117
122
|
Next steps:
|
|
118
123
|
1. Reload your shell: source ~/.zshrc (or open a new terminal)
|
|
119
|
-
2.
|
|
120
|
-
3. Map a workspace: ${Q} add ~/acme --gh acme --email you@acme.com`),process.exit(0)},it=`List the configured workspaces. Run \`${Q} doctor\` to verify
|
|
124
|
+
2. Map a workspace: ${Z} add ~/acme`),process.exit(0)},ot=`List the configured workspaces. Run \`${Z} doctor\` to verify
|
|
121
125
|
that their tokens and identities actually resolve.
|
|
122
126
|
|
|
123
127
|
Usage:
|
|
124
|
-
$ ${
|
|
128
|
+
$ ${Z} list
|
|
125
129
|
|
|
126
130
|
Options:
|
|
127
|
-
-h, --help Display help message`,
|
|
131
|
+
-h, --help Display help message`,st=e=>T.filter(t=>!!e[t]).join(`, `)||`none`,ct=t=>{let{values:n}=e({allowPositionals:!0,options:{help:{type:`boolean`,short:`h`}},args:t});n.help&&(console.log(ot),process.exit(0)),v()||(console.error(`No config found. Run \`${Z} init\` first.`),process.exit(1));let r=y();r.workspaces.length||(console.log(`No workspaces yet. Add one with \`${Z} 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 ${st(e.servers)}`),e.servers.slack&&console.log(` slack keychain: ${e.servers.slack.keychain}`);process.exit(0)},lt=`Remove a workspace mapping. Drops its git include and the MCP
|
|
128
132
|
servers inscope manages; leaves your keychain and gh accounts untouched. Pick a
|
|
129
133
|
workspace, or pass its path/label.
|
|
130
134
|
|
|
131
135
|
Usage:
|
|
132
|
-
$ ${
|
|
136
|
+
$ ${Z} rm [path|label]
|
|
133
137
|
|
|
134
138
|
Options:
|
|
135
139
|
-y, --yes Skip the type-the-label confirmation
|
|
136
|
-
-h, --help Display help message`,
|
|
137
|
-
${
|
|
140
|
+
-h, --help Display help message`,ut=async t=>{let{positionals:n,values:r}=e({allowPositionals:!0,options:{help:{type:`boolean`,short:`h`},yes:{type:`boolean`,short:`y`}},args:t});r.help&&(console.log(lt),process.exit(0)),v()||(console.error(`No config found. Run \`${Z} 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=C(i,a);e||(console.error(`No workspace matching "${a}".`),process.exit(1)),o=e}else M()?o=await V(`Remove which workspace?`,i.workspaces.map(e=>({label:`${e.name} (${e.path})`,value:e}))):(console.error(`Specify a workspace, e.g. \`${Z} rm <label>\`.`),process.exit(1));if(!r.yes){console.log(`\nโ Removing "${o.name}" (${o.path}) unmaps it from inscope.`);let e=await L(`Type "${o.name}" to confirm`);e!==o.name&&(console.error(`Aborted: "${e}" does not match "${o.name}".`),process.exit(1))}let{cfg:s}=ee(i,o.name);se(o),De(o.name),b(s),X(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: ${N(`security delete-generic-password -s ${o.servers.slack.keychain}`)}`),process.exit(0)},$=`Version:
|
|
141
|
+
${Z}@${He}
|
|
138
142
|
|
|
139
143
|
Per-workspace identity for Claude Code: scope MCP servers, GitHub auth, and git
|
|
140
144
|
commit identity to the directory you are in, so concurrent sessions never clash.
|
|
141
145
|
|
|
142
146
|
Usage:
|
|
143
|
-
$ ${
|
|
147
|
+
$ ${Z} <command> [options]
|
|
144
148
|
|
|
145
149
|
Commands:
|
|
146
150
|
init Create the config, generate the hook, source it from ~/.zshrc
|
|
@@ -156,4 +160,4 @@ Options:
|
|
|
156
160
|
-h, --help Display help
|
|
157
161
|
|
|
158
162
|
Author:
|
|
159
|
-
${
|
|
163
|
+
${Q.name} <${Q.email}> (${Q.url})`;(async()=>{try{let e=process.argv.slice(2),t=e[0],n=e.slice(1);switch(t){case`init`:return at(n);case`add`:return await Ge(n);case`edit`:return rt(n);case`rm`:case`remove`:return await ut(n);case`ls`:case`list`:return ct(n);case`apply`:case`sync`:return qe(n);case`doctor`:return tt(n)}(t===`-v`||t===`--version`)&&(console.log(`${Z}@${He}`),process.exit(0)),(!t||t===`-h`||t===`--help`)&&(console.log($),process.exit(0)),console.error(`unknown command: ${e.join(` `)}\n`),console.error($),process.exit(1)}catch(e){console.error(e.message),process.exit(1)}})();export{};
|
package/package.json
CHANGED