inscope 0.2.0-canary.0 → 0.2.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
@@ -41,7 +41,10 @@ Nothing sensitive is written to disk. GitHub tokens come from the `gh` keyring a
41
41
  # set up the config + hook, and source it from ~/.zshrc
42
42
  inscope init
43
43
 
44
- # map a work directory: work gh account, work email, work + slack servers
44
+ # map a workspace interactively: pick the gh account, git identity, and servers
45
+ inscope add ~/acme
46
+
47
+ # or pass flags to skip the prompts (work gh account, work email, + slack)
45
48
  inscope add ~/acme --gh acme --email you@acme.com --servers github,linear,notion,slack
46
49
 
47
50
  # map a personal directory: just your gh account and personal email
@@ -129,33 +132,36 @@ inscope doctor Verify tokens, identities, and the hook resolve correctly
129
132
 
130
133
  Run any command with `-h` for its options.
131
134
 
132
- ### `inscope add` options
135
+ ### `inscope add`
136
+
137
+ 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).
133
138
 
134
139
  ```
135
140
  --gh <account> gh account whose token this workspace uses
136
- --email <email> git commit email for this workspace
137
- --git-name <name> git commit author name (optional)
141
+ --email <email> git commit email (omit to inherit your global identity)
142
+ --git-name <name> git commit author name (omit to inherit global)
138
143
  --label <name> workspace name; defaults to the directory basename
139
144
  --servers <list> comma-separated: github,linear,notion,slack
140
- (default: github,linear,notion)
145
+ (default: github)
141
146
  --slack-keychain <s> keychain service for the Slack token
142
- (default: slack-<label>-mcp-xoxp when slack is on)
147
+ (default: SLACK_MCP_XOXP_TOKEN_<LABEL> when slack is on)
143
148
  --slack-message allow the Slack MCP server to post messages
144
149
  --seed-slack prompt for the Slack token and store it in the keychain
150
+ -y, --yes accept defaults, skip all prompts (non-interactive)
145
151
  ```
146
152
 
147
153
  ---
148
154
 
149
155
  ## 🧩 What It Manages
150
156
 
151
- | Surface | Location |
152
- | ------------ | ----------------------------------------------------------- |
153
- | Config | `~/.config/claude/workspaces.json` |
154
- | chpwd hook | `~/.config/claude/mcp-tokens.zsh` |
155
- | MCP servers | `<workspace>/.mcp.json` |
156
- | Git identity | `~/.gitconfig` includeIf + `~/.config/git/<name>.gitconfig` |
157
+ | Surface | Location |
158
+ | ------------ | ------------------------------------------------------------------- |
159
+ | Config | `~/.config/inscope/inscope.json` |
160
+ | chpwd hook | `~/.config/inscope/inscope.zsh` |
161
+ | MCP servers | `<workspace>/.mcp.json` |
162
+ | Git identity | `~/.gitconfig` includeIf + `~/.config/inscope/git/<name>.gitconfig` |
157
163
 
158
- `inscope` only touches the blocks it owns; your other `.zshrc`, `.gitconfig` and `.mcp.json` content is left alone. Edit `workspaces.json` by hand if you like, then run `inscope apply`.
164
+ `inscope` only touches the blocks it owns; your other `.zshrc`, `.gitconfig` and `.mcp.json` content is left alone. Edit `inscope.json` by hand if you like, then run `inscope apply`.
159
165
 
160
166
  ---
161
167
 
@@ -182,7 +188,7 @@ inscope add ~/acme --gh acme --servers github,linear,notion,slack --seed-slack
182
188
 
183
189
  ## 📋 Config File
184
190
 
185
- The source of truth is `~/.config/claude/workspaces.json`:
191
+ The source of truth is `~/.config/inscope/inscope.json`:
186
192
 
187
193
  ```jsonc
188
194
  {
@@ -197,10 +203,13 @@ The source of truth is `~/.config/claude/workspaces.json`:
197
203
  "github": true,
198
204
  "linear": true,
199
205
  "notion": true,
200
- "slack": { "keychain": "slack-acme-mcp-xoxp", "addMessageTool": false }
201
- }
202
- }
203
- ]
206
+ "slack": {
207
+ "keychain": "SLACK_MCP_XOXP_TOKEN_ACME",
208
+ "addMessageTool": false,
209
+ },
210
+ },
211
+ },
212
+ ],
204
213
  }
205
214
  ```
206
215
 
