inscope 0.3.2 โ†’ 0.4.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 CHANGED
@@ -10,7 +10,7 @@
10
10
  > #### `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.
11
11
 
12
12
  <p align="center">
13
- <img src="https://raw.githubusercontent.com/nrjdalal/inscope/main/.github/assets/demo.gif" alt="inscope demo: interactive add, list, and doctor" width="800" />
13
+ <img src="https://raw.githubusercontent.com/nrjdalal/inscope/main/.github/assets/demo.gif" alt="inscope demo: interactive add, list, and doctor" width="900" />
14
14
  </p>
15
15
 
16
16
  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:
@@ -48,11 +48,11 @@ inscope init
48
48
  # map a workspace interactively: pick the gh account, git identity, and servers
49
49
  inscope add ~/acme
50
50
 
51
- # or pass flags to skip the prompts (work gh account, work email, + slack)
52
- inscope add ~/acme --gh acme --email you@acme.com --servers github,linear,notion,slack
51
+ # or pass flags to skip the prompts (work gh account, work email, + servers)
52
+ inscope add ~/acme --gh neeraj-acme-org --email neeraj@acme.org --servers github,linear
53
53
 
54
54
  # map a personal directory: just your gh account and personal email
55
- inscope add ~/nrjdalal --gh nrjdalal --email you@personal.dev
55
+ inscope add ~/personal --gh nrjdalal --email hello@nrjdalal.com
56
56
 
57
57
  # list what is configured
58
58
  inscope list
@@ -70,10 +70,10 @@ inscope apply
70
70
  inscope rm ~/acme
71
71
  ```
72
72
 
73
- `cd ~/acme/api` and you are the work account, with work MCP servers and your work commit email. `cd ~/nrjdalal/blog` and you are you.
73
+ `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.
74
74
 
75
75
  <p align="center">
76
- <img src="https://raw.githubusercontent.com/nrjdalal/inscope/main/.github/assets/demo-switch.gif" alt="inscope switching git identity and tokens on cd" width="800" />
76
+ <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" />
77
77
  </p>
78
78
 
79
79
  ---
@@ -83,7 +83,7 @@ inscope rm ~/acme
83
83
  - ๐Ÿชช Per-directory identity: GitHub token, git commit email, and MCP servers scoped to `$PWD`
84
84
  - ๐Ÿงต Race-free across concurrent shells and Claude Code sessions, with no global toggles
85
85
  - ๐Ÿ” No secrets on disk: GitHub tokens from the `gh` keyring, Slack tokens from the macOS Keychain
86
- - ๐Ÿค– Generates a `.mcp.json` per workspace with uniquely named GitHub, Linear, Notion and Slack servers
86
+ - ๐Ÿค– Generates a `.mcp.json` per workspace with uniquely named GitHub, Atlassian, Linear, Notion, Plane, Sentry, Slack and Vercel servers
87
87
  - โœ‰๏ธ Git `includeIf` rules so every commit lands with the right author email per path
88
88
  - ๐Ÿช A single zsh `chpwd` hook does all the resolution; nothing else touches your shell
89
89
  - ๐Ÿฉบ `inscope doctor` verifies tokens, identities, and the hook before you trust them
@@ -115,8 +115,8 @@ inscope init
115
115
  gh auth login
116
116
 
117
117
  # 3. map your workspaces
118
- inscope add ~/acme --gh acme --email you@acme.com --servers github,linear,notion,slack
119
- inscope add ~/nrjdalal --gh nrjdalal --email you@personal.dev
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
120
120
 
121
121
  # 4. reload your shell, then verify
122
122
  source ~/.zshrc
@@ -145,7 +145,7 @@ inscope doctor Verify tokens, identities, and the hook resolve correctly
145
145
  Run any command with `-h` for its options.
146
146
 
147
147
  <p align="center">
148
- <img src="https://raw.githubusercontent.com/nrjdalal/inscope/main/.github/assets/demo-manage.gif" alt="inscope edit and rm with type-to-confirm" width="800" />
148
+ <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
149
  </p>
150
150
 
151
151
  ### `inscope add`
@@ -157,7 +157,8 @@ Run it bare and it walks you through everything: pick the GitHub account from yo
157
157
  --email <email> git commit email (omit to inherit your global identity)
158
158
  --git-name <name> git commit author name (omit to inherit global)
159
159
  --label <name> workspace name; defaults to the directory basename
160
- --servers <list> comma-separated: github,linear,notion,slack
160
+ --servers <list> comma-separated, any of: github, atlassian, linear,
161
+ notion, plane, sentry, slack, vercel
161
162
  (default: github)
162
163
  --slack-keychain <s> keychain service for the Slack token
163
164
  (default: SLACK_MCP_XOXP_TOKEN_<LABEL> when slack is on)
@@ -185,17 +186,21 @@ Run it bare and it walks you through everything: pick the GitHub account from yo
185
186
 
186
187
  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.
187
188
 
188
- | Server | Transport | Token source |
189
- | -------- | --------- | ---------------------------------------------- |
190
- | `github` | http | `GITHUB_TOKEN` from the active `gh` account |
191
- | `linear` | http | OAuth via the Linear MCP endpoint |
192
- | `notion` | http | OAuth via the Notion MCP endpoint |
193
- | `slack` | stdio | `SLACK_MCP_XOXP_TOKEN` from the macOS Keychain |
189
+ | Server | Transport | Token source |
190
+ | ----------- | --------- | ---------------------------------------------- |
191
+ | `github` | http | `GITHUB_TOKEN` from the active `gh` account |
192
+ | `atlassian` | http | OAuth via the Atlassian (Jira/Confluence) MCP |
193
+ | `linear` | http | OAuth via the Linear MCP endpoint |
194
+ | `notion` | http | OAuth via the Notion MCP endpoint |
195
+ | `plane` | http | OAuth via the Plane MCP endpoint |
196
+ | `sentry` | http | OAuth via the Sentry MCP endpoint |
197
+ | `slack` | stdio | `SLACK_MCP_XOXP_TOKEN` from the macOS Keychain |
198
+ | `vercel` | http | OAuth via the Vercel MCP endpoint |
194
199
 
195
200
  Slack is opt-in. Enable it with `--servers ...,slack`, then store the token once:
196
201
 
197
202
  ```sh
198
- inscope add ~/acme --gh acme --servers github,linear,notion,slack --seed-slack
203
+ inscope add ~/acme --gh neeraj-acme-org --servers github,slack --seed-slack
199
204
  ```
200
205
 
201
206
  `--seed-slack` prompts for the `xoxp` token and writes it to the Keychain. Pass `--slack-message` to allow the Slack MCP server to post messages.
