inscope 0.2.1 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -9,6 +9,10 @@
9
9
 
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
+ <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" />
14
+ </p>
15
+
12
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:
13
17
 
14
18
  - a `.mcp.json` at each workspace root, with uniquely named servers
@@ -53,6 +57,9 @@ inscope add ~/nrjdalal --gh nrjdalal --email you@personal.dev
53
57
  # list what is configured
54
58
  inscope list
55
59
 
60
+ # edit a workspace interactively (gh account, git identity, servers)
61
+ inscope edit acme
62
+
56
63
  # verify tokens, identities, and the hook all resolve
57
64
  inscope doctor
58
65
 
@@ -65,6 +72,10 @@ inscope rm ~/acme
65
72
 
66
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.
67
74
 
75
+ <p align="center">
76
+ <img src="https://raw.githubusercontent.com/nrjdalal/inscope/main/docs/demo-switch.gif" alt="inscope switching git identity and tokens on cd" width="800" />
77
+ </p>
78
+
68
79
  ---
69
80
 
70
81
  ## ✨ Features
@@ -120,8 +131,9 @@ Launch `claude` from inside a mapped directory (or relaunch) to pick up the iden
120
131
 
121
132
  ```
122
133
  inscope init Create the config, generate the hook, source it from ~/.zshrc
123
- inscope add <path> Map a directory to a GitHub account, git email, and MCP servers
124
- inscope rm <path> Remove a workspace mapping (alias: remove)
134
+ inscope add [path] Map a directory to a GitHub account, git email, and MCP servers
135
+ inscope edit [path] Edit a workspace interactively, then re-apply
136
+ inscope rm [path] Remove a workspace mapping (alias: remove)
125
137
  inscope list List configured workspaces (alias: ls)
126
138
  inscope apply Regenerate the hook, git includes, and .mcp.json (alias: sync)
127
139
  inscope doctor Verify tokens, identities, and the hook resolve correctly
@@ -132,6 +144,10 @@ inscope doctor Verify tokens, identities, and the hook resolve correctly
132
144
 
133
145
  Run any command with `-h` for its options.
134
146
 
147
+ <p align="center">
148
+ <img src="https://raw.githubusercontent.com/nrjdalal/inscope/main/docs/demo-manage.gif" alt="inscope edit and rm with type-to-confirm" width="800" />
149
+ </p>
150
+
135
151
  ### `inscope add`
136
152
 
137
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).
@@ -184,6 +200,8 @@ inscope add ~/acme --gh acme --servers github,linear,notion,slack --seed-slack
184
200
 
185
201
  `--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.
186
202
 
203
+ You need a Slack app with a user OAuth (`xoxp`) token first. If you don't have one, follow the [slack-mcp-server authentication guide](https://github.com/korotovsky/slack-mcp-server/blob/HEAD/docs/01-authentication-setup.md#option-2-using-slack_mcp_xoxp_token-user-oauth). inscope points you there during `add` when Slack is enabled.
204
+
187
205
  ---
188
206
 
189
207
  ## šŸ“‹ Config File