@@ -1,11 +1,11 @@
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(),`claude`,`workspaces.json`),f=()=>n.join(s(),`claude`,`mcp-tokens.zsh`),p=()=>n.join(s(),`git`),m=()=>n.join(o(),`.gitconfig`),h=()=>n.join(o(),`.zshrc`),g=e=>`# >>> inscope:${e} >>>`,_=e=>`# <<< inscope:${e} <<<`,v=e=>e.replace(/[.*+?^${}()|[\]\\]/g,`\\$&`),y=e=>RegExp(`${v(g(e))}\\n[\\s\\S]*?\\n${v(_(e))}\\n?`),b=(e,t)=>{let n=t.replace(/\n+$/,``);return`${g(e)}\n${n}\n${_(e)}\n`},x=e=>{try{return t.readFileSync(e,`utf8`)}catch{return``}},S=(e,r,i)=>{t.mkdirSync(n.dirname(e),{recursive:!0});let a=x(e),o=b(r,i),s=y(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)},ee=(e,n)=>{let r=x(e);if(!r)return;let i=r.replace(y(n),``).replace(/\n{3,}/g,`
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(),`claude`,`workspaces.json`),f=()=>n.join(s(),`claude`,`mcp-tokens.zsh`),p=()=>n.join(s(),`git`),m=()=>n.join(o(),`.gitconfig`),h=()=>n.join(o(),`.zshrc`),g=e=>`# >>> inscope:${e} >>>`,_=e=>`# <<< inscope:${e} <<<`,v=e=>e.replace(/[.*+?^${}()|[\]\\]/g,`\\$&`),y=e=>RegExp(`${v(g(e))}\\n[\\s\\S]*?\\n${v(_(e))}\\n?`),b=(e,t)=>{let n=t.replace(/\n+$/,``);return`${g(e)}\n${n}\n${_(e)}\n`},x=e=>{try{return t.readFileSync(e,`utf8`)}catch{return``}},ee=(e,r,i)=>{t.mkdirSync(n.dirname(e),{recursive:!0});let a=x(e),o=b(r,i),s=y(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)},te=(e,n)=>{let r=x(e);if(!r)return;let i=r.replace(y(n),``).replace(/\n{3,}/g,`
3
3
 
4
- `).replace(/^\n+/,``);t.writeFileSync(e,i)},C=(e,t)=>{let n=x(e).match(RegExp(`${v(g(t))}\\n([\\s\\S]*?)\\n${v(_(t))}`));return n?n[1]:null},w=`gitconfig`,T=e=>!!(e.git&&(e.git.email||e.git.name)),E=e=>n.join(p(),`${e}.gitconfig`),te=e=>l(e).replace(/\/+$/,``)+`/`,ne=e=>e.workspaces.filter(T).map(e=>`[includeIf "gitdir:${te(e.path)}"]\n\tpath = ${l(E(e.name))}`).join(`
5
- `),re=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+/,``);t.writeFileSync(e,i)},ne=(e,t)=>{let n=x(e).match(RegExp(`${v(g(t))}\\n([\\s\\S]*?)\\n${v(_(t))}`));return n?n[1]:null},S=`gitconfig`,C=e=>!!(e.git&&(e.git.email||e.git.name)),w=e=>n.join(p(),`${e}.gitconfig`),re=e=>l(e).replace(/\/+$/,``)+`/`,ie=e=>e.workspaces.filter(C).map(e=>`[includeIf "gitdir:${re(e.path)}"]\n\tpath = ${l(w(e.name))}`).join(`
5
+ `),ae=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
- `},ie=e=>{t.mkdirSync(p(),{recursive:!0});for(let n of e.workspaces)T(n)&&t.writeFileSync(E(n.name),re(n));let n=ne(e);n?S(m(),w,n):ee(m(),w)},ae=e=>{let n=E(e);t.existsSync(n)&&t.rmSync(n)},D=e=>{let t=l(e);return t===`~`?`"$HOME/"*`:t.startsWith(`~/`)?`"$HOME/${t.slice(2)}/"*`:`"${t}/"*`},O=e=>e.servers.slack?e.servers.slack.keychain:``,k=e=>{let t=[...e.workspaces].sort((e,t)=>e.name.localeCompare(t.name));return`# Managed by inscope. Do not edit by hand.
8
- # Source of truth: ~/.config/claude/workspaces.json
7
+ `},oe=e=>{t.mkdirSync(p(),{recursive:!0});for(let n of e.workspaces)C(n)&&t.writeFileSync(w(n.name),ae(n));let n=ie(e);n?ee(m(),S,n):te(m(),S)},se=e=>{let n=w(e);t.existsSync(n)&&t.rmSync(n)},ce=e=>{let t=l(e);return t===`~`?`"$HOME/"*`:t.startsWith(`~/`)?`"$HOME/${t.slice(2)}/"*`:`"${t}/"*`},le=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.
8
+ # Source of truth: ~/.config/inscope/inscope.json
9
9
  # Edit there, then run \`inscope apply\` to regenerate this file.
10
10
  #
11
11
  # One chpwd hook resolves per-workspace secrets from \$PWD on every cd: it maps
@@ -15,7 +15,7 @@ import{parseArgs as e}from"node:util";import t from"node:fs";import n from"node:
15
15
  __inscope_resolve_identity() {
16
16
  local ws
17
17
  case "\${PWD}/" in
18
- ${t.map(e=>` ${D(e.path)}) ws=${e.name} ;;`).join(`
18
+ ${t.map(e=>` ${ce(e.path)}) ws=${e.name} ;;`).join(`
19
19
  `)||` # no workspaces configured`}
20
20
  *) ws="" ;;
21
21
  esac
@@ -25,7 +25,7 @@ ${t.map(e=>` ${D(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=O(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=le(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,35 +51,41 @@ 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
- `},A=[`github`,`linear`,`notion`,`slack`],j=e=>A.map(t=>`${t}-${e}`),M=e=>n.join(u(e.path),`.mcp.json`),N=(e,t)=>e&&typeof e==`object`&&e.url?e.url:t,oe=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:N(t.linear,`https://mcp.linear.app/mcp`)}),t.notion&&(n[`notion-${e.name}`]={type:`http`,url:N(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},P=e=>{if(!t.existsSync(e))return{};try{return JSON.parse(t.readFileSync(e,`utf8`))}catch{return{}}},se=e=>{let n=M(e);return t.existsSync(n)?P(n):null},ce=e=>{let r=M(e);t.mkdirSync(n.dirname(r),{recursive:!0});let i=P(r),a=i.mcpServers&&typeof i.mcpServers==`object`?{...i.mcpServers}:{};for(let t of j(e.name))delete a[t];Object.assign(a,oe(e)),i.mcpServers=a,t.writeFileSync(r,JSON.stringify(i,null,2)+`
55
- `)},le=e=>{let n=M(e);if(!t.existsSync(n))return;let r=P(n);if(r.mcpServers&&typeof r.mcpServers==`object`)for(let t of j(e.name))delete r.mcpServers[t];t.writeFileSync(n,JSON.stringify(r,null,2)+`
56
- `)},F=`zshrc`,ue=e=>{let t=o();return e===t?`$HOME`:e.startsWith(t+n.sep)?`$HOME/${e.slice(t.length+1)}`:e},de=()=>{let e=ue(f()),t=`# Loads each workspace's tokens (GitHub, Slack) from $PWD on every cd.\n[ -r "${e}" ] && source "${e}"`;S(h(),F,t)},I=e=>{let r=f();t.mkdirSync(n.dirname(r),{recursive:!0}),t.writeFileSync(r,k(e)),ie(e),de();let i=[];for(let t of e.workspaces)ce(t),i.push(M(t));return{hook:r,gitconfig:e.workspaces.some(e=>e.git?.email||e.git?.name),mcp:i}},L=()=>({version:1,workspaces:[]}),R=()=>t.existsSync(d()),z=()=>{let e=d(),n=t.readFileSync(e,`utf8`),r=JSON.parse(n);return V(r),r},B=e=>{let r=d();t.mkdirSync(n.dirname(r),{recursive:!0}),t.writeFileSync(r,JSON.stringify(e,null,2)+`
57
- `)},V=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)}},H=e=>n.basename(u(e)),fe=(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)},pe=(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}},me=(e,t)=>{let n=fe(e,t);return n?{cfg:{...e,workspaces:e.workspaces.filter(e=>e.name!==n.name)},removed:n}:{cfg:e}},U=(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??``}},he=()=>process.platform===`darwin`,W=()=>process.env.USER||``,ge=(e,t=U)=>{let n=t(`gh`,[`auth`,`token`,`-u`,e]),r=n.stdout.trim();return n.status===0&&r?r:null},G=(e,t=U)=>{let n=t(`security`,[`find-generic-password`,`-a`,W(),`-s`,e,`-w`]);return n.status===0&&n.stdout.trim().length>0},_e=(e,t,n=U)=>{let r=n(`security`,[`add-generic-password`,`-U`,`-a`,W(),`-s`,e,`-w`,t]);if(r.status!==0)throw Error(`security add-generic-password failed: ${r.stderr.trim()||`unknown error`}`)},K=e=>`security add-generic-password -U -a "${W()||`$USER`}" -s ${e} -w 'xoxp-...'`,ve=(e,t=U)=>{let n=t(`git`,[`config`,`--file`,e,`user.email`]);return n.status===0?n.stdout.trim():null},ye=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())})});var q=`inscope`,J=`0.2.0-canary.0`,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.
59
- Re-running with the same path or label updates that workspace.
54
+ `},ue=[`github`,`linear`,`notion`,`slack`],E=e=>ue.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,de=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{}}},fe=e=>{let n=D(e);return t.existsSync(n)?k(n):null},pe=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,de(e)),i.mcpServers=a,t.writeFileSync(r,JSON.stringify(i,null,2)+`
55
+ `)},me=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
+ `)},he=e=>{let t=o();return e===t?`$HOME`:e.startsWith(t+n.sep)?`$HOME/${e.slice(t.length+1)}`:e},A=()=>{let e=he(f());return`[ -r "${e}" ] && source "${e}"`},ge=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`},_e=()=>{let e=h(),n=``;try{n=t.readFileSync(e,`utf8`)}catch{}let r=ge(n);r!==n&&t.writeFileSync(e,r)},ve=()=>{try{return t.readFileSync(h(),`utf8`).includes(A())}catch{return!1}},j=e=>{let r=f();t.mkdirSync(n.dirname(r),{recursive:!0}),t.writeFileSync(r,T(e)),oe(e),_e();let i=[];for(let t of e.workspaces)pe(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(d()),P=()=>{let e=d(),n=t.readFileSync(e,`utf8`),r=JSON.parse(n);return ye(r),r},F=e=>{let r=d();t.mkdirSync(n.dirname(r),{recursive:!0}),t.writeFileSync(r,JSON.stringify(e,null,2)+`
57
+ `)},ye=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)}},be=e=>n.basename(u(e)),xe=(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)},Se=(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}},Ce=(e,t)=>{let n=xe(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??``}},we=()=>process.platform===`darwin`,L=()=>process.env.USER||``,Te=(e,t=I)=>{let n=t(`gh`,[`auth`,`token`,`-u`,e]),r=n.stdout.trim();return n.status===0&&r?r:null},Ee=(e=I)=>{let t=e(`gh`,[`auth`,`status`]);return(t.stdout+t.stderr).trim()},De=(e=I)=>{let t=[];for(let n of Ee(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},Oe=(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-...'`,ke=(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)})}),Ae=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`,je=(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)}),Me=(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.0`,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.
63
+ Runs interactively in a terminal; pass flags or -y to skip the prompts. Re-running
64
+ with the same path or label updates that workspace.
60
65
 
