context-mode 0.9.17 → 0.9.18
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/.claude-plugin/hooks/hooks.json +38 -0
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +2 -2
- package/LICENSE +94 -21
- package/README.md +127 -3
- package/build/cli.js +54 -1
- package/build/executor.js +46 -16
- package/build/runtime.d.ts +1 -1
- package/build/runtime.js +39 -21
- package/build/security.d.ts +120 -0
- package/build/security.js +466 -0
- package/build/server.js +169 -14
- package/build/store.d.ts +8 -0
- package/build/store.js +316 -109
- package/hooks/hooks.json +38 -0
- package/hooks/pretooluse.mjs +259 -134
- package/hooks/routing-block.mjs +47 -0
- package/hooks/sessionstart.mjs +30 -0
- package/package.json +2 -2
- package/server.bundle.mjs +145 -81
- package/skills/context-mode/SKILL.md +20 -1
|
@@ -46,6 +46,44 @@
|
|
|
46
46
|
"command": "node ${CLAUDE_PLUGIN_ROOT}/hooks/pretooluse.mjs"
|
|
47
47
|
}
|
|
48
48
|
]
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
"matcher": "mcp__plugin_context-mode_context-mode__execute",
|
|
52
|
+
"hooks": [
|
|
53
|
+
{
|
|
54
|
+
"type": "command",
|
|
55
|
+
"command": "node ${CLAUDE_PLUGIN_ROOT}/hooks/pretooluse.mjs"
|
|
56
|
+
}
|
|
57
|
+
]
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
"matcher": "mcp__plugin_context-mode_context-mode__execute_file",
|
|
61
|
+
"hooks": [
|
|
62
|
+
{
|
|
63
|
+
"type": "command",
|
|
64
|
+
"command": "node ${CLAUDE_PLUGIN_ROOT}/hooks/pretooluse.mjs"
|
|
65
|
+
}
|
|
66
|
+
]
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
"matcher": "mcp__plugin_context-mode_context-mode__batch_execute",
|
|
70
|
+
"hooks": [
|
|
71
|
+
{
|
|
72
|
+
"type": "command",
|
|
73
|
+
"command": "node ${CLAUDE_PLUGIN_ROOT}/hooks/pretooluse.mjs"
|
|
74
|
+
}
|
|
75
|
+
]
|
|
76
|
+
}
|
|
77
|
+
],
|
|
78
|
+
"SessionStart": [
|
|
79
|
+
{
|
|
80
|
+
"matcher": "",
|
|
81
|
+
"hooks": [
|
|
82
|
+
{
|
|
83
|
+
"type": "command",
|
|
84
|
+
"command": "node ${CLAUDE_PLUGIN_ROOT}/hooks/sessionstart.mjs"
|
|
85
|
+
}
|
|
86
|
+
]
|
|
49
87
|
}
|
|
50
88
|
]
|
|
51
89
|
}
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
"name": "context-mode",
|
|
14
14
|
"source": "./",
|
|
15
15
|
"description": "Claude Code MCP plugin that saves 98% of your context window. Sandboxed code execution in 11 languages, FTS5 knowledge base with BM25 ranking, and intent-driven search.",
|
|
16
|
-
"version": "0.9.
|
|
16
|
+
"version": "0.9.18",
|
|
17
17
|
"author": {
|
|
18
18
|
"name": "Mert Koseoğlu"
|
|
19
19
|
},
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "context-mode",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.18",
|
|
4
4
|
"description": "Claude Code MCP plugin that saves 98% of your context window. Sandboxed code execution in 11 languages, FTS5 knowledge base with BM25 ranking, and intent-driven search.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Mert Koseoğlu",
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
},
|
|
9
9
|
"homepage": "https://github.com/mksglu/claude-context-mode#readme",
|
|
10
10
|
"repository": "https://github.com/mksglu/claude-context-mode",
|
|
11
|
-
"license": "
|
|
11
|
+
"license": "Elastic-2.0",
|
|
12
12
|
"keywords": [
|
|
13
13
|
"mcp",
|
|
14
14
|
"context-window",
|
package/LICENSE
CHANGED
|
@@ -1,21 +1,94 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
Copyright
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
1
|
+
Elastic License 2.0 (ELv2)
|
|
2
|
+
|
|
3
|
+
Copyright 2026 Mert Koseoglu
|
|
4
|
+
|
|
5
|
+
## Acceptance
|
|
6
|
+
|
|
7
|
+
By using the software, you agree to all of the terms and conditions below.
|
|
8
|
+
|
|
9
|
+
## Copyright License
|
|
10
|
+
|
|
11
|
+
The licensor grants you a non-exclusive, royalty-free, worldwide,
|
|
12
|
+
non-sublicensable, non-transferable license to use, copy, distribute, make
|
|
13
|
+
available, and prepare derivative works of the software, in each case subject
|
|
14
|
+
to the limitations and conditions below.
|
|
15
|
+
|
|
16
|
+
## Limitations
|
|
17
|
+
|
|
18
|
+
You may not provide the software to third parties as a hosted or managed
|
|
19
|
+
service, where the service provides users with access to any substantial set
|
|
20
|
+
of the features or functionality of the software.
|
|
21
|
+
|
|
22
|
+
You may not move, change, disable, or circumvent the license key
|
|
23
|
+
functionality in the software, and you may not remove or obscure any
|
|
24
|
+
functionality in the software that is protected by the license key.
|
|
25
|
+
|
|
26
|
+
You may not alter, remove, or obscure any licensing, copyright, or other
|
|
27
|
+
notices of the licensor in the software. Any use of the licensor's trademarks
|
|
28
|
+
is subject to applicable law.
|
|
29
|
+
|
|
30
|
+
## Patents
|
|
31
|
+
|
|
32
|
+
The licensor grants you a license, under any patent claims the licensor can
|
|
33
|
+
license, or becomes able to license, to make, have made, use, sell, offer for
|
|
34
|
+
sale, import and have imported the software, in each case subject to the
|
|
35
|
+
limitations and conditions in this license. This license does not cover any
|
|
36
|
+
patent claims that you cause to be infringed by modifications or additions to
|
|
37
|
+
the software. If you or your company make any written claim that the software
|
|
38
|
+
infringes or contributes to infringement of any patent, your patent license
|
|
39
|
+
for the software granted under these terms ends immediately. If your company
|
|
40
|
+
makes such a claim, your patent license ends immediately for work on behalf
|
|
41
|
+
of your company.
|
|
42
|
+
|
|
43
|
+
## Notices
|
|
44
|
+
|
|
45
|
+
You must ensure that anyone who gets a copy of any part of the software from
|
|
46
|
+
you also gets a copy of these terms.
|
|
47
|
+
|
|
48
|
+
If you modify the software, you must include in any modified copies of the
|
|
49
|
+
software prominent notices stating that you have modified the software.
|
|
50
|
+
|
|
51
|
+
## No Other Rights
|
|
52
|
+
|
|
53
|
+
These terms do not imply any licenses other than those expressly granted in
|
|
54
|
+
these terms.
|
|
55
|
+
|
|
56
|
+
## Termination
|
|
57
|
+
|
|
58
|
+
If you use the software in violation of these terms, such use is not
|
|
59
|
+
licensed, and your licenses will automatically terminate. If the licensor
|
|
60
|
+
provides you with a notice of your violation, and you cease all violation of
|
|
61
|
+
this license no later than 30 days after you receive that notice, your
|
|
62
|
+
licenses will be reinstated retroactively. However, if you violate these
|
|
63
|
+
terms after such reinstatement, any additional violation of these terms will
|
|
64
|
+
cause your licenses to terminate automatically and permanently.
|
|
65
|
+
|
|
66
|
+
## No Liability
|
|
67
|
+
|
|
68
|
+
*As far as the law allows, the software comes as is, without any warranty or
|
|
69
|
+
condition, and the licensor will not be liable to you for any damages arising
|
|
70
|
+
out of these terms or the use or nature of the software, under any kind of
|
|
71
|
+
legal claim.*
|
|
72
|
+
|
|
73
|
+
## Definitions
|
|
74
|
+
|
|
75
|
+
The **licensor** is the entity offering these terms, and the **software** is
|
|
76
|
+
the software the licensor makes available under these terms, including any
|
|
77
|
+
portion of it.
|
|
78
|
+
|
|
79
|
+
**you** refers to the individual or entity agreeing to these terms.
|
|
80
|
+
|
|
81
|
+
**your company** is any legal entity, sole proprietorship, or other kind of
|
|
82
|
+
organization that you work for, plus all organizations that have control over,
|
|
83
|
+
are under the control of, or are under common control with that organization.
|
|
84
|
+
**control** means ownership of substantially all the assets of an entity, or
|
|
85
|
+
the power to direct its management and policies by vote, contract, or
|
|
86
|
+
otherwise. Control can be direct or indirect.
|
|
87
|
+
|
|
88
|
+
**your licenses** are all the licenses granted to you for the software under
|
|
89
|
+
these terms.
|
|
90
|
+
|
|
91
|
+
**use** means anything you do with the software requiring one of your
|
|
92
|
+
licenses.
|
|
93
|
+
|
|
94
|
+
**trademark** means trademarks, service marks, and similar rights.
|
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
**The other half of the context problem.**
|
|
4
4
|
|
|
5
|
-
[](https://www.npmjs.com/package/context-mode) [](https://www.npmjs.com/package/context-mode) [](https://github.com/mksglu/claude-context-mode) [](https://www.npmjs.com/package/context-mode) [](https://www.npmjs.com/package/context-mode) [](https://github.com/mksglu/claude-context-mode) [](https://github.com/mksglu/claude-context-mode/stargazers) [](https://github.com/mksglu/claude-context-mode/network/members) [](https://github.com/mksglu/claude-context-mode/commits) [](LICENSE)
|
|
6
6
|
|
|
7
7
|
Every MCP tool call in Claude Code dumps raw data into your 200K context window. A Playwright snapshot costs 56 KB. Twenty GitHub issues cost 59 KB. One access log — 45 KB. After 30 minutes, 40% of your context is gone.
|
|
8
8
|
|
|
@@ -62,7 +62,7 @@ Code Mode showed that tool definitions can be compressed by 99.9%. Context Mode
|
|
|
62
62
|
| `execute_file` | Process files in sandbox. Raw content never leaves. | 45 KB → 155 B |
|
|
63
63
|
| `index` | Chunk markdown into FTS5 with BM25 ranking. | 60 KB → 40 B |
|
|
64
64
|
| `search` | Query indexed content with multiple queries in one call. | On-demand retrieval |
|
|
65
|
-
| `fetch_and_index` | Fetch URL,
|
|
65
|
+
| `fetch_and_index` | Fetch URL, detect content type (HTML/JSON/text), chunk and index. | 60 KB → 40 B |
|
|
66
66
|
|
|
67
67
|
## How the Sandbox Works
|
|
68
68
|
|
|
@@ -184,6 +184,130 @@ Fetch the React useEffect docs, index them, and find the cleanup pattern
|
|
|
184
184
|
with code examples. Then run /context-mode:stats.
|
|
185
185
|
```
|
|
186
186
|
|
|
187
|
+
## Security
|
|
188
|
+
|
|
189
|
+
Context Mode enforces the same permission rules you already use in Claude Code — but extends them to the MCP sandbox. If you block `sudo` in Claude Code, it's also blocked inside `execute`, `execute_file`, and `batch_execute`.
|
|
190
|
+
|
|
191
|
+
**Zero setup required.** If you haven't configured any permissions, nothing changes. This only activates when you add rules.
|
|
192
|
+
|
|
193
|
+
### Getting started
|
|
194
|
+
|
|
195
|
+
Find your settings file:
|
|
196
|
+
|
|
197
|
+
```bash
|
|
198
|
+
# macOS / Linux
|
|
199
|
+
cat ~/.claude/settings.json
|
|
200
|
+
|
|
201
|
+
# Windows
|
|
202
|
+
type %USERPROFILE%\.claude\settings.json
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
Add a `permissions` section (keep your existing settings, just add this block). Then restart Claude Code.
|
|
206
|
+
|
|
207
|
+
```json
|
|
208
|
+
{
|
|
209
|
+
"permissions": {
|
|
210
|
+
"deny": [
|
|
211
|
+
"Bash(sudo *)",
|
|
212
|
+
"Bash(rm -rf /*)",
|
|
213
|
+
"Read(.env)",
|
|
214
|
+
"Read(**/.env*)"
|
|
215
|
+
],
|
|
216
|
+
"allow": [
|
|
217
|
+
"Bash(git:*)",
|
|
218
|
+
"Bash(npm:*)"
|
|
219
|
+
]
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
The pattern is `Tool(what to match)` where `*` means "anything".
|
|
225
|
+
|
|
226
|
+
<details>
|
|
227
|
+
<summary><strong>Common deny patterns</strong> (click to expand)</summary>
|
|
228
|
+
|
|
229
|
+
**Dangerous commands:**
|
|
230
|
+
```
|
|
231
|
+
"Bash(sudo *)" — block all sudo commands
|
|
232
|
+
"Bash(rm -rf /*)" — block recursive delete from root
|
|
233
|
+
"Bash(chmod 777 *)" — block open permissions
|
|
234
|
+
"Bash(shutdown *)" — block shutdown/reboot
|
|
235
|
+
"Bash(kill -9 *)" — block force kill
|
|
236
|
+
"Bash(mkfs *)" — block filesystem format
|
|
237
|
+
"Bash(dd *)" — block disk write
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
**Network access:**
|
|
241
|
+
```
|
|
242
|
+
"Bash(curl *)" — block curl
|
|
243
|
+
"Bash(wget *)" — block wget
|
|
244
|
+
"Bash(ssh *)" — block ssh connections
|
|
245
|
+
"Bash(scp *)" — block secure copy
|
|
246
|
+
"Bash(nc *)" — block netcat
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
**Package managers and deploys:**
|
|
250
|
+
```
|
|
251
|
+
"Bash(npm publish *)" — block npm publish
|
|
252
|
+
"Bash(docker push *)" — block docker push
|
|
253
|
+
"Bash(pip install *)" — block pip install
|
|
254
|
+
"Bash(brew install *)" — block brew install
|
|
255
|
+
"Bash(apt install *)" — block apt install
|
|
256
|
+
"Bash(wrangler deploy *)" — block Cloudflare deploys
|
|
257
|
+
"Bash(terraform apply *)" — block terraform apply
|
|
258
|
+
"Bash(kubectl delete *)" — block k8s delete
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
**Sensitive files:**
|
|
262
|
+
```
|
|
263
|
+
"Read(.env)" — block .env in project root
|
|
264
|
+
"Read(**/.env*)" — block .env files everywhere
|
|
265
|
+
"Read(**/*secret*)" — block files with "secret" in the name
|
|
266
|
+
"Read(**/*credential*)" — block credential files
|
|
267
|
+
"Read(**/*.pem)" — block private keys
|
|
268
|
+
"Read(**/*id_rsa*)" — block SSH keys
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
</details>
|
|
272
|
+
|
|
273
|
+
<details>
|
|
274
|
+
<summary><strong>Common allow patterns</strong> (click to expand)</summary>
|
|
275
|
+
|
|
276
|
+
```
|
|
277
|
+
"Bash(git:*)" — allow git (with or without args)
|
|
278
|
+
"Bash(npm:*)" — allow npm
|
|
279
|
+
"Bash(npx:*)" — allow npx
|
|
280
|
+
"Bash(node:*)" — allow node
|
|
281
|
+
"Bash(python:*)" — allow python
|
|
282
|
+
"Bash(ls:*)" — allow ls
|
|
283
|
+
"Bash(cat:*)" — allow cat
|
|
284
|
+
"Bash(echo:*)" — allow echo
|
|
285
|
+
"Bash(grep:*)" — allow grep
|
|
286
|
+
"Bash(make:*)" — allow make
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
</details>
|
|
290
|
+
|
|
291
|
+
### Chained commands
|
|
292
|
+
|
|
293
|
+
Commands chained with `&&`, `;`, or `|` are split — each part is checked separately:
|
|
294
|
+
|
|
295
|
+
```
|
|
296
|
+
echo hello && sudo rm -rf /tmp
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
Blocked. Even though it starts with `echo`, the `sudo` part matches the deny rule.
|
|
300
|
+
|
|
301
|
+
### Where to put rules
|
|
302
|
+
|
|
303
|
+
Rules can go in three places (checked in this order):
|
|
304
|
+
|
|
305
|
+
1. `.claude/settings.local.json` — this project only (gitignored)
|
|
306
|
+
2. `.claude/settings.json` — this project, shared with team
|
|
307
|
+
3. `~/.claude/settings.json` — all projects
|
|
308
|
+
|
|
309
|
+
**deny** always wins over **allow**. More specific (project-level) rules override global ones.
|
|
310
|
+
|
|
187
311
|
## Requirements
|
|
188
312
|
|
|
189
313
|
- **Node.js 18+**
|
|
@@ -213,4 +337,4 @@ npm run test:all # full suite
|
|
|
213
337
|
|
|
214
338
|
## License
|
|
215
339
|
|
|
216
|
-
|
|
340
|
+
[Elastic License 2.0 (ELv2)](LICENSE) — free to use, modify, and share. You may not rebrand and redistribute this software as a competing plugin, product, or managed service.
|
package/build/cli.js
CHANGED
|
@@ -231,6 +231,24 @@ async function doctor() {
|
|
|
231
231
|
" — No PreToolUse hooks found" +
|
|
232
232
|
color.dim("\n Run: npx context-mode upgrade"));
|
|
233
233
|
}
|
|
234
|
+
// Check SessionStart hook
|
|
235
|
+
const sessionStart = hooks?.SessionStart;
|
|
236
|
+
if (sessionStart && sessionStart.length > 0) {
|
|
237
|
+
const hasSessionHook = sessionStart.some((entry) => entry.hooks?.some((h) => h.command?.includes("sessionstart.mjs")));
|
|
238
|
+
if (hasSessionHook) {
|
|
239
|
+
p.log.success(color.green("SessionStart hook: PASS") + " — SessionStart hook configured");
|
|
240
|
+
}
|
|
241
|
+
else {
|
|
242
|
+
p.log.error(color.red("SessionStart hook: FAIL") +
|
|
243
|
+
" — SessionStart exists but does not point to sessionstart.mjs" +
|
|
244
|
+
color.dim("\n Run: npx context-mode upgrade"));
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
else {
|
|
248
|
+
p.log.error(color.red("SessionStart hook: FAIL") +
|
|
249
|
+
" — No SessionStart hooks found" +
|
|
250
|
+
color.dim("\n Run: npx context-mode upgrade"));
|
|
251
|
+
}
|
|
234
252
|
}
|
|
235
253
|
else {
|
|
236
254
|
p.log.error(color.red("Hooks installed: FAIL") +
|
|
@@ -500,7 +518,7 @@ async function upgrade() {
|
|
|
500
518
|
const hookScriptPath = resolve(pluginRoot, "hooks", "pretooluse.mjs");
|
|
501
519
|
const settings = readSettings() ?? {};
|
|
502
520
|
const desiredHookEntry = {
|
|
503
|
-
matcher: "Bash|Read|Grep|
|
|
521
|
+
matcher: "Bash|Read|Grep|WebFetch|Task|mcp__plugin_context-mode_context-mode__execute|mcp__plugin_context-mode_context-mode__execute_file|mcp__plugin_context-mode_context-mode__batch_execute",
|
|
504
522
|
hooks: [
|
|
505
523
|
{
|
|
506
524
|
type: "command",
|
|
@@ -532,6 +550,41 @@ async function upgrade() {
|
|
|
532
550
|
p.log.info(color.dim("Created PreToolUse hooks section"));
|
|
533
551
|
changes.push("Created PreToolUse hooks section");
|
|
534
552
|
}
|
|
553
|
+
// --- SessionStart hook ---
|
|
554
|
+
p.log.step("Configuring SessionStart hook...");
|
|
555
|
+
const sessionHookScriptPath = resolve(pluginRoot, "hooks", "sessionstart.mjs");
|
|
556
|
+
const desiredSessionHookEntry = {
|
|
557
|
+
matcher: "",
|
|
558
|
+
hooks: [
|
|
559
|
+
{
|
|
560
|
+
type: "command",
|
|
561
|
+
command: "node " + sessionHookScriptPath,
|
|
562
|
+
},
|
|
563
|
+
],
|
|
564
|
+
};
|
|
565
|
+
const existingSessionStart = hooks.SessionStart;
|
|
566
|
+
if (existingSessionStart && Array.isArray(existingSessionStart)) {
|
|
567
|
+
const existingSessionIdx = existingSessionStart.findIndex((entry) => {
|
|
568
|
+
const entryHooks = entry.hooks;
|
|
569
|
+
return entryHooks?.some((h) => h.command?.includes("sessionstart.mjs"));
|
|
570
|
+
});
|
|
571
|
+
if (existingSessionIdx >= 0) {
|
|
572
|
+
existingSessionStart[existingSessionIdx] = desiredSessionHookEntry;
|
|
573
|
+
p.log.info(color.dim("Updated existing SessionStart hook entry"));
|
|
574
|
+
changes.push("Updated existing SessionStart hook entry");
|
|
575
|
+
}
|
|
576
|
+
else {
|
|
577
|
+
existingSessionStart.push(desiredSessionHookEntry);
|
|
578
|
+
p.log.info(color.dim("Added SessionStart hook entry"));
|
|
579
|
+
changes.push("Added SessionStart hook entry to existing hooks");
|
|
580
|
+
}
|
|
581
|
+
hooks.SessionStart = existingSessionStart;
|
|
582
|
+
}
|
|
583
|
+
else {
|
|
584
|
+
hooks.SessionStart = [desiredSessionHookEntry];
|
|
585
|
+
p.log.info(color.dim("Created SessionStart hooks section"));
|
|
586
|
+
changes.push("Created SessionStart hooks section");
|
|
587
|
+
}
|
|
535
588
|
settings.hooks = hooks;
|
|
536
589
|
// Write updated settings
|
|
537
590
|
try {
|
package/build/executor.js
CHANGED
|
@@ -36,27 +36,24 @@ export class PolyglotExecutor {
|
|
|
36
36
|
const tmpDir = mkdtempSync(join(tmpdir(), "ctx-mode-"));
|
|
37
37
|
try {
|
|
38
38
|
const filePath = this.#writeScript(tmpDir, code, language);
|
|
39
|
-
|
|
40
|
-
try {
|
|
41
|
-
cmd = buildCommand(this.#runtimes, language, filePath);
|
|
42
|
-
}
|
|
43
|
-
catch (err) {
|
|
44
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
45
|
-
return { exitCode: 1, stdout: "", stderr: msg, timedOut: false };
|
|
46
|
-
}
|
|
39
|
+
const cmd = buildCommand(this.#runtimes, language, filePath);
|
|
47
40
|
// Rust: compile then run
|
|
48
41
|
if (cmd[0] === "__rust_compile_run__") {
|
|
49
42
|
return await this.#compileAndRun(filePath, tmpDir, timeout);
|
|
50
43
|
}
|
|
51
|
-
|
|
44
|
+
// Shell commands run in the project directory so git, relative paths,
|
|
45
|
+
// and other project-aware tools work naturally. Non-shell languages
|
|
46
|
+
// run in the temp directory where their script file is written.
|
|
47
|
+
const cwd = language === "shell" ? this.#projectRoot : tmpDir;
|
|
48
|
+
return await this.#spawn(cmd, cwd, timeout);
|
|
52
49
|
}
|
|
53
50
|
finally {
|
|
54
51
|
try {
|
|
55
52
|
rmSync(tmpDir, { recursive: true, force: true });
|
|
56
53
|
}
|
|
57
54
|
catch {
|
|
58
|
-
// On Windows,
|
|
59
|
-
//
|
|
55
|
+
// On Windows, bash may still hold file handles when rmSync runs.
|
|
56
|
+
// Ignore EPERM/EBUSY — the OS will clean up %TEMP% eventually.
|
|
60
57
|
}
|
|
61
58
|
}
|
|
62
59
|
}
|
|
@@ -168,7 +165,21 @@ export class PolyglotExecutor {
|
|
|
168
165
|
// Only .cmd/.bat shims need shell on Windows; real executables don't.
|
|
169
166
|
// Using shell: true globally causes process-tree kill issues with MSYS2/Git Bash.
|
|
170
167
|
const needsShell = isWin && ["tsx", "ts-node", "elixir"].includes(cmd[0]);
|
|
171
|
-
|
|
168
|
+
// On Windows with Git Bash, pass the script as `bash -c "source /posix/path"`
|
|
169
|
+
// rather than `bash /path/to/script.sh`. This avoids MSYS2 path mangling
|
|
170
|
+
// while still allowing MSYS_NO_PATHCONV to protect non-ASCII paths in commands.
|
|
171
|
+
let spawnCmd = cmd[0];
|
|
172
|
+
let spawnArgs;
|
|
173
|
+
if (isWin && cmd.length === 2 && cmd[1]) {
|
|
174
|
+
const posixPath = cmd[1].replace(/\\/g, "/");
|
|
175
|
+
spawnArgs = [posixPath];
|
|
176
|
+
}
|
|
177
|
+
else {
|
|
178
|
+
spawnArgs = isWin
|
|
179
|
+
? cmd.slice(1).map(a => a.replace(/\\/g, "/"))
|
|
180
|
+
: cmd.slice(1);
|
|
181
|
+
}
|
|
182
|
+
const proc = spawn(spawnCmd, spawnArgs, {
|
|
172
183
|
cwd,
|
|
173
184
|
stdio: ["ignore", "pipe", "pipe"],
|
|
174
185
|
env: this.#buildSafeEnv(cwd),
|
|
@@ -269,6 +280,12 @@ export class PolyglotExecutor {
|
|
|
269
280
|
// XDG (config paths for gh, gcloud, etc.)
|
|
270
281
|
"XDG_CONFIG_HOME",
|
|
271
282
|
"XDG_DATA_HOME",
|
|
283
|
+
// SSH agent socket — required for git/jj operations that use SSH remotes.
|
|
284
|
+
// Without this, subprocesses cannot reach the agent and fall back to
|
|
285
|
+
// prompting for the key passphrase directly on the TTY, which corrupts
|
|
286
|
+
// Claude Code's PTY ownership.
|
|
287
|
+
"SSH_AUTH_SOCK",
|
|
288
|
+
"SSH_AGENT_PID",
|
|
272
289
|
];
|
|
273
290
|
const env = {
|
|
274
291
|
PATH: process.env.PATH ?? (isWin ? "" : "/usr/local/bin:/usr/bin:/bin"),
|
|
@@ -277,6 +294,7 @@ export class PolyglotExecutor {
|
|
|
277
294
|
LANG: "en_US.UTF-8",
|
|
278
295
|
PYTHONDONTWRITEBYTECODE: "1",
|
|
279
296
|
PYTHONUNBUFFERED: "1",
|
|
297
|
+
PYTHONUTF8: "1",
|
|
280
298
|
NO_COLOR: "1",
|
|
281
299
|
};
|
|
282
300
|
// Windows-critical env vars
|
|
@@ -290,6 +308,18 @@ export class PolyglotExecutor {
|
|
|
290
308
|
if (process.env[key])
|
|
291
309
|
env[key] = process.env[key];
|
|
292
310
|
}
|
|
311
|
+
// Prevent MSYS2/Git Bash from converting non-ASCII Windows paths
|
|
312
|
+
// (e.g. Chinese characters in project paths) to POSIX paths.
|
|
313
|
+
env["MSYS_NO_PATHCONV"] = "1";
|
|
314
|
+
env["MSYS2_ARG_CONV_EXCL"] = "*";
|
|
315
|
+
// Ensure Git Bash unix tools (cat, ls, head, etc.) are on PATH.
|
|
316
|
+
// The MCP server process may not inherit the full user PATH that
|
|
317
|
+
// includes Git's usr/bin directory.
|
|
318
|
+
const gitUsrBin = "C:\\Program Files\\Git\\usr\\bin";
|
|
319
|
+
const gitBin = "C:\\Program Files\\Git\\bin";
|
|
320
|
+
if (!env["PATH"].includes(gitUsrBin)) {
|
|
321
|
+
env["PATH"] = `${gitUsrBin};${gitBin};${env["PATH"]}`;
|
|
322
|
+
}
|
|
293
323
|
}
|
|
294
324
|
for (const key of passthrough) {
|
|
295
325
|
if (process.env[key]) {
|
|
@@ -305,14 +335,14 @@ export class PolyglotExecutor {
|
|
|
305
335
|
case "typescript":
|
|
306
336
|
return `const FILE_CONTENT_PATH = ${escaped};\nconst FILE_CONTENT = require("fs").readFileSync(FILE_CONTENT_PATH, "utf-8");\n${code}`;
|
|
307
337
|
case "python":
|
|
308
|
-
return `FILE_CONTENT_PATH = ${escaped}\nwith open(FILE_CONTENT_PATH, "r") as _f:\n FILE_CONTENT = _f.read()\n${code}`;
|
|
338
|
+
return `FILE_CONTENT_PATH = ${escaped}\nwith open(FILE_CONTENT_PATH, "r", encoding="utf-8") as _f:\n FILE_CONTENT = _f.read()\n${code}`;
|
|
309
339
|
case "shell": {
|
|
310
340
|
// Single-quote the path to prevent $, backtick, and ! expansion
|
|
311
341
|
const sq = "'" + absolutePath.replace(/'/g, "'\\''") + "'";
|
|
312
342
|
return `FILE_CONTENT_PATH=${sq}\nFILE_CONTENT=$(cat ${sq})\n${code}`;
|
|
313
343
|
}
|
|
314
344
|
case "ruby":
|
|
315
|
-
return `FILE_CONTENT_PATH = ${escaped}\nFILE_CONTENT = File.read(FILE_CONTENT_PATH)\n${code}`;
|
|
345
|
+
return `FILE_CONTENT_PATH = ${escaped}\nFILE_CONTENT = File.read(FILE_CONTENT_PATH, encoding: "utf-8")\n${code}`;
|
|
316
346
|
case "go":
|
|
317
347
|
return `package main\n\nimport (\n\t"fmt"\n\t"os"\n)\n\nvar FILE_CONTENT_PATH = ${escaped}\n\nfunc main() {\n\tb, _ := os.ReadFile(FILE_CONTENT_PATH)\n\tFILE_CONTENT := string(b)\n\t_ = FILE_CONTENT\n\t_ = fmt.Sprint()\n${code}\n}\n`;
|
|
318
348
|
case "rust":
|
|
@@ -320,9 +350,9 @@ export class PolyglotExecutor {
|
|
|
320
350
|
case "php":
|
|
321
351
|
return `<?php\n$FILE_CONTENT_PATH = ${escaped};\n$FILE_CONTENT = file_get_contents($FILE_CONTENT_PATH);\n${code}`;
|
|
322
352
|
case "perl":
|
|
323
|
-
return `my $FILE_CONTENT_PATH = ${escaped};\nopen(my $fh, '
|
|
353
|
+
return `my $FILE_CONTENT_PATH = ${escaped};\nopen(my $fh, '<:encoding(UTF-8)', $FILE_CONTENT_PATH) or die "Cannot open: $!";\nmy $FILE_CONTENT = do { local $/; <$fh> };\nclose($fh);\n${code}`;
|
|
324
354
|
case "r":
|
|
325
|
-
return `FILE_CONTENT_PATH <- ${escaped}\nFILE_CONTENT <- readLines(FILE_CONTENT_PATH, warn=FALSE)\nFILE_CONTENT <- paste(FILE_CONTENT, collapse="\\n")\n${code}`;
|
|
355
|
+
return `FILE_CONTENT_PATH <- ${escaped}\nFILE_CONTENT <- readLines(FILE_CONTENT_PATH, warn=FALSE, encoding="UTF-8")\nFILE_CONTENT <- paste(FILE_CONTENT, collapse="\\n")\n${code}`;
|
|
326
356
|
case "elixir":
|
|
327
357
|
return `file_content_path = ${escaped}\nfile_content = File.read!(file_content_path)\n${code}`;
|
|
328
358
|
}
|
package/build/runtime.d.ts
CHANGED
package/build/runtime.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { execSync } from "node:child_process";
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
2
3
|
const isWindows = process.platform === "win32";
|
|
3
4
|
function commandExists(cmd) {
|
|
4
5
|
try {
|
|
@@ -10,6 +11,39 @@ function commandExists(cmd) {
|
|
|
10
11
|
return false;
|
|
11
12
|
}
|
|
12
13
|
}
|
|
14
|
+
/**
|
|
15
|
+
* On Windows, resolve the first non-WSL bash in PATH.
|
|
16
|
+
* WSL bash (C:\Windows\System32\bash.exe) cannot handle Windows paths,
|
|
17
|
+
* so we skip it and prefer Git Bash or MSYS2 bash instead.
|
|
18
|
+
*/
|
|
19
|
+
function resolveWindowsBash() {
|
|
20
|
+
// First, try well-known Git Bash locations directly (works even when
|
|
21
|
+
// Git\usr\bin is not on PATH, which is common in MCP server environments
|
|
22
|
+
// that only inherit Git\cmd from the system PATH).
|
|
23
|
+
const knownPaths = [
|
|
24
|
+
"C:\\Program Files\\Git\\usr\\bin\\bash.exe",
|
|
25
|
+
"C:\\Program Files (x86)\\Git\\usr\\bin\\bash.exe",
|
|
26
|
+
];
|
|
27
|
+
for (const p of knownPaths) {
|
|
28
|
+
if (existsSync(p))
|
|
29
|
+
return p;
|
|
30
|
+
}
|
|
31
|
+
// Fallback: scan PATH via `where bash`, skipping WSL and WindowsApps entries.
|
|
32
|
+
try {
|
|
33
|
+
const result = execSync("where bash", { encoding: "utf-8", stdio: "pipe" });
|
|
34
|
+
const candidates = result.trim().split(/\r?\n/).map(p => p.trim()).filter(Boolean);
|
|
35
|
+
for (const p of candidates) {
|
|
36
|
+
const lower = p.toLowerCase();
|
|
37
|
+
if (lower.includes("system32") || lower.includes("windowsapps"))
|
|
38
|
+
continue;
|
|
39
|
+
return p;
|
|
40
|
+
}
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
13
47
|
function getVersion(cmd) {
|
|
14
48
|
try {
|
|
15
49
|
return execSync(`${cmd} --version`, {
|
|
@@ -40,15 +74,9 @@ export function detectRuntimes() {
|
|
|
40
74
|
: commandExists("python")
|
|
41
75
|
? "python"
|
|
42
76
|
: null,
|
|
43
|
-
shell:
|
|
44
|
-
? "
|
|
45
|
-
: commandExists("sh"
|
|
46
|
-
? "sh"
|
|
47
|
-
: commandExists("powershell")
|
|
48
|
-
? "powershell"
|
|
49
|
-
: commandExists("cmd.exe")
|
|
50
|
-
? "cmd.exe"
|
|
51
|
-
: null,
|
|
77
|
+
shell: isWindows
|
|
78
|
+
? (resolveWindowsBash() ?? (commandExists("sh") ? "sh" : commandExists("powershell") ? "powershell" : "cmd.exe"))
|
|
79
|
+
: commandExists("bash") ? "bash" : "sh",
|
|
52
80
|
ruby: commandExists("ruby") ? "ruby" : null,
|
|
53
81
|
go: commandExists("go") ? "go" : null,
|
|
54
82
|
rust: commandExists("rustc") ? "rustc" : null,
|
|
@@ -81,12 +109,7 @@ export function getRuntimeSummary(runtimes) {
|
|
|
81
109
|
else {
|
|
82
110
|
lines.push(` Python: not available`);
|
|
83
111
|
}
|
|
84
|
-
|
|
85
|
-
lines.push(` Shell: ${runtimes.shell} (${getVersion(runtimes.shell)})`);
|
|
86
|
-
}
|
|
87
|
-
else {
|
|
88
|
-
lines.push(` Shell: not available`);
|
|
89
|
-
}
|
|
112
|
+
lines.push(` Shell: ${runtimes.shell} (${getVersion(runtimes.shell)})`);
|
|
90
113
|
// Optional runtimes — only show if available
|
|
91
114
|
if (runtimes.ruby)
|
|
92
115
|
lines.push(` Ruby: ${runtimes.ruby} (${getVersion(runtimes.ruby)})`);
|
|
@@ -109,9 +132,7 @@ export function getRuntimeSummary(runtimes) {
|
|
|
109
132
|
return lines.join("\n");
|
|
110
133
|
}
|
|
111
134
|
export function getAvailableLanguages(runtimes) {
|
|
112
|
-
const langs = ["javascript"];
|
|
113
|
-
if (runtimes.shell)
|
|
114
|
-
langs.push("shell");
|
|
135
|
+
const langs = ["javascript", "shell"];
|
|
115
136
|
if (runtimes.typescript)
|
|
116
137
|
langs.push("typescript");
|
|
117
138
|
if (runtimes.python)
|
|
@@ -153,9 +174,6 @@ export function buildCommand(runtimes, language, filePath) {
|
|
|
153
174
|
}
|
|
154
175
|
return [runtimes.python, filePath];
|
|
155
176
|
case "shell":
|
|
156
|
-
if (!runtimes.shell) {
|
|
157
|
-
throw new Error("No shell runtime available. Install bash, sh, powershell, or cmd.");
|
|
158
|
-
}
|
|
159
177
|
return [runtimes.shell, filePath];
|
|
160
178
|
case "ruby":
|
|
161
179
|
if (!runtimes.ruby) {
|