@@ -1,10 +1,18 @@
1
1
  #!/usr/bin/env node
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`),_=e=>`# >>> inscope:${e} >>>`,v=e=>`# <<< inscope:${e} <<<`,y=e=>e.replace(/[.*+?^${}()|[\]\\]/g,`\\$&`),b=e=>RegExp(`${y(_(e))}\\n[\\s\\S]*?\\n${y(v(e))}\\n?`),ee=(e,t)=>{let n=t.replace(/\n+$/,``);return`${_(e)}\n${n}\n${v(e)}\n`},x=e=>{try{return t.readFileSync(e,`utf8`)}catch{return``}},te=(e,r,i)=>{t.mkdirSync(n.dirname(e),{recursive:!0});let a=x(e),o=ee(r,i),s=b(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)},ne=(e,n)=>{let r=x(e);if(!r)return;let i=r.replace(b(n),``).replace(/\n{3,}/g,`
3
-
4
- `).replace(/^\n+/,``);t.writeFileSync(e,i)},re=(e,t)=>{let n=x(e).match(RegExp(`${y(_(t))}\\n([\\s\\S]*?)\\n${y(v(t))}`));return n?n[1]:null},S=`gitconfig`,C=e=>!!(e.git&&(e.git.email||e.git.name)),w=e=>n.join(m(),`${e}.gitconfig`),ie=e=>l(e).replace(/\/+$/,``)+`/`,ae=e=>e.workspaces.filter(C).map(e=>`[includeIf "gitdir:${ie(e.path)}"]\n\tpath = ${l(w(e.name))}`).join(`
5
- `),oe=e=>{let t=[`# Managed by inscope. Do not edit by hand.`,`[user]`];return e.git?.email&&t.push(`\temail = ${e.git.email}`),e.git?.name&&t.push(`\tname = ${e.git.name}`),t.join(`
2
+ import{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(`
6
14
  `)+`
7
- `},se=e=>{t.mkdirSync(m(),{recursive:!0});for(let n of e.workspaces)C(n)&&t.writeFileSync(w(n.name),oe(n));let n=ae(e);n?te(h(),S,n):ne(h(),S)},ce=e=>{let n=w(e);t.existsSync(n)&&t.rmSync(n)},le=e=>{let t=l(e);return t===`~`?`"$HOME/"*`:t.startsWith(`~/`)?`"$HOME/${t.slice(2)}/"*`:`"${t}/"*`},ue=e=>e.servers.slack?e.servers.slack.keychain:``,T=e=>{let t=[...e.workspaces].sort((e,t)=>e.name.localeCompare(t.name));return`# Managed by inscope. Do not edit by hand.
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.
8
16
  # Source of truth: ~/.config/inscope/inscope.json
9
17
  # Edit there, then run \`inscope apply\` to regenerate this file.
10
18
  #
@@ -15,7 +23,7 @@ import{parseArgs as e}from"node:util";import t from"node:fs";import n from"node:
15
23
  __inscope_resolve_identity() {
16
24
  local ws
17
25
  case "\${PWD}/" in
18
- ${t.map(e=>` ${le(e.path)}) ws=${e.name} ;;`).join(`
26
+ ${t.map(e=>` ${Se(e.path)}) ws=${e.name} ;;`).join(`
19
27
  `)||` # no workspaces configured`}
20
28
  *) ws="" ;;
21
29
  esac
@@ -25,7 +33,7 @@ ${t.map(e=>` ${le(e.path)}) ws=${e.name} ;;`).join(`
25
33
 
26
34
  local gh_user="" slack_svc=""
27
35
  case "$ws" in
28
- ${t.map(e=>{let t=[];e.gh&&t.push(`gh_user=${e.gh}`);let n=ue(e);return n&&t.push(`slack_svc=${n}`),` ${e.name}) ${t.length?t.join(`; `):`:`} ;;`}).join(`
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(`
29
37
  `)||` # no workspaces configured`}
30
38
  *) return ;; # outside a mapped workspace: nothing set
31
39
  esac
@@ -51,20 +59,14 @@ autoload -Uz add-zsh-hook
51
59
  add-zsh-hook chpwd __inscope_resolve_identity
52
60
  __inscope_ws="__init__" # force the first resolve, clearing any inherited token
53
61
  __inscope_resolve_identity