61
66
  Usage:
62
- $ ${q} add <path> [options]
67
+ $ ${q} add [path] [options]
63
68
 
64
69
  Options:
65
70
  --gh <account> gh account whose token this workspace uses
66
- --email <email> git commit email for this workspace
67
- --git-name <name> git commit author name (optional)
71
+ --email <email> git commit email (omit to inherit your global identity)
72
+ --git-name <name> git commit author name (omit to inherit global)
68
73
  --label <name> workspace name; defaults to the directory basename
69
74
  --servers <list> comma-separated: github,linear,notion,slack
70
- (default: github,linear,notion)
75
+ (default: github)
71
76
  --slack-keychain <s> keychain service for the Slack token
72
- (default: slack-<label>-mcp-xoxp when slack is on)
77
+ (default: SLACK_MCP_XOXP_TOKEN_<LABEL> when slack is on)
73
78
  --slack-message allow the Slack MCP server to post messages
74
79
  --seed-slack prompt for the Slack token and store it in the keychain
75
- -h, --help Display help message`,be=async t=>{let{positionals:n,values:r}=e({allowPositionals:!0,options:{help:{type:`boolean`,short:`h`},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=n[0];if(!i)throw Error(X);let a=r.label||H(i),o=(r.servers??`github,linear,notion`).split(`,`).map(e=>e.trim()).filter(Boolean),s=o.includes(`slack`)||!!r[`slack-keychain`]||!!r[`seed-slack`],c=r[`slack-keychain`]||`slack-${a}-mcp-xoxp`,u={github:o.includes(`github`),linear:o.includes(`linear`),notion:o.includes(`notion`),slack:s?{keychain:c,addMessageTool:!!r[`slack-message`]}:!1},d=r.email||r[`git-name`]?{email:r.email,name:r[`git-name`]}:void 0,f={name:a,path:l(i),gh:r.gh,git:d,servers:u},p=pe(R()?z():L(),f);if(B(p),I(p),console.log(`✓ workspace "${a}" -> ${f.path}`),console.log(`✓ regenerated the hook, git includes, and ${f.path}/.mcp.json`),u.slack)if(r[`seed-slack`]){let e=await ye(`Paste the Slack xoxp token for ${c}: `);e?(_e(c,e),console.log(`✓ stored ${c} in the macOS keychain`)):console.error(`No token entered; skipped keychain write.`)}else G(c)||console.log(`\nSlack token not in the keychain yet. Store it once with:\n ${K(c)}`);console.log(`\nLaunch \`claude\` from ${f.path} (or relaunch) to pick up the new identity.`),process.exit(0)},xe=`Regenerate the chpwd hook, git includes, and every .mcp.json
80
+ -y, --yes accept defaults, skip all prompts (non-interactive)
81
+ -h, --help Display help message`,Ne=[{label:`github`,value:`github`,checked:!0},{label:`linear`,value:`linear`,checked:!1},{label:`notion`,value:`notion`,checked:!1},{label:`slack`,value:`slack`,checked:!1}],Pe=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||be(a);i&&!r.label&&(o=await U(`Label`,o));let s=r.gh;s===void 0&&i&&(s=await je(`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=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 Me(`MCP servers (space toggles, enter confirms)`,Ne):[`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=Se(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 Ae(`Paste the Slack xoxp token for ${m}: `);e?(Oe(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)},Fe=`Regenerate the chpwd hook, git includes, and every .mcp.json
76
82
  from your config. Idempotent: run it any time the config changes.
77
83
 
78
84
  Usage:
79
85
  $ ${q} apply
80
86
 
81
87
  Options:
82
- -h, --help Display help message`,Se=t=>{let{values:n}=e({allowPositionals:!0,options:{help:{type:`boolean`,short:`h`}},args:t});n.help&&(console.log(xe),process.exit(0)),R()||(console.error(`No config found. Run \`${q} init\` first.`),process.exit(1));let r=z(),i=I(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)},Ce=e=>{try{return t.readFileSync(e,`utf8`)}catch{return null}},we=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},Te=(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)})},Ee=(e=U)=>{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}},De=(e,n=U)=>{let r=[];he()||r.push({status:`warn`,label:`platform`,detail:`inscope's secret resolution targets macOS (gh keyring + Keychain)`});let i=f(),a=Ce(i);a===null?r.push({status:`fail`,label:`hook`,detail:`missing ${i}; run \`inscope init\``}):a===k(e)?r.push({status:`ok`,label:`hook`,detail:i}):r.push({status:`warn`,label:`hook`,detail:"out of date; run `inscope apply`"}),r.push(C(h(),F)===null?{status:`warn`,label:`zshrc`,detail:"does not source the hook; run `inscope init`"}:{status:`ok`,label:`zshrc`,detail:`sources the hook`}),e.workspaces.some(T)&&r.push(C(m(),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 e.workspaces){let e=`[${i.name}]`;if(i.gh&&r.push(ge(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(G(t,n)?{status:`ok`,label:`${e} slack`,detail:t}:{status:`fail`,label:`${e} slack`,detail:`${t} not in keychain; run \`${K(t)}\``})}if(T(i)){let a=E(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=ve(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=se(i);if(a===null)r.push({status:`warn`,label:`${e} mcp`,detail:"no .mcp.json; run `inscope apply`"});else{let t=j(i.name).filter(e=>a.mcpServers?.[e]);r.push({status:`ok`,label:`${e} mcp`,detail:`${t.length} server(s)`});let n=we(a);n.length&&r.push({status:`warn`,label:`${e} mcp`,detail:`unpinned: ${n.join(`, `)}`})}}return r},Oe=`Verify the setup: gh tokens resolve, keychain entries exist,
88
+ -h, --help Display help message`,Ie=t=>{let{values:n}=e({allowPositionals:!0,options:{help:{type:`boolean`,short:`h`}},args:t});n.help&&(console.log(Fe),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)},Le=e=>{try{return t.readFileSync(e,`utf8`)}catch{return null}},Re=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},ze=(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)})},Be=(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}},Ve=(e,n=I)=>{let r=[];we()||r.push({status:`warn`,label:`platform`,detail:`inscope's secret resolution targets macOS (gh keyring + Keychain)`});let i=f(),a=Le(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(ve()?{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(ne(m(),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(Te(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=ke(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=fe(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=Re(a);n.length&&r.push({status:`warn`,label:`${e} mcp`,detail:`unpinned: ${n.join(`, `)}`})}}return r},He=`Verify the setup: gh tokens resolve, keychain entries exist,
83
89
  git emails match per path, the hook is current, and no MCP server is unpinned.
84
90
  Exits non-zero if any check fails.
85
91
 
@@ -87,34 +93,34 @@ Usage:
87
93
  $ ${q} doctor
88
94
 
89
95
  Options:
90
- -h, --help Display help message`,ke={ok:`✓`,warn:`!`,fail:`✗`},Ae=t=>{let{values:n}=e({allowPositionals:!0,options:{help:{type:`boolean`,short:`h`}},args:t});n.help&&(console.log(Oe),process.exit(0)),R()||(console.error(`No config found. Run \`${q} init\` first.`),process.exit(1));let r=z(),i=De(r);for(let e of i)console.log(`${ke[e.status]} ${e.label}${e.detail?` ${e.detail}`:``}`);let a=Te(r);if(a){let e=Ee();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(`
91
- All checks passed.`),process.exit(0)},je=`Set up inscope: create the config, generate the chpwd hook, and
96
+ -h, --help Display help message`,Ue={ok:`✓`,warn:`!`,fail:`✗`},Z=t=>{let{values:n}=e({allowPositionals:!0,options:{help:{type:`boolean`,short:`h`}},args:t});n.help&&(console.log(He),process.exit(0)),N()||(console.error(`No config found. Run \`${q} init\` first.`),process.exit(1));let r=P(),i=Ve(r);for(let e of i)console.log(`${Ue[e.status]} ${e.label}${e.detail?` ${e.detail}`:``}`);let a=ze(r);if(a){let e=Be();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)},We=`Set up inscope: create the config, generate the chpwd hook, and
92
98
  source it from ~/.zshrc. Safe to run again; it never overwrites your config.
93
99
 
94
100
  Usage:
95
101
  $ ${q} init
96
102
 
97
103
  Options:
98
- -h, --help Display help message`,Me=t=>{let{values:n}=e({allowPositionals:!0,options:{help:{type:`boolean`,short:`h`}},args:t});n.help&&(console.log(je),process.exit(0));let r;R()?(r=z(),console.log(`Using existing config at ${d()}`)):(r=L(),B(r),console.log(`Created ${d()}`)),I(r),console.log(`Generated the chpwd hook and added a source line to ~/.zshrc.`),console.log(`
104
+ -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));let r;N()?(r=P(),console.log(`Using existing config at ${d()}`)):(r=M(),F(r),console.log(`Created ${d()}`)),j(r),console.log(`Generated the chpwd hook and added a source line to ~/.zshrc.`),console.log(`
99
105
  Next steps:
100
106
  1. Reload your shell: source ~/.zshrc (or open a new terminal)
101
107
  2. Sign each GitHub account in: gh auth login
102
108
  3. Map a workspace: ${q} add ~/acme --gh acme --email you@acme.com
103
- `),process.exit(0)},Ne=`List the configured workspaces. Run \`${q} doctor\` to verify
109
+ `),process.exit(0)},Ke=`List the configured workspaces. Run \`${q} doctor\` to verify
104
110
  that their tokens and identities actually resolve.
105
111
 
106
112
  Usage:
107
113
  $ ${q} list
108
114
 
109
115
  Options:
110
- -h, --help Display help message`,Z=e=>[e.github&&`github`,e.linear&&`linear`,e.notion&&`notion`,e.slack&&`slack`].filter(Boolean).join(`, `)||`none`,Pe=t=>{let{values:n}=e({allowPositionals:!0,options:{help:{type:`boolean`,short:`h`}},args:t});n.help&&(console.log(Ne),process.exit(0)),R()||(console.error(`No config found. Run \`${q} init\` first.`),process.exit(1));let r=z();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 ${Z(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
116
+ -h, --help Display help message`,qe=e=>[e.github&&`github`,e.linear&&`linear`,e.notion&&`notion`,e.slack&&`slack`].filter(Boolean).join(`, `)||`none`,Je=t=>{let{values:n}=e({allowPositionals:!0,options:{help:{type:`boolean`,short:`h`}},args:t});n.help&&(console.log(Ke),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 ${qe(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
111
117
  servers inscope manages; leaves your keychain and gh accounts untouched.
112
118
 
113
119
  Usage:
114
120
  $ ${q} rm <path|label>
115
121
 
116
122
  Options:
117
- -h, --help Display help message`,Fe=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);R()||(console.error(`No config found. Run \`${q} init\` first.`),process.exit(1));let{cfg:a,removed:o}=me(z(),i);o||(console.error(`No workspace matching "${i}".`),process.exit(1)),le(o),ae(o.name),B(a),I(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:
123
+ -h, --help Display help message`,Ye=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}=Ce(P(),i);o||(console.error(`No workspace matching "${i}".`),process.exit(1)),me(o),se(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:
118
124
  ${q}@${J}
119
125
 
120
126
  Per-workspace identity for Claude Code: scope MCP servers, GitHub auth, and git
@@ -136,4 +142,4 @@ Options:
136
142
  -h, --help Display help
137
143
 
138
144
  Author:
139
- ${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 Me(n);case`add`:return await be(n);case`rm`:case`remove`:return Fe(n);case`ls`:case`list`:return Pe(n);case`apply`:case`sync`:return Se(n);case`doctor`:return Ae(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{};
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 Ge(n);case`add`:return await Pe(n);case`rm`:case`remove`:return Ye(n);case`ls`:case`list`:return Je(n);case`apply`:case`sync`:return Ie(n);case`doctor`:return Z(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{};
package/dist/index.d.mts CHANGED
@@ -41,8 +41,9 @@ declare const removeWorkspace: (cfg: Config, key: string) => {
41
41
  };
42
42
  //#endregion
43
43
  //#region src/apply.d.ts
44
- declare const ZSHRC_BLOCK_ID = "zshrc";
44
+ declare const renderZshrcSource: (current: string) => string;
45
45
  declare const ensureZshrcSource: () => void;
46
+ declare const zshrcSourcesHook: () => boolean;
46
47
  type ApplyResult = {
47
48
  hook: string;
48
49
  gitconfig: boolean;
@@ -63,6 +64,8 @@ declare const defaultRunner: Runner;
63
64
  declare const isMacOS: () => boolean;
64
65
  declare const ghToken: (account: string, run?: Runner) => string | null;
65
66
  declare const ghStatus: (run?: Runner) => string;
67
+ declare const ghAccounts: (run?: Runner) => string[];
68
+ declare const gitGlobal: (key: string, run?: Runner) => string | null;
66
69
  declare const keychainHas: (service: string, run?: Runner) => boolean;
67
70
  declare const keychainSet: (service: string, token: string, run?: Runner) => void;
68
71
  declare const keychainSetCommand: (service: string) => string;
@@ -104,4 +107,4 @@ declare const readMcp: (ws: Workspace) => Record<string, any> | null;
104
107
  declare const applyMcp: (ws: Workspace) => void;
105
108
  declare const removeMcp: (ws: Workspace) => void;
106
109
  //#endregion
107
- export { ApplyResult, CONFIG_VERSION, Check, CheckStatus, Config, HttpServer, RunResult, Runner, SLACK_MCP_VERSION, Servers, SlackServer, Workspace, ZSHRC_BLOCK_ID, applyAll, applyGitconfig, applyMcp, configExists, currentWorkspace, defaultConfig, defaultRunner, ensureZshrcSource, findWorkspace, ghStatus, ghToken, gitEmailForFile, isMacOS, keychainHas, keychainSet, keychainSetCommand, labelFromPath, liveSnapshot, loadConfig, managedKeys, mcpFilePath, readMcp, removeMcp, removeWorkspace, renderGitInclude, renderHook, renderMcp, renderPerWorkspaceGitconfig, renderServers, runDoctor, saveConfig, upsertWorkspace, validateConfig };
110
+ export { ApplyResult, CONFIG_VERSION, Check, CheckStatus, Config, HttpServer, RunResult, Runner, SLACK_MCP_VERSION, Servers, SlackServer, Workspace, applyAll, applyGitconfig, applyMcp, configExists, currentWorkspace, defaultConfig, defaultRunner, ensureZshrcSource, findWorkspace, ghAccounts, ghStatus, ghToken, gitEmailForFile, gitGlobal, isMacOS, keychainHas, keychainSet, keychainSetCommand, labelFromPath, liveSnapshot, loadConfig, managedKeys, mcpFilePath, readMcp, removeMcp, removeWorkspace, renderGitInclude, renderHook, renderMcp, renderPerWorkspaceGitconfig, renderServers, renderZshrcSource, runDoctor, saveConfig, upsertWorkspace, validateConfig, zshrcSourcesHook };
package/dist/index.mjs CHANGED
@@ -1,11 +1,11 @@
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(),`claude`,`workspaces.json`),u=()=>t.join(a(),`claude`,`mcp-tokens.zsh`),d=()=>t.join(a(),`git`),f=()=>t.join(i(),`.gitconfig`),p=()=>t.join(i(),`.zshrc`),ee=1,te=()=>({version:1,workspaces:[]}),m=()=>e.existsSync(l()),h=()=>{let t=l(),n=e.readFileSync(t,`utf8`),r=JSON.parse(n);return _(r),r},g=n=>{let r=l();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)}},ne=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)},re=(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}},ie=(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?`),ae=(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``}},w=(n,r,i)=>{e.mkdirSync(t.dirname(n),{recursive:!0});let a=C(n),o=ae(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)},oe=(t,n)=>{let r=C(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(),`claude`,`workspaces.json`),u=()=>t.join(a(),`claude`,`mcp-tokens.zsh`),d=()=>t.join(a(),`git`),f=()=>t.join(i(),`.gitconfig`),p=()=>t.join(i(),`.zshrc`),ee=1,m=()=>({version:1,workspaces:[]}),te=()=>e.existsSync(l()),ne=()=>{let t=l(),n=e.readFileSync(t,`utf8`),r=JSON.parse(n);return h(r),r},re=n=>{let r=l();e.mkdirSync(t.dirname(r),{recursive:!0}),e.writeFileSync(r,JSON.stringify(n,null,2)+`
2
+ `)},h=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)),g=(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}},_=(e,t)=>{let n=g(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?`),S=(e,t)=>{let n=t.replace(/\n+$/,``);return`${v(e)}\n${n}\n${y(e)}\n`},C=t=>{try{return e.readFileSync(t,`utf8`)}catch{return``}},oe=(n,r,i)=>{e.mkdirSync(t.dirname(n),{recursive:!0});let a=C(n),o=S(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)},se=(t,n)=>{let r=C(t);if(!r)return;let i=r.replace(x(n),``).replace(/\n{3,}/g,`
3
3
 
4
- `).replace(/^\n+/,``);e.writeFileSync(t,i)},T=(e,t)=>{let n=C(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(d(),`${e}.gitconfig`),k=e=>s(e).replace(/\/+$/,``)+`/`,A=e=>e.workspaces.filter(D).map(e=>`[includeIf "gitdir:${k(e.path)}"]\n\tpath = ${s(O(e.name))}`).join(`
5
- `),j=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)},ce=(e,t)=>{let n=C(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(d(),`${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
- `},M=t=>{e.mkdirSync(d(),{recursive:!0});for(let n of t.workspaces)D(n)&&e.writeFileSync(O(n.name),j(n));let n=A(t);n?w(f(),E,n):oe(f(),E)},se=e=>{let t=s(e);return t===`~`?`"$HOME/"*`:t.startsWith(`~/`)?`"$HOME/${t.slice(2)}/"*`:`"${t}/"*`},ce=e=>e.servers.slack?e.servers.slack.keychain:``,N=e=>{let t=[...e.workspaces].sort((e,t)=>e.name.localeCompare(t.name));return`# Managed by inscope. Do not edit by hand.
8
- # Source of truth: ~/.config/claude/workspaces.json
7
+ `},A=t=>{e.mkdirSync(d(),{recursive:!0});for(let n of t.workspaces)T(n)&&e.writeFileSync(E(n.name),k(n));let n=O(t);n?oe(f(),w,n):se(f(),w)},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:``,j=e=>{let t=[...e.workspaces].sort((e,t)=>e.name.localeCompare(t.name));return`# Managed by inscope. Do not edit by hand.
8
+ # Source of truth: ~/.config/inscope/inscope.json
9
9
  # Edit there, then run \`inscope apply\` to regenerate this file.
10
10
  #
11
11
  # One chpwd hook resolves per-workspace secrets from \$PWD on every cd: it maps
@@ -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=>` ${se(e.path)}) ws=${e.name} ;;`).join(`
18
+ ${t.map(e=>` ${le(e.path)}) ws=${e.name} ;;`).join(`
19
19
  `)||` # no workspaces configured`}
20
20
  *) ws="" ;;
21
21
  esac
@@ -25,7 +25,7 @@ ${t.map(e=>` ${se(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=ce(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=ue(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
- `},P=`1.3.0`,le=[`github`,`linear`,`notion`,`slack`],F=e=>le.map(t=>`${t}-${e}`),I=e=>t.join(c(e.path),`.mcp.json`),L=(e,t)=>e&&typeof e==`object`&&e.url?e.url:t,R=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:L(t.linear,`https://mcp.linear.app/mcp`)}),t.notion&&(n[`notion-${e.name}`]={type:`http`,url:L(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@${P}`,`--transport`,`stdio`],env:r}}return n},z=e=>({mcpServers:R(e)}),B=t=>{if(!e.existsSync(t))return{};try{return JSON.parse(e.readFileSync(t,`utf8`))}catch{return{}}},V=t=>{let n=I(t);return e.existsSync(n)?B(n):null},H=n=>{let r=I(n);e.mkdirSync(t.dirname(r),{recursive:!0});let i=B(r),a=i.mcpServers&&typeof i.mcpServers==`object`?{...i.mcpServers}:{};for(let e of F(n.name))delete a[e];Object.assign(a,R(n)),i.mcpServers=a,e.writeFileSync(r,JSON.stringify(i,null,2)+`
55
- `)},U=t=>{let n=I(t);if(!e.existsSync(n))return;let r=B(n);if(r.mcpServers&&typeof r.mcpServers==`object`)for(let e of F(t.name))delete r.mcpServers[e];e.writeFileSync(n,JSON.stringify(r,null,2)+`
56
- `)},W=`zshrc`,ue=e=>{let n=i();return e===n?`$HOME`:e.startsWith(n+t.sep)?`$HOME/${e.slice(n.length+1)}`:e},G=()=>{let e=ue(u()),t=`# Loads each workspace's tokens (GitHub, Slack) from $PWD on every cd.\n[ -r "${e}" ] && source "${e}"`;w(p(),W,t)},de=n=>{let r=u();e.mkdirSync(t.dirname(r),{recursive:!0}),e.writeFileSync(r,N(n)),M(n),G();let i=[];for(let e of n.workspaces)H(e),i.push(I(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},fe=(e=K)=>{let t=e(`gh`,[`auth`,`status`]);return(t.stdout+t.stderr).trim()},X=(e,t=K)=>{let n=t(`security`,[`find-generic-password`,`-a`,J(),`-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`,J(),`-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 "${J()||`$USER`}" -s ${e} -w 'xoxp-...'`,Q=(e,t=K)=>{let n=t(`git`,[`config`,`--file`,e,`user.email`]);return n.status===0?n.stdout.trim():null},me=t=>{try{return e.readFileSync(t,`utf8`)}catch{return null}},he=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},ge=(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)})},$=(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}},_e=(t,n=K)=>{let r=[];q()||r.push({status:`warn`,label:`platform`,detail:`inscope's secret resolution targets macOS (gh keyring + Keychain)`});let i=u(),a=me(i);a===null?r.push({status:`fail`,label:`hook`,detail:`missing ${i}; run \`inscope init\``}):a===N(t)?r.push({status:`ok`,label:`hook`,detail:i}):r.push({status:`warn`,label:`hook`,detail:"out of date; run `inscope apply`"}),r.push(T(p(),W)===null?{status:`warn`,label:`zshrc`,detail:"does not source the hook; run `inscope init`"}:{status:`ok`,label:`zshrc`,detail:`sources the hook`}),t.workspaces.some(D)&&r.push(T(f(),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(X(e,n)?{status:`ok`,label:`${t} slack`,detail:e}:{status:`fail`,label:`${t} slack`,detail:`${e} not in keychain; run \`${Z(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=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=V(i);if(a===null)r.push({status:`warn`,label:`${t} mcp`,detail:"no .mcp.json; run `inscope apply`"});else{let e=F(i.name).filter(e=>a.mcpServers?.[e]);r.push({status:`ok`,label:`${t} mcp`,detail:`${e.length} server(s)`});let n=he(a);n.length&&r.push({status:`warn`,label:`${t} mcp`,detail:`unpinned: ${n.join(`, `)}`})}}return r};export{ee as CONFIG_VERSION,P as SLACK_MCP_VERSION,W as ZSHRC_BLOCK_ID,de as applyAll,M as applyGitconfig,H as applyMcp,m as configExists,ge as currentWorkspace,te as defaultConfig,K as defaultRunner,G as ensureZshrcSource,v as findWorkspace,fe as ghStatus,Y as ghToken,Q as gitEmailForFile,q as isMacOS,X as keychainHas,pe as keychainSet,Z as keychainSetCommand,ne as labelFromPath,$ as liveSnapshot,h as loadConfig,F as managedKeys,I as mcpFilePath,V as readMcp,U as removeMcp,ie as removeWorkspace,A as renderGitInclude,N as renderHook,z as renderMcp,j as renderPerWorkspaceGitconfig,R as renderServers,_e as runDoctor,g as saveConfig,re as upsertWorkspace,_ as validateConfig};
54
+ `},M=`1.3.0`,de=[`github`,`linear`,`notion`,`slack`],N=e=>de.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=>{let n=P(t);return e.existsSync(n)?R(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,I(n)),i.mcpServers=a,e.writeFileSync(r,JSON.stringify(i,null,2)+`
55
+ `)},V=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
+ `)},fe=e=>{let n=i();return e===n?`$HOME`:e.startsWith(n+t.sep)?`$HOME/${e.slice(n.length+1)}`:e},H=()=>{let e=fe(u());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=p(),n=``;try{n=e.readFileSync(t,`utf8`)}catch{}let r=U(n);r!==n&&e.writeFileSync(t,r)},G=()=>{try{return e.readFileSync(p(),`utf8`).includes(H())}catch{return!1}},pe=n=>{let r=u();e.mkdirSync(t.dirname(r),{recursive:!0}),e.writeFileSync(r,j(n)),A(n),W();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}},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()},me=(e=K)=>{let t=[];for(let n of X(e).matchAll(/account (\S+) \(/g))t.includes(n[1])||t.push(n[1]);return t},he=(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},ge=(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},_e=t=>{try{return e.readFileSync(t,`utf8`)}catch{return null}},ve=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},ye=(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)})},be=(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=(t,n=K)=>{let r=[];q()||r.push({status:`warn`,label:`platform`,detail:`inscope's secret resolution targets macOS (gh keyring + Keychain)`});let i=u(),a=_e(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(ce(f(),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=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=ve(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,pe as applyAll,A as applyGitconfig,B as applyMcp,te as configExists,ye as currentWorkspace,m as defaultConfig,K as defaultRunner,W as ensureZshrcSource,g as findWorkspace,me as ghAccounts,X as ghStatus,Y as ghToken,$ as gitEmailForFile,he as gitGlobal,q as isMacOS,Z as keychainHas,ge as keychainSet,Q as keychainSetCommand,ie as labelFromPath,be as liveSnapshot,ne as loadConfig,N as managedKeys,P as mcpFilePath,z as readMcp,V as removeMcp,_ as removeWorkspace,O as renderGitInclude,j as renderHook,L as renderMcp,k as renderPerWorkspaceGitconfig,I as renderServers,U as renderZshrcSource,xe as runDoctor,re as saveConfig,ae as upsertWorkspace,h as validateConfig,G as zshrcSourcesHook};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "inscope",
3
- "version": "0.2.0-canary.0",
3
+ "version": "0.2.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",