inscope 0.1.0 โ†’ 0.2.0-canary.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
@@ -1,41 +1,104 @@
1
- # inscope
1
+ # Inscope
2
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.
3
+ **Per-workspace identity for [Claude Code](https://claude.com/claude-code): scope MCP servers, GitHub auth, and git commit identity to the directory you are in.**
7
4
 
8
- You describe each workspace once; `inscope` owns the moving parts and keeps them
9
- in sync:
5
+ [![Twitter](https://img.shields.io/twitter/follow/nrjdalal_dev?label=%40nrjdalal_dev)](https://twitter.com/nrjdalal_dev)
6
+ [![npm](https://img.shields.io/npm/v/inscope?color=red&logo=npm)](https://www.npmjs.com/package/inscope)
7
+ [![downloads](https://img.shields.io/npm/dt/inscope?color=red&logo=npm)](https://www.npmjs.com/package/inscope)
8
+ [![stars](https://img.shields.io/github/stars/nrjdalal/inscope?color=blue)](https://github.com/nrjdalal/inscope)
9
+
10
+ > #### `cd` into a project and you are the right person: the right GitHub token, the right MCP servers, the right git commit email, all resolved live from `$PWD`. No toggles, no profile switching, and it holds up with several Claude Code sessions open at once.
11
+
12
+ Concurrent sessions in different projects should never bleed work and personal accounts into each other. You describe each workspace once; `inscope` owns the moving parts and keeps them in sync:
10
13
 
11
14
  - a `.mcp.json` at each workspace root, with uniquely named servers
12
15
  - a single zsh `chpwd` hook that resolves the right tokens from `$PWD`
13
16
  - git `includeIf` rules so commits get the right email per path
14
17
 
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.
18
+ Nothing sensitive is written to disk. GitHub tokens come from the `gh` keyring and Slack tokens from the macOS Keychain, resolved live by the hook.
19
+
20
+ > Background and the why behind the design: [Race-Free Identity in Claude Code](https://zerostarter.dev/blog/mcp-per-workspace).
21
+
22
+ ---
23
+
24
+ ### Table of Contents
25
+
26
+ - [Some Examples](#-some-examples)
27
+ - [Features](#-features)
28
+ - [Requirements](#-requirements)
29
+ - [Quick Usage](#-quick-usage)
30
+ - [Commands](#-commands)
31
+ - [What It Manages](#-what-it-manages)
32
+ - [MCP Servers](#-mcp-servers)
33
+ - [Config File](#-config-file)
34
+ - [Contributing](#-contributing)
35
+
36
+ ---
37
+
38
+ ## ๐Ÿ“– Some Examples
39
+
40
+ ```sh
41
+ # set up the config + hook, and source it from ~/.zshrc
42
+ inscope init
43
+
44
+ # map a work directory: work gh account, work email, work + slack servers
45
+ inscope add ~/acme --gh acme --email you@acme.com --servers github,linear,notion,slack
46
+
47
+ # map a personal directory: just your gh account and personal email
48
+ inscope add ~/nrjdalal --gh nrjdalal --email you@personal.dev
49
+
50
+ # list what is configured
51
+ inscope list
52
+
53
+ # verify tokens, identities, and the hook all resolve
54
+ inscope doctor
55
+
56
+ # regenerate everything after editing the config by hand
57
+ inscope apply
58
+
59
+ # remove a workspace mapping
60
+ inscope rm ~/acme
61
+ ```
62
+
63
+ `cd ~/acme/api` and you are the work account, with work MCP servers and your work commit email. `cd ~/nrjdalal/blog` and you are you.
64
+
65
+ ---
66
+
67
+ ## โœจ Features
68
+
69
+ - ๐Ÿชช Per-directory identity: GitHub token, git commit email, and MCP servers scoped to `$PWD`
70
+ - ๐Ÿงต Race-free across concurrent shells and Claude Code sessions, with no global toggles
71
+ - ๐Ÿ” No secrets on disk: GitHub tokens from the `gh` keyring, Slack tokens from the macOS Keychain
72
+ - ๐Ÿค– Generates a `.mcp.json` per workspace with uniquely named GitHub, Linear, Notion and Slack servers
73
+ - โœ‰๏ธ Git `includeIf` rules so every commit lands with the right author email per path
74
+ - ๐Ÿช A single zsh `chpwd` hook does all the resolution; nothing else touches your shell
75
+ - ๐Ÿฉบ `inscope doctor` verifies tokens, identities, and the hook before you trust them
76
+ - โ™ป๏ธ Idempotent and surgical: only the managed blocks in `.zshrc`, `.gitconfig` and `.mcp.json` are touched
77
+
78
+ ---
17
79
 
18
- > Background and the why behind the design:
19
- > [Race-Free Identity in Claude Code](https://zerostarter.dev/blog/mcp-per-workspace).
80
+ ## ๐Ÿงฐ Requirements
20
81
 
21
- ## Requirements
82
+ macOS, zsh, [`gh`](https://cli.github.com), and [Claude Code](https://claude.com/claude-code).
22
83
 
23
- macOS, zsh, [`gh`](https://cli.github.com), and Claude Code.
84
+ ---
24
85
 
25
- ## Install
86
+ ## ๐Ÿš€ Quick Usage
26
87
 
27
- ```bash
88
+ Install globally (the CLI manages your shell hook, so a global install is expected):
89
+
90
+ ```sh
28
91
  npm i -g inscope
29
92
  ```
30
93
 
31
- ## Quickstart
94
+ Then walk through the setup once:
32
95
 
33
- ```bash
96
+ ```sh
34
97
  # 1. set up the config + hook, and source it from ~/.zshrc
35
98
  inscope init
36
99
 
37
- # 2. sign each GitHub account into gh (once)
38
- gh auth login # repeat per account
100
+ # 2. sign each GitHub account into gh (once per account)
101
+ gh auth login
39
102
 
40
103
  # 3. map your workspaces
41
104
  inscope add ~/acme --gh acme --email you@acme.com --servers github,linear,notion,slack
@@ -46,24 +109,44 @@ source ~/.zshrc
46
109
  inscope doctor
47
110
  ```
48
111
 
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.
112
+ Launch `claude` from inside a mapped directory (or relaunch) to pick up the identity. No toggles, and it holds up with several terminals open at once.
113
+
114
+ ---
52
115
 
53
- ## Commands
116
+ ## ๐Ÿ”ง Commands
54
117
 
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 |
118
+ ```
119
+ inscope init Create the config, generate the hook, source it from ~/.zshrc
120
+ inscope add <path> Map a directory to a GitHub account, git email, and MCP servers
121
+ inscope rm <path> Remove a workspace mapping (alias: remove)
122
+ inscope list List configured workspaces (alias: ls)
123
+ inscope apply Regenerate the hook, git includes, and .mcp.json (alias: sync)
124
+ inscope doctor Verify tokens, identities, and the hook resolve correctly
125
+
126
+ -v, --version Display version
127
+ -h, --help Display help
128
+ ```
63
129
 
64
130
  Run any command with `-h` for its options.
65
131
 
66
- ## What it manages
132
+ ### `inscope add` options
133
+
134
+ ```
135
+ --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)
138
+ --label <name> workspace name; defaults to the directory basename
139
+ --servers <list> comma-separated: github,linear,notion,slack
140
+ (default: github,linear,notion)
141
+ --slack-keychain <s> keychain service for the Slack token
142
+ (default: slack-<label>-mcp-xoxp when slack is on)
143
+ --slack-message allow the Slack MCP server to post messages
144
+ --seed-slack prompt for the Slack token and store it in the keychain
145
+ ```
146
+
147
+ ---
148
+
149
+ ## ๐Ÿงฉ What It Manages
67
150
 
68
151
  | Surface | Location |
69
152
  | ------------ | ----------------------------------------------------------- |
@@ -72,10 +155,65 @@ Run any command with `-h` for its options.
72
155
  | MCP servers | `<workspace>/.mcp.json` |
73
156
  | Git identity | `~/.gitconfig` includeIf + `~/.config/git/<name>.gitconfig` |
74
157
 
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.
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`.
159
+
160
+ ---
161
+
162
+ ## ๐Ÿค– MCP Servers
163
+
164
+ Each enabled server is written into the workspace `.mcp.json` with a name suffixed by the workspace label (for example `github-acme`), so servers from different workspaces never collide.
165
+
166
+ | Server | Transport | Token source |
167
+ | -------- | --------- | ---------------------------------------------- |
168
+ | `github` | http | `GITHUB_TOKEN` from the active `gh` account |
169
+ | `linear` | http | OAuth via the Linear MCP endpoint |
170
+ | `notion` | http | OAuth via the Notion MCP endpoint |
171
+ | `slack` | stdio | `SLACK_MCP_XOXP_TOKEN` from the macOS Keychain |
172
+
173
+ Slack is opt-in. Enable it with `--servers ...,slack`, then store the token once:
174
+
175
+ ```sh
176
+ inscope add ~/acme --gh acme --servers github,linear,notion,slack --seed-slack
177
+ ```
178
+
179
+ `--seed-slack` prompts for the `xoxp` token and writes it to the Keychain. Pass `--slack-message` to allow the Slack MCP server to post messages.
180
+
181
+ ---
182
+
183
+ ## ๐Ÿ“‹ Config File
184
+
185
+ The source of truth is `~/.config/claude/workspaces.json`:
186
+
187
+ ```jsonc
188
+ {
189
+ "version": 1,
190
+ "workspaces": [
191
+ {
192
+ "name": "acme",
193
+ "path": "~/acme",
194
+ "gh": "acme",
195
+ "git": { "email": "you@acme.com" },
196
+ "servers": {
197
+ "github": true,
198
+ "linear": true,
199
+ "notion": true,
200
+ "slack": { "keychain": "slack-acme-mcp-xoxp", "addMessageTool": false }
201
+ }
202
+ }
203
+ ]
204
+ }
205
+ ```
206
+
207
+ Edit it directly, then run `inscope apply` to regenerate the hook, git includes, and every `.mcp.json`. `inscope doctor` will tell you if anything no longer resolves.
208
+
209
+ ---
210
+
211
+ ## ๐Ÿค Contributing
212
+
213
+ Issues and pull requests are welcome. Run the tests with `bun test` and the type checks with `bun run typecheck` before opening a PR.
214
+
215
+ ---
78
216
 
79
217
  ## License
80
218
 
81
- MIT
219
+ [MIT](./LICENSE) ยฉ [Neeraj Dalal](https://nrjdalal.com)
@@ -55,7 +55,7 @@ __inscope_resolve_identity
55
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
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
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.
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
59
  Re-running with the same path or label updates that workspace.
60
60
 
61
61
  Usage:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "inscope",
3
- "version": "0.1.0",
3
+ "version": "0.2.0-canary.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",
@@ -26,49 +26,10 @@
26
26
  "type": "module",
27
27
  "exports": "./dist/index.mjs",
28
28
  "types": "./dist/index.d.mts",
29
- "bin": {
30
- "inscope": "./dist/bin/index.mjs"
31
- },
32
29
  "files": [
33
30
  "dist"
34
31
  ],
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"
32
+ "bin": {
33
+ "inscope": "dist/bin/index.mjs"
73
34
  }
74
35
  }