inscope 0.8.3 โ†’ 0.8.5

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
@@ -7,31 +7,37 @@
7
7
  [![downloads](https://img.shields.io/npm/dt/inscope?color=red&logo=npm)](https://www.npmjs.com/package/inscope)
8
8
  [![stars](https://img.shields.io/github/stars/nrjdalal/inscope?color=blue)](https://github.com/nrjdalal/inscope)
9
9
 
10
- ๐Ÿ“– **The why behind the design:** [Race-Free Identity in Claude Code](https://zerostarter.dev/blog/mcp-per-workspace) aka multiple gh, linear, notion, slack and other accounts.
10
+ ๐Ÿ“– **The why behind the design:** [Race-Free Identity in Claude Code](https://zerostarter.dev/blog/mcp-per-workspace), aka multiple gh, linear, notion, slack and other accounts.
11
11
 
12
12
  > #### `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.
13
13
 
14
14
  <p align="center">
15
- <img src="https://raw.githubusercontent.com/nrjdalal/demo-kit/main/inscope/demo.gif" alt="inscope demo: interactive add, list, and doctor" width="900" />
15
+ <img src="https://raw.githubusercontent.com/nrjdalal/inscope/main/.github/assets/demo.gif" alt="inscope flips git identity and the GitHub token per directory on cd" width="900" />
16
16
  </p>
17
17
 
18
- 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:
18
+ You describe each workspace once; `inscope` owns the moving parts and keeps them in sync from a single source of truth:
19
19
 
20
20
  - a `.mcp.json` at each workspace root, with uniquely named servers
21
- - a single zsh `chpwd` hook that resolves the right tokens from `$PWD`
22
- - git `includeIf` rules so commits get the right email per path
21
+ - one zsh `chpwd` hook that resolves the right tokens from `$PWD`
22
+ - git `includeIf` rules so commits land with the right author email per path
23
23
 
24
- 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.
24
+ 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. It is race-free across concurrent shells and Claude Code sessions, with no global toggles, and idempotent: only the blocks it owns inside `.zshrc`, `.gitconfig`, and `.mcp.json` are ever touched.
25
25
 
26
26
  ---
27
27
 
28
28
  ### Table of Contents
29
29
 
30
- - [Some Examples](#-some-examples)
31
- - [Features](#-features)
30
+ - [Install](#-install)
32
31
  - [Requirements](#-requirements)
33
- - [Quick Usage](#-quick-usage)
34
32
  - [Commands](#-commands)
33
+ - [`inscope init`](#inscope-init)
34
+ - [`inscope add`](#inscope-add)
35
+ - [`inscope edit`](#inscope-edit)
36
+ - [`inscope rm`](#inscope-rm)
37
+ - [`inscope list`](#inscope-list)
38
+ - [`inscope diff`](#inscope-diff)
39
+ - [`inscope apply`](#inscope-apply)
40
+ - [`inscope doctor`](#inscope-doctor)
35
41
  - [What It Manages](#-what-it-manages)
36
42
  - [MCP Servers](#-mcp-servers)
37
43
  - [Config File](#-config-file)
@@ -39,57 +45,7 @@ Nothing sensitive is written to disk. GitHub tokens come from the `gh` keyring a
39
45
 
40
46
  ---
41
47
 
42
- ## ๐Ÿ“– Some Examples
43
-
44
- ```sh
45
- # set up the config + hook, and source it from ~/.zshrc
46
- inscope init
47
-
48
- # map a workspace โ€” inscope prompts for the gh account, git identity, and servers
49
- inscope add ~/acme
50
- inscope add ~/personal
51
-
52
- # edit a workspace interactively
53
- inscope edit acme
54
-
55
- # list what is configured, and verify everything resolves
56
- inscope list
57
- inscope doctor
58
-
59
- # remove a workspace (asks you to type the label to confirm)
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 ~/personal/blog` and you are you.
64
-
65
- <p align="center">
66
- <img src="https://raw.githubusercontent.com/nrjdalal/demo-kit/main/inscope/demo-switch.gif" alt="inscope switching git identity and tokens on cd" width="900" />
67
- </p>
68
-
69
- ---
70
-
71
- ## โœจ Features
72
-
73
- - ๐Ÿชช Per-directory identity: GitHub token, git commit email, and MCP servers scoped to `$PWD`
74
- - ๐Ÿงต Race-free across concurrent shells and Claude Code sessions, with no global toggles
75
- - ๐Ÿ” No secrets on disk: GitHub tokens from the `gh` keyring, Slack tokens from the macOS Keychain
76
- - ๐Ÿค– One `.mcp.json` per workspace with uniquely named servers โ€” GitHub plus OAuth connectors for Atlassian, Canva, ClickUp, HubSpot, Intercom, Linear, monday, Notion, Plane, Sentry, Slack, Stripe, Vercel and Webflow
77
- - โœ‰๏ธ Git `includeIf` rules so every commit lands with the right author email per path
78
- - ๐Ÿช A single zsh `chpwd` hook does all the resolution; nothing else touches your shell
79
- - ๐Ÿฉบ `inscope doctor` verifies tokens, identities, and the hook before you trust them
80
- - โ™ป๏ธ Idempotent and surgical: only the managed blocks in `.zshrc`, `.gitconfig` and `.mcp.json` are touched
81
-
82
- ---
83
-
84
- ## ๐Ÿงฐ Requirements
85
-
86
- macOS, zsh, and [Claude Code](https://claude.com/claude-code).
87
-
88
- [`gh`](https://cli.github.com) is needed only for workspaces that scope a GitHub account.
89
-
90
- ---
91
-
92
- ## ๐Ÿš€ Quick Usage
48
+ ## ๐Ÿš€ Install
93
49
 
94
50
  Install globally (the CLI manages your shell hook, so a global install is expected):
95
51
 
@@ -97,13 +53,13 @@ Install globally (the CLI manages your shell hook, so a global install is expect
97
53
  npm i -g inscope
98
54
  ```
99
55
 
100
- Scoping GitHub accounts? Sign each one into `gh` once with `gh auth login` (that's gh's own command, not inscope). inscope reads tokens from the accounts you've signed in.
56
+ Scoping GitHub accounts? Sign each one into `gh` once with `gh auth login` (that is gh's own command, not inscope); inscope reads tokens from the accounts you have signed in. Then:
101
57
 
102
58
  ```sh
103
59
  # set up the config + hook, and source it from ~/.zshrc
104
60
  inscope init
105
61
 
106
- # map a workspace โ€” inscope walks you through the gh account, git identity, and servers
62
+ # map a workspace - inscope walks you through gh account, git identity, and servers
107
63
  inscope add ~/acme
108
64
  inscope add ~/personal
109
65
 
@@ -112,9 +68,9 @@ source ~/.zshrc
112
68
  inscope doctor
113
69
  ```
114
70
 
115
- 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.
71
+ `cd ~/acme/api` and you are the work account, with work MCP servers and your work commit email. `cd ~/personal/blog` and you are you. Launch `claude` from inside a mapped directory (or relaunch) to pick up the identity.
116
72
 
117
- Prefer flags or CI? Every prompt has a flag, and `-y` skips them all:
73
+ Prefer flags or CI? Every prompt has a flag, and `-y` takes the defaults non-interactively:
118
74
 
119
75
  ```sh
120
76
  inscope add ~/acme --gh <account> --email you@work.com --servers github,linear -y
@@ -122,31 +78,47 @@ inscope add ~/acme --gh <account> --email you@work.com --servers github,linear -
122
78
 
123
79
  ---
124
80
 
81
+ ## ๐Ÿงฐ Requirements
82
+
83
+ macOS, zsh, and [Claude Code](https://claude.com/claude-code).
84
+
85
+ [`gh`](https://cli.github.com) is needed only for workspaces that scope a GitHub account.
86
+
87
+ ---
88
+
125
89
  ## ๐Ÿ”ง Commands
126
90
 
127
91
  ```
128
- inscope init Create the config, generate the hook, source it from ~/.zshrc
129
- inscope add [path] Map a directory to a GitHub account, git email, and MCP servers
130
- inscope edit [path] Edit a workspace interactively, then re-apply
131
- inscope rm [path] Remove a workspace mapping (alias: remove)
132
- inscope list List configured workspaces (alias: ls)
133
- inscope diff Preview what apply would change; --adopt pulls on-disk extras back
134
- inscope apply Regenerate the hook, git includes, and .mcp.json (alias: sync)
135
- inscope doctor Verify tokens, identities, and the hook resolve correctly
136
-
137
- -v, --version Display version
138
- -h, --help Display help
92
+ inscope init Create the config, generate the hook, source it from ~/.zshrc
93
+ inscope add [path] Map a directory to a GitHub account, git email, and MCP servers
94
+ inscope edit [path] Edit a workspace interactively, then re-apply
95
+ inscope rm [path] Remove a workspace mapping (alias: remove)
96
+ inscope list List configured workspaces (alias: ls)
97
+ inscope diff Preview what apply would change; --adopt pulls on-disk extras back
98
+ inscope apply Regenerate the hook, git includes, and .mcp.json (alias: sync)
99
+ inscope doctor Verify tokens, identities, and the hook resolve correctly
100
+
101
+ -v, --version Display version
102
+ -h, --help Display help
139
103
  ```
140
104
 
141
- Run any command with `-h` for its options.
105
+ Run any command with `-h` for its full options.
106
+
107
+ ### `inscope init`
108
+
109
+ Create the config, generate the chpwd hook, and add a source line to `~/.zshrc`. Safe to run again; it never overwrites your config.
142
110
 
143
111
  <p align="center">
144
- <img src="https://raw.githubusercontent.com/nrjdalal/demo-kit/main/inscope/demo-manage.gif" alt="inscope edit and rm with type-to-confirm" width="900" />
112
+ <img src="https://raw.githubusercontent.com/nrjdalal/inscope/main/.github/assets/init.gif" alt="inscope init creating the config and hook" width="900" />
145
113
  </p>
146
114
 
147
115
  ### `inscope add`
148
116
 
149
- 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).
117
+ Map a directory. 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. Enabling Slack adds a keychain prompt and a Yes/No for posting messages. Pass any flag to skip its prompt, or `-y` to take the defaults non-interactively (for scripts and CI).
118
+
119
+ <p align="center">
120
+ <img src="https://raw.githubusercontent.com/nrjdalal/inscope/main/.github/assets/add.gif" alt="inscope add: gh picker, git identity, server multiselect, and the Slack prompts" width="900" />
121
+ </p>
150
122
 
151
123
  ```
152
124
  --gh <account> gh account whose token this workspace uses
@@ -164,12 +136,52 @@ Run it bare and it walks you through everything: pick the GitHub account from yo
164
136
  -y, --yes accept defaults, skip all prompts (non-interactive)
165
137
  ```
166
138
 
139
+ ### `inscope edit`
140
+
141
+ Step through a workspace's prompts pre-filled with its current values (pick it, or pass its path/label), then inscope re-applies on save.
142
+
143
+ <p align="center">
144
+ <img src="https://raw.githubusercontent.com/nrjdalal/inscope/main/.github/assets/edit.gif" alt="inscope edit: prompts pre-filled with the workspace's current values" width="900" />
145
+ </p>
146
+
147
+ ### `inscope rm`
148
+
149
+ Remove a workspace mapping (alias `remove`). Drops its git include and the MCP servers inscope manages; your keychain entries and gh accounts are left untouched. Asks you to type the label to confirm, or pass `-y` to skip the prompt.
150
+
151
+ <p align="center">
152
+ <img src="https://raw.githubusercontent.com/nrjdalal/inscope/main/.github/assets/rm.gif" alt="inscope rm with a type-the-label confirm" width="900" />
153
+ </p>
154
+
155
+ ### `inscope list`
156
+
157
+ List the configured workspaces with their path, gh account, git email, and enabled servers (alias `ls`). Run `inscope doctor` to verify they actually resolve.
158
+
159
+ <p align="center">
160
+ <img src="https://raw.githubusercontent.com/nrjdalal/inscope/main/.github/assets/list.gif" alt="inscope list showing the configured workspaces" width="900" />
161
+ </p>
162
+
167
163
  ### `inscope diff`
168
164
 
169
- Preview exactly what `apply` would write: a colored diff of the hook, git includes, and each `.mcp.json` against your config. `--adopt` pulls config-expressible on-disk settings (a Slack add-message tool, a custom server URL) back into the config, so the next apply keeps them instead of dropping them.
165
+ Preview exactly what `apply` would write: a colored diff of the hook, git includes, and each `.mcp.json` against your config. `--adopt` pulls config-expressible on-disk settings (a Slack add-message tool, a custom server URL) back into the config, so the next apply keeps them instead of dropping them. `--exit-code` exits non-zero when anything is out of sync, so it works as a CI or pre-commit gate.
166
+
167
+ <p align="center">
168
+ <img src="https://raw.githubusercontent.com/nrjdalal/inscope/main/.github/assets/diff.gif" alt="inscope diff: colored drift, then --adopt back-syncs an on-disk setting into the config" width="900" />
169
+ </p>
170
+
171
+ ### `inscope apply`
172
+
173
+ Regenerate the hook, git includes, and every `.mcp.json` from the config (alias `sync`). Idempotent and surgical: only the managed blocks are touched, and writes are atomic. Run it any time you edit `inscope.json` by hand.
170
174
 
171
175
  <p align="center">
172
- <img src="https://raw.githubusercontent.com/nrjdalal/demo-kit/main/inscope/demo-diff.gif" alt="inscope diff: preview drift as a colored diff, then --adopt back-syncs an on-disk setting into the config" width="900" />
176
+ <img src="https://raw.githubusercontent.com/nrjdalal/inscope/main/.github/assets/apply.gif" alt="inscope apply regenerating the hook, git includes, and each .mcp.json" width="900" />
177
+ </p>
178
+
179
+ ### `inscope doctor`
180
+
181
+ Verify that tokens, identities, the hook, and each `.mcp.json` resolve correctly. Exits non-zero if anything fails, so it doubles as a health gate.
182
+
183
+ <p align="center">
184
+ <img src="https://raw.githubusercontent.com/nrjdalal/inscope/main/.github/assets/doctor.gif" alt="inscope doctor verifying tokens, identities, and the hook" width="900" />
173
185
  </p>
174
186
 
175
187
  ---
@@ -183,7 +195,7 @@ Preview exactly what `apply` would write: a colored diff of the hook, git includ
183
195
  | MCP servers | `<workspace>/.mcp.json` |
184
196
  | Git identity | `~/.gitconfig` includeIf + `~/.config/inscope/git/<name>.gitconfig` |
185
197
 
186
- `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`.
198
+ `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`.
187
199
 
188
200
  ---
189
201
 
@@ -209,7 +221,7 @@ Each enabled server is written into the workspace `.mcp.json` with a name suffix
209
221
  | `vercel` | http | OAuth |
210
222
  | `webflow` | http | OAuth |
211
223
 
212
- Slack is opt-in. Enable it with `--servers ...,slack`, then store the token once:
224
+ Slack is opt-in. Enable it during `add` (shown above), or with flags, then store the token once:
213
225
 
214
226
  ```sh
215
227
  inscope add ~/acme --gh neeraj-acme-org --servers github,slack --seed-slack
@@ -219,10 +231,6 @@ inscope add ~/acme --gh neeraj-acme-org --servers github,slack --seed-slack
219
231
 
220
232
  You need a Slack app with a user OAuth (`xoxp`) token first. If you don't have one, follow the [slack-mcp-server authentication guide](https://github.com/korotovsky/slack-mcp-server/blob/HEAD/docs/01-authentication-setup.md#option-2-using-slack_mcp_xoxp_token-user-oauth). inscope points you there during `add` when Slack is enabled.
221
233
 
222
- <p align="center">
223
- <img src="https://raw.githubusercontent.com/nrjdalal/demo-kit/main/inscope/demo-slack.gif" alt="inscope adding Slack: keychain prompt and Yes/No selector confirms" width="900" />
224
- </p>
225
-
226
234
  ---
227
235
 
228
236
  ## ๐Ÿ“‹ Config File
@@ -258,7 +266,7 @@ Edit it directly, then run `inscope apply` to regenerate the hook, git includes,
258
266
 
259
267
  ## ๐Ÿค Contributing
260
268
 
261
- Issues and pull requests are welcome. Run the tests with `bun test` and the type checks with `bun run typecheck` before opening a PR.
269
+ Issues and pull requests are welcome. Run the tests with `bun test` and the type checks with `bun run typecheck` before opening a PR. See [CONTRIBUTING.md](./CONTRIBUTING.md) for the toolchain and architecture.
262
270
 
263
271
  ---
264
272
 
@@ -1,20 +1,19 @@
1
1
  #!/usr/bin/env node
2
- import e from"node:fs";import{parseArgs as t}from"node:util";import n from"node:path";import r from"node:os";import{spawnSync as i}from"node:child_process";import a from"node:readline";const o=()=>r.homedir(),s=()=>process.env.XDG_CONFIG_HOME?.trim()||n.join(o(),`.config`),c=e=>e===`~`?o():e.startsWith(`~/`)?n.join(o(),e.slice(2)):e,l=e=>{let t=c(e),r=o();return t===r?`~`:t.startsWith(r+n.sep)?`~/`+t.slice(r.length+1):t},u=e=>n.resolve(c(e)),d=()=>n.join(s(),`inscope`),f=()=>n.join(d(),`inscope.json`),p=()=>n.join(d(),`inscope.zsh`),m=()=>n.join(d(),`git`),h=()=>n.join(o(),`.gitconfig`),g=()=>n.join(o(),`.zshrc`),_=()=>({version:1,workspaces:[]}),v=()=>e.existsSync(f()),y=()=>{let t=f(),n=e.readFileSync(t,`utf8`),r=JSON.parse(n),i=te(r);if(i)throw Error(i);try{E(r)}catch(e){let n=e instanceof Error?e.message:String(e);throw Error(`${n}\nFix it in ${l(t)}, then re-run.`)}return r},b=t=>{E(t);let r=f();e.mkdirSync(n.dirname(r),{recursive:!0}),e.writeFileSync(r,JSON.stringify(t,null,2)+`
3
- `)},x=/^[A-Za-z0-9._-]+$/,S=e=>e?x.test(e)?null:`use only letters, digits, dot (.), dash (-), or underscore (_)`:`must not be empty`,C=/[\\"`$\n]/,w=e=>C.test(e)?'must not contain a backslash (\\), quote ("), backtick (`), $, or newline':null,ee=e=>e?w(e):`must not be empty`,T=e=>/[\n\r]/.test(e)?`must not contain a newline`:null,te=e=>typeof e.version==`number`&&e.version>1?`config version ${e.version} is newer than this inscope supports (max 1); upgrade inscope`:null,ne=e=>e.toLowerCase().replace(/[^a-z0-9._-]+/g,`-`).replace(/^[-.]+|[-.]+$/g,``),E=e=>{if(!e||typeof e!=`object`)throw Error(`config is not an object`);let t=te(e);if(t)throw Error(t);if(!Array.isArray(e.workspaces))throw Error(`config.workspaces must be an array`);let n=new Set;for(let t of e.workspaces){if(!t.name)throw Error(`a workspace is missing a name`);let e=S(t.name);if(e)throw Error(`workspace name "${t.name}" is invalid: ${e}`);if(!t.path)throw Error(`workspace "${t.name}" is missing a path`);let r=ee(t.path);if(r)throw Error(`workspace "${t.name}" path "${t.path}" is invalid: ${r}`);if(t.gh){let e=w(t.gh);if(e)throw Error(`workspace "${t.name}" gh account "${t.gh}" is invalid: ${e}`)}if(t.git?.email){let e=T(t.git.email);if(e)throw Error(`workspace "${t.name}" git email "${t.git.email}" is invalid: ${e}`)}if(t.git?.name){let e=T(t.git.name);if(e)throw Error(`workspace "${t.name}" git name "${t.git.name}" is invalid: ${e}`)}let i=t.servers?.slack;if(i&&i.keychain){let e=w(i.keychain);if(e)throw Error(`workspace "${t.name}" Slack keychain "${i.keychain}" is invalid: ${e}`)}if(n.has(t.name))throw Error(`duplicate workspace name "${t.name}"`);n.add(t.name)}},re=e=>ne(n.basename(u(e)))||`workspace`,ie=(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)},ae=(e,t,n)=>{let r=u(t);return e.workspaces.find(e=>e.name!==n&&u(e.path)===r)},oe=(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}},se=(e,t)=>{let n=ie(e,t);return n?{cfg:{...e,workspaces:e.workspaces.filter(e=>e.name!==n.name)},removed:n}:{cfg:e}},D={atlassian:`https://mcp.atlassian.com/v1/mcp`,canva:`https://mcp.canva.com/mcp`,clickup:`https://mcp.clickup.com/mcp`,hubspot:`https://mcp.hubspot.com`,intercom:`https://mcp.intercom.com/mcp`,linear:`https://mcp.linear.app/mcp`,monday:`https://mcp.monday.com/mcp`,notion:`https://mcp.notion.com/mcp`,plane:`https://mcp.plane.so/http/mcp`,sentry:`https://mcp.sentry.dev/mcp`,stripe:`https://mcp.stripe.com`,vercel:`https://mcp.vercel.com`,webflow:`https://mcp.webflow.com/`},O=[`github`,`atlassian`,`canva`,`clickup`,`hubspot`,`intercom`,`linear`,`monday`,`notion`,`plane`,`sentry`,`slack`,`stripe`,`vercel`,`webflow`],k=e=>O.map(t=>`${t}-${e}`),A=e=>n.join(u(e.path),`.mcp.json`),ce=(e,t)=>e&&typeof e==`object`&&e.url?e.url:t,le=e=>{let t=e.servers,n={};for(let r of O){let i=t[r];if(!i)continue;let a=`${r}-${e.name}`;if(r===`github`)n[a]={type:`http`,url:`https://api.githubcopilot.com/mcp/`,headers:{Authorization:"Bearer ${GITHUB_TOKEN}"}};else if(r===`slack`){let e=i,t={SLACK_MCP_XOXP_TOKEN:"${SLACK_MCP_XOXP_TOKEN}"};e.addMessageTool&&(t.SLACK_MCP_ADD_MESSAGE_TOOL=`true`),n[a]={type:`stdio`,command:`npx`,args:[`-y`,`slack-mcp-server@1.3.0`,`--transport`,`stdio`],env:t}}else n[a]={type:`http`,url:ce(i,D[r])}}return n},ue=t=>{if(!e.existsSync(t))return{};try{return JSON.parse(e.readFileSync(t,`utf8`))}catch{return{}}},de=t=>{if(!e.existsSync(t))return{};try{return JSON.parse(e.readFileSync(t,`utf8`))}catch{throw Error(`${t} is not valid JSON; fix or remove it, then re-run inscope (left it untouched)`)}},fe=t=>{let n=A(t);return e.existsSync(n)?ue(n):null},pe=t=>{let r=A(t);e.mkdirSync(n.dirname(r),{recursive:!0});let i=de(r),a=i.mcpServers&&typeof i.mcpServers==`object`?{...i.mcpServers}:{};for(let e of k(t.name))delete a[e];Object.assign(a,le(t)),i.mcpServers=a,e.writeFileSync(r,JSON.stringify(i,null,2)+`
4
- `)},me=t=>{let n=A(t);if(!e.existsSync(n))return;let r=de(n);if(r.mcpServers&&typeof r.mcpServers==`object`)for(let e of k(t.name))delete r.mcpServers[e];e.writeFileSync(n,JSON.stringify(r,null,2)+`
5
- `)},j=(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`,M=()=>process.env.USER||``,ge=(e,t=j)=>{let n=t(`gh`,[`auth`,`token`,`-u`,e]),r=n.stdout.trim();return n.status===0&&r?r:null},_e=(e=j)=>{let t=e(`gh`,[`auth`,`status`]);return(t.stdout+t.stderr).trim()},ve=(e=j)=>{let t=[];for(let n of _e(e).matchAll(/account (\S+) \(/g))t.includes(n[1])||t.push(n[1]);return t},ye=(e,t=j)=>{let n=t(`git`,[`config`,`--global`,e]),r=n.stdout.trim();return n.status===0&&r?r:null},N=(e,t=j)=>{let n=t(`security`,[`find-generic-password`,`-a`,M(),`-s`,e,`-w`]);return n.status===0&&n.stdout.trim().length>0},be=(e,t,n=j)=>{let r=n(`security`,[`add-generic-password`,`-U`,`-a`,M(),`-s`,e,`-w`,t]);if(r.status!==0)throw Error(`security add-generic-password failed: ${r.stderr.trim()||`unknown error`}`)},xe=e=>`security add-generic-password -U -a "${M()||`$USER`}" -s ${e} -w 'xoxp-...'`,Se=(e,t=j)=>{let n=t(`git`,[`config`,`--file`,e,`user.email`]);return n.status===0?n.stdout.trim():null},P=()=>!!(process.stdin.isTTY&&process.stdout.isTTY),Ce=(e,t=e)=>process.stdout.isTTY?`\x1b]8;;${e}\x07${t}\x1b]8;;\x07`:e,F=e=>t=>process.stdout.isTTY?`\x1b[${e}m${t}\x1b[0m`:t,I=F(`38;5;208`),we=F(`38;2;63;185;80`),L=F(`38;2;210;153;34`),Te=F(`38;2;248;81;73`),R=e=>{let t=process.stdin;t.isTTY&&typeof t.setRawMode==`function`&&t.setRawMode(e)};let z=``;const Ee=e=>new Promise(t=>{process.stdout.write(e);let n=()=>{let e=z.indexOf(`
6
- `);if(e<0)return!1;let n=z.slice(0,e).replace(/\r$/,``);return z=z.slice(e+1),t(n),!0};if(n())return;let r=e=>{z+=e.toString(`utf8`),z.includes(`
7
- `)&&(process.stdin.off(`data`,r),process.stdin.off(`end`,i),process.stdin.pause(),n())},i=()=>{process.stdin.off(`data`,r),process.stdin.off(`end`,i);let e=z.replace(/\r$/,``);z=``,t(e)};process.stdin.on(`data`,r),process.stdin.on(`end`,i),process.stdin.resume()}),B=async(e,t=``)=>(await Ee(`${e}${t?` [${t}]`:``}: `)).trim()||t,V=async(e,t=!1)=>{if(!P()){let n=(await Ee(`${e} [${t?`Y/n`:`y/N`}]: `)).trim().toLowerCase();return n?n===`y`||n===`yes`:t}return H(e,[{label:`Yes`,value:!0},{label:`No`,value:!1}],t?0:1)},De=e=>new Promise(t=>{let n=a.createInterface({input:process.stdin,output:process.stdout}),r=n.output,i=!1;n._writeToOutput=t=>{if(!i){r.write(t),t.includes(e)&&(i=!0);return}},n.question(e,e=>{r.write(`
8
- `),n.close(),t(e.trim())})}),Oe=`\x1B[36m`,ke=`\x1B[0m`,H=(e,t,n=0)=>new Promise(r=>{if(!P()||t.length===0){r(t[Math.min(n,t.length-1)]?.value);return}let i=Math.max(0,Math.min(n,t.length-1)),o=process.stdout;o.write(e+`
9
- `);let s=e=>{e||o.write(`\x1b[${t.length}A`);for(let e=0;e<t.length;e++){let n=e===i,r=`${n?`โฏ`:` `} ${t[e].label}`;o.write(`\x1b[2K ${n?Oe+r+ke:r}\n`)}};s(!0),a.emitKeypressEvents(process.stdin),R(!0),process.stdin.resume();let c=()=>{process.stdin.off(`keypress`,l),R(!1),process.stdin.pause()},l=(e,n)=>{n.name===`up`||n.name===`k`?(i=(i-1+t.length)%t.length,s(!1)):n.name===`down`||n.name===`j`?(i=(i+1)%t.length,s(!1)):n.name===`return`||n.name===`enter`?(c(),r(t[i].value)):n.ctrl&&n.name===`c`&&(c(),o.write(`
10
- `),process.exit(130))};process.stdin.on(`keypress`,l)}),Ae=(e,t)=>new Promise(n=>{let r=t.map(e=>!!e.checked),i=()=>t.filter((e,t)=>r[t]).map(e=>e.value);if(!P()||t.length===0){n(i());return}let o=0,s=process.stdout;s.write(e+`
11
- `);let c=e=>{e||s.write(`\x1b[${t.length}A`);for(let e=0;e<t.length;e++){let n=e===o,i=`${n?`โฏ`:` `} ${r[e]?`โ—‰`:`โ—ฏ`} ${t[e].label}`;s.write(`\x1b[2K ${n?Oe+i+ke:i}\n`)}};c(!0),a.emitKeypressEvents(process.stdin),R(!0),process.stdin.resume();let l=()=>{process.stdin.off(`keypress`,u),R(!1),process.stdin.pause()},u=(e,a)=>{a.name===`up`||a.name===`k`?(o=(o-1+t.length)%t.length,c(!1)):a.name===`down`||a.name===`j`?(o=(o+1)%t.length,c(!1)):a.name===`space`||e===` `?(r[o]=!r[o],c(!1)):a.name===`return`||a.name===`enter`?(l(),n(i())):a.ctrl&&a.name===`c`&&(l(),s.write(`
12
- `),process.exit(130))};process.stdin.on(`keypress`,u)}),U=e=>`# >>> inscope:${e} >>>`,W=e=>`# <<< inscope:${e} <<<`,G=e=>e.replace(/[.*+?^${}()|[\]\\]/g,`\\$&`),je=e=>RegExp(`${G(U(e))}\\n[\\s\\S]*?\\n${G(W(e))}\\n?`),Me=(e,t)=>{let n=t.replace(/\n+$/,``);return`${U(e)}\n${n}\n${W(e)}\n`},K=t=>{try{return e.readFileSync(t,`utf8`)}catch{return``}},Ne=(t,r,i)=>{e.mkdirSync(n.dirname(t),{recursive:!0});let a=K(t),o=Me(r,i),s=je(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(t,c)},Pe=(t,n)=>{let r=K(t);if(!r)return;let i=r.replace(je(n),``).replace(/\n{3,}/g,`
13
-
14
- `).replace(/^\n+/,``);e.writeFileSync(t,i)},Fe=(e,t)=>{let n=K(e).match(RegExp(`${G(U(t))}\\n([\\s\\S]*?)\\n${G(W(t))}`));return n?n[1]:null},q=`gitconfig`,J=e=>!!(e.git&&(e.git.email||e.git.name)),Y=e=>n.join(m(),`${e}.gitconfig`),Ie=e=>l(e).replace(/\/+$/,``)+`/`,Le=e=>e.workspaces.filter(J).map(e=>`[includeIf "gitdir:${Ie(e.path)}"]\n\tpath = ${l(Y(e.name))}`).join(`
15
- `),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(`
2
+ import e from"node:fs";import{parseArgs as t}from"node:util";import n from"node:path";import r from"node:os";import{spawnSync as i}from"node:child_process";import a from"node:readline";const o=()=>r.homedir(),s=()=>process.env.XDG_CONFIG_HOME?.trim()||n.join(o(),`.config`),c=e=>e===`~`?o():e.startsWith(`~/`)?n.join(o(),e.slice(2)):e,l=e=>{let t=c(e),r=o();return t===r?`~`:t.startsWith(r+n.sep)?`~/`+t.slice(r.length+1):t},u=e=>n.resolve(c(e)),d=()=>n.join(s(),`inscope`),f=()=>n.join(d(),`inscope.json`),p=()=>n.join(d(),`inscope.zsh`),m=()=>n.join(d(),`git`),h=()=>n.join(o(),`.gitconfig`),g=()=>n.join(o(),`.zshrc`),_=t=>{try{return e.readFileSync(t,`utf8`)}catch{return null}},v=e=>_(e)??``,y=(t,r)=>{let i=t;try{i=e.realpathSync(t)}catch{}let a=n.dirname(i);e.mkdirSync(a,{recursive:!0});let o;try{o=e.statSync(i).mode}catch{}let s=n.join(a,`.${n.basename(i)}.inscope-${process.pid}.tmp`);e.writeFileSync(s,r),o!==void 0&&e.chmodSync(s,o&4095);try{e.renameSync(s,i)}catch(t){try{e.rmSync(s,{force:!0})}catch{}throw t}},b=()=>({version:1,workspaces:[]}),x=()=>e.existsSync(f()),S=()=>{let t=f(),n=e.readFileSync(t,`utf8`),r;try{r=JSON.parse(n)}catch{throw Error(`${l(t)} is not valid JSON; fix it, then re-run.`)}let i=ae(r);if(i)throw Error(i);try{T(r)}catch(e){let n=e instanceof Error?e.message:String(e);throw Error(`${n}\nFix it in ${l(t)}, then re-run.`)}return r},C=e=>{T(e),y(f(),JSON.stringify(e,null,2)+`
3
+ `)},ee=/^[A-Za-z0-9._-]+$/,te=e=>e?ee.test(e)?null:`use only letters, digits, dot (.), dash (-), or underscore (_)`:`must not be empty`,ne=/[\\"`$\n]/,w=e=>ne.test(e)?'must not contain a backslash (\\), quote ("), backtick (`), $, or newline':null,re=e=>e?w(e):`must not be empty`,ie=e=>/[\n\r]/.test(e)?`must not contain a newline`:null,ae=e=>typeof e.version==`number`&&e.version>1?`config version ${e.version} is newer than this inscope supports (max 1); upgrade inscope`:null,oe=e=>e.toLowerCase().replace(/[^a-z0-9._-]+/g,`-`).replace(/^[-.]+|[-.]+$/g,``),T=e=>{if(!e||typeof e!=`object`)throw Error(`config is not an object`);let t=ae(e);if(t)throw Error(t);if(!Array.isArray(e.workspaces))throw Error(`config.workspaces must be an array`);let n=new Set;for(let t of e.workspaces){if(!t.name)throw Error(`a workspace is missing a name`);let e=te(t.name);if(e)throw Error(`workspace name "${t.name}" is invalid: ${e}`);if(!t.path)throw Error(`workspace "${t.name}" is missing a path`);let r=re(t.path);if(r)throw Error(`workspace "${t.name}" path "${t.path}" is invalid: ${r}`);if(t.gh){let e=w(t.gh);if(e)throw Error(`workspace "${t.name}" gh account "${t.gh}" is invalid: ${e}`)}if(t.git?.email){let e=ie(t.git.email);if(e)throw Error(`workspace "${t.name}" git email "${t.git.email}" is invalid: ${e}`)}if(t.git?.name){let e=ie(t.git.name);if(e)throw Error(`workspace "${t.name}" git name "${t.git.name}" is invalid: ${e}`)}let i=t.servers?.slack;if(i&&i.keychain){let e=w(i.keychain);if(e)throw Error(`workspace "${t.name}" Slack keychain "${i.keychain}" is invalid: ${e}`)}if(n.has(t.name))throw Error(`duplicate workspace name "${t.name}"`);n.add(t.name)}},se=e=>oe(n.basename(u(e)))||`workspace`,E=(e,t)=>{let n=e.workspaces.find(e=>e.name===t);if(n)return n;let r=u(t);return e.workspaces.find(e=>u(e.path)===r)},ce=(e,t,n)=>{let r=u(t);return e.workspaces.find(e=>e.name!==n&&u(e.path)===r)},le=(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}},ue=(e,t)=>{let n=E(e,t);return n?{cfg:{...e,workspaces:e.workspaces.filter(e=>e.name!==n.name)},removed:n}:{cfg:e}},D={atlassian:`https://mcp.atlassian.com/v1/mcp`,canva:`https://mcp.canva.com/mcp`,clickup:`https://mcp.clickup.com/mcp`,hubspot:`https://mcp.hubspot.com`,intercom:`https://mcp.intercom.com/mcp`,linear:`https://mcp.linear.app/mcp`,monday:`https://mcp.monday.com/mcp`,notion:`https://mcp.notion.com/mcp`,plane:`https://mcp.plane.so/http/mcp`,sentry:`https://mcp.sentry.dev/mcp`,stripe:`https://mcp.stripe.com`,vercel:`https://mcp.vercel.com`,webflow:`https://mcp.webflow.com/`},O=[`github`,`atlassian`,`canva`,`clickup`,`hubspot`,`intercom`,`linear`,`monday`,`notion`,`plane`,`sentry`,`slack`,`stripe`,`vercel`,`webflow`],k=e=>O.map(t=>`${t}-${e}`),A=e=>n.join(u(e.path),`.mcp.json`),de=(e,t)=>e&&typeof e==`object`&&e.url?e.url:t,fe=e=>{let t=e.servers,n={};for(let r of O){let i=t[r];if(!i)continue;let a=`${r}-${e.name}`;if(r===`github`)n[a]={type:`http`,url:`https://api.githubcopilot.com/mcp/`,headers:{Authorization:"Bearer ${GITHUB_TOKEN}"}};else if(r===`slack`){let e=i,t={SLACK_MCP_XOXP_TOKEN:"${SLACK_MCP_XOXP_TOKEN}"};e.addMessageTool&&(t.SLACK_MCP_ADD_MESSAGE_TOOL=`true`),n[a]={type:`stdio`,command:`npx`,args:[`-y`,`slack-mcp-server@1.3.0`,`--transport`,`stdio`],env:t}}else n[a]={type:`http`,url:de(i,D[r])}}return n},pe=t=>{if(!e.existsSync(t))return{};try{return JSON.parse(e.readFileSync(t,`utf8`))}catch{return{}}},j=t=>{if(!e.existsSync(t))return{};try{return JSON.parse(e.readFileSync(t,`utf8`))}catch{throw Error(`${t} is not valid JSON; fix or remove it, then re-run inscope (left it untouched)`)}},me=t=>{let n=A(t);return e.existsSync(n)?pe(n):null},he=(e,t)=>{let n=e.mcpServers&&typeof e.mcpServers==`object`?{...e.mcpServers}:{};for(let e of k(t.name))delete n[e];return Object.assign(n,fe(t)),{...e,mcpServers:n}},M=e=>JSON.stringify(e,null,2)+`
4
+ `,ge=e=>{for(let t of e)j(A(t))},_e=e=>{let t=A(e);y(t,M(he(j(t),e)))},ve=t=>{let n=A(t);if(!e.existsSync(n))return;let r=j(n);if(r.mcpServers&&typeof r.mcpServers==`object`)for(let e of k(t.name))delete r.mcpServers[e];y(n,M(r))},N=(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??``}},ye=()=>process.platform===`darwin`,P=()=>process.env.USER||``,be=(e,t=N)=>{let n=t(`gh`,[`auth`,`token`,`-u`,e]),r=n.stdout.trim();return n.status===0&&r?r:null},xe=(e=N)=>{let t=e(`gh`,[`auth`,`status`]);return(t.stdout+t.stderr).trim()},Se=(e=N)=>{let t=[];for(let n of xe(e).matchAll(/account (\S+) \(/g))t.includes(n[1])||t.push(n[1]);return t},Ce=(e,t=N)=>{let n=t(`git`,[`config`,`--global`,e]),r=n.stdout.trim();return n.status===0&&r?r:null},F=(e,t=N)=>{let n=t(`security`,[`find-generic-password`,`-a`,P(),`-s`,e,`-w`]);return n.status===0&&n.stdout.trim().length>0},we=(e,t,n=N)=>{let r=n(`security`,[`add-generic-password`,`-U`,`-a`,P(),`-s`,e,`-w`,t]);if(r.status!==0)throw Error(`security add-generic-password failed: ${r.stderr.trim()||`unknown error`}`)},Te=e=>`security add-generic-password -U -a "${P()||`$USER`}" -s ${e} -w 'xoxp-...'`,Ee=(e,t=N)=>{let n=t(`git`,[`config`,`--file`,e,`user.email`]);return n.status===0?n.stdout.trim():null},I=()=>!!(process.stdin.isTTY&&process.stdout.isTTY),De=(e,t=e)=>process.stdout.isTTY?`\x1b]8;;${e}\x07${t}\x1b]8;;\x07`:e,L=e=>t=>process.stdout.isTTY?`\x1b[${e}m${t}\x1b[0m`:t,R=L(`38;5;208`),Oe=L(`38;2;63;185;80`),z=L(`38;2;210;153;34`),B=L(`38;2;248;81;73`),V=e=>{let t=process.stdin;t.isTTY&&typeof t.setRawMode==`function`&&t.setRawMode(e)},ke=()=>{V(!0),process.stdin.resume();let e=()=>V(!1);return process.once(`exit`,e),()=>{process.removeListener(`exit`,e),V(!1),process.stdin.pause()}};let H=``;const Ae=e=>new Promise(t=>{process.stdout.write(e);let n=()=>{let e=H.indexOf(`
5
+ `);if(e<0)return!1;let n=H.slice(0,e).replace(/\r$/,``);return H=H.slice(e+1),t(n),!0};if(n())return;let r=e=>{H+=e.toString(`utf8`),H.includes(`
6
+ `)&&(process.stdin.off(`data`,r),process.stdin.off(`end`,i),process.stdin.pause(),n())},i=()=>{process.stdin.off(`data`,r),process.stdin.off(`end`,i);let e=H.replace(/\r$/,``);H=``,t(e)};process.stdin.on(`data`,r),process.stdin.on(`end`,i),process.stdin.resume()}),U=async(e,t=``)=>(await Ae(`${e}${t?` [${t}]`:``}: `)).trim()||t,W=async(e,t=!1)=>{if(!I()){let n=(await Ae(`${e} [${t?`Y/n`:`y/N`}]: `)).trim().toLowerCase();return n?n===`y`||n===`yes`:t}return G(e,[{label:`Yes`,value:!0},{label:`No`,value:!1}],t?0:1)},je=e=>new Promise(t=>{let n=a.createInterface({input:process.stdin,output:process.stdout}),r=n.output,i=!1;n._writeToOutput=t=>{if(!i){r.write(t),t.includes(e)&&(i=!0);return}},n.question(e,e=>{r.write(`
7
+ `),n.close(),t(e.trim())})}),Me=`\x1B[36m`,Ne=`\x1B[0m`,G=(e,t,n=0)=>new Promise(r=>{if(!I()||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+`
8
+ `);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?Me+r+Ne:r}\n`)}};s(!0),a.emitKeypressEvents(process.stdin);let c=ke(),l=()=>{process.stdin.off(`keypress`,u),c()},u=(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`?(l(),r(t[i].value)):n.ctrl&&n.name===`c`&&(l(),o.write(`
9
+ `),process.exit(130))};process.stdin.on(`keypress`,u)}),Pe=(e,t)=>new Promise(n=>{let r=t.map(e=>!!e.checked),i=()=>t.filter((e,t)=>r[t]).map(e=>e.value);if(!I()||t.length===0){n(i());return}let o=0,s=process.stdout;s.write(e+`
10
+ `);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?Me+i+Ne:i}\n`)}};c(!0),a.emitKeypressEvents(process.stdin);let l=ke(),u=()=>{process.stdin.off(`keypress`,d),l()},d=(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`?(u(),n(i())):a.ctrl&&a.name===`c`&&(u(),s.write(`
11
+ `),process.exit(130))};process.stdin.on(`keypress`,d)}),K=e=>`# >>> inscope:${e} >>>`,q=e=>`# <<< inscope:${e} <<<`,J=e=>e.replace(/[.*+?^${}()|[\]\\]/g,`\\$&`),Fe=e=>RegExp(`${J(K(e))}\\n[\\s\\S]*?\\n${J(q(e))}\\n?`),Ie=(e,t)=>{let n=t.replace(/\n+$/,``);return`${K(e)}\n${n}\n${q(e)}\n`},Le=(e,t,n)=>{let r=v(e),i=Ie(t,n),a=Fe(t),o;if(a.test(r))o=r.replace(a,i);else{let e=r.replace(/\n*$/,``);o=e.length?`${e}\n\n${i}`:i}y(e,o)},Re=(e,t)=>{let n=v(e);n&&y(e,n.replace(Fe(t),``).replace(/\n{3,}/g,`
12
+
13
+ `).replace(/^\n+/,``))},ze=(e,t)=>{let n=v(e).match(RegExp(`${J(K(t))}\\n([\\s\\S]*?)\\n${J(q(t))}`));return n?n[1]:null},Y=`gitconfig`,X=e=>!!(e.git&&(e.git.email||e.git.name)),Z=e=>n.join(m(),`${e}.gitconfig`),Be=e=>l(e).replace(/\/+$/,``)+`/`,Ve=e=>e.workspaces.filter(X).map(e=>`[includeIf "gitdir:${Be(e.path)}"]\n\tpath = ${l(Z(e.name))}`).join(`
14
+ `),He=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(`
16
15
  `)+`
17
- `},ze=t=>{e.mkdirSync(m(),{recursive:!0});for(let n of t.workspaces)J(n)&&e.writeFileSync(Y(n.name),Re(n));let n=Le(t);n?Ne(h(),q,n):Pe(h(),q)},Be=t=>{let n=Y(t);e.existsSync(n)&&e.rmSync(n)},Ve=e=>{let t=l(e);return t===`~`?`"$HOME/"*`:t.startsWith(`~/`)?`"$HOME/${t.slice(2)}/"*`:`"${t}/"*`},He=e=>e.servers.slack?e.servers.slack.keychain:``,X=e=>{let t=[...e.workspaces].sort((e,t)=>e.name.localeCompare(t.name));return`# Managed by inscope. Do not edit by hand.
16
+ `},Ue=t=>{e.mkdirSync(m(),{recursive:!0});for(let e of t.workspaces)X(e)?y(Z(e.name),He(e)):We(e.name);let n=Ve(t);n?Le(h(),Y,n):Re(h(),Y)},We=t=>{let n=Z(t);e.existsSync(n)&&e.rmSync(n)},Ge=e=>{let t=l(e);return t===`~`?`"$HOME/"*`:t.startsWith(`~/`)?`"$HOME/${t.slice(2)}/"*`:`"${t}/"*`},Ke=e=>e.servers.slack?e.servers.slack.keychain:``,qe=e=>{let t=[...e.workspaces].sort((e,t)=>e.name.localeCompare(t.name));return`# Managed by inscope. Do not edit by hand.
18
17
  # Source of truth: ~/.config/inscope/inscope.json
19
18
  # Edit there, then run \`inscope apply\` to regenerate this file.
20
19
  #
@@ -25,7 +24,7 @@ import e from"node:fs";import{parseArgs as t}from"node:util";import n from"node:
25
24
  __inscope_resolve_identity() {
26
25
  local ws
27
26
  case "\${PWD}/" in
28
- ${[...t].sort((e,t)=>l(t.path).length-l(e.path).length).map(e=>` ${Ve(e.path)}) ws="${e.name}" ;;`).join(`
27
+ ${[...t].sort((e,t)=>l(t.path).length-l(e.path).length).map(e=>` ${Ge(e.path)}) ws="${e.name}" ;;`).join(`
29
28
  `)||` # no workspaces configured`}
30
29
  *) ws="" ;;
31
30
  esac
@@ -35,7 +34,7 @@ ${[...t].sort((e,t)=>l(t.path).length-l(e.path).length).map(e=>` ${Ve(e.path)
35
34
 
36
35
  local gh_user="" slack_svc=""
37
36
  case "$ws" in
38
- ${t.map(e=>{let t=[];e.gh&&t.push(`gh_user="${e.gh}"`);let n=He(e);return n&&t.push(`slack_svc="${n}"`),` ${e.name}) ${t.length?t.join(`; `):`:`} ;;`}).join(`
37
+ ${t.map(e=>{let t=[];e.gh&&t.push(`gh_user="${e.gh}"`);let n=Ke(e);return n&&t.push(`slack_svc="${n}"`),` ${e.name}) ${t.length?t.join(`; `):`:`} ;;`}).join(`
39
38
  `)||` # no workspaces configured`}
40
39
  *) return ;; # outside a mapped workspace: nothing set
41
40
  esac
@@ -61,13 +60,13 @@ autoload -Uz add-zsh-hook
61
60
  add-zsh-hook chpwd __inscope_resolve_identity
62
61
  __inscope_ws="__init__" # force the first resolve, clearing any inherited token
63
62
  __inscope_resolve_identity
64
- `},Ue=e=>{let t=o();return e===t?`$HOME`:e.startsWith(t+n.sep)?`$HOME/${e.slice(t.length+1)}`:e},We=()=>{let e=Ue(p());return`[ -r "${e}" ] && source "${e}"`},Ge=e=>{let t=We();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`},Ke=()=>{let t=g(),n=``;try{n=e.readFileSync(t,`utf8`)}catch{}let r=Ge(n);r!==n&&e.writeFileSync(t,r)},qe=()=>{try{return e.readFileSync(g(),`utf8`).includes(We())}catch{return!1}},Z=t=>{let r=p();e.mkdirSync(n.dirname(r),{recursive:!0}),e.writeFileSync(r,X(t)),ze(t),Ke();let i=[];for(let e of t.workspaces)pe(e),i.push(A(e));return{hook:r,gitconfig:t.workspaces.some(e=>e.git?.email||e.git?.name),mcp:i}},Je=O,Ye=e=>`SLACK_MCP_XOXP_TOKEN_${e.toUpperCase().replace(/[^A-Z0-9]+/g,`_`)}`,Xe=e=>O.filter(t=>!!e[t]),Ze=(e,t)=>{let n={};for(let r of O)n[r]=r===`slack`?t?{keychain:t.keychain,addMessageTool:t.addMessageTool}:!1:e.includes(r);return n},Qe=e=>e?`global: ${e}`:`no global set`,$e=e=>{let t=v()?y():_(),n=t.workspaces.find(t=>t.name===e.name),r=oe(t,e);b(r),Z(r),n&&u(n.path)!==u(e.path)&&me(n)},et=async(e,t)=>{if(!e.servers.slack)return;let n=e.servers.slack.keychain;if(t){let e=await De(`Paste the Slack xoxp token for ${n}: `);e?(be(n,e),console.log(`\nโœ“ stored ${n} in the macOS keychain`)):console.error(`
65
- No token entered; skipped keychain write.`)}else N(n)||console.log(`\nSlack token not in the keychain yet. Store it once with:\n${I(xe(n))}\n\nSetup guide: ${I(Ce(`https://github.com/korotovsky/slack-mcp-server/blob/HEAD/docs/01-authentication-setup.md#option-2-using-slack_mcp_xoxp_token-user-oauth`))}`)};var Q=`inscope`,tt=`0.8.3`,nt={name:`Neeraj Dalal`,email:`admin@nrjdalal.com`,url:`https://nrjdalal.com`};const rt=`Map a directory to a GitHub account, git email, and MCP servers.
63
+ `},Je=e=>{let t=o();return e===t?`$HOME`:e.startsWith(t+n.sep)?`$HOME/${e.slice(t.length+1)}`:e},Ye=()=>{let e=Je(p());return`[ -r "${e}" ] && source "${e}"`},Xe=e=>{let t=Ye();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`},Ze=()=>{let e=g(),t=v(e),n=Xe(t);n!==t&&y(e,n)},Qe=()=>v(g()).includes(Ye()),Q=e=>{ge(e.workspaces);let t=p();y(t,qe(e)),Ue(e),Ze();let n=[];for(let t of e.workspaces)_e(t),n.push(A(t));return{hook:t,gitconfig:e.workspaces.some(e=>e.git?.email||e.git?.name),mcp:n}},$e=O,et=e=>`SLACK_MCP_XOXP_TOKEN_${e.toUpperCase().replace(/[^A-Z0-9]+/g,`_`)}`,tt=e=>O.filter(t=>!!e[t]),nt=(e,t)=>{let n={};for(let r of O)n[r]=r===`slack`?t?{keychain:t.keychain,addMessageTool:t.addMessageTool}:!1:e.includes(r);return n},rt=e=>e?`global: ${e}`:`no global set`,it=e=>{let t=x()?S():b(),n=t.workspaces.find(t=>t.name===e.name),r=le(t,e);C(r),Q(r),n&&u(n.path)!==u(e.path)&&ve(n)},at=async(e,t)=>{if(!e.servers.slack)return;let n=e.servers.slack.keychain;if(t){let e=await je(`Paste the Slack xoxp token for ${n}: `);e?(we(n,e),console.log(`\nโœ“ stored ${n} in the macOS keychain`)):console.error(`
64
+ No token entered; skipped keychain write.`)}else F(n)||console.log(`\nSlack token not in the keychain yet. Store it once with:\n${R(Te(n))}\n\nSetup guide: ${R(De(`https://github.com/korotovsky/slack-mcp-server/blob/HEAD/docs/01-authentication-setup.md#option-2-using-slack_mcp_xoxp_token-user-oauth`))}`)};var $=`inscope`,ot=`0.8.5`,st={name:`Neeraj Dalal`,email:`admin@nrjdalal.com`,url:`https://nrjdalal.com`};const ct=`Map a directory to a GitHub account, git email, and MCP servers.
66
65
  Runs interactively in a terminal; pass flags or -y to skip the prompts. Re-running
67
66
  with the same label updates that workspace; each directory maps to one workspace.
68
67
 
69
68
  Usage:
70
- $ ${Q} add [path] [options]
69
+ $ ${$} add [path] [options]
71
70
 
72
71
  Options:
73
72
  --gh <account> gh account whose token this workspace uses
@@ -83,23 +82,22 @@ Options:
83
82
  --slack-message allow the Slack MCP server to post messages
84
83
  --seed-slack prompt for the Slack token and store it in the keychain
85
84
  -y, --yes accept defaults, skip all prompts (non-interactive)
86
- -h, --help Display help message`,it=O.map(e=>({label:e,value:e,checked:e===`github`})),at=async n=>{let{positionals:r,values:i}=t({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:n});i.help&&(console.log(rt),process.exit(0));let a=P()&&!i.yes;a&&console.log();let o=r[0];if(!o)if(a)o=await B(`Workspace directory`,process.cwd());else throw Error(rt);let s=ee(o);s&&(console.error(`\nInvalid workspace path "${o}": ${s}`),process.exit(1)),e.existsSync(u(o))||console.error(L(`Warning: ${l(o)} does not exist yet; it will be created.`)+`
87
- `);let c=i.label||re(o);a&&!i.label&&(c=await B(`Label`,c));let d=S(c);if(d&&(console.error(`\nInvalid label "${c}": ${d}`),process.exit(1)),v()){let e=ae(y(),o,c);e&&(console.error(`\n${l(o)} is already mapped to workspace "${e.name}". Run \`${Q} edit ${e.name}\` to change it, or \`${Q} rm ${e.name}\` first.`),process.exit(1))}let f=i.gh;f===void 0&&a&&(f=await H(`
88
- GitHub account for this workspace`,[...ve().map(e=>({label:e,value:e})),{label:`(none)`,value:``}])||void 0);let p=i.email,m=i[`git-name`];a&&(p===void 0&&(p=await B(`Git email (${Qe(ye(`user.email`))})`)||void 0),m===void 0&&(m=await B(`Git name (${Qe(ye(`user.name`))})`)||void 0));let h;if(i.servers!==void 0){h=i.servers.split(`,`).map(e=>e.trim()).filter(Boolean);let e=new Set(O),t=h.filter(t=>!e.has(t));t.length&&console.error(L(`\nIgnoring unknown server(s): ${t.join(`, `)}`))}else h=a?await Ae(`MCP servers (space toggles, enter confirms)`,it):[`github`];let g=h.includes(`slack`)||!!i[`slack-keychain`]||!!i[`seed-slack`],_=i[`slack-keychain`]||Ye(c),b=!!i[`slack-message`],x=!!i[`seed-slack`];g&&a&&(console.log(`
89
- Slack uses a user OAuth (xoxp) token.`),i[`slack-keychain`]||(_=await B(`Slack keychain service`,_)),i[`slack-message`]||(b=await V(`Allow Slack to post messages?`,!0)),i[`seed-slack`]||(x=await V(`Store the Slack token now?`,!0)));let C=f?w(f):null;if(C&&(console.error(`\nInvalid gh account "${f}": ${C}`),process.exit(1)),g){let e=w(_);e&&(console.error(`\nInvalid Slack keychain service "${_}": ${e}`),process.exit(1))}let T={name:c,path:l(o),gh:f,git:p||m?{email:p,name:m}:void 0,servers:Ze(h,g?{keychain:_,addMessageTool:b}:null)};$e(T),console.log(`\nโœ“ workspace "${c}" -> ${T.path}`),console.log(`โœ“ regenerated the hook, git includes, and ${T.path}/.mcp.json`),await et(T,x),console.log(`\nLaunch \`claude\` from ${T.path} (or relaunch) to pick up the new identity.`),process.exit(0)},ot=`Regenerate the chpwd hook, git includes, and every .mcp.json
85
+ -h, --help Display help message`,lt=O.map(e=>({label:e,value:e,checked:e===`github`})),ut=async n=>{let{positionals:r,values:i}=t({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:n});i.help&&(console.log(ct),process.exit(0));let a=I()&&!i.yes;a&&console.log();let o=r[0];if(!o)if(a)o=await U(`Workspace directory`,process.cwd());else throw Error(ct);let s=re(o);s&&(console.error(`\nInvalid workspace path "${o}": ${s}`),process.exit(1)),e.existsSync(u(o))||console.error(z(`Warning: ${l(o)} does not exist yet; it will be created.`)+`
86
+ `);let c=i.label||se(o);a&&!i.label&&(c=await U(`Label`,c));let d=te(c);if(d&&(console.error(`\nInvalid label "${c}": ${d}`),process.exit(1)),x()){let e=ce(S(),o,c);e&&(console.error(`\n${l(o)} is already mapped to workspace "${e.name}". Run \`${$} edit ${e.name}\` to change it, or \`${$} rm ${e.name}\` first.`),process.exit(1))}let f=i.gh;f===void 0&&a&&(f=await G(`
87
+ GitHub account for this workspace`,[...Se().map(e=>({label:e,value:e})),{label:`(none)`,value:``}])||void 0);let p=i.email,m=i[`git-name`];a&&(p===void 0&&(p=await U(`Git email (${rt(Ce(`user.email`))})`)||void 0),m===void 0&&(m=await U(`Git name (${rt(Ce(`user.name`))})`)||void 0));let h;if(i.servers!==void 0){h=i.servers.split(`,`).map(e=>e.trim()).filter(Boolean);let e=new Set(O),t=h.filter(t=>!e.has(t));t.length&&console.error(z(`\nIgnoring unknown server(s): ${t.join(`, `)}`))}else h=a?await Pe(`MCP servers (space toggles, enter confirms)`,lt):[`github`];let g=h.includes(`slack`)||!!i[`slack-keychain`]||!!i[`seed-slack`],_=i[`slack-keychain`]||et(c),v=!!i[`slack-message`],y=!!i[`seed-slack`];g&&a&&(console.log(`
88
+ Slack uses a user OAuth (xoxp) token.`),i[`slack-keychain`]||(_=await U(`Slack keychain service`,_)),i[`slack-message`]||(v=await W(`Allow Slack to post messages?`,!0)),i[`seed-slack`]||(y=await W(`Store the Slack token now?`,!0)));let b=f?w(f):null;if(b&&(console.error(`\nInvalid gh account "${f}": ${b}`),process.exit(1)),g){let e=w(_);e&&(console.error(`\nInvalid Slack keychain service "${_}": ${e}`),process.exit(1))}let C={name:c,path:l(o),gh:f,git:p||m?{email:p,name:m}:void 0,servers:nt(h,g?{keychain:_,addMessageTool:v}:null)};it(C),console.log(`\nโœ“ workspace "${c}" -> ${C.path}`),console.log(`โœ“ regenerated the hook, git includes, and ${C.path}/.mcp.json`),await at(C,y),console.log(`\nLaunch \`claude\` from ${C.path} (or relaunch) to pick up the new identity.`),process.exit(0)},dt=`Regenerate the chpwd hook, git includes, and every .mcp.json
90
89
  from your config. Idempotent: run it any time the config changes.
91
90
 
92
91
  Usage:
93
- $ ${Q} apply
92
+ $ ${$} apply
94
93
 
95
94
  Options:
96
- -h, --help Display help message`,st=e=>{let{values:n}=t({allowPositionals:!0,options:{help:{type:`boolean`,short:`h`}},args:e});n.help&&(console.log(ot),process.exit(0)),v()||(console.error(`No config found. Run \`${Q} init\` first.`),process.exit(1));let r=y(),i=Z(r);console.log(`\nโœ“ 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)},$=t=>{try{return e.readFileSync(t,`utf8`)}catch{return``}},ct=e=>{let t=$(e);if(!t)return null;try{return JSON.parse(t)}catch{return null}},lt=e=>{let t=ct(A(e))??{},n=t.mcpServers&&typeof t.mcpServers==`object`?{...t.mcpServers}:{};for(let t of k(e.name))delete n[t];return Object.assign(n,le(e)),t.mcpServers=n,JSON.stringify(t,null,2)+`
97
- `},ut=t=>{let n=A(t);if(!e.existsSync(n))return null;try{return JSON.parse(e.readFileSync(n,`utf8`)),null}catch{return"invalid JSON; `apply` will not touch it until you fix it"}},dt=e=>{let t=[],n=p();t.push({label:`hook`,path:n,current:$(n),next:X(e)}),t.push({label:`gitconfig`,path:h(),current:Fe(h(),q)??``,next:Le(e)});for(let n of e.workspaces){if(!J(n))continue;let e=Y(n.name);t.push({label:`gitconfig:${n.name}`,path:e,current:$(e),next:Re(n)})}for(let n of e.workspaces){let e=A(n),r=ut(n);if(r){t.push({label:`mcp:${n.name}`,path:e,current:``,next:``,error:r});continue}t.push({label:`mcp:${n.name}`,path:e,current:$(e),next:lt(n)})}return t.filter(e=>e.error!=null||e.current!==e.next)},ft=(e,t)=>{let n=e.length?e.split(`
95
+ -h, --help Display help message`,ft=e=>{let{values:n}=t({allowPositionals:!0,options:{help:{type:`boolean`,short:`h`}},args:e});n.help&&(console.log(dt),process.exit(0)),x()||(console.error(`No config found. Run \`${$} init\` first.`),process.exit(1));let r=S(),i=Q(r);console.log(`\nโœ“ 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)},pt=e=>{let t=v(e);if(!t)return null;try{return JSON.parse(t)}catch{return null}},mt=e=>M(he(pt(A(e))??{},e)),ht=t=>{let n=A(t);if(!e.existsSync(n))return null;try{return JSON.parse(e.readFileSync(n,`utf8`)),null}catch{return"invalid JSON; `apply` will not touch it until you fix it"}},gt=e=>{let t=[],n=p();t.push({label:`hook`,path:n,current:v(n),next:qe(e)}),t.push({label:`gitconfig`,path:h(),current:ze(h(),Y)??``,next:Ve(e)});for(let n of e.workspaces){if(!X(n))continue;let e=Z(n.name);t.push({label:`gitconfig:${n.name}`,path:e,current:v(e),next:He(n)})}for(let n of e.workspaces){let e=A(n),r=ht(n);if(r){t.push({label:`mcp:${n.name}`,path:e,current:``,next:``,error:r});continue}t.push({label:`mcp:${n.name}`,path:e,current:v(e),next:mt(n)})}return t.filter(e=>e.error!=null||e.current!==e.next)},_t=(e,t)=>{let n=e.length?e.split(`
98
96
  `):[],r=t.length?t.split(`
99
97
  `):[],i=n.length,a=r.length,o=Array.from({length:i+1},()=>Array.from({length:a+1},()=>0));for(let e=i-1;e>=0;e--)for(let t=a-1;t>=0;t--)o[e][t]=n[e]===r[t]?o[e+1][t+1]+1:Math.max(o[e+1][t],o[e][t+1]);let s=[],c=0,l=0;for(;c<i&&l<a;)n[c]===r[l]?(s.push(` ${n[c]}`),c++,l++):o[c+1][l]>=o[c][l+1]?(s.push(`- ${n[c]}`),c++):(s.push(`+ ${r[l]}`),l++);for(;c<i;)s.push(`- ${n[c++]}`);for(;l<a;)s.push(`+ ${r[l++]}`);return s.join(`
100
- `)},pt=e=>{let t=[],n=e.workspaces.map(e=>{let n=ct(A(e))?.mcpServers;if(!n||typeof n!=`object`)return e;let r=e.servers,i=e.servers.slack;i&&!i.addMessageTool&&n[`slack-${e.name}`]?.env?.SLACK_MCP_ADD_MESSAGE_TOOL===`true`&&(r={...r,slack:{...i,addMessageTool:!0}},t.push(`${e.name}: slack.addMessageTool = true`));for(let i of O){if(i===`github`||i===`slack`)continue;let a=n[`${i}-${e.name}`]?.url;if(typeof a!=`string`)continue;let o=e.servers[i];if(!o){r={...r,[i]:a===D[i]?!0:{url:a}},t.push(`${e.name}: ${i} = ${a===D[i]?`enabled`:a}`);continue}a!==((typeof o==`object`?o.url:void 0)??D[i])&&(r={...r,[i]:{url:a}},t.push(`${e.name}: ${i}.url = ${a}`))}return r===e.servers?e:{...e,servers:r}});return{cfg:{...e,workspaces:n},changes:t}},mt=e=>{if(!process.stdout.isTTY)return e;let t=process.stdout.columns||80;return e.split(`
98
+ `)},vt=e=>{let t=[],n=e.workspaces.map(e=>{let n=pt(A(e))?.mcpServers;if(!n||typeof n!=`object`)return e;let r=e.servers,i=e.servers.slack;i&&!i.addMessageTool&&n[`slack-${e.name}`]?.env?.SLACK_MCP_ADD_MESSAGE_TOOL===`true`&&(r={...r,slack:{...i,addMessageTool:!0}},t.push(`${e.name}: slack.addMessageTool = true`));for(let i of O){if(i===`github`||i===`slack`)continue;let a=n[`${i}-${e.name}`]?.url;if(typeof a!=`string`)continue;let o=e.servers[i];if(!o){r={...r,[i]:a===D[i]?!0:{url:a}},t.push(`${e.name}: ${i} = ${a===D[i]?`enabled`:a}`);continue}a!==((typeof o==`object`?o.url:void 0)??D[i])&&(r={...r,[i]:{url:a}},t.push(`${e.name}: ${i}.url = ${a}`))}return r===e.servers?e:{...e,servers:r}});return{cfg:{...e,workspaces:n},changes:t}},yt=e=>{if(!process.stdout.isTTY)return e;let t=process.stdout.columns||80;return e.split(`
101
99
  `).map(e=>{if(!e.startsWith(`- `)&&!e.startsWith(`+ `))return e;let n=` `.repeat(Math.max(0,t-e.length));return`\x1b[${e.startsWith(`- `)?`48;2;48;27;31;38;2;248;81;73`:`48;2;18;38;30;38;2;63;185;80`}m${e}${n}\x1b[0m`}).join(`
102
- `)},ht=`Show what \`${Q} apply\` would change: a diff of each managed
100
+ `)},bt=`Show what \`${$} apply\` would change: a diff of each managed
103
101
  artifact (the zsh hook, git includes, and .mcp.json files) against what your
104
102
  config would generate. Read-only.
105
103
 
@@ -108,64 +106,65 @@ With --adopt, pull settings that exist in your .mcp.json but not your config
108
106
  next apply keeps them instead of dropping them.
109
107
 
110
108
  Usage:
111
- $ ${Q} diff [--adopt]
109
+ $ ${$} diff [--adopt] [--exit-code]
112
110
 
113
111
  Options:
114
- --adopt Write config-expressible on-disk settings back into the config
115
- -h, --help Display help message`,gt=e=>{let{values:n}=t({allowPositionals:!0,options:{help:{type:`boolean`,short:`h`},adopt:{type:`boolean`}},args:e});n.help&&(console.log(ht),process.exit(0)),v()||(console.error(`No config found. Run \`${Q} init\` first.`),process.exit(1));let r=y();if(n.adopt){let{cfg:e,changes:t}=pt(r);t.length||(console.log(`
116
- Nothing to adopt: the config already covers your .mcp.json settings.`),process.exit(0)),E(e),b(e),console.log(`
117
- Adopted into config:`);for(let e of t)console.log(` ${e}`);console.log(`\nRun ${I(`${Q} apply`)} to regenerate from the updated config.`),process.exit(0)}let i=dt(r);i.length||(console.log("\nIn sync. `apply` would change nothing."),process.exit(0));for(let e of i)console.log(`\n${I(`${l(e.path)} (${e.label})`)}`),e.error?console.log(Te(` ${e.error}`)):console.log(mt(ft(e.current,e.next)));let{changes:a}=pt(r);if(a.length){console.log("\nThese .mcp.json settings aren't in your config, so `apply` would drop them:");for(let e of a)console.log(` ${e}`);console.log(`\nRun ${I(`${Q} diff --adopt`)} to keep them by writing them into the config.`)}process.exit(0)},_t=t=>{try{return e.readFileSync(t,`utf8`)}catch{return null}},vt=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`&&e.endsWith(`@latest`)))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},yt=(e,t=process.cwd())=>{let r=n.resolve(t),i,a=-1;for(let t of e.workspaces){let e=u(t.path);(r===e||r.startsWith(e+n.sep))&&e.length>a&&(i=t,a=e.length)}return i},bt=(e=j)=>{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}},xt=(t,n=j)=>{let r=[];he()||r.push({status:`warn`,label:`platform`,detail:`inscope's secret resolution targets macOS (gh keyring + Keychain)`});let i=p(),a=_t(i);a===null?r.push({status:`fail`,label:`hook`,detail:`missing ${i}; run \`inscope init\``}):a===X(t)?r.push({status:`ok`,label:`hook`,detail:i}):r.push({status:`warn`,label:`hook`,detail:"out of date; run `inscope apply`"}),r.push(qe()?{status:`ok`,label:`zshrc`,detail:`sources the hook`}:{status:`warn`,label:`zshrc`,detail:"does not source the hook; run `inscope init`"}),t.workspaces.some(J)&&r.push(Fe(h(),q)===null?{status:`fail`,label:`gitconfig`,detail:"missing includeIf block; run `inscope apply`"}:{status:`ok`,label:`gitconfig`,detail:`includeIf block present`});for(let i of t.workspaces){let t=`[${i.name}]`;if(i.gh&&r.push(ge(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(N(e,n)?{status:`ok`,label:`${t} slack`,detail:e}:{status:`fail`,label:`${t} slack`,detail:`${e} not in keychain; run \`${xe(e)}\``})}if(J(i)){let a=Y(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=Se(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=ut(i);if(a)r.push({status:`fail`,label:`${t} mcp`,detail:a});else{let e=fe(i);if(e===null)r.push({status:`warn`,label:`${t} mcp`,detail:"no .mcp.json; run `inscope apply`"});else{let n=k(i.name).filter(t=>e.mcpServers?.[t]);r.push({status:`ok`,label:`${t} mcp`,detail:`${n.length} server(s)`}),_t(A(i))!==lt(i)&&r.push({status:`warn`,label:`${t} mcp`,detail:"out of date; run `inscope diff`"});let a=vt(e);a.length&&r.push({status:`warn`,label:`${t} mcp`,detail:`unpinned: ${a.join(`, `)}`})}}}return r},St=`Verify the setup: gh tokens resolve, keychain entries exist,
112
+ --adopt Write config-expressible on-disk settings back into the config
113
+ --exit-code Exit 1 if anything is out of sync (for CI / pre-commit gates)
114
+ -h, --help Display help message`,xt=e=>{let{values:n}=t({allowPositionals:!0,options:{help:{type:`boolean`,short:`h`},adopt:{type:`boolean`},"exit-code":{type:`boolean`}},args:e});n.help&&(console.log(bt),process.exit(0)),x()||(console.error(`No config found. Run \`${$} init\` first.`),process.exit(1));let r=S();if(n.adopt){let{cfg:e,changes:t}=vt(r);t.length||(console.log(`
115
+ Nothing to adopt: the config already covers your .mcp.json settings.`),process.exit(0)),T(e),C(e),console.log(`
116
+ Adopted into config:`);for(let e of t)console.log(` ${e}`);console.log(`\nRun ${R(`${$} apply`)} to regenerate from the updated config.`),process.exit(0)}let i=gt(r);i.length||(console.log("\nIn sync. `apply` would change nothing."),process.exit(0));let a=n[`exit-code`]?1:0;for(let e of i)console.log(`\n${R(`${l(e.path)} (${e.label})`)}`),e.error?console.log(B(` ${e.error}`)):console.log(yt(_t(e.current,e.next)));let{changes:o}=vt(r);if(o.length){console.log("\nThese .mcp.json settings aren't in your config, so `apply` would drop them:");for(let e of o)console.log(` ${e}`);console.log(`\nRun ${R(`${$} diff --adopt`)} to keep them by writing them into the config.`)}process.exit(a)},St=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`&&e.endsWith(`@latest`)))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},Ct=(e,t=process.cwd())=>{let r=n.resolve(t),i,a=-1;for(let t of e.workspaces){let e=u(t.path);(r===e||r.startsWith(e+n.sep))&&e.length>a&&(i=t,a=e.length)}return i},wt=(e=N)=>{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}},Tt=(t,r=N)=>{let i=[];ye()||i.push({status:`warn`,label:`platform`,detail:`inscope's secret resolution targets macOS (gh keyring + Keychain)`});let a=process.env.SHELL??``;a&&!/(^|\/)zsh$/.test(a)&&i.push({status:`warn`,label:`shell`,detail:`login shell is ${n.basename(a)}; inscope targets zsh (the hook is written to ~/.zshrc)`});let o=p(),s=_(o);s===null?i.push({status:`fail`,label:`hook`,detail:`missing ${o}; run \`inscope init\``}):s===qe(t)?i.push({status:`ok`,label:`hook`,detail:o}):i.push({status:`warn`,label:`hook`,detail:"out of date; run `inscope apply`"}),i.push(Qe()?{status:`ok`,label:`zshrc`,detail:`sources the hook`}:{status:`warn`,label:`zshrc`,detail:"does not source the hook; run `inscope init`"}),t.workspaces.some(X)&&i.push(ze(h(),Y)===null?{status:`fail`,label:`gitconfig`,detail:"missing includeIf block; run `inscope apply`"}:{status:`ok`,label:`gitconfig`,detail:`includeIf block present`});for(let n of t.workspaces){let t=`[${n.name}]`;if(n.gh&&i.push(be(n.gh,r)?{status:`ok`,label:`${t} gh`,detail:`token for ${n.gh}`}:{status:`fail`,label:`${t} gh`,detail:`no token for ${n.gh}; run \`gh auth login\``}),n.servers.slack){let e=n.servers.slack.keychain;i.push(F(e,r)?{status:`ok`,label:`${t} slack`,detail:e}:{status:`fail`,label:`${t} slack`,detail:`${e} not in keychain; run \`${Te(e)}\``})}if(X(n)){let a=Z(n.name);if(!e.existsSync(a))i.push({status:`fail`,label:`${t} git`,detail:`missing ${a}; run \`inscope apply\``});else if(n.git?.email){let e=Ee(a,r);i.push(e===n.git.email?{status:`ok`,label:`${t} git`,detail:n.git.email}:{status:`fail`,label:`${t} git`,detail:`email is ${e??`unset`}, expected ${n.git.email}`})}}let a=ht(n);if(a)i.push({status:`fail`,label:`${t} mcp`,detail:a});else{let e=me(n);if(e===null)i.push({status:`warn`,label:`${t} mcp`,detail:"no .mcp.json; run `inscope apply`"});else{let r=k(n.name).filter(t=>e.mcpServers?.[t]);i.push({status:`ok`,label:`${t} mcp`,detail:`${r.length} server(s)`}),_(A(n))!==mt(n)&&i.push({status:`warn`,label:`${t} mcp`,detail:"out of date; run `inscope diff`"});let a=St(e);a.length&&i.push({status:`warn`,label:`${t} mcp`,detail:`unpinned: ${a.join(`, `)}`})}}}return i},Et=`Verify the setup: gh tokens resolve, keychain entries exist,
118
117
  git emails match per path, the hook is current, and no MCP server is unpinned.
119
118
  Exits non-zero if any check fails.
120
119
 
121
120
  Usage:
122
- $ ${Q} doctor
121
+ $ ${$} doctor
123
122
 
124
123
  Options:
125
- -h, --help Display help message`,Ct={ok:`โœ“`,warn:`!`,fail:`โœ—`},wt={ok:we,warn:L,fail:Te},Tt=e=>{let{values:n}=t({allowPositionals:!0,options:{help:{type:`boolean`,short:`h`}},args:e});n.help&&(console.log(St),process.exit(0)),v()||(console.error(`No config found. Run \`${Q} init\` first.`),process.exit(1));let r=y(),i=xt(r),a=i.map(e=>`${wt[e.status](Ct[e.status])} ${e.label}${e.detail?` ${e.detail}`:``}`).join(`
126
- `);console.log(`\n${a}`);let o=yt(r);if(o){let e=bt();console.log(`\nThis shell (${o.name}):`),console.log(` pwd ${e.pwd}`),console.log(` gh ${e.gh}`),console.log(` git ${e.gitEmail}`),console.log(` token ${e.tokenSet?`set`:`unset`}`)}let s=i.filter(e=>e.status===`fail`).length;s&&(console.log(`\n${Te(`${s} check(s) failed.`)}`),process.exit(1)),console.log(`\n${we(`All checks passed.`)}`),process.exit(0)},Et=`Edit a configured workspace interactively, then re-apply.
124
+ -h, --help Display help message`,Dt={ok:`โœ“`,warn:`!`,fail:`โœ—`},Ot={ok:Oe,warn:z,fail:B},kt=e=>{let{values:n}=t({allowPositionals:!0,options:{help:{type:`boolean`,short:`h`}},args:e});n.help&&(console.log(Et),process.exit(0)),x()||(console.error(`No config found. Run \`${$} init\` first.`),process.exit(1));let r=S(),i=Tt(r),a=i.map(e=>`${Ot[e.status](Dt[e.status])} ${e.label}${e.detail?` ${e.detail}`:``}`).join(`
125
+ `);console.log(`\n${a}`);let o=Ct(r);if(o){let e=wt();console.log(`\nThis shell (${o.name}):`),console.log(` pwd ${e.pwd}`),console.log(` gh ${e.gh}`),console.log(` git ${e.gitEmail}`),console.log(` token ${e.tokenSet?`set`:`unset`}`)}let s=i.filter(e=>e.status===`fail`).length;s&&(console.log(`\n${B(`${s} check(s) failed.`)}`),process.exit(1)),console.log(`\n${Oe(`All checks passed.`)}`),process.exit(0)},At=`Edit a configured workspace interactively, then re-apply.
127
126
  Pick a workspace (or pass its path/label), step through the prompts pre-filled
128
127
  with its current values, and inscope regenerates everything on save.
129
128
 
130
129
  Usage:
131
- $ ${Q} edit [path|label]
130
+ $ ${$} edit [path|label]
132
131
 
133
132
  Options:
134
- -h, --help Display help message`,Dt=async e=>{let{positionals:n,values:r}=t({allowPositionals:!0,options:{help:{type:`boolean`,short:`h`}},args:e});r.help&&(console.log(Et),process.exit(0)),v()||(console.error(`No config found. Run \`${Q} init\` first.`),process.exit(1));let i=y();i.workspaces.length||(console.error(`No workspaces yet. Add one with \`${Q} add <path>\`.`),process.exit(1));let a=n[0],o=await(async()=>{if(a){let e=ie(i,a);return e||(console.error(`No workspace matching "${a}".`),process.exit(1)),e}if(i.workspaces.length===1)return i.workspaces[0];if(P())return H(`Edit which workspace?`,i.workspaces.map(e=>({label:`${e.name} (${e.path})`,value:e})));console.error(`Specify a workspace, e.g. \`${Q} edit <label>\`.`),process.exit(1)})();console.log(`\nEditing "${o.name}" (${o.path})\n`);let s=[...ve().map(e=>({label:e,value:e})),{label:`(none)`,value:``}],c=await H(`GitHub account`,s,Math.max(0,s.findIndex(e=>e.value===(o.gh??``))))||void 0,l=o.git?.email,u=await B(l?`Git email (enter keeps ${l}, "-" to inherit global)`:`Git email (enter to inherit global)`,l??``),d=u===`-`?void 0:u||void 0,f=o.git?.name,p=await B(f?`Git name (enter keeps ${f}, "-" to inherit global)`:`Git name (enter to inherit global)`,f??``),m=p===`-`?void 0:p||void 0,h=Xe(o.servers),g=await Ae(`MCP servers (space toggles, enter confirms)`,Je.map(e=>({label:e,value:e,checked:h.includes(e)}))),_=g.includes(`slack`),b=o.servers.slack?o.servers.slack.keychain:Ye(o.name),x=o.servers.slack?!!o.servers.slack.addMessageTool:!1,S=!1;if(_&&(console.log(`
135
- Slack uses a user OAuth (xoxp) token.`),b=await B(`Slack keychain service`,b),x=await V(`Allow Slack to post messages?`,x),N(b)||(S=await V(`Store the Slack token now?`,!0))),_){let e=w(b);e&&(console.error(`\nInvalid Slack keychain service "${b}": ${e}`),process.exit(1))}let C={name:o.name,path:o.path,gh:c,git:d||m?{email:d,name:m}:void 0,servers:Ze(g,_?{keychain:b,addMessageTool:x}:null)};$e(C),console.log(`\nโœ“ updated "${C.name}" -> ${C.path}`),await et(C,S),console.log(`\nRelaunch \`claude\` from ${C.path} to pick up the changes.`),process.exit(0)},Ot=`Set up inscope: create the config, generate the chpwd hook, and
133
+ -h, --help Display help message`,jt=async e=>{let{positionals:n,values:r}=t({allowPositionals:!0,options:{help:{type:`boolean`,short:`h`}},args:e});r.help&&(console.log(At),process.exit(0)),x()||(console.error(`No config found. Run \`${$} init\` first.`),process.exit(1));let i=S();i.workspaces.length||(console.error(`No workspaces yet. Add one with \`${$} add <path>\`.`),process.exit(1));let a=n[0],o=await(async()=>{if(a){let e=E(i,a);return e||(console.error(`No workspace matching "${a}".`),process.exit(1)),e}if(i.workspaces.length===1)return i.workspaces[0];if(I())return G(`Edit which workspace?`,i.workspaces.map(e=>({label:`${e.name} (${e.path})`,value:e})));console.error(`Specify a workspace, e.g. \`${$} edit <label>\`.`),process.exit(1)})();console.log(`\nEditing "${o.name}" (${o.path})\n`);let s=[...Se().map(e=>({label:e,value:e})),{label:`(none)`,value:``}],c=await G(`GitHub account`,s,Math.max(0,s.findIndex(e=>e.value===(o.gh??``))))||void 0,l=o.git?.email,u=await U(l?`Git email (enter keeps ${l}, "-" to inherit global)`:`Git email (enter to inherit global)`,l??``),d=u===`-`?void 0:u||void 0,f=o.git?.name,p=await U(f?`Git name (enter keeps ${f}, "-" to inherit global)`:`Git name (enter to inherit global)`,f??``),m=p===`-`?void 0:p||void 0,h=tt(o.servers),g=await Pe(`MCP servers (space toggles, enter confirms)`,$e.map(e=>({label:e,value:e,checked:h.includes(e)}))),_=g.includes(`slack`),v=o.servers.slack?o.servers.slack.keychain:et(o.name),y=o.servers.slack?!!o.servers.slack.addMessageTool:!1,b=!1;if(_&&(console.log(`
134
+ Slack uses a user OAuth (xoxp) token.`),v=await U(`Slack keychain service`,v),y=await W(`Allow Slack to post messages?`,y),F(v)||(b=await W(`Store the Slack token now?`,!0))),_){let e=w(v);e&&(console.error(`\nInvalid Slack keychain service "${v}": ${e}`),process.exit(1))}let C={name:o.name,path:o.path,gh:c,git:d||m?{email:d,name:m}:void 0,servers:nt(g,_?{keychain:v,addMessageTool:y}:null)};it(C),console.log(`\nโœ“ updated "${C.name}" -> ${C.path}`),await at(C,b),console.log(`\nRelaunch \`claude\` from ${C.path} to pick up the changes.`),process.exit(0)},Mt=`Set up inscope: create the config, generate the chpwd hook, and
136
135
  source it from ~/.zshrc. Safe to run again; it never overwrites your config.
137
136
 
138
137
  Usage:
139
- $ ${Q} init
138
+ $ ${$} init
140
139
 
141
140
  Options:
142
- -h, --help Display help message`,kt=e=>{let{values:n}=t({allowPositionals:!0,options:{help:{type:`boolean`,short:`h`}},args:e});n.help&&(console.log(Ot),process.exit(0));let r;v()?(r=y(),console.log(`\nUsing existing config at ${f()}`)):(r=_(),b(r),console.log(`\nCreated ${f()}`)),Z(r),console.log(`Generated the chpwd hook and added a source line to ~/.zshrc.`),console.log(`
141
+ -h, --help Display help message`,Nt=e=>{let{values:n}=t({allowPositionals:!0,options:{help:{type:`boolean`,short:`h`}},args:e});n.help&&(console.log(Mt),process.exit(0));let r;x()?(r=S(),console.log(`\nUsing existing config at ${f()}`)):(r=b(),C(r),console.log(`\nCreated ${f()}`)),Q(r),console.log(`Generated the chpwd hook and added a source line to ~/.zshrc.`),console.log(`
143
142
  Next steps:
144
143
  1. Reload your shell: source ~/.zshrc (or open a new terminal)
145
- 2. Map a workspace: ${Q} add ~/acme`),process.exit(0)},At=`List the configured workspaces. Run \`${Q} doctor\` to verify
144
+ 2. Map a workspace: ${$} add ~/acme`),process.exit(0)},Pt=`List the configured workspaces. Run \`${$} doctor\` to verify
146
145
  that their tokens and identities actually resolve.
147
146
 
148
147
  Usage:
149
- $ ${Q} list
148
+ $ ${$} list
150
149
 
151
150
  Options:
152
- -h, --help Display help message`,jt=e=>O.filter(t=>!!e[t]).join(`, `)||`none`,Mt=e=>{let{values:n}=t({allowPositionals:!0,options:{help:{type:`boolean`,short:`h`}},args:e});n.help&&(console.log(At),process.exit(0)),v()||(console.error(`No config found. Run \`${Q} init\` first.`),process.exit(1));let r=y();r.workspaces.length||(console.log(`No workspaces yet. Add one with \`${Q} add <path> --gh <account>\`.`),process.exit(0));for(let e of r.workspaces)console.log(`\n${e.name}`),console.log(` path ${e.path}`),console.log(` gh ${e.gh??`(none)`}`),console.log(` git ${e.git?.email??`(default)`}`),console.log(` servers ${jt(e.servers)}`),e.servers.slack&&console.log(` slack keychain: ${e.servers.slack.keychain}`);process.exit(0)},Nt=`Remove a workspace mapping. Drops its git include and the MCP
151
+ -h, --help Display help message`,Ft=e=>{let{values:n}=t({allowPositionals:!0,options:{help:{type:`boolean`,short:`h`}},args:e});n.help&&(console.log(Pt),process.exit(0)),x()||(console.error(`No config found. Run \`${$} init\` first.`),process.exit(1));let r=S();r.workspaces.length||(console.log(`No workspaces yet. Add one with \`${$} add <path> --gh <account>\`.`),process.exit(0));for(let e of r.workspaces)console.log(`\n${e.name}`),console.log(` path ${e.path}`),console.log(` gh ${e.gh??`(none)`}`),console.log(` git ${e.git?.email??`(default)`}`),console.log(` servers ${tt(e.servers).join(`, `)||`none`}`),e.servers.slack&&console.log(` slack keychain: ${e.servers.slack.keychain}`);process.exit(0)},It=`Remove a workspace mapping. Drops its git include and the MCP
153
152
  servers inscope manages; leaves your keychain and gh accounts untouched. Pick a
154
153
  workspace, or pass its path/label.
155
154
 
156
155
  Usage:
157
- $ ${Q} rm [path|label]
156
+ $ ${$} rm [path|label]
158
157
 
159
158
  Options:
160
159
  -y, --yes Skip the type-the-label confirmation
161
- -h, --help Display help message`,Pt=async e=>{let{positionals:n,values:r}=t({allowPositionals:!0,options:{help:{type:`boolean`,short:`h`},yes:{type:`boolean`,short:`y`}},args:e});r.help&&(console.log(Nt),process.exit(0)),v()||(console.error(`No config found. Run \`${Q} init\` first.`),process.exit(1));let i=y();i.workspaces.length||(console.error(`No workspaces to remove.`),process.exit(1));let a=n[0],o;if(a){let e=ie(i,a);e||(console.error(`No workspace matching "${a}".`),process.exit(1)),o=e}else P()?o=await H(`Remove which workspace?`,i.workspaces.map(e=>({label:`${e.name} (${e.path})`,value:e}))):(console.error(`Specify a workspace, e.g. \`${Q} rm <label>\`.`),process.exit(1));if(!r.yes){console.log(`\nโš  Removing "${o.name}" (${o.path}) unmaps it from inscope.`);let e=await B(`Type "${o.name}" to confirm`);e!==o.name&&(console.error(`Aborted: "${e}" does not match "${o.name}".`),process.exit(1))}let{cfg:s}=se(i,o.name);me(o),Be(o.name),b(s),Z(s),console.log(`\nโœ“ removed workspace "${o.name}"`),o.servers.slack&&console.log(`\nNote: the keychain entry ${o.servers.slack.keychain} was left in place.\nDelete it with: ${I(`security delete-generic-password -s ${o.servers.slack.keychain}`)}`),process.exit(0)},Ft=`Version:
162
- ${Q}@${tt}
160
+ -h, --help Display help message`,Lt=async e=>{let{positionals:n,values:r}=t({allowPositionals:!0,options:{help:{type:`boolean`,short:`h`},yes:{type:`boolean`,short:`y`}},args:e});r.help&&(console.log(It),process.exit(0)),x()||(console.error(`No config found. Run \`${$} init\` first.`),process.exit(1));let i=S();i.workspaces.length||(console.error(`No workspaces to remove.`),process.exit(1));let a=n[0],o;if(a){let e=E(i,a);e||(console.error(`No workspace matching "${a}".`),process.exit(1)),o=e}else I()?o=await G(`Remove which workspace?`,i.workspaces.map(e=>({label:`${e.name} (${e.path})`,value:e}))):(console.error(`Specify a workspace, e.g. \`${$} rm <label>\`.`),process.exit(1));if(!r.yes){console.log(`\nโš  Removing "${o.name}" (${o.path}) unmaps it from inscope.`);let e=await U(`Type "${o.name}" to confirm`);e!==o.name&&(console.error(`Aborted: "${e}" does not match "${o.name}".`),process.exit(1))}let{cfg:s}=ue(i,o.name);ve(o),We(o.name),C(s),Q(s),console.log(`\nโœ“ removed workspace "${o.name}"`),o.servers.slack&&console.log(`\nNote: the keychain entry ${o.servers.slack.keychain} was left in place.\nDelete it with: ${R(`security delete-generic-password -s ${o.servers.slack.keychain}`)}`),process.exit(0)},Rt=`Version:
161
+ ${$}@${ot}
163
162
 
164
163
  Per-workspace identity for Claude Code: scope MCP servers, GitHub auth, and git
165
164
  commit identity to the directory you are in, so concurrent sessions never clash.
166
165
 
167
166
  Usage:
168
- $ ${Q} <command> [options]
167
+ $ ${$} <command> [options]
169
168
 
170
169
  Commands:
171
170
  init Create the config, generate the hook, source it from ~/.zshrc
@@ -182,4 +181,4 @@ Options:
182
181
  -h, --help Display help
183
182
 
184
183
  Author:
185
- ${nt.name} <${nt.email}> (${nt.url})`;(async()=>{try{let e=process.argv.slice(2),t=e[0],n=e.slice(1);switch(t){case`init`:return kt(n);case`add`:return await at(n);case`edit`:return Dt(n);case`rm`:case`remove`:return await Pt(n);case`ls`:case`list`:return Mt(n);case`diff`:return gt(n);case`apply`:case`sync`:return st(n);case`doctor`:return Tt(n)}(t===`-v`||t===`--version`)&&(console.log(`${Q}@${tt}`),process.exit(0)),(!t||t===`-h`||t===`--help`)&&(console.log(Ft),process.exit(0)),console.error(`unknown command: ${e.join(` `)}\n`),console.error(Ft),process.exit(1)}catch(e){console.error(e.message),process.exit(1)}})();export{};
184
+ ${st.name} <${st.email}> (${st.url})`;(async()=>{try{let e=process.argv.slice(2),t=e[0],n=e.slice(1);switch(t){case`init`:return Nt(n);case`add`:return await ut(n);case`edit`:return jt(n);case`rm`:case`remove`:return await Lt(n);case`ls`:case`list`:return Ft(n);case`diff`:return xt(n);case`apply`:case`sync`:return ft(n);case`doctor`:return kt(n)}(t===`-v`||t===`--version`)&&(console.log(`${$}@${ot}`),process.exit(0)),(!t||t===`-h`||t===`--help`)&&(console.log(Rt),process.exit(0)),console.error(`unknown command: ${e.join(` `)}\n`),console.error(Rt),process.exit(1)}catch(e){console.error(e.message),process.exit(1)}})();export{};
package/dist/index.mjs CHANGED
@@ -1,10 +1,10 @@
1
- import e from"node:fs";import t from"node:path";import n from"node:os";import{spawnSync as r}from"node:child_process";const i=()=>n.homedir(),a=()=>process.env.XDG_CONFIG_HOME?.trim()||t.join(i(),`.config`),o=e=>e===`~`?i():e.startsWith(`~/`)?t.join(i(),e.slice(2)):e,s=e=>{let n=o(e),r=i();return n===r?`~`:n.startsWith(r+t.sep)?`~/`+n.slice(r.length+1):n},c=e=>t.resolve(o(e)),l=()=>t.join(a(),`inscope`),u=()=>t.join(l(),`inscope.json`),d=()=>t.join(l(),`inscope.zsh`),f=()=>t.join(l(),`git`),p=()=>t.join(i(),`.gitconfig`),m=()=>t.join(i(),`.zshrc`),ee=1,te=()=>({version:1,workspaces:[]}),ne=()=>e.existsSync(u()),re=()=>{let t=u(),n=e.readFileSync(t,`utf8`),r=JSON.parse(n),i=y(r);if(i)throw Error(i);try{x(r)}catch(e){let n=e instanceof Error?e.message:String(e);throw Error(`${n}\nFix it in ${s(t)}, then re-run.`)}return r},ie=n=>{x(n);let r=u();e.mkdirSync(t.dirname(r),{recursive:!0}),e.writeFileSync(r,JSON.stringify(n,null,2)+`
2
- `)},h=/^[A-Za-z0-9._-]+$/,g=e=>e?h.test(e)?null:`use only letters, digits, dot (.), dash (-), or underscore (_)`:`must not be empty`,ae=/[\\"`$\n]/,_=e=>ae.test(e)?'must not contain a backslash (\\), quote ("), backtick (`), $, or newline':null,oe=e=>e?_(e):`must not be empty`,v=e=>/[\n\r]/.test(e)?`must not contain a newline`:null,y=e=>typeof e.version==`number`&&e.version>1?`config version ${e.version} is newer than this inscope supports (max 1); upgrade inscope`:null,b=e=>e.toLowerCase().replace(/[^a-z0-9._-]+/g,`-`).replace(/^[-.]+|[-.]+$/g,``),x=e=>{if(!e||typeof e!=`object`)throw Error(`config is not an object`);let t=y(e);if(t)throw Error(t);if(!Array.isArray(e.workspaces))throw Error(`config.workspaces must be an array`);let n=new Set;for(let t of e.workspaces){if(!t.name)throw Error(`a workspace is missing a name`);let e=g(t.name);if(e)throw Error(`workspace name "${t.name}" is invalid: ${e}`);if(!t.path)throw Error(`workspace "${t.name}" is missing a path`);let r=oe(t.path);if(r)throw Error(`workspace "${t.name}" path "${t.path}" is invalid: ${r}`);if(t.gh){let e=_(t.gh);if(e)throw Error(`workspace "${t.name}" gh account "${t.gh}" is invalid: ${e}`)}if(t.git?.email){let e=v(t.git.email);if(e)throw Error(`workspace "${t.name}" git email "${t.git.email}" is invalid: ${e}`)}if(t.git?.name){let e=v(t.git.name);if(e)throw Error(`workspace "${t.name}" git name "${t.git.name}" is invalid: ${e}`)}let i=t.servers?.slack;if(i&&i.keychain){let e=_(i.keychain);if(e)throw Error(`workspace "${t.name}" Slack keychain "${i.keychain}" is invalid: ${e}`)}if(n.has(t.name))throw Error(`duplicate workspace name "${t.name}"`);n.add(t.name)}},se=e=>b(t.basename(c(e)))||`workspace`,ce=(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)},le=(e,t,n)=>{let r=c(t);return e.workspaces.find(e=>e.name!==n&&c(e.path)===r)},ue=(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}},de=(e,t)=>{let n=ce(e,t);return n?{cfg:{...e,workspaces:e.workspaces.filter(e=>e.name!==n.name)},removed:n}:{cfg:e}},S=e=>`# >>> inscope:${e} >>>`,C=e=>`# <<< inscope:${e} <<<`,w=e=>e.replace(/[.*+?^${}()|[\]\\]/g,`\\$&`),fe=e=>RegExp(`${w(S(e))}\\n[\\s\\S]*?\\n${w(C(e))}\\n?`),pe=(e,t)=>{let n=t.replace(/\n+$/,``);return`${S(e)}\n${n}\n${C(e)}\n`},T=t=>{try{return e.readFileSync(t,`utf8`)}catch{return``}},me=(n,r,i)=>{e.mkdirSync(t.dirname(n),{recursive:!0});let a=T(n),o=pe(r,i),s=fe(r),c;if(s.test(a))c=a.replace(s,o);else{let e=a.replace(/\n*$/,``);c=e.length?`${e}\n\n${o}`:o}e.writeFileSync(n,c)},he=(t,n)=>{let r=T(t);if(!r)return;let i=r.replace(fe(n),``).replace(/\n{3,}/g,`
1
+ import e from"node:fs";import t from"node:path";import n from"node:os";import{spawnSync as r}from"node:child_process";const i=()=>n.homedir(),a=()=>process.env.XDG_CONFIG_HOME?.trim()||t.join(i(),`.config`),o=e=>e===`~`?i():e.startsWith(`~/`)?t.join(i(),e.slice(2)):e,s=e=>{let n=o(e),r=i();return n===r?`~`:n.startsWith(r+t.sep)?`~/`+n.slice(r.length+1):n},c=e=>t.resolve(o(e)),l=()=>t.join(a(),`inscope`),u=()=>t.join(l(),`inscope.json`),d=()=>t.join(l(),`inscope.zsh`),f=()=>t.join(l(),`git`),p=()=>t.join(i(),`.gitconfig`),m=()=>t.join(i(),`.zshrc`),h=t=>{try{return e.readFileSync(t,`utf8`)}catch{return null}},g=e=>h(e)??``,_=(n,r)=>{let i=n;try{i=e.realpathSync(n)}catch{}let a=t.dirname(i);e.mkdirSync(a,{recursive:!0});let o;try{o=e.statSync(i).mode}catch{}let s=t.join(a,`.${t.basename(i)}.inscope-${process.pid}.tmp`);e.writeFileSync(s,r),o!==void 0&&e.chmodSync(s,o&4095);try{e.renameSync(s,i)}catch(t){try{e.rmSync(s,{force:!0})}catch{}throw t}},ee=1,te=()=>({version:1,workspaces:[]}),ne=()=>e.existsSync(u()),re=()=>{let t=u(),n=e.readFileSync(t,`utf8`),r;try{r=JSON.parse(n)}catch{throw Error(`${s(t)} is not valid JSON; fix it, then re-run.`)}let i=C(r);if(i)throw Error(i);try{T(r)}catch(e){let n=e instanceof Error?e.message:String(e);throw Error(`${n}\nFix it in ${s(t)}, then re-run.`)}return r},ie=e=>{T(e),_(u(),JSON.stringify(e,null,2)+`
2
+ `)},v=/^[A-Za-z0-9._-]+$/,y=e=>e?v.test(e)?null:`use only letters, digits, dot (.), dash (-), or underscore (_)`:`must not be empty`,ae=/[\\"`$\n]/,b=e=>ae.test(e)?'must not contain a backslash (\\), quote ("), backtick (`), $, or newline':null,x=e=>e?b(e):`must not be empty`,S=e=>/[\n\r]/.test(e)?`must not contain a newline`:null,C=e=>typeof e.version==`number`&&e.version>1?`config version ${e.version} is newer than this inscope supports (max 1); upgrade inscope`:null,w=e=>e.toLowerCase().replace(/[^a-z0-9._-]+/g,`-`).replace(/^[-.]+|[-.]+$/g,``),T=e=>{if(!e||typeof e!=`object`)throw Error(`config is not an object`);let t=C(e);if(t)throw Error(t);if(!Array.isArray(e.workspaces))throw Error(`config.workspaces must be an array`);let n=new Set;for(let t of e.workspaces){if(!t.name)throw Error(`a workspace is missing a name`);let e=y(t.name);if(e)throw Error(`workspace name "${t.name}" is invalid: ${e}`);if(!t.path)throw Error(`workspace "${t.name}" is missing a path`);let r=x(t.path);if(r)throw Error(`workspace "${t.name}" path "${t.path}" is invalid: ${r}`);if(t.gh){let e=b(t.gh);if(e)throw Error(`workspace "${t.name}" gh account "${t.gh}" is invalid: ${e}`)}if(t.git?.email){let e=S(t.git.email);if(e)throw Error(`workspace "${t.name}" git email "${t.git.email}" is invalid: ${e}`)}if(t.git?.name){let e=S(t.git.name);if(e)throw Error(`workspace "${t.name}" git name "${t.git.name}" is invalid: ${e}`)}let i=t.servers?.slack;if(i&&i.keychain){let e=b(i.keychain);if(e)throw Error(`workspace "${t.name}" Slack keychain "${i.keychain}" is invalid: ${e}`)}if(n.has(t.name))throw Error(`duplicate workspace name "${t.name}"`);n.add(t.name)}},oe=e=>w(t.basename(c(e)))||`workspace`,E=(e,t)=>{let n=e.workspaces.find(e=>e.name===t);if(n)return n;let r=c(t);return e.workspaces.find(e=>c(e.path)===r)},se=(e,t,n)=>{let r=c(t);return e.workspaces.find(e=>e.name!==n&&c(e.path)===r)},ce=(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}},le=(e,t)=>{let n=E(e,t);return n?{cfg:{...e,workspaces:e.workspaces.filter(e=>e.name!==n.name)},removed:n}:{cfg:e}},D=e=>`# >>> inscope:${e} >>>`,O=e=>`# <<< inscope:${e} <<<`,k=e=>e.replace(/[.*+?^${}()|[\]\\]/g,`\\$&`),ue=e=>RegExp(`${k(D(e))}\\n[\\s\\S]*?\\n${k(O(e))}\\n?`),de=(e,t)=>{let n=t.replace(/\n+$/,``);return`${D(e)}\n${n}\n${O(e)}\n`},fe=(e,t,n)=>{let r=g(e),i=de(t,n),a=ue(t),o;if(a.test(r))o=r.replace(a,i);else{let e=r.replace(/\n*$/,``);o=e.length?`${e}\n\n${i}`:i}_(e,o)},pe=(e,t)=>{let n=g(e);n&&_(e,n.replace(ue(t),``).replace(/\n{3,}/g,`
3
3
 
4
- `).replace(/^\n+/,``);e.writeFileSync(t,i)},E=(e,t)=>{let n=T(e).match(RegExp(`${w(S(t))}\\n([\\s\\S]*?)\\n${w(C(t))}`));return n?n[1]:null},D=`gitconfig`,O=e=>!!(e.git&&(e.git.email||e.git.name)),k=e=>t.join(f(),`${e}.gitconfig`),ge=e=>s(e).replace(/\/+$/,``)+`/`,A=e=>e.workspaces.filter(O).map(e=>`[includeIf "gitdir:${ge(e.path)}"]\n\tpath = ${s(k(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+/,``))},A=(e,t)=>{let n=g(e).match(RegExp(`${k(D(t))}\\n([\\s\\S]*?)\\n${k(O(t))}`));return n?n[1]:null},j=`gitconfig`,M=e=>!!(e.git&&(e.git.email||e.git.name)),N=e=>t.join(f(),`${e}.gitconfig`),me=e=>s(e).replace(/\/+$/,``)+`/`,P=e=>e.workspaces.filter(M).map(e=>`[includeIf "gitdir:${me(e.path)}"]\n\tpath = ${s(N(e.name))}`).join(`
5
+ `),F=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(f(),{recursive:!0});for(let n of t.workspaces)O(n)&&e.writeFileSync(k(n.name),j(n));let n=A(t);n?me(p(),D,n):he(p(),D)},_e=e=>{let t=s(e);return t===`~`?`"$HOME/"*`:t.startsWith(`~/`)?`"$HOME/${t.slice(2)}/"*`:`"${t}/"*`},ve=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.
7
+ `},I=t=>{e.mkdirSync(f(),{recursive:!0});for(let e of t.workspaces)M(e)?_(N(e.name),F(e)):he(e.name);let n=P(t);n?fe(p(),j,n):pe(p(),j)},he=t=>{let n=N(t);e.existsSync(n)&&e.rmSync(n)},ge=e=>{let t=s(e);return t===`~`?`"$HOME/"*`:t.startsWith(`~/`)?`"$HOME/${t.slice(2)}/"*`:`"${t}/"*`},_e=e=>e.servers.slack?e.servers.slack.keychain:``,L=e=>{let t=[...e.workspaces].sort((e,t)=>e.name.localeCompare(t.name));return`# Managed by inscope. Do not edit by hand.
8
8
  # Source of truth: ~/.config/inscope/inscope.json
9
9
  # Edit there, then run \`inscope apply\` to regenerate this file.
10
10
  #
@@ -15,7 +15,7 @@ import e from"node:fs";import t from"node:path";import n from"node:os";import{sp
15
15
  __inscope_resolve_identity() {
16
16
  local ws
17
17
  case "\${PWD}/" in
18
- ${[...t].sort((e,t)=>s(t.path).length-s(e.path).length).map(e=>` ${_e(e.path)}) ws="${e.name}" ;;`).join(`
18
+ ${[...t].sort((e,t)=>s(t.path).length-s(e.path).length).map(e=>` ${ge(e.path)}) ws="${e.name}" ;;`).join(`
19
19
  `)||` # no workspaces configured`}
20
20
  *) ws="" ;;
21
21
  esac
@@ -25,7 +25,7 @@ ${[...t].sort((e,t)=>s(t.path).length-s(e.path).length).map(e=>` ${_e(e.path)
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=ve(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=_e(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,10 +51,8 @@ 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`,F={atlassian:`https://mcp.atlassian.com/v1/mcp`,canva:`https://mcp.canva.com/mcp`,clickup:`https://mcp.clickup.com/mcp`,hubspot:`https://mcp.hubspot.com`,intercom:`https://mcp.intercom.com/mcp`,linear:`https://mcp.linear.app/mcp`,monday:`https://mcp.monday.com/mcp`,notion:`https://mcp.notion.com/mcp`,plane:`https://mcp.plane.so/http/mcp`,sentry:`https://mcp.sentry.dev/mcp`,stripe:`https://mcp.stripe.com`,vercel:`https://mcp.vercel.com`,webflow:`https://mcp.webflow.com/`},I=[`github`,`atlassian`,`canva`,`clickup`,`hubspot`,`intercom`,`linear`,`monday`,`notion`,`plane`,`sentry`,`slack`,`stripe`,`vercel`,`webflow`],L=e=>I.map(t=>`${t}-${e}`),R=e=>t.join(c(e.path),`.mcp.json`),ye=(e,t)=>e&&typeof e==`object`&&e.url?e.url:t,z=e=>{let t=e.servers,n={};for(let r of I){let i=t[r];if(!i)continue;let a=`${r}-${e.name}`;if(r===`github`)n[a]={type:`http`,url:`https://api.githubcopilot.com/mcp/`,headers:{Authorization:"Bearer ${GITHUB_TOKEN}"}};else if(r===`slack`){let e=i,t={SLACK_MCP_XOXP_TOKEN:"${SLACK_MCP_XOXP_TOKEN}"};e.addMessageTool&&(t.SLACK_MCP_ADD_MESSAGE_TOOL=`true`),n[a]={type:`stdio`,command:`npx`,args:[`-y`,`slack-mcp-server@${P}`,`--transport`,`stdio`],env:t}}else n[a]={type:`http`,url:ye(i,F[r])}}return n},be=e=>({mcpServers:z(e)}),xe=t=>{if(!e.existsSync(t))return{};try{return JSON.parse(e.readFileSync(t,`utf8`))}catch{return{}}},B=t=>{if(!e.existsSync(t))return{};try{return JSON.parse(e.readFileSync(t,`utf8`))}catch{throw Error(`${t} is not valid JSON; fix or remove it, then re-run inscope (left it untouched)`)}},V=t=>{let n=R(t);return e.existsSync(n)?xe(n):null},H=n=>{let r=R(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 L(n.name))delete a[e];Object.assign(a,z(n)),i.mcpServers=a,e.writeFileSync(r,JSON.stringify(i,null,2)+`
55
- `)},Se=t=>{let n=R(t);if(!e.existsSync(n))return;let r=B(n);if(r.mcpServers&&typeof r.mcpServers==`object`)for(let e of L(t.name))delete r.mcpServers[e];e.writeFileSync(n,JSON.stringify(r,null,2)+`
56
- `)},Ce=e=>{let n=i();return e===n?`$HOME`:e.startsWith(n+t.sep)?`$HOME/${e.slice(n.length+1)}`:e},U=()=>{let e=Ce(d());return`[ -r "${e}" ] && source "${e}"`},W=e=>{let t=U();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`},G=()=>{let t=m(),n=``;try{n=e.readFileSync(t,`utf8`)}catch{}let r=W(n);r!==n&&e.writeFileSync(t,r)},K=()=>{try{return e.readFileSync(m(),`utf8`).includes(U())}catch{return!1}},we=n=>{let r=d();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(R(e));return{hook:r,gitconfig:n.workspaces.some(e=>e.git?.email||e.git?.name),mcp:i}},q=t=>{try{return e.readFileSync(t,`utf8`)}catch{return``}},J=e=>{let t=q(e);if(!t)return null;try{return JSON.parse(t)}catch{return null}},Y=e=>{let t=J(R(e))??{},n=t.mcpServers&&typeof t.mcpServers==`object`?{...t.mcpServers}:{};for(let t of L(e.name))delete n[t];return Object.assign(n,z(e)),t.mcpServers=n,JSON.stringify(t,null,2)+`
57
- `},X=t=>{let n=R(t);if(!e.existsSync(n))return null;try{return JSON.parse(e.readFileSync(n,`utf8`)),null}catch{return"invalid JSON; `apply` will not touch it until you fix it"}},Te=e=>{let t=[],n=d();t.push({label:`hook`,path:n,current:q(n),next:N(e)}),t.push({label:`gitconfig`,path:p(),current:E(p(),D)??``,next:A(e)});for(let n of e.workspaces){if(!O(n))continue;let e=k(n.name);t.push({label:`gitconfig:${n.name}`,path:e,current:q(e),next:j(n)})}for(let n of e.workspaces){let e=R(n),r=X(n);if(r){t.push({label:`mcp:${n.name}`,path:e,current:``,next:``,error:r});continue}t.push({label:`mcp:${n.name}`,path:e,current:q(e),next:Y(n)})}return t.filter(e=>e.error!=null||e.current!==e.next)},Ee=(e,t)=>{let n=e.length?e.split(`
54
+ `},R=`1.3.0`,z={atlassian:`https://mcp.atlassian.com/v1/mcp`,canva:`https://mcp.canva.com/mcp`,clickup:`https://mcp.clickup.com/mcp`,hubspot:`https://mcp.hubspot.com`,intercom:`https://mcp.intercom.com/mcp`,linear:`https://mcp.linear.app/mcp`,monday:`https://mcp.monday.com/mcp`,notion:`https://mcp.notion.com/mcp`,plane:`https://mcp.plane.so/http/mcp`,sentry:`https://mcp.sentry.dev/mcp`,stripe:`https://mcp.stripe.com`,vercel:`https://mcp.vercel.com`,webflow:`https://mcp.webflow.com/`},B=[`github`,`atlassian`,`canva`,`clickup`,`hubspot`,`intercom`,`linear`,`monday`,`notion`,`plane`,`sentry`,`slack`,`stripe`,`vercel`,`webflow`],V=e=>B.map(t=>`${t}-${e}`),H=e=>t.join(c(e.path),`.mcp.json`),ve=(e,t)=>e&&typeof e==`object`&&e.url?e.url:t,U=e=>{let t=e.servers,n={};for(let r of B){let i=t[r];if(!i)continue;let a=`${r}-${e.name}`;if(r===`github`)n[a]={type:`http`,url:`https://api.githubcopilot.com/mcp/`,headers:{Authorization:"Bearer ${GITHUB_TOKEN}"}};else if(r===`slack`){let e=i,t={SLACK_MCP_XOXP_TOKEN:"${SLACK_MCP_XOXP_TOKEN}"};e.addMessageTool&&(t.SLACK_MCP_ADD_MESSAGE_TOOL=`true`),n[a]={type:`stdio`,command:`npx`,args:[`-y`,`slack-mcp-server@${R}`,`--transport`,`stdio`],env:t}}else n[a]={type:`http`,url:ve(i,z[r])}}return n},ye=e=>({mcpServers:U(e)}),be=t=>{if(!e.existsSync(t))return{};try{return JSON.parse(e.readFileSync(t,`utf8`))}catch{return{}}},W=t=>{if(!e.existsSync(t))return{};try{return JSON.parse(e.readFileSync(t,`utf8`))}catch{throw Error(`${t} is not valid JSON; fix or remove it, then re-run inscope (left it untouched)`)}},G=t=>{let n=H(t);return e.existsSync(n)?be(n):null},K=(e,t)=>{let n=e.mcpServers&&typeof e.mcpServers==`object`?{...e.mcpServers}:{};for(let e of V(t.name))delete n[e];return Object.assign(n,U(t)),{...e,mcpServers:n}},q=e=>JSON.stringify(e,null,2)+`
55
+ `,xe=e=>{for(let t of e)W(H(t))},Se=e=>{let t=H(e);_(t,q(K(W(t),e)))},Ce=t=>{let n=H(t);if(!e.existsSync(n))return;let r=W(n);if(r.mcpServers&&typeof r.mcpServers==`object`)for(let e of V(t.name))delete r.mcpServers[e];_(n,q(r))},we=e=>{let n=i();return e===n?`$HOME`:e.startsWith(n+t.sep)?`$HOME/${e.slice(n.length+1)}`:e},Te=()=>{let e=we(d());return`[ -r "${e}" ] && source "${e}"`},Ee=e=>{let t=Te();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`},De=()=>{let e=m(),t=g(e),n=Ee(t);n!==t&&_(e,n)},Oe=()=>g(m()).includes(Te()),ke=e=>{xe(e.workspaces);let t=d();_(t,L(e)),I(e),De();let n=[];for(let t of e.workspaces)Se(t),n.push(H(t));return{hook:t,gitconfig:e.workspaces.some(e=>e.git?.email||e.git?.name),mcp:n}},Ae=e=>{let t=g(e);if(!t)return null;try{return JSON.parse(t)}catch{return null}},J=e=>q(K(Ae(H(e))??{},e)),Y=t=>{let n=H(t);if(!e.existsSync(n))return null;try{return JSON.parse(e.readFileSync(n,`utf8`)),null}catch{return"invalid JSON; `apply` will not touch it until you fix it"}},je=e=>{let t=[],n=d();t.push({label:`hook`,path:n,current:g(n),next:L(e)}),t.push({label:`gitconfig`,path:p(),current:A(p(),j)??``,next:P(e)});for(let n of e.workspaces){if(!M(n))continue;let e=N(n.name);t.push({label:`gitconfig:${n.name}`,path:e,current:g(e),next:F(n)})}for(let n of e.workspaces){let e=H(n),r=Y(n);if(r){t.push({label:`mcp:${n.name}`,path:e,current:``,next:``,error:r});continue}t.push({label:`mcp:${n.name}`,path:e,current:g(e),next:J(n)})}return t.filter(e=>e.error!=null||e.current!==e.next)},Me=(e,t)=>{let n=e.length?e.split(`
58
56
  `):[],r=t.length?t.split(`
59
57
  `):[],i=n.length,a=r.length,o=Array.from({length:i+1},()=>Array.from({length:a+1},()=>0));for(let e=i-1;e>=0;e--)for(let t=a-1;t>=0;t--)o[e][t]=n[e]===r[t]?o[e+1][t+1]+1:Math.max(o[e+1][t],o[e][t+1]);let s=[],c=0,l=0;for(;c<i&&l<a;)n[c]===r[l]?(s.push(` ${n[c]}`),c++,l++):o[c+1][l]>=o[c][l+1]?(s.push(`- ${n[c]}`),c++):(s.push(`+ ${r[l]}`),l++);for(;c<i;)s.push(`- ${n[c++]}`);for(;l<a;)s.push(`+ ${r[l++]}`);return s.join(`
60
- `)},De=e=>{let t=[],n=e.workspaces.map(e=>{let n=J(R(e))?.mcpServers;if(!n||typeof n!=`object`)return e;let r=e.servers,i=e.servers.slack;i&&!i.addMessageTool&&n[`slack-${e.name}`]?.env?.SLACK_MCP_ADD_MESSAGE_TOOL===`true`&&(r={...r,slack:{...i,addMessageTool:!0}},t.push(`${e.name}: slack.addMessageTool = true`));for(let i of I){if(i===`github`||i===`slack`)continue;let a=n[`${i}-${e.name}`]?.url;if(typeof a!=`string`)continue;let o=e.servers[i];if(!o){r={...r,[i]:a===F[i]?!0:{url:a}},t.push(`${e.name}: ${i} = ${a===F[i]?`enabled`:a}`);continue}a!==((typeof o==`object`?o.url:void 0)??F[i])&&(r={...r,[i]:{url:a}},t.push(`${e.name}: ${i}.url = ${a}`))}return r===e.servers?e:{...e,servers:r}});return{cfg:{...e,workspaces:n},changes:t}},Z=(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??``}},Oe=()=>process.platform===`darwin`,Q=()=>process.env.USER||``,ke=(e,t=Z)=>{let n=t(`gh`,[`auth`,`token`,`-u`,e]),r=n.stdout.trim();return n.status===0&&r?r:null},Ae=(e=Z)=>{let t=e(`gh`,[`auth`,`status`]);return(t.stdout+t.stderr).trim()},je=(e=Z)=>{let t=[];for(let n of Ae(e).matchAll(/account (\S+) \(/g))t.includes(n[1])||t.push(n[1]);return t},Me=(e,t=Z)=>{let n=t(`git`,[`config`,`--global`,e]),r=n.stdout.trim();return n.status===0&&r?r:null},Ne=(e,t=Z)=>{let n=t(`security`,[`find-generic-password`,`-a`,Q(),`-s`,e,`-w`]);return n.status===0&&n.stdout.trim().length>0},Pe=(e,t,n=Z)=>{let r=n(`security`,[`add-generic-password`,`-U`,`-a`,Q(),`-s`,e,`-w`,t]);if(r.status!==0)throw Error(`security add-generic-password failed: ${r.stderr.trim()||`unknown error`}`)},Fe=e=>`security add-generic-password -U -a "${Q()||`$USER`}" -s ${e} -w 'xoxp-...'`,Ie=(e,t=Z)=>{let n=t(`git`,[`config`,`--file`,e,`user.email`]);return n.status===0?n.stdout.trim():null},$=t=>{try{return e.readFileSync(t,`utf8`)}catch{return null}},Le=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`&&e.endsWith(`@latest`)))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},Re=(e,n=process.cwd())=>{let r=t.resolve(n),i,a=-1;for(let n of e.workspaces){let e=c(n.path);(r===e||r.startsWith(e+t.sep))&&e.length>a&&(i=n,a=e.length)}return i},ze=(e=Z)=>{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}},Be=(t,n=Z)=>{let r=[];Oe()||r.push({status:`warn`,label:`platform`,detail:`inscope's secret resolution targets macOS (gh keyring + Keychain)`});let i=d(),a=$(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(K()?{status:`ok`,label:`zshrc`,detail:`sources the hook`}:{status:`warn`,label:`zshrc`,detail:"does not source the hook; run `inscope init`"}),t.workspaces.some(O)&&r.push(E(p(),D)===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(ke(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(Ne(e,n)?{status:`ok`,label:`${t} slack`,detail:e}:{status:`fail`,label:`${t} slack`,detail:`${e} not in keychain; run \`${Fe(e)}\``})}if(O(i)){let a=k(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=Ie(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=X(i);if(a)r.push({status:`fail`,label:`${t} mcp`,detail:a});else{let e=V(i);if(e===null)r.push({status:`warn`,label:`${t} mcp`,detail:"no .mcp.json; run `inscope apply`"});else{let n=L(i.name).filter(t=>e.mcpServers?.[t]);r.push({status:`ok`,label:`${t} mcp`,detail:`${n.length} server(s)`}),$(R(i))!==Y(i)&&r.push({status:`warn`,label:`${t} mcp`,detail:"out of date; run `inscope diff`"});let a=Le(e);a.length&&r.push({status:`warn`,label:`${t} mcp`,detail:`unpinned: ${a.join(`, `)}`})}}}return r};export{ee as CONFIG_VERSION,P as SLACK_MCP_VERSION,h as WORKSPACE_NAME_RE,De as adoptable,we as applyAll,M as applyGitconfig,H as applyMcp,Te as computeDrift,ne as configExists,y as configVersionError,Re as currentWorkspace,te as defaultConfig,Z as defaultRunner,Ee as diffLines,G as ensureZshrcSource,ce as findWorkspace,je as ghAccounts,Ae as ghStatus,ke as ghToken,Ie as gitEmailForFile,Me as gitGlobal,v as gitValueError,_ as hookValueError,Oe as isMacOS,Ne as keychainHas,Pe as keychainSet,Fe as keychainSetCommand,se as labelFromPath,ze as liveSnapshot,re as loadConfig,L as managedKeys,X as mcpError,R as mcpFilePath,Y as mcpTarget,le as pathConflict,V as readMcp,Se as removeMcp,de as removeWorkspace,A as renderGitInclude,N as renderHook,be as renderMcp,j as renderPerWorkspaceGitconfig,z as renderServers,W as renderZshrcSource,Be as runDoctor,ie as saveConfig,b as slugify,ue as upsertWorkspace,x as validateConfig,g as workspaceNameError,oe as workspacePathError,K as zshrcSourcesHook};
58
+ `)},Ne=e=>{let t=[],n=e.workspaces.map(e=>{let n=Ae(H(e))?.mcpServers;if(!n||typeof n!=`object`)return e;let r=e.servers,i=e.servers.slack;i&&!i.addMessageTool&&n[`slack-${e.name}`]?.env?.SLACK_MCP_ADD_MESSAGE_TOOL===`true`&&(r={...r,slack:{...i,addMessageTool:!0}},t.push(`${e.name}: slack.addMessageTool = true`));for(let i of B){if(i===`github`||i===`slack`)continue;let a=n[`${i}-${e.name}`]?.url;if(typeof a!=`string`)continue;let o=e.servers[i];if(!o){r={...r,[i]:a===z[i]?!0:{url:a}},t.push(`${e.name}: ${i} = ${a===z[i]?`enabled`:a}`);continue}a!==((typeof o==`object`?o.url:void 0)??z[i])&&(r={...r,[i]:{url:a}},t.push(`${e.name}: ${i}.url = ${a}`))}return r===e.servers?e:{...e,servers:r}});return{cfg:{...e,workspaces:n},changes:t}},X=(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??``}},Pe=()=>process.platform===`darwin`,Z=()=>process.env.USER||``,Fe=(e,t=X)=>{let n=t(`gh`,[`auth`,`token`,`-u`,e]),r=n.stdout.trim();return n.status===0&&r?r:null},Ie=(e=X)=>{let t=e(`gh`,[`auth`,`status`]);return(t.stdout+t.stderr).trim()},Le=(e=X)=>{let t=[];for(let n of Ie(e).matchAll(/account (\S+) \(/g))t.includes(n[1])||t.push(n[1]);return t},Re=(e,t=X)=>{let n=t(`git`,[`config`,`--global`,e]),r=n.stdout.trim();return n.status===0&&r?r:null},Q=(e,t=X)=>{let n=t(`security`,[`find-generic-password`,`-a`,Z(),`-s`,e,`-w`]);return n.status===0&&n.stdout.trim().length>0},ze=(e,t,n=X)=>{let r=n(`security`,[`add-generic-password`,`-U`,`-a`,Z(),`-s`,e,`-w`,t]);if(r.status!==0)throw Error(`security add-generic-password failed: ${r.stderr.trim()||`unknown error`}`)},Be=e=>`security add-generic-password -U -a "${Z()||`$USER`}" -s ${e} -w 'xoxp-...'`,$=(e,t=X)=>{let n=t(`git`,[`config`,`--file`,e,`user.email`]);return n.status===0?n.stdout.trim():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`&&e.endsWith(`@latest`)))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},He=(e,n=process.cwd())=>{let r=t.resolve(n),i,a=-1;for(let n of e.workspaces){let e=c(n.path);(r===e||r.startsWith(e+t.sep))&&e.length>a&&(i=n,a=e.length)}return i},Ue=(e=X)=>{let t=e(`gh`,[`api`,`user`,`--jq`,`.login`]),n=e(`git`,[`config`,`user.email`]);return{pwd:process.cwd(),gh:t.status===0&&t.stdout.trim()?t.stdout.trim():`none`,gitEmail:n.status===0?n.stdout.trim():`none`,tokenSet:!!process.env.GITHUB_TOKEN}},We=(n,r=X)=>{let i=[];Pe()||i.push({status:`warn`,label:`platform`,detail:`inscope's secret resolution targets macOS (gh keyring + Keychain)`});let a=process.env.SHELL??``;a&&!/(^|\/)zsh$/.test(a)&&i.push({status:`warn`,label:`shell`,detail:`login shell is ${t.basename(a)}; inscope targets zsh (the hook is written to ~/.zshrc)`});let o=d(),s=h(o);s===null?i.push({status:`fail`,label:`hook`,detail:`missing ${o}; run \`inscope init\``}):s===L(n)?i.push({status:`ok`,label:`hook`,detail:o}):i.push({status:`warn`,label:`hook`,detail:"out of date; run `inscope apply`"}),i.push(Oe()?{status:`ok`,label:`zshrc`,detail:`sources the hook`}:{status:`warn`,label:`zshrc`,detail:"does not source the hook; run `inscope init`"}),n.workspaces.some(M)&&i.push(A(p(),j)===null?{status:`fail`,label:`gitconfig`,detail:"missing includeIf block; run `inscope apply`"}:{status:`ok`,label:`gitconfig`,detail:`includeIf block present`});for(let t of n.workspaces){let n=`[${t.name}]`;if(t.gh&&i.push(Fe(t.gh,r)?{status:`ok`,label:`${n} gh`,detail:`token for ${t.gh}`}:{status:`fail`,label:`${n} gh`,detail:`no token for ${t.gh}; run \`gh auth login\``}),t.servers.slack){let e=t.servers.slack.keychain;i.push(Q(e,r)?{status:`ok`,label:`${n} slack`,detail:e}:{status:`fail`,label:`${n} slack`,detail:`${e} not in keychain; run \`${Be(e)}\``})}if(M(t)){let a=N(t.name);if(!e.existsSync(a))i.push({status:`fail`,label:`${n} git`,detail:`missing ${a}; run \`inscope apply\``});else if(t.git?.email){let e=$(a,r);i.push(e===t.git.email?{status:`ok`,label:`${n} git`,detail:t.git.email}:{status:`fail`,label:`${n} git`,detail:`email is ${e??`unset`}, expected ${t.git.email}`})}}let a=Y(t);if(a)i.push({status:`fail`,label:`${n} mcp`,detail:a});else{let e=G(t);if(e===null)i.push({status:`warn`,label:`${n} mcp`,detail:"no .mcp.json; run `inscope apply`"});else{let r=V(t.name).filter(t=>e.mcpServers?.[t]);i.push({status:`ok`,label:`${n} mcp`,detail:`${r.length} server(s)`}),h(H(t))!==J(t)&&i.push({status:`warn`,label:`${n} mcp`,detail:"out of date; run `inscope diff`"});let a=Ve(e);a.length&&i.push({status:`warn`,label:`${n} mcp`,detail:`unpinned: ${a.join(`, `)}`})}}}return i};export{ee as CONFIG_VERSION,R as SLACK_MCP_VERSION,v as WORKSPACE_NAME_RE,Ne as adoptable,ke as applyAll,I as applyGitconfig,Se as applyMcp,je as computeDrift,ne as configExists,C as configVersionError,He as currentWorkspace,te as defaultConfig,X as defaultRunner,Me as diffLines,De as ensureZshrcSource,E as findWorkspace,Le as ghAccounts,Ie as ghStatus,Fe as ghToken,$ as gitEmailForFile,Re as gitGlobal,S as gitValueError,b as hookValueError,Pe as isMacOS,Q as keychainHas,ze as keychainSet,Be as keychainSetCommand,oe as labelFromPath,Ue as liveSnapshot,re as loadConfig,V as managedKeys,Y as mcpError,H as mcpFilePath,J as mcpTarget,se as pathConflict,G as readMcp,Ce as removeMcp,le as removeWorkspace,P as renderGitInclude,L as renderHook,ye as renderMcp,F as renderPerWorkspaceGitconfig,U as renderServers,Ee as renderZshrcSource,We as runDoctor,ie as saveConfig,w as slugify,ce as upsertWorkspace,T as validateConfig,y as workspaceNameError,x as workspacePathError,Oe as zshrcSourcesHook};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "inscope",
3
- "version": "0.8.3",
3
+ "version": "0.8.5",
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
  "chpwd",