inscope 0.4.0 โ 0.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +48 -51
- package/dist/bin/index.mjs +16 -15
- package/dist/index.d.mts +7 -0
- package/dist/index.mjs +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -45,29 +45,19 @@ Nothing sensitive is written to disk. GitHub tokens come from the `gh` keyring a
|
|
|
45
45
|
# set up the config + hook, and source it from ~/.zshrc
|
|
46
46
|
inscope init
|
|
47
47
|
|
|
48
|
-
# map a workspace
|
|
48
|
+
# map a workspace โ inscope prompts for the gh account, git identity, and servers
|
|
49
49
|
inscope add ~/acme
|
|
50
|
+
inscope add ~/personal
|
|
50
51
|
|
|
51
|
-
#
|
|
52
|
-
inscope add ~/acme --gh neeraj-acme-org --email neeraj@acme.org --servers github,linear
|
|
53
|
-
|
|
54
|
-
# map a personal directory: just your gh account and personal email
|
|
55
|
-
inscope add ~/personal --gh nrjdalal --email hello@nrjdalal.com
|
|
56
|
-
|
|
57
|
-
# list what is configured
|
|
58
|
-
inscope list
|
|
59
|
-
|
|
60
|
-
# edit a workspace interactively (gh account, git identity, servers)
|
|
52
|
+
# edit a workspace interactively
|
|
61
53
|
inscope edit acme
|
|
62
54
|
|
|
63
|
-
#
|
|
55
|
+
# list what is configured, and verify everything resolves
|
|
56
|
+
inscope list
|
|
64
57
|
inscope doctor
|
|
65
58
|
|
|
66
|
-
#
|
|
67
|
-
inscope
|
|
68
|
-
|
|
69
|
-
# remove a workspace mapping
|
|
70
|
-
inscope rm ~/acme
|
|
59
|
+
# remove a workspace (asks you to type the label to confirm)
|
|
60
|
+
inscope rm acme
|
|
71
61
|
```
|
|
72
62
|
|
|
73
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.
|
|
@@ -83,7 +73,7 @@ inscope rm ~/acme
|
|
|
83
73
|
- ๐ชช Per-directory identity: GitHub token, git commit email, and MCP servers scoped to `$PWD`
|
|
84
74
|
- ๐งต Race-free across concurrent shells and Claude Code sessions, with no global toggles
|
|
85
75
|
- ๐ No secrets on disk: GitHub tokens from the `gh` keyring, Slack tokens from the macOS Keychain
|
|
86
|
-
- ๐ค
|
|
76
|
+
- ๐ค One `.mcp.json` per workspace with uniquely named servers โ GitHub plus OAuth connectors for Atlassian, Canva, ClickUp, HubSpot, Intercom, Linear, monday, Notion, Plane, Sentry, Slack, Stripe, Vercel and Webflow
|
|
87
77
|
- โ๏ธ Git `includeIf` rules so every commit lands with the right author email per path
|
|
88
78
|
- ๐ช A single zsh `chpwd` hook does all the resolution; nothing else touches your shell
|
|
89
79
|
- ๐ฉบ `inscope doctor` verifies tokens, identities, and the hook before you trust them
|
|
@@ -105,26 +95,29 @@ Install globally (the CLI manages your shell hook, so a global install is expect
|
|
|
105
95
|
npm i -g inscope
|
|
106
96
|
```
|
|
107
97
|
|
|
108
|
-
|
|
98
|
+
Prerequisite: sign each GitHub account into `gh` once with `gh auth login` (that's gh's own command, not inscope). inscope reads tokens from the accounts you've signed in.
|
|
109
99
|
|
|
110
100
|
```sh
|
|
111
|
-
#
|
|
101
|
+
# set up the config + hook, and source it from ~/.zshrc
|
|
112
102
|
inscope init
|
|
113
103
|
|
|
114
|
-
#
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
# 3. map your workspaces
|
|
118
|
-
inscope add ~/acme --gh neeraj-acme-org --email neeraj@acme.org --servers github,linear
|
|
119
|
-
inscope add ~/personal --gh nrjdalal --email hello@nrjdalal.com
|
|
104
|
+
# map a workspace โ inscope walks you through the gh account, git identity, and servers
|
|
105
|
+
inscope add ~/acme
|
|
106
|
+
inscope add ~/personal
|
|
120
107
|
|
|
121
|
-
#
|
|
108
|
+
# reload your shell, then verify
|
|
122
109
|
source ~/.zshrc
|
|
123
110
|
inscope doctor
|
|
124
111
|
```
|
|
125
112
|
|
|
126
113
|
Launch `claude` from inside a mapped directory (or relaunch) to pick up the identity. No toggles, and it holds up with several terminals open at once.
|
|
127
114
|
|
|
115
|
+
Prefer flags or CI? Every prompt has a flag, and `-y` skips them all:
|
|
116
|
+
|
|
117
|
+
```sh
|
|
118
|
+
inscope add ~/acme --gh <account> --email you@work.com --servers github,linear -y
|
|
119
|
+
```
|
|
120
|
+
|
|
128
121
|
---
|
|
129
122
|
|
|
130
123
|
## ๐ง Commands
|
|
@@ -153,18 +146,19 @@ Run any command with `-h` for its options.
|
|
|
153
146
|
Run it bare and it walks you through everything: pick the GitHub account from your signed-in `gh` accounts, accept your global git identity or set a per-workspace one, and toggle which MCP servers to enable. Pass any flag to skip its prompt, or `-y` to take the defaults non-interactively (for scripts and CI).
|
|
154
147
|
|
|
155
148
|
```
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
-
|
|
149
|
+
--gh <account> gh account whose token this workspace uses
|
|
150
|
+
--email <email> git commit email (omit to inherit your global identity)
|
|
151
|
+
--git-name <name> git commit author name (omit to inherit global)
|
|
152
|
+
--label <name> workspace name; defaults to the directory basename
|
|
153
|
+
--servers <list> comma-separated, any of: github, atlassian, canva,
|
|
154
|
+
clickup, hubspot, intercom, linear, monday, notion,
|
|
155
|
+
plane, sentry, slack, stripe, vercel, webflow
|
|
156
|
+
(default: github)
|
|
157
|
+
--slack-keychain <s> keychain service for the Slack token
|
|
158
|
+
(default: SLACK_MCP_XOXP_TOKEN_<LABEL> when slack is on)
|
|
159
|
+
--slack-message allow the Slack MCP server to post messages
|
|
160
|
+
--seed-slack prompt for the Slack token and store it in the keychain
|
|
161
|
+
-y, --yes accept defaults, skip all prompts (non-interactive)
|
|
168
162
|
```
|
|
169
163
|
|
|
170
164
|
---
|
|
@@ -186,16 +180,23 @@ Run it bare and it walks you through everything: pick the GitHub account from yo
|
|
|
186
180
|
|
|
187
181
|
Each enabled server is written into the workspace `.mcp.json` with a name suffixed by the workspace label (for example `github-acme`), so servers from different workspaces never collide.
|
|
188
182
|
|
|
189
|
-
| Server | Transport |
|
|
183
|
+
| Server | Transport | Auth |
|
|
190
184
|
| ----------- | --------- | ---------------------------------------------- |
|
|
191
185
|
| `github` | http | `GITHUB_TOKEN` from the active `gh` account |
|
|
192
|
-
| `atlassian` | http | OAuth
|
|
193
|
-
| `
|
|
194
|
-
| `
|
|
195
|
-
| `
|
|
196
|
-
| `
|
|
186
|
+
| `atlassian` | http | OAuth (Jira / Confluence) |
|
|
187
|
+
| `canva` | http | OAuth |
|
|
188
|
+
| `clickup` | http | OAuth |
|
|
189
|
+
| `hubspot` | http | OAuth |
|
|
190
|
+
| `intercom` | http | OAuth |
|
|
191
|
+
| `linear` | http | OAuth |
|
|
192
|
+
| `monday` | http | OAuth |
|
|
193
|
+
| `notion` | http | OAuth |
|
|
194
|
+
| `plane` | http | OAuth |
|
|
195
|
+
| `sentry` | http | OAuth |
|
|
197
196
|
| `slack` | stdio | `SLACK_MCP_XOXP_TOKEN` from the macOS Keychain |
|
|
198
|
-
| `
|
|
197
|
+
| `stripe` | http | OAuth |
|
|
198
|
+
| `vercel` | http | OAuth |
|
|
199
|
+
| `webflow` | http | OAuth |
|
|
199
200
|
|
|
200
201
|
Slack is opt-in. Enable it with `--servers ...,slack`, then store the token once:
|
|
201
202
|
|
|
@@ -224,16 +225,12 @@ The source of truth is `~/.config/inscope/inscope.json`:
|
|
|
224
225
|
"git": { "email": "neeraj@acme.org" },
|
|
225
226
|
"servers": {
|
|
226
227
|
"github": true,
|
|
227
|
-
"atlassian": false,
|
|
228
228
|
"linear": true,
|
|
229
|
-
"notion": false,
|
|
230
|
-
"plane": false,
|
|
231
|
-
"sentry": false,
|
|
232
229
|
"slack": {
|
|
233
230
|
"keychain": "SLACK_MCP_XOXP_TOKEN_ACME",
|
|
234
231
|
"addMessageTool": false,
|
|
235
232
|
},
|
|
236
|
-
|
|
233
|
+
// every other server (atlassian, canva, โฆ webflow) defaults to false
|
|
237
234
|
},
|
|
238
235
|
},
|
|
239
236
|
],
|
package/dist/bin/index.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
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
|
-
`)},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`,linear:`https://mcp.linear.app/mcp`,notion:`https://mcp.notion.com/mcp`,plane:`https://mcp.plane.so/http/mcp`,sentry:`https://mcp.sentry.dev/mcp`,vercel:`https://mcp.vercel.com
|
|
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
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 P=``;const F=e=>new Promise(t=>{process.stdout.write(e);let n=()=>{let e=P.indexOf(`
|
|
6
6
|
`);if(e<0)return!1;let n=P.slice(0,e).replace(/\r$/,``);return P=P.slice(e+1),t(n),!0};if(n())return;let r=e=>{P+=e.toString(`utf8`),P.includes(`
|
|
@@ -61,7 +61,7 @@ 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
|
-
`},ke=e=>{let t=o();return e===t?`$HOME`:e.startsWith(t+n.sep)?`$HOME/${e.slice(t.length+1)}`:e},Ae=()=>{let e=ke(p());return`[ -r "${e}" ] && source "${e}"`},je=e=>{let t=Ae();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`},Me=()=>{let e=g(),n=``;try{n=t.readFileSync(e,`utf8`)}catch{}let r=je(n);r!==n&&t.writeFileSync(e,r)},Ne=()=>{try{return t.readFileSync(g(),`utf8`).includes(Ae())}catch{return!1}},X=e=>{let r=p();t.mkdirSync(n.dirname(r),{recursive:!0}),t.writeFileSync(r,Oe(e)),we(e),Me();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}},Z=`https://github.com/korotovsky/slack-mcp-server/blob/HEAD/docs/01-authentication-setup.md#option-2-using-slack_mcp_xoxp_token-user-oauth`,Pe=T,Fe=e=>`SLACK_MCP_XOXP_TOKEN_${e.toUpperCase().replace(/[^A-Z0-9]+/g,`_`)}`,Ie=e=>T.filter(t=>!!e[t]),Le=(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},Re=e=>{let t=w(v()?y():_(),e);b(t),X(t)},ze=async(e,t)=>{if(!e.servers.slack)return;let n=e.servers.slack.keychain;if(t){let e=await ge(`Paste the Slack xoxp token for ${n}: `);e?(pe(n,e),console.log(`โ stored ${n} in the macOS keychain`)):console.error(`No token entered; skipped keychain write.`)}else j(n)||console.log(`\nSlack token not in the keychain yet. Create a Slack app (xoxp user OAuth):\n ${Z}\nthen store the token once with:\n ${me(n)}`)};var Q=`inscope`,Be=`0.
|
|
64
|
+
`},ke=e=>{let t=o();return e===t?`$HOME`:e.startsWith(t+n.sep)?`$HOME/${e.slice(t.length+1)}`:e},Ae=()=>{let e=ke(p());return`[ -r "${e}" ] && source "${e}"`},je=e=>{let t=Ae();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`},Me=()=>{let e=g(),n=``;try{n=t.readFileSync(e,`utf8`)}catch{}let r=je(n);r!==n&&t.writeFileSync(e,r)},Ne=()=>{try{return t.readFileSync(g(),`utf8`).includes(Ae())}catch{return!1}},X=e=>{let r=p();t.mkdirSync(n.dirname(r),{recursive:!0}),t.writeFileSync(r,Oe(e)),we(e),Me();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}},Z=`https://github.com/korotovsky/slack-mcp-server/blob/HEAD/docs/01-authentication-setup.md#option-2-using-slack_mcp_xoxp_token-user-oauth`,Pe=T,Fe=e=>`SLACK_MCP_XOXP_TOKEN_${e.toUpperCase().replace(/[^A-Z0-9]+/g,`_`)}`,Ie=e=>T.filter(t=>!!e[t]),Le=(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},Re=e=>{let t=w(v()?y():_(),e);b(t),X(t)},ze=async(e,t)=>{if(!e.servers.slack)return;let n=e.servers.slack.keychain;if(t){let e=await ge(`Paste the Slack xoxp token for ${n}: `);e?(pe(n,e),console.log(`โ stored ${n} in the macOS keychain`)):console.error(`No token entered; skipped keychain write.`)}else j(n)||console.log(`\nSlack token not in the keychain yet. Create a Slack app (xoxp user OAuth):\n ${Z}\nthen store the token once with:\n ${me(n)}`)};var Q=`inscope`,Be=`0.5.1`,$={name:`Neeraj Dalal`,email:`admin@nrjdalal.com`,url:`https://nrjdalal.com`};const Ve=`Map a directory to a GitHub account, git email, and MCP servers.
|
|
65
65
|
Runs interactively in a terminal; pass flags or -y to skip the prompts. Re-running
|
|
66
66
|
with the same path or label updates that workspace.
|
|
67
67
|
|
|
@@ -69,19 +69,20 @@ Usage:
|
|
|
69
69
|
$ ${Q} add [path] [options]
|
|
70
70
|
|
|
71
71
|
Options:
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
-
|
|
84
|
-
-
|
|
72
|
+
--gh <account> gh account whose token this workspace uses
|
|
73
|
+
--email <email> git commit email (omit to inherit your global identity)
|
|
74
|
+
--git-name <name> git commit author name (omit to inherit global)
|
|
75
|
+
--label <name> workspace name; defaults to the directory basename
|
|
76
|
+
--servers <list> comma-separated, any of: github, atlassian, canva,
|
|
77
|
+
clickup, hubspot, intercom, linear, monday, notion,
|
|
78
|
+
plane, sentry, slack, stripe, vercel, webflow
|
|
79
|
+
(default: github)
|
|
80
|
+
--slack-keychain <s> keychain service for the Slack token
|
|
81
|
+
(default: SLACK_MCP_XOXP_TOKEN_<LABEL> when slack is on)
|
|
82
|
+
--slack-message allow the Slack MCP server to post messages
|
|
83
|
+
--seed-slack prompt for the Slack token and store it in the keychain
|
|
84
|
+
-y, --yes accept defaults, skip all prompts (non-interactive)
|
|
85
|
+
-h, --help Display help message`,He=T.map(e=>({label:e,value:e,checked:e===`github`})),Ue=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(Ve),process.exit(0));let i=M()&&!r.yes,a=n[0];if(!a)if(i)a=await I(`Workspace directory`,process.cwd());else throw Error(Ve);let o=r.label||S(a);i&&!r.label&&(o=await I(`Label`,o));let s=r.gh;s===void 0&&i&&(s=await B(`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 I(`Git email${e?` [${e} ยท global]`:``} (enter to inherit global)`)||void 0}if(u===void 0){let e=fe(`user.name`);u=await I(`Git name${e?` [${e} ยท global]`:``} (enter to inherit global)`)||void 0}}let d;d=r.servers===void 0?i?await V(`MCP servers (space toggles, enter confirms)`,He):[`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`]||Fe(o),m=!!r[`slack-message`],h=!!r[`seed-slack`];f&&i&&(console.log(`\nSlack uses a user OAuth (xoxp) token. If you haven't created the app yet,\nfollow the setup guide:\n ${Z}`),r[`slack-keychain`]||(p=await I(`Slack keychain service`,p)),r[`slack-message`]||(m=await L(`Allow Slack to post messages?`,!0)),r[`seed-slack`]||(h=await L(`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:Le(d,f?{keychain:p,addMessageTool:m}:null)};Re(g),console.log(`\nโ workspace "${o}" -> ${g.path}`),console.log(`โ regenerated the hook, git includes, and ${g.path}/.mcp.json`),await ze(g,h),console.log(`\nLaunch \`claude\` from ${g.path} (or relaunch) to pick up the new identity.`),process.exit(0)},We=`Regenerate the chpwd hook, git includes, and every .mcp.json
|
|
85
86
|
from your config. Idempotent: run it any time the config changes.
|
|
86
87
|
|
|
87
88
|
Usage:
|
package/dist/index.d.mts
CHANGED
|
@@ -9,12 +9,19 @@ type HttpServer = {
|
|
|
9
9
|
type Servers = {
|
|
10
10
|
github?: boolean;
|
|
11
11
|
atlassian?: boolean | HttpServer;
|
|
12
|
+
canva?: boolean | HttpServer;
|
|
13
|
+
clickup?: boolean | HttpServer;
|
|
14
|
+
hubspot?: boolean | HttpServer;
|
|
15
|
+
intercom?: boolean | HttpServer;
|
|
12
16
|
linear?: boolean | HttpServer;
|
|
17
|
+
monday?: boolean | HttpServer;
|
|
13
18
|
notion?: boolean | HttpServer;
|
|
14
19
|
plane?: boolean | HttpServer;
|
|
15
20
|
sentry?: boolean | HttpServer;
|
|
16
21
|
slack?: SlackServer | false;
|
|
22
|
+
stripe?: boolean | HttpServer;
|
|
17
23
|
vercel?: boolean | HttpServer;
|
|
24
|
+
webflow?: boolean | HttpServer;
|
|
18
25
|
};
|
|
19
26
|
type Workspace = {
|
|
20
27
|
name: string;
|
package/dist/index.mjs
CHANGED
|
@@ -51,6 +51,6 @@ 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
|
-
`},j=`1.3.0`,pe={atlassian:`https://mcp.atlassian.com/v1/mcp`,linear:`https://mcp.linear.app/mcp`,notion:`https://mcp.notion.com/mcp`,plane:`https://mcp.plane.so/http/mcp`,sentry:`https://mcp.sentry.dev/mcp`,vercel:`https://mcp.vercel.com
|
|
54
|
+
`},j=`1.3.0`,pe={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/`},M=[`github`,`atlassian`,`canva`,`clickup`,`hubspot`,`intercom`,`linear`,`monday`,`notion`,`plane`,`sentry`,`slack`,`stripe`,`vercel`,`webflow`],N=e=>M.map(t=>`${t}-${e}`),P=e=>t.join(c(e.path),`.mcp.json`),me=(e,t)=>e&&typeof e==`object`&&e.url?e.url:t,F=e=>{let t=e.servers,n={};for(let r of M){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@${j}`,`--transport`,`stdio`],env:t}}else n[a]={type:`http`,url:me(i,pe[r])}}return n},I=e=>({mcpServers:F(e)}),L=t=>{if(!e.existsSync(t))return{};try{return JSON.parse(e.readFileSync(t,`utf8`))}catch{return{}}},R=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)`)}},z=t=>{let n=P(t);return e.existsSync(n)?L(n):null},B=n=>{let r=P(n);e.mkdirSync(t.dirname(r),{recursive:!0});let i=R(r),a=i.mcpServers&&typeof i.mcpServers==`object`?{...i.mcpServers}:{};for(let e of N(n.name))delete a[e];Object.assign(a,F(n)),i.mcpServers=a,e.writeFileSync(r,JSON.stringify(i,null,2)+`
|
|
55
55
|
`)},he=t=>{let n=P(t);if(!e.existsSync(n))return;let r=R(n);if(r.mcpServers&&typeof r.mcpServers==`object`)for(let e of N(t.name))delete r.mcpServers[e];e.writeFileSync(n,JSON.stringify(r,null,2)+`
|
|
56
56
|
`)},ge=e=>{let n=i();return e===n?`$HOME`:e.startsWith(n+t.sep)?`$HOME/${e.slice(n.length+1)}`:e},V=()=>{let e=ge(d());return`[ -r "${e}" ] && source "${e}"`},H=e=>{let t=V();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`},U=()=>{let t=m(),n=``;try{n=e.readFileSync(t,`utf8`)}catch{}let r=H(n);r!==n&&e.writeFileSync(t,r)},W=()=>{try{return e.readFileSync(m(),`utf8`).includes(V())}catch{return!1}},_e=n=>{let r=d();e.mkdirSync(t.dirname(r),{recursive:!0}),e.writeFileSync(r,A(n)),k(n),U();let i=[];for(let e of n.workspaces)B(e),i.push(P(e));return{hook:r,gitconfig:n.workspaces.some(e=>e.git?.email||e.git?.name),mcp:i}},G=(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??``}},K=()=>process.platform===`darwin`,q=()=>process.env.USER||``,J=(e,t=G)=>{let n=t(`gh`,[`auth`,`token`,`-u`,e]),r=n.stdout.trim();return n.status===0&&r?r:null},Y=(e=G)=>{let t=e(`gh`,[`auth`,`status`]);return(t.stdout+t.stderr).trim()},ve=(e=G)=>{let t=[];for(let n of Y(e).matchAll(/account (\S+) \(/g))t.includes(n[1])||t.push(n[1]);return t},ye=(e,t=G)=>{let n=t(`git`,[`config`,`--global`,e]),r=n.stdout.trim();return n.status===0&&r?r:null},X=(e,t=G)=>{let n=t(`security`,[`find-generic-password`,`-a`,q(),`-s`,e,`-w`]);return n.status===0&&n.stdout.trim().length>0},be=(e,t,n=G)=>{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`}`)},Z=e=>`security add-generic-password -U -a "${q()||`$USER`}" -s ${e} -w 'xoxp-...'`,Q=(e,t=G)=>{let n=t(`git`,[`config`,`--file`,e,`user.email`]);return n.status===0?n.stdout.trim():null},xe=t=>{try{return e.readFileSync(t,`utf8`)}catch{return null}},$=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},Se=(e,n=process.cwd())=>{let r=t.resolve(n);return e.workspaces.find(e=>{let n=c(e.path);return r===n||r.startsWith(n+t.sep)})},Ce=(e=G)=>{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=(t,n=G)=>{let r=[];K()||r.push({status:`warn`,label:`platform`,detail:`inscope's secret resolution targets macOS (gh keyring + Keychain)`});let i=d(),a=xe(i);a===null?r.push({status:`fail`,label:`hook`,detail:`missing ${i}; run \`inscope init\``}):a===A(t)?r.push({status:`ok`,label:`hook`,detail:i}):r.push({status:`warn`,label:`hook`,detail:"out of date; run `inscope apply`"}),r.push(W()?{status:`ok`,label:`zshrc`,detail:`sources the hook`}:{status:`warn`,label:`zshrc`,detail:"does not source the hook; run `inscope init`"}),t.workspaces.some(T)&&r.push(le(p(),w)===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(J(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(X(e,n)?{status:`ok`,label:`${t} slack`,detail:e}:{status:`fail`,label:`${t} slack`,detail:`${e} not in keychain; run \`${Z(e)}\``})}if(T(i)){let a=E(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=Q(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=z(i);if(a===null)r.push({status:`warn`,label:`${t} mcp`,detail:"no .mcp.json; run `inscope apply`"});else{let e=N(i.name).filter(e=>a.mcpServers?.[e]);r.push({status:`ok`,label:`${t} mcp`,detail:`${e.length} server(s)`});let n=$(a);n.length&&r.push({status:`warn`,label:`${t} mcp`,detail:`unpinned: ${n.join(`, `)}`})}}return r};export{ee as CONFIG_VERSION,j as SLACK_MCP_VERSION,_e as applyAll,k as applyGitconfig,B as applyMcp,g as configExists,Se as currentWorkspace,h as defaultConfig,G as defaultRunner,U as ensureZshrcSource,v as findWorkspace,ve as ghAccounts,Y as ghStatus,J as ghToken,Q as gitEmailForFile,ye as gitGlobal,K as isMacOS,X as keychainHas,be as keychainSet,Z as keychainSetCommand,re as labelFromPath,Ce as liveSnapshot,te as loadConfig,N as managedKeys,P as mcpFilePath,z as readMcp,he as removeMcp,ae as removeWorkspace,D as renderGitInclude,A as renderHook,I as renderMcp,O as renderPerWorkspaceGitconfig,F as renderServers,H as renderZshrcSource,we as runDoctor,ne as saveConfig,ie as upsertWorkspace,_ as validateConfig,W as zshrcSourcesHook};
|
package/package.json
CHANGED