inscope 0.1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Neeraj Dalal
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,81 @@
1
+ # inscope
2
+
3
+ Per-workspace identity for [Claude Code](https://claude.com/claude-code). Scope
4
+ MCP servers, GitHub auth, and git commit identity to the directory you are in,
5
+ so concurrent sessions in different projects never bleed work and personal
6
+ accounts into each other.
7
+
8
+ You describe each workspace once; `inscope` owns the moving parts and keeps them
9
+ in sync:
10
+
11
+ - a `.mcp.json` at each workspace root, with uniquely named servers
12
+ - a single zsh `chpwd` hook that resolves the right tokens from `$PWD`
13
+ - git `includeIf` rules so commits get the right email per path
14
+
15
+ Nothing sensitive is written to disk. GitHub tokens come from the `gh` keyring
16
+ and Slack tokens from the macOS Keychain, resolved live by the hook.
17
+
18
+ > Background and the why behind the design:
19
+ > [Race-Free Identity in Claude Code](https://zerostarter.dev/blog/mcp-per-workspace).
20
+
21
+ ## Requirements
22
+
23
+ macOS, zsh, [`gh`](https://cli.github.com), and Claude Code.
24
+
25
+ ## Install
26
+
27
+ ```bash
28
+ npm i -g inscope
29
+ ```
30
+
31
+ ## Quickstart
32
+
33
+ ```bash
34
+ # 1. set up the config + hook, and source it from ~/.zshrc
35
+ inscope init
36
+
37
+ # 2. sign each GitHub account into gh (once)
38
+ gh auth login # repeat per account
39
+
40
+ # 3. map your workspaces
41
+ inscope add ~/acme --gh acme --email you@acme.com --servers github,linear,notion,slack
42
+ inscope add ~/nrjdalal --gh nrjdalal --email you@personal.dev
43
+
44
+ # 4. reload your shell, then verify
45
+ source ~/.zshrc
46
+ inscope doctor
47
+ ```
48
+
49
+ `cd ~/acme/api` and you are the work account, with work MCP servers and your
50
+ work commit email. `cd ~/nrjdalal/blog` and you are you. No toggles, and it
51
+ holds up with several terminals open at once.
52
+
53
+ ## Commands
54
+
55
+ | Command | What it does |
56
+ | -------------- | -------------------------------------------------------------------- |
57
+ | `inscope init` | create the config, generate the hook, source it from `~/.zshrc` |
58
+ | `inscope add` | map a directory to a gh account, git email, and MCP servers |
59
+ | `inscope rm` | remove a workspace mapping |
60
+ | `inscope list` | list configured workspaces |
61
+ | `inscope apply` | regenerate the hook, git includes, and every `.mcp.json` from config |
62
+ | `inscope doctor` | verify tokens, identities, and the hook resolve correctly |
63
+
64
+ Run any command with `-h` for its options.
65
+
66
+ ## What it manages
67
+
68
+ | Surface | Location |
69
+ | ------------ | ----------------------------------------------------------- |
70
+ | Config | `~/.config/claude/workspaces.json` |
71
+ | chpwd hook | `~/.config/claude/mcp-tokens.zsh` |
72
+ | MCP servers | `<workspace>/.mcp.json` |
73
+ | Git identity | `~/.gitconfig` includeIf + `~/.config/git/<name>.gitconfig` |
74
+
75
+ Edit `workspaces.json` by hand if you like, then run `inscope apply`. It only
76
+ touches the blocks it owns; your other `.zshrc`, `.gitconfig`, and `.mcp.json`
77
+ content is left alone.
78
+
79
+ ## License
80
+
81
+ MIT
@@ -0,0 +1,139 @@
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,`
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(`
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
9
+ # Edit there, then run \`inscope apply\` to regenerate this file.
10
+ #
11
+ # One chpwd hook resolves per-workspace secrets from \$PWD on every cd: it maps
12
+ # the current directory to a workspace, then pulls that workspace's GitHub token
13
+ # from the gh keyring and Slack token from the macOS keychain. Nothing sensitive
14
+ # is written to disk, and there is no shared mutable state for sessions to race.
15
+ __inscope_resolve_identity() {
16
+ local ws
17
+ case "\${PWD}/" in
18
+ ${t.map(e=>` ${D(e.path)}) ws=${e.name} ;;`).join(`
19
+ `)||` # no workspaces configured`}
20
+ *) ws="" ;;
21
+ esac
22
+ [[ "$ws" == "$__inscope_ws" ]] && return # workspace unchanged, skip the lookups
23
+ __inscope_ws="$ws"
24
+ unset GITHUB_TOKEN GH_TOKEN SLACK_MCP_XOXP_TOKEN # clear previous (and any inherited) tokens
25
+
26
+ local gh_user="" slack_svc=""
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(`
29
+ `)||` # no workspaces configured`}
30
+ *) return ;; # outside a mapped workspace: nothing set
31
+ esac
32
+
33
+ local tok
34
+ if [[ -n "$gh_user" ]]; then
35
+ if tok="$(gh auth token -u "$gh_user" 2>/dev/null)" && [[ -n "$tok" ]]; then
36
+ export GITHUB_TOKEN="$tok" GH_TOKEN="$tok"
37
+ else
38
+ print -u2 "inscope: no gh token for $gh_user; GITHUB_TOKEN/GH_TOKEN unset"
39
+ fi
40
+ fi
41
+ if [[ -n "$slack_svc" ]]; then
42
+ if tok="$(security find-generic-password -a "$USER" -s "$slack_svc" -w 2>/dev/null)" && [[ -n "$tok" ]]; then
43
+ export SLACK_MCP_XOXP_TOKEN="$tok"
44
+ else
45
+ print -u2 "inscope: $slack_svc not in keychain; SLACK_MCP_XOXP_TOKEN unset"
46
+ fi
47
+ fi
48
+ }
49
+
50
+ autoload -Uz add-zsh-hook
51
+ add-zsh-hook chpwd __inscope_resolve_identity
52
+ __inscope_ws="__init__" # force the first resolve, clearing any inherited token
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.1.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.
60
+
61
+ Usage:
62
+ $ ${q} add <path> [options]
63
+
64
+ Options:
65
+ --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)
68
+ --label <name> workspace name; defaults to the directory basename
69
+ --servers <list> comma-separated: github,linear,notion,slack
70
+ (default: github,linear,notion)
71
+ --slack-keychain <s> keychain service for the Slack token
72
+ (default: slack-<label>-mcp-xoxp when slack is on)
73
+ --slack-message allow the Slack MCP server to post messages
74
+ --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
76
+ from your config. Idempotent: run it any time the config changes.
77
+
78
+ Usage:
79
+ $ ${q} apply
80
+
81
+ 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,
83
+ git emails match per path, the hook is current, and no MCP server is unpinned.
84
+ Exits non-zero if any check fails.
85
+
86
+ Usage:
87
+ $ ${q} doctor
88
+
89
+ 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
92
+ source it from ~/.zshrc. Safe to run again; it never overwrites your config.
93
+
94
+ Usage:
95
+ $ ${q} init
96
+
97
+ 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(`
99
+ Next steps:
100
+ 1. Reload your shell: source ~/.zshrc (or open a new terminal)
101
+ 2. Sign each GitHub account in: gh auth login
102
+ 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
104
+ that their tokens and identities actually resolve.
105
+
106
+ Usage:
107
+ $ ${q} list
108
+
109
+ 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
111
+ servers inscope manages; leaves your keychain and gh accounts untouched.
112
+
113
+ Usage:
114
+ $ ${q} rm <path|label>
115
+
116
+ 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:
118
+ ${q}@${J}
119
+
120
+ Per-workspace identity for Claude Code: scope MCP servers, GitHub auth, and git
121
+ commit identity to the directory you are in, so concurrent sessions never clash.
122
+
123
+ Usage:
124
+ $ ${q} <command> [options]
125
+
126
+ Commands:
127
+ init Create the config, generate the hook, source it from ~/.zshrc
128
+ add <path> Map a directory to a GitHub account, git email, and MCP servers
129
+ rm <path> Remove a workspace mapping (alias: remove)
130
+ list List configured workspaces (alias: ls)
131
+ apply Regenerate the hook, git includes, and .mcp.json (alias: sync)
132
+ doctor Verify tokens, identities, and the hook resolve correctly
133
+
134
+ Options:
135
+ -v, --version Display version
136
+ -h, --help Display help
137
+
138
+ 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{};
@@ -0,0 +1,107 @@
1
+ //#region src/config.d.ts
2
+ type SlackServer = {
3
+ keychain: string;
4
+ addMessageTool?: boolean;
5
+ };
6
+ type HttpServer = {
7
+ url?: string;
8
+ };
9
+ type Servers = {
10
+ github?: boolean;
11
+ linear?: boolean | HttpServer;
12
+ notion?: boolean | HttpServer;
13
+ slack?: SlackServer | false;
14
+ };
15
+ type Workspace = {
16
+ name: string;
17
+ path: string;
18
+ gh?: string;
19
+ git?: {
20
+ email?: string;
21
+ name?: string;
22
+ };
23
+ servers: Servers;
24
+ };
25
+ type Config = {
26
+ version: number;
27
+ workspaces: Workspace[];
28
+ };
29
+ declare const CONFIG_VERSION = 1;
30
+ declare const defaultConfig: () => Config;
31
+ declare const configExists: () => boolean;
32
+ declare const loadConfig: () => Config;
33
+ declare const saveConfig: (cfg: Config) => void;
34
+ declare const validateConfig: (cfg: Config) => void;
35
+ declare const labelFromPath: (p: string) => string;
36
+ declare const findWorkspace: (cfg: Config, key: string) => Workspace | undefined;
37
+ declare const upsertWorkspace: (cfg: Config, ws: Workspace) => Config;
38
+ declare const removeWorkspace: (cfg: Config, key: string) => {
39
+ cfg: Config;
40
+ removed?: Workspace;
41
+ };
42
+ //#endregion
43
+ //#region src/apply.d.ts
44
+ declare const ZSHRC_BLOCK_ID = "zshrc";
45
+ declare const ensureZshrcSource: () => void;
46
+ type ApplyResult = {
47
+ hook: string;
48
+ gitconfig: boolean;
49
+ mcp: string[];
50
+ };
51
+ declare const applyAll: (cfg: Config) => ApplyResult;
52
+ //#endregion
53
+ //#region src/secrets.d.ts
54
+ type RunResult = {
55
+ status: number;
56
+ stdout: string;
57
+ stderr: string;
58
+ };
59
+ type Runner = (cmd: string, args: string[], opts?: {
60
+ input?: string;
61
+ }) => RunResult;
62
+ declare const defaultRunner: Runner;
63
+ declare const isMacOS: () => boolean;
64
+ declare const ghToken: (account: string, run?: Runner) => string | null;
65
+ declare const ghStatus: (run?: Runner) => string;
66
+ declare const keychainHas: (service: string, run?: Runner) => boolean;
67
+ declare const keychainSet: (service: string, token: string, run?: Runner) => void;
68
+ declare const keychainSetCommand: (service: string) => string;
69
+ declare const gitEmailForFile: (file: string, run?: Runner) => string | null;
70
+ //#endregion
71
+ //#region src/doctor.d.ts
72
+ type CheckStatus = "ok" | "warn" | "fail";
73
+ type Check = {
74
+ status: CheckStatus;
75
+ label: string;
76
+ detail?: string;
77
+ };
78
+ declare const currentWorkspace: (cfg: Config, cwd?: string) => Workspace | undefined;
79
+ declare const liveSnapshot: (run?: Runner) => {
80
+ pwd: string;
81
+ gh: string;
82
+ gitEmail: string;
83
+ tokenSet: boolean;
84
+ };
85
+ declare const runDoctor: (cfg: Config, run?: Runner) => Check[];
86
+ //#endregion
87
+ //#region src/generators/hook.d.ts
88
+ declare const renderHook: (cfg: Config) => string;
89
+ //#endregion
90
+ //#region src/generators/gitconfig.d.ts
91
+ declare const renderGitInclude: (cfg: Config) => string;
92
+ declare const renderPerWorkspaceGitconfig: (ws: Workspace) => string;
93
+ declare const applyGitconfig: (cfg: Config) => void;
94
+ //#endregion
95
+ //#region src/generators/mcp.d.ts
96
+ declare const SLACK_MCP_VERSION = "1.3.0";
97
+ declare const managedKeys: (name: string) => string[];
98
+ declare const mcpFilePath: (ws: Workspace) => string;
99
+ declare const renderServers: (ws: Workspace) => Record<string, unknown>;
100
+ declare const renderMcp: (ws: Workspace) => {
101
+ mcpServers: Record<string, unknown>;
102
+ };
103
+ declare const readMcp: (ws: Workspace) => Record<string, any> | null;
104
+ declare const applyMcp: (ws: Workspace) => void;
105
+ declare const removeMcp: (ws: Workspace) => void;
106
+ //#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 };
package/dist/index.mjs ADDED
@@ -0,0 +1,56 @@
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,`
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(`
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
9
+ # Edit there, then run \`inscope apply\` to regenerate this file.
10
+ #
11
+ # One chpwd hook resolves per-workspace secrets from \$PWD on every cd: it maps
12
+ # the current directory to a workspace, then pulls that workspace's GitHub token
13
+ # from the gh keyring and Slack token from the macOS keychain. Nothing sensitive
14
+ # is written to disk, and there is no shared mutable state for sessions to race.
15
+ __inscope_resolve_identity() {
16
+ local ws
17
+ case "\${PWD}/" in
18
+ ${t.map(e=>` ${se(e.path)}) ws=${e.name} ;;`).join(`
19
+ `)||` # no workspaces configured`}
20
+ *) ws="" ;;
21
+ esac
22
+ [[ "$ws" == "$__inscope_ws" ]] && return # workspace unchanged, skip the lookups
23
+ __inscope_ws="$ws"
24
+ unset GITHUB_TOKEN GH_TOKEN SLACK_MCP_XOXP_TOKEN # clear previous (and any inherited) tokens
25
+
26
+ local gh_user="" slack_svc=""
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(`
29
+ `)||` # no workspaces configured`}
30
+ *) return ;; # outside a mapped workspace: nothing set
31
+ esac
32
+
33
+ local tok
34
+ if [[ -n "$gh_user" ]]; then
35
+ if tok="$(gh auth token -u "$gh_user" 2>/dev/null)" && [[ -n "$tok" ]]; then
36
+ export GITHUB_TOKEN="$tok" GH_TOKEN="$tok"
37
+ else
38
+ print -u2 "inscope: no gh token for $gh_user; GITHUB_TOKEN/GH_TOKEN unset"
39
+ fi
40
+ fi
41
+ if [[ -n "$slack_svc" ]]; then
42
+ if tok="$(security find-generic-password -a "$USER" -s "$slack_svc" -w 2>/dev/null)" && [[ -n "$tok" ]]; then
43
+ export SLACK_MCP_XOXP_TOKEN="$tok"
44
+ else
45
+ print -u2 "inscope: $slack_svc not in keychain; SLACK_MCP_XOXP_TOKEN unset"
46
+ fi
47
+ fi
48
+ }
49
+
50
+ autoload -Uz add-zsh-hook
51
+ add-zsh-hook chpwd __inscope_resolve_identity
52
+ __inscope_ws="__init__" # force the first resolve, clearing any inherited token
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};
package/package.json ADDED
@@ -0,0 +1,74 @@
1
+ {
2
+ "name": "inscope",
3
+ "version": "0.1.0",
4
+ "description": "Per-workspace identity for Claude Code: scope MCP servers, GitHub auth, and git commit identity to the directory you are in.",
5
+ "keywords": [
6
+ "claude-code",
7
+ "mcp",
8
+ "cli",
9
+ "git",
10
+ "github",
11
+ "identity",
12
+ "workspace",
13
+ "zsh",
14
+ "chpwd"
15
+ ],
16
+ "homepage": "https://github.com/nrjdalal/inscope#readme",
17
+ "bugs": "https://github.com/nrjdalal/inscope/issues",
18
+ "repository": "nrjdalal/inscope",
19
+ "funding": "https://github.com/sponsors/nrjdalal",
20
+ "license": "MIT",
21
+ "author": {
22
+ "name": "Neeraj Dalal",
23
+ "email": "admin@nrjdalal.com",
24
+ "url": "https://nrjdalal.com"
25
+ },
26
+ "type": "module",
27
+ "exports": "./dist/index.mjs",
28
+ "types": "./dist/index.d.mts",
29
+ "bin": {
30
+ "inscope": "./dist/bin/index.mjs"
31
+ },
32
+ "files": [
33
+ "dist"
34
+ ],
35
+ "scripts": {
36
+ "bin": "tsdown && node dist/bin/index.mjs",
37
+ "build": "tsdown",
38
+ "dev": "tsdown --watch",
39
+ "test": "bun test",
40
+ "typecheck": "tsc --noEmit",
41
+ "prepare": "npx simple-git-hooks"
42
+ },
43
+ "simple-git-hooks": {
44
+ "pre-commit": "npx lint-staged",
45
+ "commit-msg": "npx commitlint --edit $1"
46
+ },
47
+ "commitlint": {
48
+ "extends": [
49
+ "@commitlint/config-conventional"
50
+ ]
51
+ },
52
+ "lint-staged": {
53
+ "*": "prettier --write --ignore-unknown",
54
+ "package.json": "sort-package-json"
55
+ },
56
+ "prettier": {
57
+ "plugins": [
58
+ "@ianvs/prettier-plugin-sort-imports"
59
+ ],
60
+ "semi": false
61
+ },
62
+ "devDependencies": {
63
+ "@commitlint/cli": "^19.8.1",
64
+ "@commitlint/config-conventional": "^19.8.1",
65
+ "@ianvs/prettier-plugin-sort-imports": "^4.4.2",
66
+ "@types/node": "^22.15.32",
67
+ "lint-staged": "^15.5.2",
68
+ "prettier": "^3.5.3",
69
+ "simple-git-hooks": "^2.13.0",
70
+ "sort-package-json": "^3.2.1",
71
+ "tsdown": "^0.16.4",
72
+ "typescript": "^5.8.3"
73
+ }
74
+ }