inscope 0.8.5 โ 0.8.7
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 +53 -13
- package/dist/bin/index.mjs +39 -37
- package/dist/index.d.mts +5 -1
- package/dist/index.mjs +9 -9
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -7,6 +7,8 @@
|
|
|
7
7
|
[](https://www.npmjs.com/package/inscope)
|
|
8
8
|
[](https://github.com/nrjdalal/inscope)
|
|
9
9
|
|
|
10
|
+
๐ `Zero dependencies` / `Nothing sensitive on disk` / `One zsh hook` / `Race-free across concurrent sessions`
|
|
11
|
+
|
|
10
12
|
๐ **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
13
|
|
|
12
14
|
> #### `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.
|
|
@@ -27,7 +29,8 @@ Nothing sensitive is written to disk: GitHub tokens come from the `gh` keyring a
|
|
|
27
29
|
|
|
28
30
|
### Table of Contents
|
|
29
31
|
|
|
30
|
-
- [
|
|
32
|
+
- [Features](#-features)
|
|
33
|
+
- [Quick Usage](#-quick-usage)
|
|
31
34
|
- [Requirements](#-requirements)
|
|
32
35
|
- [Commands](#-commands)
|
|
33
36
|
- [`inscope init`](#inscope-init)
|
|
@@ -41,41 +44,54 @@ Nothing sensitive is written to disk: GitHub tokens come from the `gh` keyring a
|
|
|
41
44
|
- [What It Manages](#-what-it-manages)
|
|
42
45
|
- [MCP Servers](#-mcp-servers)
|
|
43
46
|
- [Config File](#-config-file)
|
|
47
|
+
- [Install Globally (Optional)](#-install-globally-optional)
|
|
44
48
|
- [Contributing](#-contributing)
|
|
49
|
+
- [More Tools](#-more-tools)
|
|
45
50
|
|
|
46
51
|
---
|
|
47
52
|
|
|
48
|
-
##
|
|
53
|
+
## โจ Features
|
|
49
54
|
|
|
50
|
-
|
|
55
|
+
- ๐ชช Per-directory identity: GitHub token, git commit email, and MCP servers scoped to `$PWD`
|
|
56
|
+
- ๐งต Race-free across concurrent shells and Claude Code sessions, with no global toggles
|
|
57
|
+
- ๐ No secrets on disk: GitHub tokens from the `gh` keyring, Slack tokens from the macOS Keychain
|
|
58
|
+
- ๐ค 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
|
|
59
|
+
- โ๏ธ Git `includeIf` rules so every commit lands with the right author email per path
|
|
60
|
+
- ๐ช A single zsh `chpwd` hook does all the resolution; nothing else touches your shell
|
|
61
|
+
- ๐ฉบ `inscope doctor` verifies tokens, identities, and the hook before you trust them
|
|
62
|
+
- โป๏ธ Idempotent and surgical: only the managed blocks in `.zshrc`, `.gitconfig`, and `.mcp.json` are touched
|
|
51
63
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
64
|
+
---
|
|
65
|
+
|
|
66
|
+
## ๐ Quick Usage
|
|
55
67
|
|
|
56
|
-
|
|
68
|
+
No install required, just prefix any command with `npx`:
|
|
57
69
|
|
|
58
70
|
```sh
|
|
59
71
|
# set up the config + hook, and source it from ~/.zshrc
|
|
60
|
-
inscope init
|
|
72
|
+
npx inscope init
|
|
61
73
|
|
|
62
74
|
# map a workspace - inscope walks you through gh account, git identity, and servers
|
|
63
|
-
inscope add ~/acme
|
|
64
|
-
inscope add ~/personal
|
|
75
|
+
npx inscope add ~/acme
|
|
76
|
+
npx inscope add ~/personal
|
|
65
77
|
|
|
66
78
|
# reload your shell, then verify
|
|
67
79
|
source ~/.zshrc
|
|
68
|
-
inscope doctor
|
|
80
|
+
npx inscope doctor
|
|
69
81
|
```
|
|
70
82
|
|
|
83
|
+
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.
|
|
84
|
+
|
|
71
85
|
`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.
|
|
72
86
|
|
|
73
87
|
Prefer flags or CI? Every prompt has a flag, and `-y` takes the defaults non-interactively:
|
|
74
88
|
|
|
75
89
|
```sh
|
|
76
|
-
inscope add ~/acme --gh <account> --email you@work.com --servers github,linear -y
|
|
90
|
+
npx inscope add ~/acme --gh <account> --email you@work.com --servers github,linear -y
|
|
77
91
|
```
|
|
78
92
|
|
|
93
|
+
Running these a lot? Drop the `npx` with a [global install](#-install-globally-optional).
|
|
94
|
+
|
|
79
95
|
---
|
|
80
96
|
|
|
81
97
|
## ๐งฐ Requirements
|
|
@@ -131,6 +147,8 @@ Map a directory. Run it bare and it walks you through everything: pick the GitHu
|
|
|
131
147
|
(default: github)
|
|
132
148
|
--slack-keychain <s> keychain service for the Slack token
|
|
133
149
|
(default: SLACK_MCP_XOXP_TOKEN_<LABEL> when slack is on)
|
|
150
|
+
--slack-package <p> Slack MCP server package: slack-mcp-server (default,
|
|
151
|
+
pinned) or @nrjdalal/slack-mcp-server (kept on latest)
|
|
134
152
|
--slack-message allow the Slack MCP server to post messages
|
|
135
153
|
--seed-slack prompt for the Slack token and store it in the keychain
|
|
136
154
|
-y, --yes accept defaults, skip all prompts (non-interactive)
|
|
@@ -224,11 +242,13 @@ Each enabled server is written into the workspace `.mcp.json` with a name suffix
|
|
|
224
242
|
Slack is opt-in. Enable it during `add` (shown above), or with flags, then store the token once:
|
|
225
243
|
|
|
226
244
|
```sh
|
|
227
|
-
inscope add ~/acme --gh neeraj-acme-org --servers github,slack --seed-slack
|
|
245
|
+
npx inscope add ~/acme --gh neeraj-acme-org --servers github,slack --seed-slack
|
|
228
246
|
```
|
|
229
247
|
|
|
230
248
|
`--seed-slack` prompts for the `xoxp` token and writes it to the Keychain. Pass `--slack-message` to allow the Slack MCP server to post messages.
|
|
231
249
|
|
|
250
|
+
The Slack setup also lets you pick the server package: the original [`slack-mcp-server`](https://github.com/korotovsky/slack-mcp-server) (pinned to a known-good version, the default) or [`@nrjdalal/slack-mcp-server`](https://www.npmjs.com/package/@nrjdalal/slack-mcp-server) (kept on `latest`). Choose it in the prompt, or pass `--slack-package @nrjdalal/slack-mcp-server`.
|
|
251
|
+
|
|
232
252
|
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.
|
|
233
253
|
|
|
234
254
|
---
|
|
@@ -264,12 +284,32 @@ Edit it directly, then run `inscope apply` to regenerate the hook, git includes,
|
|
|
264
284
|
|
|
265
285
|
---
|
|
266
286
|
|
|
287
|
+
## ๐ฆ Install Globally (Optional)
|
|
288
|
+
|
|
289
|
+
Reaching for inscope often? Install it once and drop the `npx`:
|
|
290
|
+
|
|
291
|
+
```sh
|
|
292
|
+
npm i -g inscope
|
|
293
|
+
inscope <command> [options]
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
---
|
|
297
|
+
|
|
267
298
|
## ๐ค Contributing
|
|
268
299
|
|
|
269
300
|
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.
|
|
270
301
|
|
|
271
302
|
---
|
|
272
303
|
|
|
304
|
+
## ๐ More Tools
|
|
305
|
+
|
|
306
|
+
- [gitpick](https://github.com/nrjdalal/gitpick) - clone exactly the files, folders, or branches you need from any repo
|
|
307
|
+
- [zerostarter](https://github.com/nrjdalal/zerostarter) - the tooling and practices inscope is built on
|
|
308
|
+
|
|
309
|
+
More at [github.com/nrjdalal](https://github.com/nrjdalal).
|
|
310
|
+
|
|
311
|
+
---
|
|
312
|
+
|
|
273
313
|
## License
|
|
274
314
|
|
|
275
315
|
[MIT](./LICENSE) ยฉ [Neeraj Dalal](https://nrjdalal.com)
|
package/dist/bin/index.mjs
CHANGED
|
@@ -1,19 +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`),_=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:[]}),
|
|
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]/,
|
|
4
|
-
`,
|
|
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=()=>process.env.HOME?.trim()||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=[`slack-mcp-server`,`@nrjdalal/slack-mcp-server`],x=`slack-mcp-server`,S=()=>({version:1,workspaces:[]}),C=()=>e.existsSync(f()),w=()=>{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{D(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},T=e=>{D(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]/,E=e=>ne.test(e)?'must not contain a backslash (\\), quote ("), backtick (`), $, or newline':null,re=e=>e?E(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,``),D=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=E(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=E(i.keychain);if(e)throw Error(`workspace "${t.name}" Slack keychain "${i.keychain}" is invalid: ${e}`)}let a=i&&i.package;if(a&&!b.includes(a))throw Error(`workspace "${t.name}" Slack package "${a}" is invalid: use one of ${b.join(`, `)}`);if(n.has(t.name))throw Error(`duplicate workspace name "${t.name}"`);n.add(t.name)}},se=e=>oe(n.basename(u(e)))||`workspace`,O=(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=O(e,t);return n?{cfg:{...e,workspaces:e.workspaces.filter(e=>e.name!==n.name)},removed:n}:{cfg:e}},de=(e=x)=>e===`@nrjdalal/slack-mcp-server`?`@nrjdalal/slack-mcp-server@latest`:`slack-mcp-server@1.3.0`,k={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/`},A=[`github`,`atlassian`,`canva`,`clickup`,`hubspot`,`intercom`,`linear`,`monday`,`notion`,`plane`,`sentry`,`slack`,`stripe`,`vercel`,`webflow`],fe=e=>A.map(t=>`${t}-${e}`),j=e=>n.join(u(e.path),`.mcp.json`),pe=(e,t)=>e&&typeof e==`object`&&e.url?e.url:t,me=e=>{let t=e.servers,n={};for(let r of A){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`,de(e.package),`--transport`,`stdio`],env:t}}else n[a]={type:`http`,url:pe(i,k[r])}}return n},he=t=>{if(!e.existsSync(t))return{};try{return JSON.parse(e.readFileSync(t,`utf8`))}catch{return{}}},M=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)`)}},ge=t=>{let n=j(t);return e.existsSync(n)?he(n):null},_e=(e,t)=>{let n=e.mcpServers&&typeof e.mcpServers==`object`?{...e.mcpServers}:{};for(let e of fe(t.name))delete n[e];return Object.assign(n,me(t)),{...e,mcpServers:n}},N=e=>JSON.stringify(e,null,2)+`
|
|
4
|
+
`,ve=e=>{for(let t of e)M(j(t))},ye=e=>{let t=j(e);y(t,N(_e(M(t),e)))},be=t=>{let n=j(t);if(!e.existsSync(n))return;let r=M(n);if(r.mcpServers&&typeof r.mcpServers==`object`)for(let e of fe(t.name))delete r.mcpServers[e];y(n,N(r))},P=(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??``}},xe=()=>process.platform===`darwin`,F=()=>process.env.USER||``,Se=(e,t=P)=>{let n=t(`gh`,[`auth`,`token`,`-u`,e]),r=n.stdout.trim();return n.status===0&&r?r:null},Ce=(e=P)=>{let t=e(`gh`,[`auth`,`status`]);return(t.stdout+t.stderr).trim()},we=(e=P)=>{let t=[];for(let n of Ce(e).matchAll(/account (\S+) \(/g))t.includes(n[1])||t.push(n[1]);return t},Te=(e,t=P)=>{let n=t(`git`,[`config`,`--global`,e]),r=n.stdout.trim();return n.status===0&&r?r:null},Ee=(e,t=P)=>{let n=t(`security`,[`find-generic-password`,`-a`,F(),`-s`,e,`-w`]);return n.status===0&&n.stdout.trim().length>0},De=(e,t,n=P)=>{let r=n(`security`,[`add-generic-password`,`-U`,`-a`,F(),`-s`,e,`-w`,t]);if(r.status!==0)throw Error(`security add-generic-password failed: ${r.stderr.trim()||`unknown error`}`)},Oe=e=>`security add-generic-password -U -a "${F()||`$USER`}" -s ${e} -w 'xoxp-...'`,ke=(e,t=P)=>{let n=t(`git`,[`config`,`--file`,e,`user.email`]);return n.status===0?n.stdout.trim():null},I=()=>!!(process.stdin.isTTY&&process.stdout.isTTY),Ae=(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`),je=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)},Me=()=>{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 Ne=e=>new Promise(t=>{process.stdout.write(e);let n=()=>{let e=H.indexOf(`
|
|
5
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
|
|
7
|
-
`),n.close(),t(e.trim())})}),
|
|
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?
|
|
9
|
-
`),process.exit(130))};process.stdin.on(`keypress`,u)}),
|
|
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?
|
|
11
|
-
`),process.exit(130))};process.stdin.on(`keypress`,d)}),K=e=>`# >>> inscope:${e} >>>`,
|
|
12
|
-
|
|
13
|
-
`).replace(/^\n+/,``))},
|
|
14
|
-
`),
|
|
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 Ne(`${e}${t?` [${t}]`:``}: `)).trim()||t,W=async(e,t=!1)=>{if(!I()){let n=(await Ne(`${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)},Pe=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())})}),Fe=`\x1B[36m`,Ie=`\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?Fe+r+Ie:r}\n`)}};s(!0),a.emitKeypressEvents(process.stdin);let c=Me(),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)}),Le=(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?Fe+i+Ie:i}\n`)}};c(!0),a.emitKeypressEvents(process.stdin);let l=Me(),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} >>>`,Re=e=>`# <<< inscope:${e} <<<`,q=e=>e.replace(/[.*+?^${}()|[\]\\]/g,`\\$&`),ze=e=>RegExp(`${q(K(e))}\\n[\\s\\S]*?\\n${q(Re(e))}\\n?`),Be=(e,t)=>{let n=t.replace(/\n+$/,``);return`${K(e)}\n${n}\n${Re(e)}\n`},Ve=(e,t,n)=>{let r=v(e),i=Be(t,n),a=ze(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)},He=(e,t)=>{let n=v(e);n&&y(e,n.replace(ze(t),``).replace(/\n{3,}/g,`
|
|
12
|
+
|
|
13
|
+
`).replace(/^\n+/,``))},Ue=(e,t)=>{let n=v(e).match(RegExp(`${q(K(t))}\\n([\\s\\S]*?)\\n${q(Re(t))}`));return n?n[1]:null},J=`gitconfig`,Y=e=>!!(e.git&&(e.git.email||e.git.name)),X=e=>n.join(m(),`${e}.gitconfig`),We=e=>l(e).replace(/\/+$/,``)+`/`,Ge=e=>e.workspaces.filter(Y).map(e=>`[includeIf "gitdir:${We(e.path)}"]\n\tpath = ${l(X(e.name))}`).join(`
|
|
14
|
+
`),Ke=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(`
|
|
15
15
|
`)+`
|
|
16
|
-
`},
|
|
16
|
+
`},qe=t=>{e.mkdirSync(m(),{recursive:!0});for(let e of t.workspaces)Y(e)?y(X(e.name),Ke(e)):Je(e.name);let n=Ge(t);n?Ve(h(),J,n):He(h(),J)},Je=t=>{let n=X(t);e.existsSync(n)&&e.rmSync(n)},Ye=e=>{let t=l(e);return t===`~`?`"$HOME/"*`:t.startsWith(`~/`)?`"$HOME/${t.slice(2)}/"*`:`"${t}/"*`},Xe=e=>e.servers.slack?e.servers.slack.keychain:``,Ze=e=>{let t=[...e.workspaces].sort((e,t)=>e.name.localeCompare(t.name));return`# Managed by inscope. Do not edit by hand.
|
|
17
17
|
# Source of truth: ~/.config/inscope/inscope.json
|
|
18
18
|
# Edit there, then run \`inscope apply\` to regenerate this file.
|
|
19
19
|
#
|
|
@@ -24,7 +24,7 @@ import e from"node:fs";import{parseArgs as t}from"node:util";import n from"node:
|
|
|
24
24
|
__inscope_resolve_identity() {
|
|
25
25
|
local ws
|
|
26
26
|
case "\${PWD}/" in
|
|
27
|
-
${[...t].sort((e,t)=>l(t.path).length-l(e.path).length).map(e=>` ${
|
|
27
|
+
${[...t].sort((e,t)=>l(t.path).length-l(e.path).length).map(e=>` ${Ye(e.path)}) ws="${e.name}" ;;`).join(`
|
|
28
28
|
`)||` # no workspaces configured`}
|
|
29
29
|
*) ws="" ;;
|
|
30
30
|
esac
|
|
@@ -34,7 +34,7 @@ ${[...t].sort((e,t)=>l(t.path).length-l(e.path).length).map(e=>` ${Ge(e.path)
|
|
|
34
34
|
|
|
35
35
|
local gh_user="" slack_svc=""
|
|
36
36
|
case "$ws" in
|
|
37
|
-
${t.map(e=>{let t=[];e.gh&&t.push(`gh_user="${e.gh}"`);let n=
|
|
37
|
+
${t.map(e=>{let t=[];e.gh&&t.push(`gh_user="${e.gh}"`);let n=Xe(e);return n&&t.push(`slack_svc="${n}"`),` ${e.name}) ${t.length?t.join(`; `):`:`} ;;`}).join(`
|
|
38
38
|
`)||` # no workspaces configured`}
|
|
39
39
|
*) return ;; # outside a mapped workspace: nothing set
|
|
40
40
|
esac
|
|
@@ -60,8 +60,8 @@ autoload -Uz add-zsh-hook
|
|
|
60
60
|
add-zsh-hook chpwd __inscope_resolve_identity
|
|
61
61
|
__inscope_ws="__init__" # force the first resolve, clearing any inherited token
|
|
62
62
|
__inscope_resolve_identity
|
|
63
|
-
`},
|
|
64
|
-
No token entered; skipped keychain write.`)}else
|
|
63
|
+
`},Qe=e=>{let t=o();return e===t?`$HOME`:e.startsWith(t+n.sep)?`$HOME/${e.slice(t.length+1)}`:e},$e=()=>{let e=Qe(p());return`[ -r "${e}" ] && source "${e}"`},et=e=>{let t=$e();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`},tt=()=>{let e=g(),t=v(e),n=et(t);n!==t&&y(e,n)},nt=()=>v(g()).includes($e()),Z=e=>{ve(e.workspaces);let t=p();y(t,Ze(e)),qe(e),tt();let n=[];for(let t of e.workspaces)ye(t),n.push(j(t));return{hook:t,gitconfig:e.workspaces.some(e=>e.git?.email||e.git?.name),mcp:n}},rt=A,it=e=>`SLACK_MCP_XOXP_TOKEN_${e.toUpperCase().replace(/[^A-Z0-9]+/g,`_`)}`,at=e=>A.filter(t=>!!e[t]),ot=(e,t)=>{let n={};for(let r of A)if(r===`slack`){if(!t){n[r]=!1;continue}let e={keychain:t.keychain,addMessageTool:t.addMessageTool};t.package&&t.package!==x&&(e.package=t.package),n[r]=e}else n[r]=e.includes(r);return n},Q=[{label:`slack-mcp-server (korotovsky, pinned)`,value:`slack-mcp-server`},{label:`@nrjdalal/slack-mcp-server (latest)`,value:`@nrjdalal/slack-mcp-server`}],st=e=>{let t=(e??``).trim().toLowerCase();return t?[`@nrjdalal/slack-mcp-server`,`nrjdalal`,`nrj`].includes(t)?`@nrjdalal/slack-mcp-server`:[`slack-mcp-server`,`default`,`original`,`korotovsky`].includes(t)?`slack-mcp-server`:null:x},ct=e=>e?`global: ${e}`:`no global set`,lt=e=>{let t=C()?w():S(),n=t.workspaces.find(t=>t.name===e.name),r=le(t,e);T(r),Z(r),n&&u(n.path)!==u(e.path)&&be(n)},ut=async(e,t)=>{if(!e.servers.slack)return;let n=e.servers.slack.keychain;if(t){let e=await Pe(`Paste the Slack xoxp token for ${n}: `);e?(De(n,e),console.log(`\nโ stored ${n} in the macOS keychain`)):console.error(`
|
|
64
|
+
No token entered; skipped keychain write.`)}else Ee(n)||console.log(`\nSlack token not in the keychain yet. Store it once with:\n${R(Oe(n))}\n\nSetup guide: ${R(Ae(`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`,dt=`0.8.7`,ft={name:`Neeraj Dalal`,email:`admin@nrjdalal.com`,url:`https://nrjdalal.com`};const pt=`Map a directory to a GitHub account, git email, and MCP servers.
|
|
65
65
|
Runs interactively in a terminal; pass flags or -y to skip the prompts. Re-running
|
|
66
66
|
with the same label updates that workspace; each directory maps to one workspace.
|
|
67
67
|
|
|
@@ -79,25 +79,27 @@ Options:
|
|
|
79
79
|
(default: github)
|
|
80
80
|
--slack-keychain <s> keychain service for the Slack token
|
|
81
81
|
(default: SLACK_MCP_XOXP_TOKEN_<LABEL> when slack is on)
|
|
82
|
+
--slack-package <p> Slack MCP server package: slack-mcp-server (default,
|
|
83
|
+
pinned) or @nrjdalal/slack-mcp-server (latest)
|
|
82
84
|
--slack-message allow the Slack MCP server to post messages
|
|
83
85
|
--seed-slack prompt for the Slack token and store it in the keychain
|
|
84
86
|
-y, --yes accept defaults, skip all prompts (non-interactive)
|
|
85
|
-
-h, --help Display help message`,
|
|
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)),
|
|
87
|
-
GitHub account for this workspace`,[...
|
|
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
|
|
87
|
+
-h, --help Display help message`,mt=A.map(e=>({label:e,value:e,checked:e===`github`})),ht=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-package":{type:`string`},"slack-message":{type:`boolean`},"seed-slack":{type:`boolean`}},args:n});i.help&&(console.log(pt),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(pt);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.`)+`
|
|
88
|
+
`);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)),C()){let e=ce(w(),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(`
|
|
89
|
+
GitHub account for this workspace`,[...we().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 (${ct(Te(`user.email`))})`)||void 0),m===void 0&&(m=await U(`Git name (${ct(Te(`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(A),t=h.filter(t=>!e.has(t));t.length&&console.error(z(`\nIgnoring unknown server(s): ${t.join(`, `)}`))}else h=a?await Le(`MCP servers (space toggles, enter confirms)`,mt):[`github`];let g=h.includes(`slack`)||!!i[`slack-keychain`]||!!i[`slack-package`]||!!i[`seed-slack`],_=i[`slack-keychain`]||it(c),v=!!i[`slack-message`],y=!!i[`seed-slack`],b=st(i[`slack-package`]);b===null&&(console.error(`\nInvalid --slack-package "${i[`slack-package`]}": use slack-mcp-server or @nrjdalal/slack-mcp-server`),process.exit(1));let x=b;g&&a&&(console.log(`
|
|
90
|
+
Slack uses a user OAuth (xoxp) token.`),i[`slack-package`]||(x=await G(`Slack MCP server package`,Q,Math.max(0,Q.findIndex(e=>e.value===x)))),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 S=f?E(f):null;if(S&&(console.error(`\nInvalid gh account "${f}": ${S}`),process.exit(1)),g){let e=E(_);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:ot(h,g?{keychain:_,addMessageTool:v,package:x}:null)};lt(T),console.log(`\nโ workspace "${c}" -> ${T.path}`),console.log(`โ regenerated the hook, git includes, and ${T.path}/.mcp.json`),await ut(T,y),console.log(`\nLaunch \`claude\` from ${T.path} (or relaunch) to pick up the new identity.`),process.exit(0)},gt=`Regenerate the chpwd hook, git includes, and every .mcp.json
|
|
89
91
|
from your config. Idempotent: run it any time the config changes.
|
|
90
92
|
|
|
91
93
|
Usage:
|
|
92
94
|
$ ${$} apply
|
|
93
95
|
|
|
94
96
|
Options:
|
|
95
|
-
-h, --help Display help message`,
|
|
97
|
+
-h, --help Display help message`,_t=e=>{let{values:n}=t({allowPositionals:!0,options:{help:{type:`boolean`,short:`h`}},args:e});n.help&&(console.log(gt),process.exit(0)),C()||(console.error(`No config found. Run \`${$} init\` first.`),process.exit(1));let r=w(),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)},vt=e=>{let t=v(e);if(!t)return null;try{return JSON.parse(t)}catch{return null}},yt=e=>N(_e(vt(j(e))??{},e)),bt=t=>{let n=j(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"}},xt=e=>{let t=[],n=p();t.push({label:`hook`,path:n,current:v(n),next:Ze(e)}),t.push({label:`gitconfig`,path:h(),current:Ue(h(),J)??``,next:Ge(e)});for(let n of e.workspaces){if(!Y(n))continue;let e=X(n.name);t.push({label:`gitconfig:${n.name}`,path:e,current:v(e),next:Ke(n)})}for(let n of e.workspaces){let e=j(n),r=bt(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:yt(n)})}return t.filter(e=>e.error!=null||e.current!==e.next)},St=(e,t)=>{let n=e.length?e.split(`
|
|
96
98
|
`):[],r=t.length?t.split(`
|
|
97
99
|
`):[],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(`
|
|
98
|
-
`)},
|
|
100
|
+
`)},Ct=e=>{let t=[],n=e.workspaces.map(e=>{let n=vt(j(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 A){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===k[i]?!0:{url:a}},t.push(`${e.name}: ${i} = ${a===k[i]?`enabled`:a}`);continue}a!==((typeof o==`object`?o.url:void 0)??k[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}},wt=e=>{if(!process.stdout.isTTY)return e;let t=process.stdout.columns||80;return e.split(`
|
|
99
101
|
`).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(`
|
|
100
|
-
`)},
|
|
102
|
+
`)},Tt=`Show what \`${$} apply\` would change: a diff of each managed
|
|
101
103
|
artifact (the zsh hook, git includes, and .mcp.json files) against what your
|
|
102
104
|
config would generate. Read-only.
|
|
103
105
|
|
|
@@ -111,9 +113,9 @@ Usage:
|
|
|
111
113
|
Options:
|
|
112
114
|
--adopt Write config-expressible on-disk settings back into the config
|
|
113
115
|
--exit-code Exit 1 if anything is out of sync (for CI / pre-commit gates)
|
|
114
|
-
-h, --help Display help message`,
|
|
115
|
-
Nothing to adopt: the config already covers your .mcp.json settings.`),process.exit(0)),
|
|
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=
|
|
116
|
+
-h, --help Display help message`,Et=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(Tt),process.exit(0)),C()||(console.error(`No config found. Run \`${$} init\` first.`),process.exit(1));let r=w();if(n.adopt){let{cfg:e,changes:t}=Ct(r);t.length||(console.log(`
|
|
117
|
+
Nothing to adopt: the config already covers your .mcp.json settings.`),process.exit(0)),D(e),T(e),console.log(`
|
|
118
|
+
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=xt(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(wt(St(e.current,e.next)));let{changes:o}=Ct(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)},Dt=de(`@nrjdalal/slack-mcp-server`),Ot=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.includes(Dt)){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},kt=(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},At=(e=P)=>{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}},jt=(t,r=P)=>{let i=[];xe()||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===Ze(t)?i.push({status:`ok`,label:`hook`,detail:o}):i.push({status:`warn`,label:`hook`,detail:"out of date; run `inscope apply`"}),i.push(nt()?{status:`ok`,label:`zshrc`,detail:`sources the hook`}:{status:`warn`,label:`zshrc`,detail:"does not source the hook; run `inscope init`"}),t.workspaces.some(Y)&&i.push(Ue(h(),J)===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(Se(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(Ee(e,r)?{status:`ok`,label:`${t} slack`,detail:e}:{status:`fail`,label:`${t} slack`,detail:`${e} not in keychain; run \`${Oe(e)}\``})}if(Y(n)){let a=X(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=ke(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=bt(n);if(a)i.push({status:`fail`,label:`${t} mcp`,detail:a});else{let e=ge(n);if(e===null)i.push({status:`warn`,label:`${t} mcp`,detail:"no .mcp.json; run `inscope apply`"});else{let r=fe(n.name).filter(t=>e.mcpServers?.[t]);i.push({status:`ok`,label:`${t} mcp`,detail:`${r.length} server(s)`}),_(j(n))!==yt(n)&&i.push({status:`warn`,label:`${t} mcp`,detail:"out of date; run `inscope diff`"});let a=Ot(e);a.length&&i.push({status:`warn`,label:`${t} mcp`,detail:`unpinned: ${a.join(`, `)}`})}}}return i},Mt=`Verify the setup: gh tokens resolve, keychain entries exist,
|
|
117
119
|
git emails match per path, the hook is current, and no MCP server is unpinned.
|
|
118
120
|
Exits non-zero if any check fails.
|
|
119
121
|
|
|
@@ -121,8 +123,8 @@ Usage:
|
|
|
121
123
|
$ ${$} doctor
|
|
122
124
|
|
|
123
125
|
Options:
|
|
124
|
-
-h, --help Display help message`,
|
|
125
|
-
`);console.log(`\n${a}`);let o=
|
|
126
|
+
-h, --help Display help message`,Nt={ok:`โ`,warn:`!`,fail:`โ`},Pt={ok:je,warn:z,fail:B},Ft=e=>{let{values:n}=t({allowPositionals:!0,options:{help:{type:`boolean`,short:`h`}},args:e});n.help&&(console.log(Mt),process.exit(0)),C()||(console.error(`No config found. Run \`${$} init\` first.`),process.exit(1));let r=w(),i=jt(r),a=i.map(e=>`${Pt[e.status](Nt[e.status])} ${e.label}${e.detail?` ${e.detail}`:``}`).join(`
|
|
127
|
+
`);console.log(`\n${a}`);let o=kt(r);if(o){let e=At();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${je(`All checks passed.`)}`),process.exit(0)},It=`Edit a configured workspace interactively, then re-apply.
|
|
126
128
|
Pick a workspace (or pass its path/label), step through the prompts pre-filled
|
|
127
129
|
with its current values, and inscope regenerates everything on save.
|
|
128
130
|
|
|
@@ -130,25 +132,25 @@ Usage:
|
|
|
130
132
|
$ ${$} edit [path|label]
|
|
131
133
|
|
|
132
134
|
Options:
|
|
133
|
-
-h, --help Display help message`,
|
|
134
|
-
Slack uses a user OAuth (xoxp) token.`),v=await U(`Slack keychain service`,v),y=await W(`Allow Slack to post messages?`,y),
|
|
135
|
+
-h, --help Display help message`,Lt=async e=>{let{positionals:n,values:r}=t({allowPositionals:!0,options:{help:{type:`boolean`,short:`h`}},args:e});r.help&&(console.log(It),process.exit(0)),C()||(console.error(`No config found. Run \`${$} init\` first.`),process.exit(1));let i=w();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=O(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=[...we().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=at(o.servers),g=await Le(`MCP servers (space toggles, enter confirms)`,rt.map(e=>({label:e,value:e,checked:h.includes(e)}))),_=g.includes(`slack`),v=o.servers.slack?o.servers.slack.keychain:it(o.name),y=o.servers.slack?!!o.servers.slack.addMessageTool:!1,b=o.servers.slack?o.servers.slack.package??x:x,S=!1;if(_&&(console.log(`
|
|
136
|
+
Slack uses a user OAuth (xoxp) token.`),b=await G(`Slack MCP server package`,Q,Math.max(0,Q.findIndex(e=>e.value===b))),v=await U(`Slack keychain service`,v),y=await W(`Allow Slack to post messages?`,y),Ee(v)||(S=await W(`Store the Slack token now?`,!0))),_){let e=E(v);e&&(console.error(`\nInvalid Slack keychain service "${v}": ${e}`),process.exit(1))}let T={name:o.name,path:o.path,gh:c,git:d||m?{email:d,name:m}:void 0,servers:ot(g,_?{keychain:v,addMessageTool:y,package:b}:null)};lt(T),console.log(`\nโ updated "${T.name}" -> ${T.path}`),await ut(T,S),console.log(`\nRelaunch \`claude\` from ${T.path} to pick up the changes.`),process.exit(0)},Rt=`Set up inscope: create the config, generate the chpwd hook, and
|
|
135
137
|
source it from ~/.zshrc. Safe to run again; it never overwrites your config.
|
|
136
138
|
|
|
137
139
|
Usage:
|
|
138
140
|
$ ${$} init
|
|
139
141
|
|
|
140
142
|
Options:
|
|
141
|
-
-h, --help Display help message`,
|
|
143
|
+
-h, --help Display help message`,zt=e=>{let{values:n}=t({allowPositionals:!0,options:{help:{type:`boolean`,short:`h`}},args:e});n.help&&(console.log(Rt),process.exit(0));let r;C()?(r=w(),console.log(`\nUsing existing config at ${f()}`)):(r=S(),T(r),console.log(`\nCreated ${f()}`)),Z(r),console.log(`Generated the chpwd hook and added a source line to ~/.zshrc.`),console.log(`
|
|
142
144
|
Next steps:
|
|
143
145
|
1. Reload your shell: source ~/.zshrc (or open a new terminal)
|
|
144
|
-
2. Map a workspace: ${$} add ~/acme`),process.exit(0)},
|
|
146
|
+
2. Map a workspace: ${$} add ~/acme`),process.exit(0)},Bt=`List the configured workspaces. Run \`${$} doctor\` to verify
|
|
145
147
|
that their tokens and identities actually resolve.
|
|
146
148
|
|
|
147
149
|
Usage:
|
|
148
150
|
$ ${$} list
|
|
149
151
|
|
|
150
152
|
Options:
|
|
151
|
-
-h, --help Display help message`,
|
|
153
|
+
-h, --help Display help message`,Vt=e=>{let{values:n}=t({allowPositionals:!0,options:{help:{type:`boolean`,short:`h`}},args:e});n.help&&(console.log(Bt),process.exit(0)),C()||(console.error(`No config found. Run \`${$} init\` first.`),process.exit(1));let r=w();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 ${at(e.servers).join(`, `)||`none`}`),e.servers.slack&&console.log(` slack keychain: ${e.servers.slack.keychain}`);process.exit(0)},Ht=`Remove a workspace mapping. Drops its git include and the MCP
|
|
152
154
|
servers inscope manages; leaves your keychain and gh accounts untouched. Pick a
|
|
153
155
|
workspace, or pass its path/label.
|
|
154
156
|
|
|
@@ -157,8 +159,8 @@ Usage:
|
|
|
157
159
|
|
|
158
160
|
Options:
|
|
159
161
|
-y, --yes Skip the type-the-label confirmation
|
|
160
|
-
-h, --help Display help message`,
|
|
161
|
-
${$}@${
|
|
162
|
+
-h, --help Display help message`,Ut=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(Ht),process.exit(0)),C()||(console.error(`No config found. Run \`${$} init\` first.`),process.exit(1));let i=w();i.workspaces.length||(console.error(`No workspaces to remove.`),process.exit(1));let a=n[0],o;if(a){let e=O(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);be(o),Je(o.name),T(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: ${R(`security delete-generic-password -s ${o.servers.slack.keychain}`)}`),process.exit(0)},Wt=`Version:
|
|
163
|
+
${$}@${dt}
|
|
162
164
|
|
|
163
165
|
Per-workspace identity for Claude Code: scope MCP servers, GitHub auth, and git
|
|
164
166
|
commit identity to the directory you are in, so concurrent sessions never clash.
|
|
@@ -181,4 +183,4 @@ Options:
|
|
|
181
183
|
-h, --help Display help
|
|
182
184
|
|
|
183
185
|
Author:
|
|
184
|
-
${
|
|
186
|
+
${ft.name} <${ft.email}> (${ft.url})`;(async()=>{try{let e=process.argv.slice(2),t=e[0],n=e.slice(1);switch(t){case`init`:return zt(n);case`add`:return await ht(n);case`edit`:return Lt(n);case`rm`:case`remove`:return await Ut(n);case`ls`:case`list`:return Vt(n);case`diff`:return Et(n);case`apply`:case`sync`:return _t(n);case`doctor`:return Ft(n)}(t===`-v`||t===`--version`)&&(console.log(`${$}@${dt}`),process.exit(0)),(!t||t===`-h`||t===`--help`)&&(console.log(Wt),process.exit(0)),console.error(`unknown command: ${e.join(` `)}\n`),console.error(Wt),process.exit(1)}catch(e){console.error(e.message),process.exit(1)}})();export{};
|
package/dist/index.d.mts
CHANGED
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
//#region src/config.d.ts
|
|
2
|
+
declare const SLACK_PACKAGES: readonly ["slack-mcp-server", "@nrjdalal/slack-mcp-server"];
|
|
3
|
+
type SlackPackage = (typeof SLACK_PACKAGES)[number];
|
|
4
|
+
declare const DEFAULT_SLACK_PACKAGE: SlackPackage;
|
|
2
5
|
type SlackServer = {
|
|
3
6
|
keychain: string;
|
|
4
7
|
addMessageTool?: boolean;
|
|
8
|
+
package?: SlackPackage;
|
|
5
9
|
};
|
|
6
10
|
type HttpServer = {
|
|
7
11
|
url?: string;
|
|
@@ -143,4 +147,4 @@ declare const readMcp: (ws: Workspace) => Record<string, any> | null;
|
|
|
143
147
|
declare const applyMcp: (ws: Workspace) => void;
|
|
144
148
|
declare const removeMcp: (ws: Workspace) => void;
|
|
145
149
|
//#endregion
|
|
146
|
-
export { ApplyResult, CONFIG_VERSION, Check, CheckStatus, Config, Drift, HttpServer, RunResult, Runner, SLACK_MCP_VERSION, Servers, SlackServer, WORKSPACE_NAME_RE, Workspace, adoptable, applyAll, applyGitconfig, applyMcp, computeDrift, configExists, configVersionError, currentWorkspace, defaultConfig, defaultRunner, diffLines, ensureZshrcSource, findWorkspace, ghAccounts, ghStatus, ghToken, gitEmailForFile, gitGlobal, gitValueError, hookValueError, isMacOS, keychainHas, keychainSet, keychainSetCommand, labelFromPath, liveSnapshot, loadConfig, managedKeys, mcpError, mcpFilePath, mcpTarget, pathConflict, readMcp, removeMcp, removeWorkspace, renderGitInclude, renderHook, renderMcp, renderPerWorkspaceGitconfig, renderServers, renderZshrcSource, runDoctor, saveConfig, slugify, upsertWorkspace, validateConfig, workspaceNameError, workspacePathError, zshrcSourcesHook };
|
|
150
|
+
export { ApplyResult, CONFIG_VERSION, Check, CheckStatus, Config, DEFAULT_SLACK_PACKAGE, Drift, HttpServer, RunResult, Runner, SLACK_MCP_VERSION, SLACK_PACKAGES, Servers, SlackPackage, SlackServer, WORKSPACE_NAME_RE, Workspace, adoptable, applyAll, applyGitconfig, applyMcp, computeDrift, configExists, configVersionError, currentWorkspace, defaultConfig, defaultRunner, diffLines, ensureZshrcSource, findWorkspace, ghAccounts, ghStatus, ghToken, gitEmailForFile, gitGlobal, gitValueError, hookValueError, isMacOS, keychainHas, keychainSet, keychainSetCommand, labelFromPath, liveSnapshot, loadConfig, managedKeys, mcpError, mcpFilePath, mcpTarget, pathConflict, readMcp, removeMcp, removeWorkspace, renderGitInclude, renderHook, renderMcp, renderPerWorkspaceGitconfig, renderServers, renderZshrcSource, runDoctor, saveConfig, slugify, upsertWorkspace, validateConfig, workspaceNameError, workspacePathError, zshrcSourcesHook };
|
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`),
|
|
2
|
-
`)},
|
|
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=()=>process.env.HOME?.trim()||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`),ee=()=>t.join(i(),`.zshrc`),m=t=>{try{return e.readFileSync(t,`utf8`)}catch{return null}},h=e=>m(e)??``,g=(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}},_=[`slack-mcp-server`,`@nrjdalal/slack-mcp-server`],v=`slack-mcp-server`,te=1,ne=()=>({version:1,workspaces:[]}),re=()=>e.existsSync(u()),ie=()=>{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=w(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},ae=e=>{T(e),g(u(),JSON.stringify(e,null,2)+`
|
|
2
|
+
`)},y=/^[A-Za-z0-9._-]+$/,b=e=>e?y.test(e)?null:`use only letters, digits, dot (.), dash (-), or underscore (_)`:`must not be empty`,oe=/[\\"`$\n]/,x=e=>oe.test(e)?'must not contain a backslash (\\), quote ("), backtick (`), $, or newline':null,S=e=>e?x(e):`must not be empty`,C=e=>/[\n\r]/.test(e)?`must not contain a newline`:null,w=e=>typeof e.version==`number`&&e.version>1?`config version ${e.version} is newer than this inscope supports (max 1); upgrade inscope`:null,se=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=w(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=b(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=S(t.path);if(r)throw Error(`workspace "${t.name}" path "${t.path}" is invalid: ${r}`);if(t.gh){let e=x(t.gh);if(e)throw Error(`workspace "${t.name}" gh account "${t.gh}" is invalid: ${e}`)}if(t.git?.email){let e=C(t.git.email);if(e)throw Error(`workspace "${t.name}" git email "${t.git.email}" is invalid: ${e}`)}if(t.git?.name){let e=C(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=x(i.keychain);if(e)throw Error(`workspace "${t.name}" Slack keychain "${i.keychain}" is invalid: ${e}`)}let a=i&&i.package;if(a&&!_.includes(a))throw Error(`workspace "${t.name}" Slack package "${a}" is invalid: use one of ${_.join(`, `)}`);if(n.has(t.name))throw Error(`duplicate workspace name "${t.name}"`);n.add(t.name)}},ce=e=>se(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)},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=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,`\\$&`),fe=e=>RegExp(`${k(D(e))}\\n[\\s\\S]*?\\n${k(O(e))}\\n?`),pe=(e,t)=>{let n=t.replace(/\n+$/,``);return`${D(e)}\n${n}\n${O(e)}\n`},me=(e,t,n)=>{let r=h(e),i=pe(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}g(e,o)},he=(e,t)=>{let n=h(e);n&&g(e,n.replace(fe(t),``).replace(/\n{3,}/g,`
|
|
3
3
|
|
|
4
|
-
`).replace(/^\n+/,``))},A=(e,t)=>{let n=
|
|
4
|
+
`).replace(/^\n+/,``))},A=(e,t)=>{let n=h(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`),ge=e=>s(e).replace(/\/+$/,``)+`/`,P=e=>e.workspaces.filter(M).map(e=>`[includeIf "gitdir:${ge(e.path)}"]\n\tpath = ${s(N(e.name))}`).join(`
|
|
5
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
|
-
`},I=t=>{e.mkdirSync(f(),{recursive:!0});for(let e of t.workspaces)M(e)?
|
|
7
|
+
`},I=t=>{e.mkdirSync(f(),{recursive:!0});for(let e of t.workspaces)M(e)?g(N(e.name),F(e)):_e(e.name);let n=P(t);n?me(p(),j,n):he(p(),j)},_e=t=>{let n=N(t);e.existsSync(n)&&e.rmSync(n)},ve=e=>{let t=s(e);return t===`~`?`"$HOME/"*`:t.startsWith(`~/`)?`"$HOME/${t.slice(2)}/"*`:`"${t}/"*`},ye=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=>` ${
|
|
18
|
+
${[...t].sort((e,t)=>s(t.path).length-s(e.path).length).map(e=>` ${ve(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=>` ${ge(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=
|
|
28
|
+
${t.map(e=>{let t=[];e.gh&&t.push(`gh_user="${e.gh}"`);let n=ye(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,8 +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
|
-
`},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/`},
|
|
55
|
-
`,
|
|
54
|
+
`},R=`1.3.0`,z=(e=v)=>e===`@nrjdalal/slack-mcp-server`?`@nrjdalal/slack-mcp-server@latest`:`slack-mcp-server@${R}`,B={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/`},V=[`github`,`atlassian`,`canva`,`clickup`,`hubspot`,`intercom`,`linear`,`monday`,`notion`,`plane`,`sentry`,`slack`,`stripe`,`vercel`,`webflow`],H=e=>V.map(t=>`${t}-${e}`),U=e=>t.join(c(e.path),`.mcp.json`),be=(e,t)=>e&&typeof e==`object`&&e.url?e.url:t,W=e=>{let t=e.servers,n={};for(let r of V){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`,z(e.package),`--transport`,`stdio`],env:t}}else n[a]={type:`http`,url:be(i,B[r])}}return n},xe=e=>({mcpServers:W(e)}),Se=t=>{if(!e.existsSync(t))return{};try{return JSON.parse(e.readFileSync(t,`utf8`))}catch{return{}}},G=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)`)}},K=t=>{let n=U(t);return e.existsSync(n)?Se(n):null},Ce=(e,t)=>{let n=e.mcpServers&&typeof e.mcpServers==`object`?{...e.mcpServers}:{};for(let e of H(t.name))delete n[e];return Object.assign(n,W(t)),{...e,mcpServers:n}},q=e=>JSON.stringify(e,null,2)+`
|
|
55
|
+
`,we=e=>{for(let t of e)G(U(t))},J=e=>{let t=U(e);g(t,q(Ce(G(t),e)))},Te=t=>{let n=U(t);if(!e.existsSync(n))return;let r=G(n);if(r.mcpServers&&typeof r.mcpServers==`object`)for(let e of H(t.name))delete r.mcpServers[e];g(n,q(r))},Ee=e=>{let n=i();return e===n?`$HOME`:e.startsWith(n+t.sep)?`$HOME/${e.slice(n.length+1)}`:e},De=()=>{let e=Ee(d());return`[ -r "${e}" ] && source "${e}"`},Oe=e=>{let t=De();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 e=ee(),t=h(e),n=Oe(t);n!==t&&g(e,n)},Ae=()=>h(ee()).includes(De()),je=e=>{we(e.workspaces);let t=d();g(t,L(e)),I(e),ke();let n=[];for(let t of e.workspaces)J(t),n.push(U(t));return{hook:t,gitconfig:e.workspaces.some(e=>e.git?.email||e.git?.name),mcp:n}},Me=e=>{let t=h(e);if(!t)return null;try{return JSON.parse(t)}catch{return null}},Y=e=>q(Ce(Me(U(e))??{},e)),X=t=>{let n=U(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"}},Ne=e=>{let t=[],n=d();t.push({label:`hook`,path:n,current:h(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:h(e),next:F(n)})}for(let n of e.workspaces){let e=U(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:h(e),next:Y(n)})}return t.filter(e=>e.error!=null||e.current!==e.next)},Pe=(e,t)=>{let n=e.length?e.split(`
|
|
56
56
|
`):[],r=t.length?t.split(`
|
|
57
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(`
|
|
58
|
-
`)},
|
|
58
|
+
`)},Fe=e=>{let t=[],n=e.workspaces.map(e=>{let n=Me(U(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 V){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===B[i]?!0:{url:a}},t.push(`${e.name}: ${i} = ${a===B[i]?`enabled`:a}`);continue}a!==((typeof o==`object`?o.url:void 0)??B[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??``}},Ie=()=>process.platform===`darwin`,Q=()=>process.env.USER||``,Le=(e,t=Z)=>{let n=t(`gh`,[`auth`,`token`,`-u`,e]),r=n.stdout.trim();return n.status===0&&r?r:null},Re=(e=Z)=>{let t=e(`gh`,[`auth`,`status`]);return(t.stdout+t.stderr).trim()},ze=(e=Z)=>{let t=[];for(let n of Re(e).matchAll(/account (\S+) \(/g))t.includes(n[1])||t.push(n[1]);return t},Be=(e,t=Z)=>{let n=t(`git`,[`config`,`--global`,e]),r=n.stdout.trim();return n.status===0&&r?r:null},Ve=(e,t=Z)=>{let n=t(`security`,[`find-generic-password`,`-a`,Q(),`-s`,e,`-w`]);return n.status===0&&n.stdout.trim().length>0},He=(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`}`)},Ue=e=>`security add-generic-password -U -a "${Q()||`$USER`}" -s ${e} -w 'xoxp-...'`,$=(e,t=Z)=>{let n=t(`git`,[`config`,`--file`,e,`user.email`]);return n.status===0?n.stdout.trim():null},We=z(`@nrjdalal/slack-mcp-server`),Ge=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.includes(We)){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},Ke=(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},qe=(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}},Je=(n,r=Z)=>{let i=[];Ie()||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=m(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(Ae()?{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(Le(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(Ve(e,r)?{status:`ok`,label:`${n} slack`,detail:e}:{status:`fail`,label:`${n} slack`,detail:`${e} not in keychain; run \`${Ue(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=X(t);if(a)i.push({status:`fail`,label:`${n} mcp`,detail:a});else{let e=K(t);if(e===null)i.push({status:`warn`,label:`${n} mcp`,detail:"no .mcp.json; run `inscope apply`"});else{let r=H(t.name).filter(t=>e.mcpServers?.[t]);i.push({status:`ok`,label:`${n} mcp`,detail:`${r.length} server(s)`}),m(U(t))!==Y(t)&&i.push({status:`warn`,label:`${n} mcp`,detail:"out of date; run `inscope diff`"});let a=Ge(e);a.length&&i.push({status:`warn`,label:`${n} mcp`,detail:`unpinned: ${a.join(`, `)}`})}}}return i};export{te as CONFIG_VERSION,v as DEFAULT_SLACK_PACKAGE,R as SLACK_MCP_VERSION,_ as SLACK_PACKAGES,y as WORKSPACE_NAME_RE,Fe as adoptable,je as applyAll,I as applyGitconfig,J as applyMcp,Ne as computeDrift,re as configExists,w as configVersionError,Ke as currentWorkspace,ne as defaultConfig,Z as defaultRunner,Pe as diffLines,ke as ensureZshrcSource,E as findWorkspace,ze as ghAccounts,Re as ghStatus,Le as ghToken,$ as gitEmailForFile,Be as gitGlobal,C as gitValueError,x as hookValueError,Ie as isMacOS,Ve as keychainHas,He as keychainSet,Ue as keychainSetCommand,ce as labelFromPath,qe as liveSnapshot,ie as loadConfig,H as managedKeys,X as mcpError,U as mcpFilePath,Y as mcpTarget,le as pathConflict,K as readMcp,Te as removeMcp,de as removeWorkspace,P as renderGitInclude,L as renderHook,xe as renderMcp,F as renderPerWorkspaceGitconfig,W as renderServers,Oe as renderZshrcSource,Je as runDoctor,ae as saveConfig,se as slugify,ue as upsertWorkspace,T as validateConfig,b as workspaceNameError,S as workspacePathError,Ae as zshrcSourcesHook};
|