54
- `},de=[`github`,`linear`,`notion`,`slack`],E=e=>de.map(t=>`${t}-${e}`),D=e=>n.join(u(e.path),`.mcp.json`),O=(e,t)=>e&&typeof e==`object`&&e.url?e.url:t,fe=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:O(t.linear,`https://mcp.linear.app/mcp`)}),t.notion&&(n[`notion-${e.name}`]={type:`http`,url:O(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},k=e=>{if(!t.existsSync(e))return{};try{return JSON.parse(t.readFileSync(e,`utf8`))}catch{return{}}},pe=e=>{let n=D(e);return t.existsSync(n)?k(n):null},me=e=>{let r=D(e);t.mkdirSync(n.dirname(r),{recursive:!0});let i=k(r),a=i.mcpServers&&typeof i.mcpServers==`object`?{...i.mcpServers}:{};for(let t of E(e.name))delete a[t];Object.assign(a,fe(e)),i.mcpServers=a,t.writeFileSync(r,JSON.stringify(i,null,2)+`
55
- `)},he=e=>{let n=D(e);if(!t.existsSync(n))return;let r=k(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)+`
56
- `)},ge=e=>{let t=o();return e===t?`$HOME`:e.startsWith(t+n.sep)?`$HOME/${e.slice(t.length+1)}`:e},A=()=>{let e=ge(p());return`[ -r "${e}" ] && source "${e}"`},_e=e=>{let t=A();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`},ve=()=>{let e=g(),n=``;try{n=t.readFileSync(e,`utf8`)}catch{}let r=_e(n);r!==n&&t.writeFileSync(e,r)},ye=()=>{try{return t.readFileSync(g(),`utf8`).includes(A())}catch{return!1}},j=e=>{let r=p();t.mkdirSync(n.dirname(r),{recursive:!0}),t.writeFileSync(r,T(e)),se(e),ve();let i=[];for(let t of e.workspaces)me(t),i.push(D(t));return{hook:r,gitconfig:e.workspaces.some(e=>e.git?.email||e.git?.name),mcp:i}},M=()=>({version:1,workspaces:[]}),N=()=>t.existsSync(f()),P=()=>{let e=f(),n=t.readFileSync(e,`utf8`),r=JSON.parse(n);return be(r),r},F=e=>{let r=f();t.mkdirSync(n.dirname(r),{recursive:!0}),t.writeFileSync(r,JSON.stringify(e,null,2)+`
57
- `)},be=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)}},xe=e=>n.basename(u(e)),Se=(e,t)=>{let n=e.workspaces.find(e=>e.name===t);if(n)return n;let r=u(t);return e.workspaces.find(e=>u(e.path)===r)},Ce=(e,t)=>{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}},we=(e,t)=>{let n=Se(e,t);return n?{cfg:{...e,workspaces:e.workspaces.filter(e=>e.name!==n.name)},removed:n}:{cfg:e}},I=(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`,L=()=>process.env.USER||``,Ee=(e,t=I)=>{let n=t(`gh`,[`auth`,`token`,`-u`,e]),r=n.stdout.trim();return n.status===0&&r?r:null},De=(e=I)=>{let t=e(`gh`,[`auth`,`status`]);return(t.stdout+t.stderr).trim()},Oe=(e=I)=>{let t=[];for(let n of De(e).matchAll(/account (\S+) \(/g))t.includes(n[1])||t.push(n[1]);return t},R=(e,t=I)=>{let n=t(`git`,[`config`,`--global`,e]),r=n.stdout.trim();return n.status===0&&r?r:null},z=(e,t=I)=>{let n=t(`security`,[`find-generic-password`,`-a`,L(),`-s`,e,`-w`]);return n.status===0&&n.stdout.trim().length>0},ke=(e,t,n=I)=>{let r=n(`security`,[`add-generic-password`,`-U`,`-a`,L(),`-s`,e,`-w`,t]);if(r.status!==0)throw Error(`security add-generic-password failed: ${r.stderr.trim()||`unknown error`}`)},B=e=>`security add-generic-password -U -a "${L()||`$USER`}" -s ${e} -w 'xoxp-...'`,Ae=(e,t=I)=>{let n=t(`git`,[`config`,`--file`,e,`user.email`]);return n.status===0?n.stdout.trim():null},V=()=>!!(process.stdin.isTTY&&process.stdout.isTTY),H=e=>{let t=process.stdin;t.isTTY&&typeof t.setRawMode==`function`&&t.setRawMode(e)},U=(e,t=``)=>new Promise(n=>{let r=a.createInterface({input:process.stdin,output:process.stdout}),i=t?` [${t}]`:``;r.question(`${e}${i}: `,e=>{r.close(),n(e.trim()||t)})}),W=(e,t=!1)=>new Promise(n=>{let r=a.createInterface({input:process.stdin,output:process.stdout});r.question(`${e} [${t?`Y/n`:`y/N`}]: `,e=>{r.close();let i=e.trim().toLowerCase();n(i?i===`y`||i===`yes`:t)})}),je=e=>new Promise(t=>{let n=a.createInterface({input:process.stdin,output:process.stdout}),r=n.output,i=!1;n._writeToOutput=t=>{if(!i){r.write(t),t.includes(e)&&(i=!0);return}},n.question(e,e=>{r.write(`
58
- `),n.close(),t(e.trim())})}),G=`\x1B[36m`,K=`\x1B[0m`,Me=(e,t,n=0)=>new Promise(r=>{if(!V()||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+`
59
- `);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?G+r+K:r}\n`)}};s(!0),a.emitKeypressEvents(process.stdin),H(!0),process.stdin.resume();let c=()=>{process.stdin.off(`keypress`,l),H(!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(`
60
- `),process.exit(130))};process.stdin.on(`keypress`,l)}),Ne=(e,t)=>new Promise(n=>{let r=t.map(e=>!!e.checked),i=()=>t.filter((e,t)=>r[t]).map(e=>e.value);if(!V()||t.length===0){n(i());return}let o=0,s=process.stdout;s.write(e+`
61
- `);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?G+i+K:i}\n`)}};c(!0),a.emitKeypressEvents(process.stdin),H(!0),process.stdin.resume();let l=()=>{process.stdin.off(`keypress`,u),H(!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(`
62
- `),process.exit(130))};process.stdin.on(`keypress`,u)});var q=`inscope`,J=`0.2.1`,Y={name:`Neeraj Dalal`,email:`admin@nrjdalal.com`,url:`https://nrjdalal.com`};const X=`Map a directory to a GitHub account, git email, and MCP servers.
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.1`,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.
63
65
  Runs interactively in a terminal; pass flags or -y to skip the prompts. Re-running
64
66
  with the same path or label updates that workspace.
65
67
 
66
68
  Usage:
67
- $ ${q} add [path] [options]
69
+ $ ${Z} add [path] [options]
68
70
 
69
71
  Options:
70
72
  --gh <account> gh account whose token this workspace uses
@@ -78,61 +80,72 @@ Options:
78
80
  --slack-message allow the Slack MCP server to post messages
79
81
  --seed-slack prompt for the Slack token and store it in the keychain
80
82
  -y, --yes accept defaults, skip all prompts (non-interactive)
81
- -h, --help Display help message`,Pe=[{label:`github`,value:`github`,checked:!0},{label:`linear`,value:`linear`,checked:!1},{label:`notion`,value:`notion`,checked:!1},{label:`slack`,value:`slack`,checked:!1}],Fe=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(X),process.exit(0));let i=V()&&!r.yes,a=n[0];if(!a)if(i)a=await U(`Workspace directory`,process.cwd());else throw Error(X);let o=r.label||xe(a);i&&!r.label&&(o=await U(`Label`,o));let s=r.gh;s===void 0&&i&&(s=await Me(`GitHub account for this workspace`,[...Oe().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=R(`user.email`);c=await U(`Git email${e?` [${e} Ā· global]`:``} (enter to inherit global)`)||void 0}if(u===void 0){let e=R(`user.name`);u=await U(`Git name${e?` [${e} Ā· global]`:``} (enter to inherit global)`)||void 0}}let d;d=r.servers===void 0?i?await Ne(`MCP servers (space toggles, enter confirms)`,Pe):[`github`]:r.servers.split(`,`).map(e=>e.trim()).filter(Boolean);let f=d.includes(`slack`)||!!r[`slack-keychain`]||!!r[`seed-slack`],p=o.toUpperCase().replace(/[^A-Z0-9]+/g,`_`),m=r[`slack-keychain`]||`SLACK_MCP_XOXP_TOKEN_${p}`,h=!!r[`slack-message`],g=!!r[`seed-slack`];f&&i&&(r[`slack-keychain`]||(m=await U(`Slack keychain service`,m)),r[`slack-message`]||(h=await W(`Allow Slack to post messages?`,!1)),r[`seed-slack`]||(g=await W(`Store the Slack token now?`,!1)));let _={github:d.includes(`github`),linear:d.includes(`linear`),notion:d.includes(`notion`),slack:f?{keychain:m,addMessageTool:h}:!1},v=c||u?{email:c,name:u}:void 0,y={name:o,path:l(a),gh:s,git:v,servers:_},b=Ce(N()?P():M(),y);if(F(b),j(b),console.log(`\nāœ“ workspace "${o}" -> ${y.path}`),console.log(`āœ“ regenerated the hook, git includes, and ${y.path}/.mcp.json`),_.slack)if(g){let e=await je(`Paste the Slack xoxp token for ${m}: `);e?(ke(m,e),console.log(`āœ“ stored ${m} in the macOS keychain`)):console.error(`No token entered; skipped keychain write.`)}else z(m)||console.log(`\nSlack token not in the keychain yet. Store it once with:\n ${B(m)}`);console.log(`\nLaunch \`claude\` from ${y.path} (or relaunch) to pick up the new identity.`),process.exit(0)},Ie=`Regenerate the chpwd hook, git includes, and every .mcp.json
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
82
84
  from your config. Idempotent: run it any time the config changes.
83
85
 
84
86
  Usage:
85
- $ ${q} apply
87
+ $ ${Z} apply
86
88
 
87
89
  Options:
88
- -h, --help Display help message`,Le=t=>{let{values:n}=e({allowPositionals:!0,options:{help:{type:`boolean`,short:`h`}},args:t});n.help&&(console.log(Ie),process.exit(0)),N()||(console.error(`No config found. Run \`${q} init\` first.`),process.exit(1));let r=P(),i=j(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)},Re=e=>{try{return t.readFileSync(e,`utf8`)}catch{return null}},ze=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},Be=(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)})},Ve=(e=I)=>{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}},He=(e,n=I)=>{let r=[];Te()||r.push({status:`warn`,label:`platform`,detail:`inscope's secret resolution targets macOS (gh keyring + Keychain)`});let i=p(),a=Re(i);a===null?r.push({status:`fail`,label:`hook`,detail:`missing ${i}; run \`inscope init\``}):a===T(e)?r.push({status:`ok`,label:`hook`,detail:i}):r.push({status:`warn`,label:`hook`,detail:"out of date; run `inscope apply`"}),r.push(ye()?{status:`ok`,label:`zshrc`,detail:`sources the hook`}:{status:`warn`,label:`zshrc`,detail:"does not source the hook; run `inscope init`"}),e.workspaces.some(C)&&r.push(re(h(),S)===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(Ee(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(z(t,n)?{status:`ok`,label:`${e} slack`,detail:t}:{status:`fail`,label:`${e} slack`,detail:`${t} not in keychain; run \`${B(t)}\``})}if(C(i)){let a=w(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=Ae(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=pe(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=ze(a);n.length&&r.push({status:`warn`,label:`${e} mcp`,detail:`unpinned: ${n.join(`, `)}`})}}return r},Ue=`Verify the setup: gh tokens resolve, keychain entries exist,
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,
89
91
  git emails match per path, the hook is current, and no MCP server is unpinned.
90
92
  Exits non-zero if any check fails.
91
93
 
92
94
  Usage:
93
- $ ${q} doctor
95
+ $ ${Z} doctor
96
+
97
+ 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.
100
+ Pick a workspace (or pass its path/label), step through the prompts pre-filled
101
+ with its current values, and inscope regenerates everything on save.
102
+
103
+ Usage:
104
+ $ ${Z} edit [path|label]
94
105
 
95
106
  Options:
96
- -h, --help Display help message`,Z={ok:`āœ“`,warn:`!`,fail:`āœ—`},We=t=>{let{values:n}=e({allowPositionals:!0,options:{help:{type:`boolean`,short:`h`}},args:t});n.help&&(console.log(Ue),process.exit(0)),N()||(console.error(`No config found. Run \`${q} init\` first.`),process.exit(1));let r=P(),i=He(r);for(let e of i)console.log(`${Z[e.status]} ${e.label}${e.detail?` ${e.detail}`:``}`);let a=Be(r);if(a){let e=Ve();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(`
97
- All checks passed.`),process.exit(0)},Ge=`Set up inscope: create the config, generate the chpwd hook, and
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
98
108
  source it from ~/.zshrc. Safe to run again; it never overwrites your config.
99
109
 
100
110
  Usage:
101
- $ ${q} init
111
+ $ ${Z} init
102
112
 
103
113
  Options:
104
- -h, --help Display help message`,Ke=t=>{let{values:n}=e({allowPositionals:!0,options:{help:{type:`boolean`,short:`h`}},args:t});n.help&&(console.log(Ge),process.exit(0));let r;N()?(r=P(),console.log(`Using existing config at ${f()}`)):(r=M(),F(r),console.log(`Created ${f()}`)),j(r),console.log(`Generated the chpwd hook and added a source line to ~/.zshrc.`),console.log(`
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(`
105
115
  Next steps:
106
116
  1. Reload your shell: source ~/.zshrc (or open a new terminal)
107
117
  2. Sign each GitHub account in: gh auth login
108
- 3. Map a workspace: ${q} add ~/acme --gh acme --email you@acme.com
109
- `),process.exit(0)},qe=`List the configured workspaces. Run \`${q} doctor\` to verify
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
110
120
  that their tokens and identities actually resolve.
111
121
 
112
122
  Usage:
113
- $ ${q} list
123
+ $ ${Z} list
114
124
 
115
125
  Options:
116
- -h, --help Display help message`,Je=e=>[e.github&&`github`,e.linear&&`linear`,e.notion&&`notion`,e.slack&&`slack`].filter(Boolean).join(`, `)||`none`,Ye=t=>{let{values:n}=e({allowPositionals:!0,options:{help:{type:`boolean`,short:`h`}},args:t});n.help&&(console.log(qe),process.exit(0)),N()||(console.error(`No config found. Run \`${q} init\` first.`),process.exit(1));let r=P();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 ${Je(e.servers)}`),e.servers.slack&&console.log(` slack keychain: ${e.servers.slack.keychain}`);process.exit(0)},Q=`Remove a workspace mapping. Drops its git include and the MCP
117
- servers inscope manages; leaves your keychain and gh accounts untouched.
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
127
+ servers inscope manages; leaves your keychain and gh accounts untouched. Pick a
128
+ workspace, or pass its path/label.
118
129
 
119
130
  Usage:
120
- $ ${q} rm <path|label>
131
+ $ ${Z} rm [path|label]
121
132
 
122
133
  Options:
123
- -h, --help Display help message`,Xe=t=>{let{positionals:n,values:r}=e({allowPositionals:!0,options:{help:{type:`boolean`,short:`h`}},args:t});r.help&&(console.log(Q),process.exit(0));let i=n[0];if(!i)throw Error(Q);N()||(console.error(`No config found. Run \`${q} init\` first.`),process.exit(1));let{cfg:a,removed:o}=we(P(),i);o||(console.error(`No workspace matching "${i}".`),process.exit(1)),he(o),ce(o.name),F(a),j(a),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)},$=`Version:
124
- ${q}@${J}
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}
125
137
 
126
138
  Per-workspace identity for Claude Code: scope MCP servers, GitHub auth, and git
127
139
  commit identity to the directory you are in, so concurrent sessions never clash.
128
140
 
129
141
  Usage:
130
- $ ${q} <command> [options]
142
+ $ ${Z} <command> [options]
131
143
 
132
144
  Commands:
133
145
  init Create the config, generate the hook, source it from ~/.zshrc
134
- add <path> Map a directory to a GitHub account, git email, and MCP servers
135
- rm <path> Remove a workspace mapping (alias: remove)
146
+ add [path] Map a directory to a GitHub account, git email, and MCP servers
147
+ edit [path] Edit a workspace interactively, then re-apply
148
+ rm [path] Remove a workspace mapping (alias: remove)
136
149
  list List configured workspaces (alias: ls)
137
150
  apply Regenerate the hook, git includes, and .mcp.json (alias: sync)
138
151
  doctor Verify tokens, identities, and the hook resolve correctly
@@ -142,4 +155,4 @@ Options:
142
155
  -h, --help Display help
143
156
 
144
157
  Author:
145
- ${Y.name} <${Y.email}> (${Y.url})`;(async()=>{try{let e=process.argv.slice(2),t=e[0],n=e.slice(1);switch(t){case`init`:return Ke(n);case`add`:return await Fe(n);case`rm`:case`remove`:return Xe(n);case`ls`:case`list`:return Ye(n);case`apply`:case`sync`:return Le(n);case`doctor`:return We(n)}(t===`-v`||t===`--version`)&&(console.log(`${q}@${J}`),process.exit(0)),(!t||t===`-h`||t===`--help`)&&(console.log($),process.exit(0)),console.error(`unknown command: ${e.join(` `)}\n`),console.error($),process.exit(1)}catch(e){console.error(e.message),process.exit(1)}})();export{};
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{};
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,h=()=>({version:1,workspaces:[]}),te=()=>e.existsSync(u()),ne=()=>{let t=u(),n=e.readFileSync(t,`utf8`),r=JSON.parse(n);return _(r),r},g=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?`),C=(e,t)=>{let n=t.replace(/\n+$/,``);return`${y(e)}\n${n}\n${b(e)}\n`},w=t=>{try{return e.readFileSync(t,`utf8`)}catch{return``}},oe=(n,r,i)=>{e.mkdirSync(t.dirname(n),{recursive:!0});let a=w(n),o=C(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)},T=(t,n)=>{let r=w(t);if(!r)return;let i=r.replace(S(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,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,`
3
3
 
4
- `).replace(/^\n+/,``);e.writeFileSync(t,i)},se=(e,t)=>{let n=w(e).match(RegExp(`${x(y(t))}\\n([\\s\\S]*?)\\n${x(b(t))}`));return n?n[1]:null},E=`gitconfig`,D=e=>!!(e.git&&(e.git.email||e.git.name)),O=e=>t.join(f(),`${e}.gitconfig`),ce=e=>s(e).replace(/\/+$/,``)+`/`,k=e=>e.workspaces.filter(D).map(e=>`[includeIf "gitdir:${ce(e.path)}"]\n\tpath = ${s(O(e.name))}`).join(`
5
- `),A=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=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(`
6
6
  `)+`
7
- `},j=t=>{e.mkdirSync(f(),{recursive:!0});for(let n of t.workspaces)D(n)&&e.writeFileSync(O(n.name),A(n));let n=k(t);n?oe(p(),E,n):T(p(),E)},le=e=>{let t=s(e);return t===`~`?`"$HOME/"*`:t.startsWith(`~/`)?`"$HOME/${t.slice(2)}/"*`:`"${t}/"*`},ue=e=>e.servers.slack?e.servers.slack.keychain:``,M=e=>{let t=[...e.workspaces].sort((e,t)=>e.name.localeCompare(t.name));return`# Managed by inscope. Do not edit by hand.
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.
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=>` ${le(e.path)}) ws=${e.name} ;;`).join(`
18
+ ${t.map(e=>` ${ue(e.path)}) ws=${e.name} ;;`).join(`
19
19
  `)||` # no workspaces configured`}
20
20
  *) ws="" ;;
21
21
  esac
@@ -25,7 +25,7 @@ ${t.map(e=>` ${le(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=ue(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=de(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
- `},N=`1.3.0`,de=[`github`,`linear`,`notion`,`slack`],P=e=>de.map(t=>`${t}-${e}`),F=e=>t.join(c(e.path),`.mcp.json`),I=(e,t)=>e&&typeof e==`object`&&e.url?e.url:t,L=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:I(t.linear,`https://mcp.linear.app/mcp`)}),t.notion&&(n[`notion-${e.name}`]={type:`http`,url:I(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@${N}`,`--transport`,`stdio`],env:r}}return n},R=e=>({mcpServers:L(e)}),z=t=>{if(!e.existsSync(t))return{};try{return JSON.parse(e.readFileSync(t,`utf8`))}catch{return{}}},B=t=>{let n=F(t);return e.existsSync(n)?z(n):null},V=n=>{let r=F(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 P(n.name))delete a[e];Object.assign(a,L(n)),i.mcpServers=a,e.writeFileSync(r,JSON.stringify(i,null,2)+`
55
- `)},fe=t=>{let n=F(t);if(!e.existsSync(n))return;let r=z(n);if(r.mcpServers&&typeof r.mcpServers==`object`)for(let e of P(t.name))delete r.mcpServers[e];e.writeFileSync(n,JSON.stringify(r,null,2)+`
56
- `)},pe=e=>{let n=i();return e===n?`$HOME`:e.startsWith(n+t.sep)?`$HOME/${e.slice(n.length+1)}`:e},H=()=>{let e=pe(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}},me=n=>{let r=d();e.mkdirSync(t.dirname(r),{recursive:!0}),e.writeFileSync(r,M(n)),j(n),W();let i=[];for(let e of n.workspaces)V(e),i.push(F(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()},he=(e=K)=>{let t=[];for(let n of X(e).matchAll(/account (\S+) \(/g))t.includes(n[1])||t.push(n[1]);return t},ge=(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},_e=(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},ve=t=>{try{return e.readFileSync(t,`utf8`)}catch{return null}},ye=e=>{let t=[],n=e?.mcpServers;if(!n||typeof n!=`object`)return t;for(let[e,r]of Object.entries(n)){let n=Array.isArray(r?.args)?r.args:[];if(n.some(e=>typeof e==`string`&&/@latest$/.test(e)))t.push(e);else if(r?.command===`npx`){let r=n.find(e=>typeof e==`string`&&!e.startsWith(`-`));r&&!r.includes(`@`)&&t.push(e)}}return t},be=(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)})},xe=(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}},Se=(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=ve(i);a===null?r.push({status:`fail`,label:`hook`,detail:`missing ${i}; run \`inscope init\``}):a===M(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(D)&&r.push(se(p(),E)===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(D(i)){let a=O(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=P(i.name).filter(e=>a.mcpServers?.[e]);r.push({status:`ok`,label:`${t} mcp`,detail:`${e.length} server(s)`});let n=ye(a);n.length&&r.push({status:`warn`,label:`${t} mcp`,detail:`unpinned: ${n.join(`, `)}`})}}return r};export{ee as CONFIG_VERSION,N as SLACK_MCP_VERSION,me as applyAll,j as applyGitconfig,V as applyMcp,te as configExists,be as currentWorkspace,h as defaultConfig,K as defaultRunner,W as ensureZshrcSource,v as findWorkspace,he as ghAccounts,X as ghStatus,Y as ghToken,$ as gitEmailForFile,ge as gitGlobal,q as isMacOS,Z as keychainHas,_e as keychainSet,Q as keychainSetCommand,re as labelFromPath,xe as liveSnapshot,ne as loadConfig,P as managedKeys,F as mcpFilePath,B as readMcp,fe as removeMcp,ae as removeWorkspace,k as renderGitInclude,M as renderHook,R as renderMcp,A as renderPerWorkspaceGitconfig,L as renderServers,U as renderZshrcSource,Se as runDoctor,g as saveConfig,ie as upsertWorkspace,_ as validateConfig,G as zshrcSourcesHook};
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};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "inscope",
3
- "version": "0.2.1",
3
+ "version": "0.3.1",
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",