inscope 0.4.0-canary.0 โ†’ 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/docs/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,15 +48,18 @@ 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
59
59
 
60
+ # edit a workspace interactively (gh account, git identity, servers)
61
+ inscope edit acme
62
+
60
63
  # verify tokens, identities, and the hook all resolve
61
64
  inscope doctor
62
65
 
@@ -67,7 +70,11 @@ inscope apply
67
70
  inscope rm ~/acme
68
71
  ```
69
72
 
70
- `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
+
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="900" />
77
+ </p>
71
78
 
72
79
  ---
73
80
 
@@ -76,7 +83,7 @@ inscope rm ~/acme
76
83
  - ๐Ÿชช Per-directory identity: GitHub token, git commit email, and MCP servers scoped to `$PWD`
77
84
  - ๐Ÿงต Race-free across concurrent shells and Claude Code sessions, with no global toggles
78
85
  - ๐Ÿ” No secrets on disk: GitHub tokens from the `gh` keyring, Slack tokens from the macOS Keychain
79
- - ๐Ÿค– 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
80
87
  - โœ‰๏ธ Git `includeIf` rules so every commit lands with the right author email per path
81
88
  - ๐Ÿช A single zsh `chpwd` hook does all the resolution; nothing else touches your shell
82
89
  - ๐Ÿฉบ `inscope doctor` verifies tokens, identities, and the hook before you trust them
@@ -108,8 +115,8 @@ inscope init
108
115
  gh auth login
109
116
 
110
117
  # 3. map your workspaces
111
- inscope add ~/acme --gh acme --email you@acme.com --servers github,linear,notion,slack
112
- 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
113
120
 
114
121
  # 4. reload your shell, then verify
115
122
  source ~/.zshrc
@@ -137,6 +144,10 @@ inscope doctor Verify tokens, identities, and the hook resolve correctly
137
144
 
138
145
  Run any command with `-h` for its options.
139
146
 
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="900" />
149
+ </p>
150
+
140
151
  ### `inscope add`
141
152
 
142
153
  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).
@@ -146,7 +157,8 @@ Run it bare and it walks you through everything: pick the GitHub account from yo
146
157
  --email <email> git commit email (omit to inherit your global identity)
147
158
  --git-name <name> git commit author name (omit to inherit global)
148
159
  --label <name> workspace name; defaults to the directory basename
149
- --servers <list> comma-separated: github,linear,notion,slack
160
+ --servers <list> comma-separated, any of: github, atlassian, linear,
161
+ notion, plane, sentry, slack, vercel
150
162
  (default: github)
151
163
  --slack-keychain <s> keychain service for the Slack token
152
164
  (default: SLACK_MCP_XOXP_TOKEN_<LABEL> when slack is on)
@@ -174,17 +186,21 @@ Run it bare and it walks you through everything: pick the GitHub account from yo
174
186
 
175
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.
176
188
 
177
- | Server | Transport | Token source |
178
- | -------- | --------- | ---------------------------------------------- |
179
- | `github` | http | `GITHUB_TOKEN` from the active `gh` account |
180
- | `linear` | http | OAuth via the Linear MCP endpoint |
181
- | `notion` | http | OAuth via the Notion MCP endpoint |
182
- | `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 |
183
199
 
184
200
  Slack is opt-in. Enable it with `--servers ...,slack`, then store the token once:
185
201
 
186
202
  ```sh
187
- 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
188
204
  ```
189
205
 
190
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.
@@ -204,16 +220,20 @@ The source of truth is `~/.config/inscope/inscope.json`:
204
220
  {
205
221
  "name": "acme",
206
222
  "path": "~/acme",
207
- "gh": "acme",
208
- "git": { "email": "you@acme.com" },
223
+ "gh": "neeraj-acme-org",
224
+ "git": { "email": "neeraj@acme.org" },
209
225
  "servers": {
210
226
  "github": true,
227
+ "atlassian": false,
211
228
  "linear": true,
212
- "notion": true,
229
+ "notion": false,
230
+ "plane": false,
231
+ "sentry": false,
213
232
  "slack": {
214
233
  "keychain": "SLACK_MCP_XOXP_TOKEN_ACME",
215
234
  "addMessageTool": false,
216
235
  },
236
+ "vercel": false,
217
237
  },
218
238
  },
219
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.4.0-canary.0`,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.4.0-canary.0",
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",