@@ -215,16 +220,20 @@ The source of truth is `~/.config/inscope/inscope.json`:
215
220
  {
216
221
  "name": "acme",
217
222
  "path": "~/acme",
218
- "gh": "acme",
219
- "git": { "email": "you@acme.com" },
223
+ "gh": "neeraj-acme-org",
224
+ "git": { "email": "neeraj@acme.org" },
220
225
  "servers": {
221
226
  "github": true,
227
+ "atlassian": false,
222
228
  "linear": true,
223
- "notion": true,
229
+ "notion": false,
230
+ "plane": false,
231
+ "sentry": false,
224
232
  "slack": {
225
233
  "keychain": "SLACK_MCP_XOXP_TOKEN_ACME",
226
234
  "addMessageTool": false,
227
235
  },
236
+ "vercel": false,
228
237
  },
229
238
  },
230
239
  ],
@@ -1,18 +1,20 @@
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}},T=(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??``}},te=()=>process.platform===`darwin`,E=()=>process.env.USER||``,ne=(e,t=T)=>{let n=t(`gh`,[`auth`,`token`,`-u`,e]),r=n.stdout.trim();return n.status===0&&r?r:null},re=(e=T)=>{let t=e(`gh`,[`auth`,`status`]);return(t.stdout+t.stderr).trim()},ie=(e=T)=>{let t=[];for(let n of re(e).matchAll(/account (\S+) \(/g))t.includes(n[1])||t.push(n[1]);return t},D=(e,t=T)=>{let n=t(`git`,[`config`,`--global`,e]),r=n.stdout.trim();return n.status===0&&r?r:null},O=(e,t=T)=>{let n=t(`security`,[`find-generic-password`,`-a`,E(),`-s`,e,`-w`]);return n.status===0&&n.stdout.trim().length>0},ae=(e,t,n=T)=>{let r=n(`security`,[`add-generic-password`,`-U`,`-a`,E(),`-s`,e,`-w`,t]);if(r.status!==0)throw Error(`security add-generic-password failed: ${r.stderr.trim()||`unknown error`}`)},oe=e=>`security add-generic-password -U -a "${E()||`$USER`}" -s ${e} -w 'xoxp-...'`,se=(e,t=T)=>{let n=t(`git`,[`config`,`--file`,e,`user.email`]);return n.status===0?n.stdout.trim():null},k=()=>!!(process.stdin.isTTY&&process.stdout.isTTY),A=e=>{let t=process.stdin;t.isTTY&&typeof t.setRawMode==`function`&&t.setRawMode(e)};let j=``;const M=e=>new Promise(t=>{process.stdout.write(e);let n=()=>{let e=j.indexOf(`
4
- `);if(e<0)return!1;let n=j.slice(0,e).replace(/\r$/,``);return j=j.slice(e+1),t(n),!0};if(n())return;let r=e=>{j+=e.toString(`utf8`),j.includes(`
5
- `)&&(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=j.replace(/\r$/,``);j=``,t(e)};process.stdin.on(`data`,r),process.stdin.on(`end`,i),process.stdin.resume()}),N=async(e,t=``)=>(await M(`${e}${t?` [${t}]`:``}: `)).trim()||t,P=async(e,t=!1)=>{let n=(await M(`${e} [${t?`Y/n`:`y/N`}]: `)).trim().toLowerCase();return n?n===`y`||n===`yes`:t},ce=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(`
6
- `),n.close(),t(e.trim())})}),le=`\x1B[36m`,ue=`\x1B[0m`,F=(e,t,n=0)=>new Promise(r=>{if(!k()||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+`
7
- `);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?le+r+ue:r}\n`)}};s(!0),a.emitKeypressEvents(process.stdin),A(!0),process.stdin.resume();let c=()=>{process.stdin.off(`keypress`,l),A(!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(`
8
- `),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(!k()||t.length===0){n(i());return}let o=0,s=process.stdout;s.write(e+`
9
- `);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?le+i+ue:i}\n`)}};c(!0),a.emitKeypressEvents(process.stdin),A(!0),process.stdin.resume();let l=()=>{process.stdin.off(`keypress`,u),A(!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(`
10
- `),process.exit(130))};process.stdin.on(`keypress`,u)}),I=e=>`# >>> inscope:${e} >>>`,L=e=>`# <<< inscope:${e} <<<`,R=e=>e.replace(/[.*+?^${}()|[\]\\]/g,`\\$&`),fe=e=>RegExp(`${R(I(e))}\\n[\\s\\S]*?\\n${R(L(e))}\\n?`),pe=(e,t)=>{let n=t.replace(/\n+$/,``);return`${I(e)}\n${n}\n${L(e)}\n`},z=e=>{try{return t.readFileSync(e,`utf8`)}catch{return``}},me=(e,r,i)=>{t.mkdirSync(n.dirname(e),{recursive:!0});let a=z(e),o=pe(r,i),s=fe(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)},he=(e,n)=>{let r=z(e);if(!r)return;let i=r.replace(fe(n),``).replace(/\n{3,}/g,`
11
-
12
- `).replace(/^\n+/,``);t.writeFileSync(e,i)},ge=(e,t)=>{let n=z(e).match(RegExp(`${R(I(t))}\\n([\\s\\S]*?)\\n${R(L(t))}`));return n?n[1]:null},B=`gitconfig`,V=e=>!!(e.git&&(e.git.email||e.git.name)),H=e=>n.join(m(),`${e}.gitconfig`),_e=e=>l(e).replace(/\/+$/,``)+`/`,ve=e=>e.workspaces.filter(V).map(e=>`[includeIf "gitdir:${_e(e.path)}"]\n\tpath = ${l(H(e.name))}`).join(`
13
- `),ye=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(`
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`},T=[`github`,`atlassian`,`linear`,`notion`,`plane`,`sentry`,`slack`,`vercel`],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
+ `)},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 P=``;const F=e=>new Promise(t=>{process.stdout.write(e);let n=()=>{let e=P.indexOf(`
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(`
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=P.replace(/\r$/,``);P=``,t(e)};process.stdin.on(`data`,r),process.stdin.on(`end`,i),process.stdin.resume()}),I=async(e,t=``)=>(await F(`${e}${t?` [${t}]`:``}: `)).trim()||t,L=async(e,t=!1)=>{let n=(await F(`${e} [${t?`Y/n`:`y/N`}]: `)).trim().toLowerCase();return n?n===`y`||n===`yes`:t},ge=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())})}),R=`\x1B[36m`,z=`\x1B[0m`,B=(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?R+r+z:r}\n`)}};s(!0),a.emitKeypressEvents(process.stdin),N(!0),process.stdin.resume();let c=()=>{process.stdin.off(`keypress`,l),N(!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)}),V=(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?R+i+z:i}\n`)}};c(!0),a.emitKeypressEvents(process.stdin),N(!0),process.stdin.resume();let l=()=>{process.stdin.off(`keypress`,u),N(!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)}),H=e=>`# >>> inscope:${e} >>>`,U=e=>`# <<< inscope:${e} <<<`,W=e=>e.replace(/[.*+?^${}()|[\]\\]/g,`\\$&`),G=e=>RegExp(`${W(H(e))}\\n[\\s\\S]*?\\n${W(U(e))}\\n?`),_e=(e,t)=>{let n=t.replace(/\n+$/,``);return`${H(e)}\n${n}\n${U(e)}\n`},K=e=>{try{return t.readFileSync(e,`utf8`)}catch{return``}},ve=(e,r,i)=>{t.mkdirSync(n.dirname(e),{recursive:!0});let a=K(e),o=_e(r,i),s=G(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)},ye=(e,n)=>{let r=K(e);if(!r)return;let i=r.replace(G(n),``).replace(/\n{3,}/g,`
13
+
14
+ `).replace(/^\n+/,``);t.writeFileSync(e,i)},be=(e,t)=>{let n=K(e).match(RegExp(`${W(H(t))}\\n([\\s\\S]*?)\\n${W(U(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`),xe=e=>l(e).replace(/\/+$/,``)+`/`,Se=e=>e.workspaces.filter(J).map(e=>`[includeIf "gitdir:${xe(e.path)}"]\n\tpath = ${l(Y(e.name))}`).join(`
15
+ `),Ce=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(`
14
16
  `)+`
15
- `},be=e=>{t.mkdirSync(m(),{recursive:!0});for(let n of e.workspaces)V(n)&&t.writeFileSync(H(n.name),ye(n));let n=ve(e);n?me(h(),B,n):he(h(),B)},xe=e=>{let n=H(e);t.existsSync(n)&&t.rmSync(n)},Se=e=>{let t=l(e);return t===`~`?`"$HOME/"*`:t.startsWith(`~/`)?`"$HOME/${t.slice(2)}/"*`:`"${t}/"*`},Ce=e=>e.servers.slack?e.servers.slack.keychain:``,U=e=>{let t=[...e.workspaces].sort((e,t)=>e.name.localeCompare(t.name));return`# Managed by inscope. Do not edit by hand.
17
+ `},we=e=>{t.mkdirSync(m(),{recursive:!0});for(let n of e.workspaces)J(n)&&t.writeFileSync(Y(n.name),Ce(n));let n=Se(e);n?ve(h(),q,n):ye(h(),q)},Te=e=>{let n=Y(e);t.existsSync(n)&&t.rmSync(n)},Ee=e=>{let t=l(e);return t===`~`?`"$HOME/"*`:t.startsWith(`~/`)?`"$HOME/${t.slice(2)}/"*`:`"${t}/"*`},De=e=>e.servers.slack?e.servers.slack.keychain:``,Oe=e=>{let t=[...e.workspaces].sort((e,t)=>e.name.localeCompare(t.name));return`# Managed by inscope. Do not edit by hand.
16
18
  # Source of truth: ~/.config/inscope/inscope.json
17
19
  # Edit there, then run \`inscope apply\` to regenerate this file.
18
20
  #
@@ -23,7 +25,7 @@ import{parseArgs as e}from"node:util";import t from"node:fs";import n from"node:
23
25
  __inscope_resolve_identity() {
24
26
  local ws
25
27
  case "\${PWD}/" in
26
- ${t.map(e=>` ${Se(e.path)}) ws=${e.name} ;;`).join(`
28
+ ${t.map(e=>` ${Ee(e.path)}) ws=${e.name} ;;`).join(`
27
29
  `)||` # no workspaces configured`}
28
30
  *) ws="" ;;
29
31
  esac
@@ -33,7 +35,7 @@ ${t.map(e=>` ${Se(e.path)}) ws=${e.name} ;;`).join(`
33
35
 
34
36
  local gh_user="" slack_svc=""
35
37
  case "$ws" in
36
- ${t.map(e=>{let t=[];e.gh&&t.push(`gh_user=${e.gh}`);let n=Ce(e);return n&&t.push(`slack_svc=${n}`),` ${e.name}) ${t.length?t.join(`; `):`:`} ;;`}).join(`
38
+ ${t.map(e=>{let t=[];e.gh&&t.push(`gh_user=${e.gh}`);let n=De(e);return n&&t.push(`slack_svc=${n}`),` ${e.name}) ${t.length?t.join(`; `):`:`} ;;`}).join(`
37
39
  `)||` # no workspaces configured`}
38
40
  *) return ;; # outside a mapped workspace: nothing set
39
41
  esac
@@ -59,87 +61,85 @@ autoload -Uz add-zsh-hook
59
61
  add-zsh-hook chpwd __inscope_resolve_identity
60
62
  __inscope_ws="__init__" # force the first resolve, clearing any inherited token
61
63
  __inscope_resolve_identity
62
- `},we=[`github`,`linear`,`notion`,`slack`],W=e=>we.map(t=>`${t}-${e}`),G=e=>n.join(u(e.path),`.mcp.json`),K=(e,t)=>e&&typeof e==`object`&&e.url?e.url:t,Te=e=>{let t=e.servers,n={};if(t.github&&(n[`github-${e.name}`]={type:`http`,url:`https://api.githubcopilot.com/mcp/`,headers:{Authorization:"Bearer ${GITHUB_TOKEN}"}}),t.linear&&(n[`linear-${e.name}`]={type:`http`,url:K(t.linear,`https://mcp.linear.app/mcp`)}),t.notion&&(n[`notion-${e.name}`]={type:`http`,url:K(t.notion,`https://mcp.notion.com/mcp`)}),t.slack){let r={SLACK_MCP_XOXP_TOKEN:"${SLACK_MCP_XOXP_TOKEN}"};t.slack.addMessageTool&&(r.SLACK_MCP_ADD_MESSAGE_TOOL=`true`),n[`slack-${e.name}`]={type:`stdio`,command:`npx`,args:[`-y`,`slack-mcp-server@1.3.0`,`--transport`,`stdio`],env:r}}return n},Ee=e=>{if(!t.existsSync(e))return{};try{return JSON.parse(t.readFileSync(e,`utf8`))}catch{return{}}},q=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)`)}},De=e=>{let n=G(e);return t.existsSync(n)?Ee(n):null},Oe=e=>{let r=G(e);t.mkdirSync(n.dirname(r),{recursive:!0});let i=q(r),a=i.mcpServers&&typeof i.mcpServers==`object`?{...i.mcpServers}:{};for(let t of W(e.name))delete a[t];Object.assign(a,Te(e)),i.mcpServers=a,t.writeFileSync(r,JSON.stringify(i,null,2)+`
63
- `)},ke=e=>{let n=G(e);if(!t.existsSync(n))return;let r=q(n);if(r.mcpServers&&typeof r.mcpServers==`object`)for(let t of W(e.name))delete r.mcpServers[t];t.writeFileSync(n,JSON.stringify(r,null,2)+`
64
- `)},Ae=e=>{let t=o();return e===t?`$HOME`:e.startsWith(t+n.sep)?`$HOME/${e.slice(t.length+1)}`:e},J=()=>{let e=Ae(p());return`[ -r "${e}" ] && source "${e}"`},je=e=>{let t=J();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(J())}catch{return!1}},Y=e=>{let r=p();t.mkdirSync(n.dirname(r),{recursive:!0}),t.writeFileSync(r,U(e)),be(e),Me();let i=[];for(let t of e.workspaces)Oe(t),i.push(G(t));return{hook:r,gitconfig:e.workspaces.some(e=>e.git?.email||e.git?.name),mcp:i}},X=`https://github.com/korotovsky/slack-mcp-server/blob/HEAD/docs/01-authentication-setup.md#option-2-using-slack_mcp_xoxp_token-user-oauth`,Pe=[`github`,`linear`,`notion`,`slack`],Fe=e=>`SLACK_MCP_XOXP_TOKEN_${e.toUpperCase().replace(/[^A-Z0-9]+/g,`_`)}`,Ie=e=>[e.github&&`github`,e.linear&&`linear`,e.notion&&`notion`,e.slack&&`slack`].filter(Boolean),Le=(e,t)=>({github:e.includes(`github`),linear:e.includes(`linear`),notion:e.includes(`notion`),slack:t?{keychain:t.keychain,addMessageTool:t.addMessageTool}:!1}),Re=e=>{let t=w(v()?y():_(),e);b(t),Y(t)},ze=async(e,t)=>{if(!e.servers.slack)return;let n=e.servers.slack.keychain;if(t){let e=await ce(`Paste the Slack xoxp token for ${n}: `);e?(ae(n,e),console.log(`โœ“ stored ${n} in the macOS keychain`)):console.error(`No token entered; skipped keychain write.`)}else O(n)||console.log(`\nSlack token not in the keychain yet. Create a Slack app (xoxp user OAuth):\n ${X}\nthen store the token once with:\n ${oe(n)}`)};var Z=`inscope`,Be=`0.3.2`,Q={name:`Neeraj Dalal`,email:`admin@nrjdalal.com`,url:`https://nrjdalal.com`};const $=`Map a directory to a GitHub account, git email, and MCP servers.
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.4.0`,$={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
 
68
68
  Usage:
69
- $ ${Z} add [path] [options]
69
+ $ ${Q} add [path] [options]
70
70
 
71
71
  Options:
72
72
  --gh <account> gh account whose token this workspace uses
73
73
  --email <email> git commit email (omit to inherit your global identity)
74
74
  --git-name <name> git commit author name (omit to inherit global)
75
75
  --label <name> workspace name; defaults to the directory basename
76
- --servers <list> comma-separated: github,linear,notion,slack
76
+ --servers <list> comma-separated, any of: github, atlassian, linear,
77
+ notion, plane, sentry, slack, vercel
77
78
  (default: github)
78
79
  --slack-keychain <s> keychain service for the Slack token
79
80
  (default: SLACK_MCP_XOXP_TOKEN_<LABEL> when slack is on)
80
81
  --slack-message allow the Slack MCP server to post messages
81
82
  --seed-slack prompt for the Slack token and store it in the keychain
82
83
  -y, --yes accept defaults, skip all prompts (non-interactive)
83
- -h, --help Display help message`,Ve=[{label:`github`,value:`github`,checked:!0},{label:`linear`,value:`linear`,checked:!1},{label:`notion`,value:`notion`,checked:!1},{label:`slack`,value:`slack`,checked:!1}],He=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($),process.exit(0));let i=k()&&!r.yes,a=n[0];if(!a)if(i)a=await N(`Workspace directory`,process.cwd());else throw Error($);let o=r.label||S(a);i&&!r.label&&(o=await N(`Label`,o));let s=r.gh;s===void 0&&i&&(s=await F(`GitHub account for this workspace`,[...ie().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=D(`user.email`);c=await N(`Git email${e?` [${e} ยท global]`:``} (enter to inherit global)`)||void 0}if(u===void 0){let e=D(`user.name`);u=await N(`Git name${e?` [${e} ยท global]`:``} (enter to inherit global)`)||void 0}}let d;d=r.servers===void 0?i?await de(`MCP servers (space toggles, enter confirms)`,Ve):[`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 ${X}`),r[`slack-keychain`]||(p=await N(`Slack keychain service`,p)),r[`slack-message`]||(m=await P(`Allow Slack to post messages?`,!0)),r[`seed-slack`]||(h=await P(`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)},Ue=`Regenerate the chpwd hook, git includes, and every .mcp.json
84
+ -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
84
85
  from your config. Idempotent: run it any time the config changes.
85
86
 
86
87
  Usage:
87
- $ ${Z} apply
88
+ $ ${Q} apply
88
89
 
89
90
  Options:
90
- -h, --help Display help message`,We=t=>{let{values:n}=e({allowPositionals:!0,options:{help:{type:`boolean`,short:`h`}},args:t});n.help&&(console.log(Ue),process.exit(0)),v()||(console.error(`No config found. Run \`${Z} init\` first.`),process.exit(1));let r=y(),i=Y(r);console.log(`โœ“ 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)},Ge=e=>{try{return t.readFileSync(e,`utf8`)}catch{return null}},Ke=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},qe=(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)})},Je=(e=T)=>{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}},Ye=(e,n=T)=>{let r=[];te()||r.push({status:`warn`,label:`platform`,detail:`inscope's secret resolution targets macOS (gh keyring + Keychain)`});let i=p(),a=Ge(i);a===null?r.push({status:`fail`,label:`hook`,detail:`missing ${i}; run \`inscope init\``}):a===U(e)?r.push({status:`ok`,label:`hook`,detail:i}):r.push({status:`warn`,label:`hook`,detail:"out of date; run `inscope apply`"}),r.push(Ne()?{status:`ok`,label:`zshrc`,detail:`sources the hook`}:{status:`warn`,label:`zshrc`,detail:"does not source the hook; run `inscope init`"}),e.workspaces.some(V)&&r.push(ge(h(),B)===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(ne(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(O(t,n)?{status:`ok`,label:`${e} slack`,detail:t}:{status:`fail`,label:`${e} slack`,detail:`${t} not in keychain; run \`${oe(t)}\``})}if(V(i)){let a=H(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=se(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=De(i);if(a===null)r.push({status:`warn`,label:`${e} mcp`,detail:"no .mcp.json; run `inscope apply`"});else{let t=W(i.name).filter(e=>a.mcpServers?.[e]);r.push({status:`ok`,label:`${e} mcp`,detail:`${t.length} server(s)`});let n=Ke(a);n.length&&r.push({status:`warn`,label:`${e} mcp`,detail:`unpinned: ${n.join(`, `)}`})}}return r},Xe=`Verify the setup: gh tokens resolve, keychain entries exist,
91
+ -h, --help Display help message`,Ge=t=>{let{values:n}=e({allowPositionals:!0,options:{help:{type:`boolean`,short:`h`}},args:t});n.help&&(console.log(We),process.exit(0)),v()||(console.error(`No config found. Run \`${Q} init\` first.`),process.exit(1));let r=y(),i=X(r);console.log(`โœ“ 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)},Ke=e=>{try{return t.readFileSync(e,`utf8`)}catch{return null}},qe=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},Je=(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)})},Ye=(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}},Xe=(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=Ke(i);a===null?r.push({status:`fail`,label:`hook`,detail:`missing ${i}; run \`inscope init\``}):a===Oe(e)?r.push({status:`ok`,label:`hook`,detail:i}):r.push({status:`warn`,label:`hook`,detail:"out of date; run `inscope apply`"}),r.push(Ne()?{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(be(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=qe(a);n.length&&r.push({status:`warn`,label:`${e} mcp`,detail:`unpinned: ${n.join(`, `)}`})}}return r},Ze=`Verify the setup: gh tokens resolve, keychain entries exist,
91
92
  git emails match per path, the hook is current, and no MCP server is unpinned.
92
93
  Exits non-zero if any check fails.
93
94
 
94
95
  Usage:
95
- $ ${Z} doctor
96
+ $ ${Q} doctor
96
97
 
97
98
  Options:
98
- -h, --help Display help message`,Ze={ok:`โœ“`,warn:`!`,fail:`โœ—`},Qe=t=>{let{values:n}=e({allowPositionals:!0,options:{help:{type:`boolean`,short:`h`}},args:t});n.help&&(console.log(Xe),process.exit(0)),v()||(console.error(`No config found. Run \`${Z} init\` first.`),process.exit(1));let r=y(),i=Ye(r);for(let e of i)console.log(`${Ze[e.status]} ${e.label}${e.detail?` ${e.detail}`:``}`);let a=qe(r);if(a){let e=Je();console.log(`\nThis shell (${a.name}):`),console.log(` pwd ${e.pwd}`),console.log(` gh ${e.gh}`),console.log(` git ${e.gitEmail}`),console.log(` token ${e.tokenSet?`set`:`unset`}`)}let o=i.filter(e=>e.status===`fail`).length;o&&(console.log(`\n${o} check(s) failed.`),process.exit(1)),console.log(`
99
- All checks passed.`),process.exit(0)},$e=`Edit a configured workspace interactively, then re-apply.
99
+ -h, --help Display help message`,Qe={ok:`โœ“`,warn:`!`,fail:`โœ—`},$e=t=>{let{values:n}=e({allowPositionals:!0,options:{help:{type:`boolean`,short:`h`}},args:t});n.help&&(console.log(Ze),process.exit(0)),v()||(console.error(`No config found. Run \`${Q} init\` first.`),process.exit(1));let r=y(),i=Xe(r);for(let e of i)console.log(`${Qe[e.status]} ${e.label}${e.detail?` ${e.detail}`:``}`);let a=Je(r);if(a){let e=Ye();console.log(`\nThis shell (${a.name}):`),console.log(` pwd ${e.pwd}`),console.log(` gh ${e.gh}`),console.log(` git ${e.gitEmail}`),console.log(` token ${e.tokenSet?`set`:`unset`}`)}let o=i.filter(e=>e.status===`fail`).length;o&&(console.log(`\n${o} check(s) failed.`),process.exit(1)),console.log(`
100
+ All checks passed.`),process.exit(0)},et=`Edit a configured workspace interactively, then re-apply.
100
101
  Pick a workspace (or pass its path/label), step through the prompts pre-filled
101
102
  with its current values, and inscope regenerates everything on save.
102
103
 
103
104
  Usage:
104
- $ ${Z} edit [path|label]
105
+ $ ${Q} edit [path|label]
105
106
 
106
107
  Options:
107
- -h, --help Display help message`,et=async t=>{let{positionals:n,values:r}=e({allowPositionals:!0,options:{help:{type:`boolean`,short:`h`}},args:t});r.help&&(console.log($e),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(k())return F(`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=[...ie().map(e=>({label:e,value:e})),{label:`(none)`,value:``}],c=await F(`GitHub account`,s,Math.max(0,s.findIndex(e=>e.value===(o.gh??``))))||void 0,l=o.git?.email,u=await N(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 N(f?`Git name (enter keeps ${f}, "-" to inherit global)`:`Git name (enter to inherit global)`,f??``),m=p===`-`?void 0:p||void 0,h=Ie(o.servers),g=await de(`MCP servers (space toggles, enter confirms)`,Pe.map(e=>({label:e,value:e,checked:h.includes(e)}))),_=g.includes(`slack`),b=o.servers.slack?o.servers.slack.keychain:Fe(o.name),x=o.servers.slack?!!o.servers.slack.addMessageTool:!1,S=!1;_&&(console.log(`\nSlack uses a user OAuth (xoxp) token. Setup guide:\n ${X}`),b=await N(`Slack keychain service`,b),x=await P(`Allow Slack to post messages?`,x),O(b)||(S=await P(`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:Le(g,_?{keychain:b,addMessageTool:x}:null)};Re(w),console.log(`\nโœ“ updated "${w.name}" -> ${w.path}`),await ze(w,S),console.log(`\nRelaunch \`claude\` from ${w.path} to pick up the changes.`),process.exit(0)},tt=`Set up inscope: create the config, generate the chpwd hook, and
108
+ -h, --help Display help message`,tt=async t=>{let{positionals:n,values:r}=e({allowPositionals:!0,options:{help:{type:`boolean`,short:`h`}},args:t});r.help&&(console.log(et),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=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 B(`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=[...de().map(e=>({label:e,value:e})),{label:`(none)`,value:``}],c=await B(`GitHub account`,s,Math.max(0,s.findIndex(e=>e.value===(o.gh??``))))||void 0,l=o.git?.email,u=await I(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 I(f?`Git name (enter keeps ${f}, "-" to inherit global)`:`Git name (enter to inherit global)`,f??``),m=p===`-`?void 0:p||void 0,h=Ie(o.servers),g=await V(`MCP servers (space toggles, enter confirms)`,Pe.map(e=>({label:e,value:e,checked:h.includes(e)}))),_=g.includes(`slack`),b=o.servers.slack?o.servers.slack.keychain:Fe(o.name),x=o.servers.slack?!!o.servers.slack.addMessageTool:!1,S=!1;_&&(console.log(`\nSlack uses a user OAuth (xoxp) token. Setup guide:\n ${Z}`),b=await I(`Slack keychain service`,b),x=await L(`Allow Slack to post messages?`,x),j(b)||(S=await L(`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:Le(g,_?{keychain:b,addMessageTool:x}:null)};Re(w),console.log(`\nโœ“ updated "${w.name}" -> ${w.path}`),await ze(w,S),console.log(`\nRelaunch \`claude\` from ${w.path} to pick up the changes.`),process.exit(0)},nt=`Set up inscope: create the config, generate the chpwd hook, and
108
109
  source it from ~/.zshrc. Safe to run again; it never overwrites your config.
109
110
 
110
111
  Usage:
111
- $ ${Z} init
112
+ $ ${Q} init
112
113
 
113
114
  Options:
114
- -h, --help Display help message`,nt=t=>{let{values:n}=e({allowPositionals:!0,options:{help:{type:`boolean`,short:`h`}},args:t});n.help&&(console.log(tt),process.exit(0));let r;v()?(r=y(),console.log(`Using existing config at ${f()}`)):(r=_(),b(r),console.log(`Created ${f()}`)),Y(r),console.log(`Generated the chpwd hook and added a source line to ~/.zshrc.`),console.log(`
115
+ -h, --help Display help message`,rt=t=>{let{values:n}=e({allowPositionals:!0,options:{help:{type:`boolean`,short:`h`}},args:t});n.help&&(console.log(nt),process.exit(0));let r;v()?(r=y(),console.log(`Using existing config at ${f()}`)):(r=_(),b(r),console.log(`Created ${f()}`)),X(r),console.log(`Generated the chpwd hook and added a source line to ~/.zshrc.`),console.log(`
115
116
  Next steps:
116
117
  1. Reload your shell: source ~/.zshrc (or open a new terminal)
117
118
  2. Sign each GitHub account in: gh auth login
118
- 3. Map a workspace: ${Z} add ~/acme --gh acme --email you@acme.com
119
- `),process.exit(0)},rt=`List the configured workspaces. Run \`${Z} doctor\` to verify
119
+ 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
120
120
  that their tokens and identities actually resolve.
121
121
 
122
122
  Usage:
123
- $ ${Z} list
123
+ $ ${Q} list
124
124
 
125
125
  Options:
126
- -h, --help Display help message`,it=e=>[e.github&&`github`,e.linear&&`linear`,e.notion&&`notion`,e.slack&&`slack`].filter(Boolean).join(`, `)||`none`,at=t=>{let{values:n}=e({allowPositionals:!0,options:{help:{type:`boolean`,short:`h`}},args:t});n.help&&(console.log(rt),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(`${e.name}`),console.log(` path ${e.path}`),console.log(` gh ${e.gh??`(none)`}`),console.log(` git ${e.git?.email??`(default)`}`),console.log(` servers ${it(e.servers)}`),e.servers.slack&&console.log(` slack keychain: ${e.servers.slack.keychain}`);process.exit(0)},ot=`Remove a workspace mapping. Drops its git include and the MCP
126
+ -h, --help Display help message`,at=e=>T.filter(t=>!!e[t]).join(`, `)||`none`,ot=t=>{let{values:n}=e({allowPositionals:!0,options:{help:{type:`boolean`,short:`h`}},args:t});n.help&&(console.log(it),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(`${e.name}`),console.log(` path ${e.path}`),console.log(` gh ${e.gh??`(none)`}`),console.log(` git ${e.git?.email??`(default)`}`),console.log(` servers ${at(e.servers)}`),e.servers.slack&&console.log(` slack keychain: ${e.servers.slack.keychain}`);process.exit(0)},st=`Remove a workspace mapping. Drops its git include and the MCP
127
127
  servers inscope manages; leaves your keychain and gh accounts untouched. Pick a
128
128
  workspace, or pass its path/label.
129
129
 
130
130
  Usage:
131
- $ ${Z} rm [path|label]
131
+ $ ${Q} rm [path|label]
132
132
 
133
133
  Options:
134
134
  -y, --yes Skip the type-the-label confirmation
135
- -h, --help Display help message`,st=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(ot),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 k()?o=await F(`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 N(`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);ke(o),xe(o.name),b(s),Y(s),console.log(`โœ“ removed workspace "${o.name}"`),o.servers.slack&&console.log(`Note: the keychain entry ${o.servers.slack.keychain} was left in place.\nDelete it with: security delete-generic-password -s ${o.servers.slack.keychain}`),process.exit(0)},ct=`Version:
136
- ${Z}@${Be}
135
+ -h, --help Display help message`,ct=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(st),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=C(i,a);e||(console.error(`No workspace matching "${a}".`),process.exit(1)),o=e}else M()?o=await B(`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 I(`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),Te(o.name),b(s),X(s),console.log(`โœ“ removed workspace "${o.name}"`),o.servers.slack&&console.log(`Note: the keychain entry ${o.servers.slack.keychain} was left in place.\nDelete it with: security delete-generic-password -s ${o.servers.slack.keychain}`),process.exit(0)},lt=`Version:
136
+ ${Q}@${Be}
137
137
 
138
138
  Per-workspace identity for Claude Code: scope MCP servers, GitHub auth, and git
139
139
  commit identity to the directory you are in, so concurrent sessions never clash.
140
140
 
141
141
  Usage:
142
- $ ${Z} <command> [options]
142
+ $ ${Q} <command> [options]
143
143
 
144
144
  Commands:
145
145
  init Create the config, generate the hook, source it from ~/.zshrc
@@ -155,4 +155,4 @@ Options:
155
155
  -h, --help Display help
156
156
 
157
157
  Author:
158
- ${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 nt(n);case`add`:return await He(n);case`edit`:return et(n);case`rm`:case`remove`:return await st(n);case`ls`:case`list`:return at(n);case`apply`:case`sync`:return We(n);case`doctor`:return Qe(n)}(t===`-v`||t===`--version`)&&(console.log(`${Z}@${Be}`),process.exit(0)),(!t||t===`-h`||t===`--help`)&&(console.log(ct),process.exit(0)),console.error(`unknown command: ${e.join(` `)}\n`),console.error(ct),process.exit(1)}catch(e){console.error(e.message),process.exit(1)}})();export{};
158
+ ${$.name} <${$.email}> (${$.url})`;(async()=>{try{let e=process.argv.slice(2),t=e[0],n=e.slice(1);switch(t){case`init`:return rt(n);case`add`:return await Ue(n);case`edit`:return tt(n);case`rm`:case`remove`:return await ct(n);case`ls`:case`list`:return ot(n);case`apply`:case`sync`:return Ge(n);case`doctor`:return $e(n)}(t===`-v`||t===`--version`)&&(console.log(`${Q}@${Be}`),process.exit(0)),(!t||t===`-h`||t===`--help`)&&(console.log(lt),process.exit(0)),console.error(`unknown command: ${e.join(` `)}\n`),console.error(lt),process.exit(1)}catch(e){console.error(e.message),process.exit(1)}})();export{};
package/dist/index.d.mts CHANGED
@@ -8,9 +8,13 @@ type HttpServer = {
8
8
  };
9
9
  type Servers = {
10
10
  github?: boolean;
11
+ atlassian?: boolean | HttpServer;
11
12
  linear?: boolean | HttpServer;
12
13
  notion?: boolean | HttpServer;
14
+ plane?: boolean | HttpServer;
15
+ sentry?: boolean | HttpServer;
13
16
  slack?: SlackServer | false;
17
+ vercel?: boolean | HttpServer;
14
18
  };
15
19
  type Workspace = {
16
20
  name: string;
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);return g(r),r},h=n=>{let r=u();e.mkdirSync(t.dirname(r),{recursive:!0}),e.writeFileSync(r,JSON.stringify(n,null,2)+`
2
- `)},g=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)}},ie=e=>t.basename(c(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)},ae=(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}},oe=(e,t)=>{let n=_(e,t);return n?{cfg:{...e,workspaces:e.workspaces.filter(e=>e.name!==n.name)},removed:n}:{cfg:e}},v=e=>`# >>> inscope:${e} >>>`,y=e=>`# <<< inscope:${e} <<<`,b=e=>e.replace(/[.*+?^${}()|[\]\\]/g,`\\$&`),x=e=>RegExp(`${b(v(e))}\\n[\\s\\S]*?\\n${b(y(e))}\\n?`),se=(e,t)=>{let n=t.replace(/\n+$/,``);return`${v(e)}\n${n}\n${y(e)}\n`},S=t=>{try{return e.readFileSync(t,`utf8`)}catch{return``}},C=(n,r,i)=>{e.mkdirSync(t.dirname(n),{recursive:!0});let a=S(n),o=se(r,i),s=x(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)},ce=(t,n)=>{let r=S(t);if(!r)return;let i=r.replace(x(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`),ee=1,h=()=>({version:1,workspaces:[]}),g=()=>e.existsSync(u()),te=()=>{let t=u(),n=e.readFileSync(t,`utf8`),r=JSON.parse(n);return _(r),r},ne=n=>{let r=u();e.mkdirSync(t.dirname(r),{recursive:!0}),e.writeFileSync(r,JSON.stringify(n,null,2)+`
2
+ `)},_=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)}},re=e=>t.basename(c(e)),v=(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)},ie=(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}},ae=(e,t)=>{let n=v(e,t);return n?{cfg:{...e,workspaces:e.workspaces.filter(e=>e.name!==n.name)},removed:n}:{cfg:e}},y=e=>`# >>> inscope:${e} >>>`,b=e=>`# <<< inscope:${e} <<<`,x=e=>e.replace(/[.*+?^${}()|[\]\\]/g,`\\$&`),S=e=>RegExp(`${x(y(e))}\\n[\\s\\S]*?\\n${x(b(e))}\\n?`),oe=(e,t)=>{let n=t.replace(/\n+$/,``);return`${y(e)}\n${n}\n${b(e)}\n`},C=t=>{try{return e.readFileSync(t,`utf8`)}catch{return``}},se=(n,r,i)=>{e.mkdirSync(t.dirname(n),{recursive:!0});let a=C(n),o=oe(r,i),s=S(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)},ce=(t,n)=>{let r=C(t);if(!r)return;let i=r.replace(S(n),``).replace(/\n{3,}/g,`
3
3
 
4
- `).replace(/^\n+/,``);e.writeFileSync(t,i)},le=(e,t)=>{let n=S(e).match(RegExp(`${b(v(t))}\\n([\\s\\S]*?)\\n${b(y(t))}`));return n?n[1]:null},w=`gitconfig`,T=e=>!!(e.git&&(e.git.email||e.git.name)),E=e=>t.join(f(),`${e}.gitconfig`),D=e=>s(e).replace(/\/+$/,``)+`/`,O=e=>e.workspaces.filter(T).map(e=>`[includeIf "gitdir:${D(e.path)}"]\n\tpath = ${s(E(e.name))}`).join(`
5
- `),k=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+/,``);e.writeFileSync(t,i)},le=(e,t)=>{let n=C(e).match(RegExp(`${x(y(t))}\\n([\\s\\S]*?)\\n${x(b(t))}`));return n?n[1]:null},w=`gitconfig`,T=e=>!!(e.git&&(e.git.email||e.git.name)),E=e=>t.join(f(),`${e}.gitconfig`),ue=e=>s(e).replace(/\/+$/,``)+`/`,D=e=>e.workspaces.filter(T).map(e=>`[includeIf "gitdir:${ue(e.path)}"]\n\tpath = ${s(E(e.name))}`).join(`
5
+ `),O=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
- `},A=t=>{e.mkdirSync(f(),{recursive:!0});for(let n of t.workspaces)T(n)&&e.writeFileSync(E(n.name),k(n));let n=O(t);n?C(p(),w,n):ce(p(),w)},ue=e=>{let t=s(e);return t===`~`?`"$HOME/"*`:t.startsWith(`~/`)?`"$HOME/${t.slice(2)}/"*`:`"${t}/"*`},de=e=>e.servers.slack?e.servers.slack.keychain:``,j=e=>{let t=[...e.workspaces].sort((e,t)=>e.name.localeCompare(t.name));return`# Managed by inscope. Do not edit by hand.
7
+ `},k=t=>{e.mkdirSync(f(),{recursive:!0});for(let n of t.workspaces)T(n)&&e.writeFileSync(E(n.name),O(n));let n=D(t);n?se(p(),w,n):ce(p(),w)},de=e=>{let t=s(e);return t===`~`?`"$HOME/"*`:t.startsWith(`~/`)?`"$HOME/${t.slice(2)}/"*`:`"${t}/"*`},fe=e=>e.servers.slack?e.servers.slack.keychain:``,A=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.map(e=>` ${ue(e.path)}) ws=${e.name} ;;`).join(`
18
+ ${t.map(e=>` ${de(e.path)}) ws=${e.name} ;;`).join(`
19
19
  `)||` # no workspaces configured`}
20
20
  *) ws="" ;;
21
21
  esac
@@ -25,7 +25,7 @@ ${t.map(e=>` ${ue(e.path)}) ws=${e.name} ;;`).join(`
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=de(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=fe(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,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
- `},M=`1.3.0`,fe=[`github`,`linear`,`notion`,`slack`],N=e=>fe.map(t=>`${t}-${e}`),P=e=>t.join(c(e.path),`.mcp.json`),F=(e,t)=>e&&typeof e==`object`&&e.url?e.url:t,I=e=>{let t=e.servers,n={};if(t.github&&(n[`github-${e.name}`]={type:`http`,url:`https://api.githubcopilot.com/mcp/`,headers:{Authorization:"Bearer ${GITHUB_TOKEN}"}}),t.linear&&(n[`linear-${e.name}`]={type:`http`,url:F(t.linear,`https://mcp.linear.app/mcp`)}),t.notion&&(n[`notion-${e.name}`]={type:`http`,url:F(t.notion,`https://mcp.notion.com/mcp`)}),t.slack){let r={SLACK_MCP_XOXP_TOKEN:"${SLACK_MCP_XOXP_TOKEN}"};t.slack.addMessageTool&&(r.SLACK_MCP_ADD_MESSAGE_TOOL=`true`),n[`slack-${e.name}`]={type:`stdio`,command:`npx`,args:[`-y`,`slack-mcp-server@${M}`,`--transport`,`stdio`],env:r}}return n},L=e=>({mcpServers:I(e)}),R=t=>{if(!e.existsSync(t))return{};try{return JSON.parse(e.readFileSync(t,`utf8`))}catch{return{}}},z=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)`)}},B=t=>{let n=P(t);return e.existsSync(n)?R(n):null},V=n=>{let r=P(n);e.mkdirSync(t.dirname(r),{recursive:!0});let i=z(r),a=i.mcpServers&&typeof i.mcpServers==`object`?{...i.mcpServers}:{};for(let e of N(n.name))delete a[e];Object.assign(a,I(n)),i.mcpServers=a,e.writeFileSync(r,JSON.stringify(i,null,2)+`
55
- `)},pe=t=>{let n=P(t);if(!e.existsSync(n))return;let r=z(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
- `)},me=e=>{let n=i();return e===n?`$HOME`:e.startsWith(n+t.sep)?`$HOME/${e.slice(n.length+1)}`:e},H=()=>{let e=me(d());return`[ -r "${e}" ] && source "${e}"`},U=e=>{let t=H();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`},W=()=>{let t=m(),n=``;try{n=e.readFileSync(t,`utf8`)}catch{}let r=U(n);r!==n&&e.writeFileSync(t,r)},G=()=>{try{return e.readFileSync(m(),`utf8`).includes(H())}catch{return!1}},he=n=>{let r=d();e.mkdirSync(t.dirname(r),{recursive:!0}),e.writeFileSync(r,j(n)),A(n),W();let i=[];for(let e of n.workspaces)V(e),i.push(P(e));return{hook:r,gitconfig:n.workspaces.some(e=>e.git?.email||e.git?.name),mcp:i}},K=(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??``}},q=()=>process.platform===`darwin`,J=()=>process.env.USER||``,Y=(e,t=K)=>{let n=t(`gh`,[`auth`,`token`,`-u`,e]),r=n.stdout.trim();return n.status===0&&r?r:null},X=(e=K)=>{let t=e(`gh`,[`auth`,`status`]);return(t.stdout+t.stderr).trim()},ge=(e=K)=>{let t=[];for(let n of X(e).matchAll(/account (\S+) \(/g))t.includes(n[1])||t.push(n[1]);return t},_e=(e,t=K)=>{let n=t(`git`,[`config`,`--global`,e]),r=n.stdout.trim();return n.status===0&&r?r:null},Z=(e,t=K)=>{let n=t(`security`,[`find-generic-password`,`-a`,J(),`-s`,e,`-w`]);return n.status===0&&n.stdout.trim().length>0},ve=(e,t,n=K)=>{let r=n(`security`,[`add-generic-password`,`-U`,`-a`,J(),`-s`,e,`-w`,t]);if(r.status!==0)throw Error(`security add-generic-password failed: ${r.stderr.trim()||`unknown error`}`)},Q=e=>`security add-generic-password -U -a "${J()||`$USER`}" -s ${e} -w 'xoxp-...'`,$=(e,t=K)=>{let n=t(`git`,[`config`,`--file`,e,`user.email`]);return n.status===0?n.stdout.trim():null},ye=t=>{try{return e.readFileSync(t,`utf8`)}catch{return null}},be=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,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)})},Se=(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}},Ce=(t,n=K)=>{let r=[];q()||r.push({status:`warn`,label:`platform`,detail:`inscope's secret resolution targets macOS (gh keyring + Keychain)`});let i=d(),a=ye(i);a===null?r.push({status:`fail`,label:`hook`,detail:`missing ${i}; run \`inscope init\``}):a===j(t)?r.push({status:`ok`,label:`hook`,detail:i}):r.push({status:`warn`,label:`hook`,detail:"out of date; run `inscope apply`"}),r.push(G()?{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(Y(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(Z(e,n)?{status:`ok`,label:`${t} slack`,detail:e}:{status:`fail`,label:`${t} slack`,detail:`${e} not in keychain; run \`${Q(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=$(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=B(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=be(a);n.length&&r.push({status:`warn`,label:`${t} mcp`,detail:`unpinned: ${n.join(`, `)}`})}}return r};export{ee as CONFIG_VERSION,M as SLACK_MCP_VERSION,he as applyAll,A as applyGitconfig,V as applyMcp,ne as configExists,xe as currentWorkspace,te as defaultConfig,K as defaultRunner,W as ensureZshrcSource,_ as findWorkspace,ge as ghAccounts,X as ghStatus,Y as ghToken,$ as gitEmailForFile,_e as gitGlobal,q as isMacOS,Z as keychainHas,ve as keychainSet,Q as keychainSetCommand,ie as labelFromPath,Se as liveSnapshot,re as loadConfig,N as managedKeys,P as mcpFilePath,B as readMcp,pe as removeMcp,oe as removeWorkspace,O as renderGitInclude,j as renderHook,L as renderMcp,k as renderPerWorkspaceGitconfig,I as renderServers,U as renderZshrcSource,Ce as runDoctor,h as saveConfig,ae as upsertWorkspace,g as validateConfig,G as zshrcSourcesHook};
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`},M=[`github`,`atlassian`,`linear`,`notion`,`plane`,`sentry`,`slack`,`vercel`],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
+ `)},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
+ `)},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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "inscope",
3
- "version": "0.3.2",
3
+ "version": "0.4.0",
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
  "claude-code",