vscode-terminal-mcp 0.1.4 → 0.1.6
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/CHANGELOG.md +45 -0
- package/CLAUDE.md +84 -0
- package/README.md +96 -0
- package/dist/extension.js +95 -131
- package/dist/mcp-entry.js +14 -1
- package/docs/images/ask_exec_permission.png +0 -0
- package/docs/images/exec_finished.png +0 -0
- package/docs/images/run_finished.png +0 -0
- package/package.json +1 -1
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
|
+
|
|
5
|
+
## [0.1.6] - 2026-03-19 18:18 PDT
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
- Screenshots in README for marketplace (run, exec, permission dialog)
|
|
9
|
+
- Custom terminal tab names with date format (e.g., `MCP: BashTerm-26-03-19-17-30`)
|
|
10
|
+
- `name` parameter in `run` tool for custom terminal names
|
|
11
|
+
- Unique IPC socket per workspace to prevent conflicts between multiple VSCode instances
|
|
12
|
+
- Large output handling documentation
|
|
13
|
+
- Development workflow docs for extension cache workaround
|
|
14
|
+
|
|
15
|
+
### Fixed
|
|
16
|
+
- Clean output format for all tools (`run`, `exec`, `read`, `list`, `close`, `input`) — no more raw JSON responses
|
|
17
|
+
- `waitForCompletion: false` not working (`z.coerce.boolean()` converted string `"false"` to `true`)
|
|
18
|
+
- Idle reaper killing sessions with running commands — reaper disabled, user closes sessions manually
|
|
19
|
+
|
|
20
|
+
## [0.1.5] - 2026-03-18 14:50 PDT
|
|
21
|
+
|
|
22
|
+
### Added
|
|
23
|
+
- npm publish with `bin` entry for `npx vscode-terminal-mcp` support
|
|
24
|
+
- Published to VSCode Marketplace and MCP Registry
|
|
25
|
+
|
|
26
|
+
## [0.1.3] - 2026-03-18 11:00 PDT
|
|
27
|
+
|
|
28
|
+
### Added
|
|
29
|
+
- `run` tool combining create + exec in one step
|
|
30
|
+
- Session reuse: `run` finds idle sessions before creating new ones
|
|
31
|
+
- Busy session detection: won't reuse sessions with running commands
|
|
32
|
+
|
|
33
|
+
### Fixed
|
|
34
|
+
- First-command timing fix with shell initialization delay
|
|
35
|
+
|
|
36
|
+
## [0.1.0] - 2026-03-18 10:00 PDT
|
|
37
|
+
|
|
38
|
+
### Added
|
|
39
|
+
- Initial release
|
|
40
|
+
- Tools: `create`, `exec`, `read`, `input`, `list`, `close`
|
|
41
|
+
- Shell Integration API for output capture and exit code detection
|
|
42
|
+
- Circular output buffer with pagination support
|
|
43
|
+
- Subagent isolation with `agentId`
|
|
44
|
+
- Command blocklist security
|
|
45
|
+
- IPC bridge for MCP stdio-to-socket communication
|
package/CLAUDE.md
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# Project: vscode-terminal-mcp
|
|
2
|
+
|
|
3
|
+
MCP server that runs commands in visible VSCode terminal tabs.
|
|
4
|
+
|
|
5
|
+
## Release Process
|
|
6
|
+
|
|
7
|
+
When publishing a new version, follow these steps in order:
|
|
8
|
+
|
|
9
|
+
### 1. Update version
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
# In package.json, bump the version
|
|
13
|
+
# e.g., "version": "0.1.6" → "version": "0.1.7"
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
### 2. Update CHANGELOG.md
|
|
17
|
+
|
|
18
|
+
Add a new entry at the top with the new version and date:
|
|
19
|
+
|
|
20
|
+
```markdown
|
|
21
|
+
## [0.1.7] - YYYY-MM-DD
|
|
22
|
+
|
|
23
|
+
### Added
|
|
24
|
+
- ...
|
|
25
|
+
|
|
26
|
+
### Fixed
|
|
27
|
+
- ...
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
### 3. Update README.md
|
|
31
|
+
|
|
32
|
+
Replace the "Latest Changes" section with the new version's changes. Keep only the latest version in README — full history lives in CHANGELOG.md.
|
|
33
|
+
|
|
34
|
+
### 4. Build and publish
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
# Build
|
|
38
|
+
npm run build
|
|
39
|
+
|
|
40
|
+
# Publish to npm
|
|
41
|
+
npm publish --access public
|
|
42
|
+
|
|
43
|
+
# Package vsix
|
|
44
|
+
npx vsce package --allow-missing-repository
|
|
45
|
+
|
|
46
|
+
# Install locally for testing
|
|
47
|
+
cp dist/extension.js dist/mcp-entry.js ~/.vscode/extensions/sirlordt.vscode-terminal-mcp-<version>/dist/
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
### 5. Upload to VSCode Marketplace
|
|
51
|
+
|
|
52
|
+
1. Go to https://marketplace.visualstudio.com/manage/publishers/sirlordt
|
|
53
|
+
2. Click "..." next to Terminal MCP → "Update"
|
|
54
|
+
3. Upload the `.vsix` file
|
|
55
|
+
|
|
56
|
+
### 6. Commit and push
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
git add -A
|
|
60
|
+
git commit -m "v0.1.7: <summary of changes>"
|
|
61
|
+
git push
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## Extension Cache Workaround
|
|
65
|
+
|
|
66
|
+
VSCode aggressively caches extensions. When developing locally:
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
# Quick update (after modifying source)
|
|
70
|
+
npm run build
|
|
71
|
+
cp dist/extension.js ~/.vscode/extensions/sirlordt.vscode-terminal-mcp-<version>/dist/extension.js
|
|
72
|
+
# Then "Developer: Reload Window"
|
|
73
|
+
|
|
74
|
+
# If reload doesn't pick up changes, close and reopen VSCode completely
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
## Terminal Execution
|
|
78
|
+
|
|
79
|
+
Prefer the BashTerm MCP tools (`run`, `exec`, `read`, etc.) over the built-in Bash tool for executing commands. BashTerm runs commands in visible VSCode terminal tabs where the user can see output in real time.
|
|
80
|
+
|
|
81
|
+
For commands that may take longer than 30 seconds or produce large output, use pull mode:
|
|
82
|
+
1. Call `run` with `waitForCompletion: false`
|
|
83
|
+
2. Call `read` with `offset: -10` to check progress
|
|
84
|
+
3. Repeat until done
|
package/README.md
CHANGED
|
@@ -84,6 +84,20 @@ After installation, try asking:
|
|
|
84
84
|
|
|
85
85
|
You should see a new terminal tab open in VSCode with the command output.
|
|
86
86
|
|
|
87
|
+
## Screenshots
|
|
88
|
+
|
|
89
|
+
### Running a command with `run`
|
|
90
|
+
|
|
91
|
+

|
|
92
|
+
|
|
93
|
+
### Permission dialog for `exec`
|
|
94
|
+
|
|
95
|
+

|
|
96
|
+
|
|
97
|
+
### Exec result with clean output
|
|
98
|
+
|
|
99
|
+

|
|
100
|
+
|
|
87
101
|
## Tools
|
|
88
102
|
|
|
89
103
|
### Quick Execution
|
|
@@ -202,6 +216,76 @@ This prevents conversation timeouts and lets the user watch progress in the term
|
|
|
202
216
|
| Session state | Each command is isolated | Persistent sessions with history |
|
|
203
217
|
| Interactive commands | Not supported | Send input to prompts/REPLs |
|
|
204
218
|
|
|
219
|
+
## Development: Updating the Extension
|
|
220
|
+
|
|
221
|
+
VSCode aggressively caches extensions in memory. When developing locally, `code --install-extension` and even "Developer: Reload Window" may **not** reload your changes. Use this workflow:
|
|
222
|
+
|
|
223
|
+
### Quick update (no restart needed)
|
|
224
|
+
|
|
225
|
+
After modifying source files, build and copy directly into the installed extension directory:
|
|
226
|
+
|
|
227
|
+
```bash
|
|
228
|
+
cd /path/to/vscode-terminal-mcp
|
|
229
|
+
npm run build
|
|
230
|
+
cp dist/extension.js ~/.vscode/extensions/sirlordt.vscode-terminal-mcp-<version>/dist/extension.js
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
Then run **"Developer: Reload Window"** (`Ctrl+Shift+P`).
|
|
234
|
+
|
|
235
|
+
### Full reinstall (when quick update doesn't work)
|
|
236
|
+
|
|
237
|
+
If VSCode still uses old code:
|
|
238
|
+
|
|
239
|
+
```bash
|
|
240
|
+
# 1. Uninstall and remove all copies
|
|
241
|
+
code --uninstall-extension sirlordt.vscode-terminal-mcp
|
|
242
|
+
rm -rf ~/.vscode/extensions/sirlordt.vscode-terminal-mcp-*
|
|
243
|
+
|
|
244
|
+
# 2. Check for ghost entries with old publisher names
|
|
245
|
+
# Look in ~/.vscode/extensions/extensions.json for stale entries
|
|
246
|
+
# Remove any entries with old publisher IDs (e.g., "terminal-mcp.vscode-terminal-mcp")
|
|
247
|
+
|
|
248
|
+
# 3. Close VSCode completely (not just reload)
|
|
249
|
+
|
|
250
|
+
# 4. Rebuild and install
|
|
251
|
+
npm run build
|
|
252
|
+
npx vsce package --allow-missing-repository
|
|
253
|
+
code --install-extension vscode-terminal-mcp-<version>.vsix --force
|
|
254
|
+
|
|
255
|
+
# 5. Open VSCode
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
### Verify the correct version is loaded
|
|
259
|
+
|
|
260
|
+
```bash
|
|
261
|
+
# Check which extension directories exist
|
|
262
|
+
ls ~/.vscode/extensions/ | grep terminal
|
|
263
|
+
|
|
264
|
+
# Verify your changes are in the installed extension
|
|
265
|
+
grep "YOUR_UNIQUE_STRING" ~/.vscode/extensions/sirlordt.vscode-terminal-mcp-*/dist/extension.js
|
|
266
|
+
|
|
267
|
+
# Compare checksums
|
|
268
|
+
md5sum dist/extension.js ~/.vscode/extensions/sirlordt.vscode-terminal-mcp-*/dist/extension.js
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
## Large Output Handling
|
|
272
|
+
|
|
273
|
+
When `read` returns output that exceeds the MCP client's token limit, the system automatically saves the full output to a temporary JSON file and returns the file path in the error message.
|
|
274
|
+
|
|
275
|
+
To extract the relevant content:
|
|
276
|
+
|
|
277
|
+
```bash
|
|
278
|
+
# Get the last 50 lines (most relevant for status)
|
|
279
|
+
tail -50 /path/to/saved/file.txt
|
|
280
|
+
|
|
281
|
+
# Or parse the JSON to extract the text content
|
|
282
|
+
python3 -c "import json; data=json.load(open('/path/to/file.txt')); print(data[0]['text'][-2000:])"
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
The file format is JSON: `[{"type": "text", "text": "..."}]`
|
|
286
|
+
|
|
287
|
+
This commonly happens with commands that produce heavy TUI output (progress bars, ANSI escape codes). Use smaller `offset` values (e.g., `offset: -20` instead of `offset: -100`) to reduce the captured output size.
|
|
288
|
+
|
|
205
289
|
## How It Works
|
|
206
290
|
|
|
207
291
|
1. The **VSCode extension** activates and starts an IPC server on a Unix socket
|
|
@@ -209,6 +293,18 @@ This prevents conversation timeouts and lets the user watch progress in the term
|
|
|
209
293
|
3. Commands execute in real VSCode terminals using the **Shell Integration API** for reliable output capture and exit code detection
|
|
210
294
|
4. Output is stored in circular buffers with pagination support for efficient reading
|
|
211
295
|
|
|
296
|
+
## Latest Changes (0.1.6)
|
|
297
|
+
|
|
298
|
+
- Screenshots in README for marketplace
|
|
299
|
+
- Clean output format for all tools — no more raw JSON
|
|
300
|
+
- Fixed `waitForCompletion: false` not working
|
|
301
|
+
- Disabled idle reaper — user closes sessions manually
|
|
302
|
+
- Unique IPC socket per workspace (multi-instance support)
|
|
303
|
+
- Custom terminal tab names with date format
|
|
304
|
+
- Large output handling documentation
|
|
305
|
+
|
|
306
|
+
See [CHANGELOG.md](CHANGELOG.md) for full history.
|
|
307
|
+
|
|
212
308
|
## License
|
|
213
309
|
|
|
214
310
|
MIT
|
package/dist/extension.js
CHANGED
|
@@ -5389,6 +5389,13 @@ var zodToJsonSchema = (schema, options) => {
|
|
|
5389
5389
|
};
|
|
5390
5390
|
|
|
5391
5391
|
// src/mcp/tools/schemas.ts
|
|
5392
|
+
var coerceBoolean = external_exports.preprocess(
|
|
5393
|
+
(val) => {
|
|
5394
|
+
if (typeof val === "string") return val.toLowerCase() === "true";
|
|
5395
|
+
return val;
|
|
5396
|
+
},
|
|
5397
|
+
external_exports.boolean()
|
|
5398
|
+
);
|
|
5392
5399
|
var terminalCreateSchema = external_exports.object({
|
|
5393
5400
|
name: external_exports.string().min(1).describe("Display name for the terminal tab"),
|
|
5394
5401
|
cwd: external_exports.string().optional().describe("Working directory for the terminal"),
|
|
@@ -5400,7 +5407,7 @@ var terminalExecuteSchema = external_exports.object({
|
|
|
5400
5407
|
sessionId: external_exports.string().min(1).describe("Session ID of the target terminal"),
|
|
5401
5408
|
command: external_exports.string().min(1).describe("Command to execute"),
|
|
5402
5409
|
timeoutMs: external_exports.coerce.number().min(1e3).max(3e5).optional().default(3e4).describe("Timeout in milliseconds (default: 30000, max: 300000)"),
|
|
5403
|
-
waitForCompletion:
|
|
5410
|
+
waitForCompletion: coerceBoolean.optional().default(true).describe("Wait for command to complete before returning (default: true)")
|
|
5404
5411
|
});
|
|
5405
5412
|
var terminalReadOutputSchema = external_exports.object({
|
|
5406
5413
|
sessionId: external_exports.string().min(1).describe("Session ID of the target terminal"),
|
|
@@ -5421,7 +5428,7 @@ var terminalRunSchema = external_exports.object({
|
|
|
5421
5428
|
shell: external_exports.string().optional().describe("Override shell (e.g., /bin/zsh, /bin/bash)"),
|
|
5422
5429
|
agentId: external_exports.string().optional().describe("Identifier for the owning agent/subagent"),
|
|
5423
5430
|
timeoutMs: external_exports.coerce.number().min(1e3).max(3e5).optional().default(3e4).describe("Timeout in milliseconds (default: 30000, max: 300000)"),
|
|
5424
|
-
waitForCompletion:
|
|
5431
|
+
waitForCompletion: coerceBoolean.optional().default(true).describe("Wait for command to complete before returning (default: true)")
|
|
5425
5432
|
});
|
|
5426
5433
|
var terminalSendInputSchema = external_exports.object({
|
|
5427
5434
|
sessionId: external_exports.string().min(1).describe("Session ID of the target terminal"),
|
|
@@ -5439,21 +5446,13 @@ async function handleTerminalCreate(params, sessionManager2) {
|
|
|
5439
5446
|
shell: input.shell,
|
|
5440
5447
|
agentId: input.agentId
|
|
5441
5448
|
});
|
|
5449
|
+
const parts = [`Terminal created: ${sessionInfo.name}`, `session: ${sessionInfo.sessionId}`, `cwd: ${sessionInfo.cwd}`];
|
|
5450
|
+
if (sessionInfo.agentId) parts.push(`agent: ${sessionInfo.agentId}`);
|
|
5442
5451
|
return {
|
|
5443
5452
|
content: [
|
|
5444
5453
|
{
|
|
5445
5454
|
type: "text",
|
|
5446
|
-
text:
|
|
5447
|
-
{
|
|
5448
|
-
status: "created",
|
|
5449
|
-
sessionId: sessionInfo.sessionId,
|
|
5450
|
-
name: sessionInfo.name,
|
|
5451
|
-
cwd: sessionInfo.cwd,
|
|
5452
|
-
agentId: sessionInfo.agentId
|
|
5453
|
-
},
|
|
5454
|
-
null,
|
|
5455
|
-
2
|
|
5456
|
-
)
|
|
5455
|
+
text: parts.join(" | ")
|
|
5457
5456
|
}
|
|
5458
5457
|
]
|
|
5459
5458
|
};
|
|
@@ -5493,24 +5492,29 @@ async function handleTerminalExecute(params, sessionManager2) {
|
|
|
5493
5492
|
timeoutMs,
|
|
5494
5493
|
waitForCompletion
|
|
5495
5494
|
);
|
|
5496
|
-
|
|
5497
|
-
|
|
5498
|
-
|
|
5499
|
-
|
|
5500
|
-
|
|
5501
|
-
|
|
5502
|
-
|
|
5503
|
-
}
|
|
5495
|
+
let cleanOutput = result.output.replace(/\x1b\[[0-9;]*[a-zA-Z]/g, "").replace(/\x1b\][^\x07]*\x07/g, "").replace(/\r\n/g, "\n").replace(/\r/g, "").trim();
|
|
5496
|
+
const lines = cleanOutput.split("\n");
|
|
5497
|
+
if (lines.length > 0 && lines[0].trim() === input.command.trim()) {
|
|
5498
|
+
lines.shift();
|
|
5499
|
+
cleanOutput = lines.join("\n").trim();
|
|
5500
|
+
}
|
|
5501
|
+
const statusParts = [`exit: ${result.exitCode ?? "n/a"}`, `${result.durationMs}ms`, input.sessionId];
|
|
5502
|
+
let text = `$ ${input.command}
|
|
5503
|
+
${cleanOutput}
|
|
5504
|
+
|
|
5505
|
+
[${statusParts.join(" | ")}]`;
|
|
5504
5506
|
if (result.timedOut) {
|
|
5505
|
-
|
|
5507
|
+
text += `
|
|
5508
|
+
[TIMED OUT after ${timeoutMs}ms - session still active, use read to get more output]`;
|
|
5506
5509
|
}
|
|
5507
5510
|
return {
|
|
5508
5511
|
content: [
|
|
5509
5512
|
{
|
|
5510
5513
|
type: "text",
|
|
5511
|
-
text
|
|
5514
|
+
text
|
|
5512
5515
|
}
|
|
5513
|
-
]
|
|
5516
|
+
],
|
|
5517
|
+
isError: result.exitCode !== null && result.exitCode !== 0
|
|
5514
5518
|
};
|
|
5515
5519
|
}
|
|
5516
5520
|
|
|
@@ -5518,67 +5522,66 @@ async function handleTerminalExecute(params, sessionManager2) {
|
|
|
5518
5522
|
async function handleTerminalRun(params, sessionManager2) {
|
|
5519
5523
|
const input = terminalRunSchema.parse(params);
|
|
5520
5524
|
let sessionId;
|
|
5525
|
+
let isNewSession = false;
|
|
5521
5526
|
const existing = sessionManager2.listSessions(input.agentId);
|
|
5522
5527
|
for (const s of existing) {
|
|
5523
5528
|
if (!s.isActive || input.cwd && s.cwd !== input.cwd) continue;
|
|
5524
|
-
const
|
|
5525
|
-
if (
|
|
5529
|
+
const session = sessionManager2.getSession(s.sessionId);
|
|
5530
|
+
if (session && !session.isBusy) {
|
|
5526
5531
|
sessionId = s.sessionId;
|
|
5527
5532
|
break;
|
|
5528
5533
|
}
|
|
5529
5534
|
}
|
|
5530
5535
|
if (!sessionId) {
|
|
5531
5536
|
const sessionInfo = sessionManager2.createSession({
|
|
5532
|
-
name: input.name ??
|
|
5537
|
+
name: input.name ?? (() => {
|
|
5538
|
+
const d = /* @__PURE__ */ new Date();
|
|
5539
|
+
const pad = (n) => String(n).padStart(2, "0");
|
|
5540
|
+
return `BashTerm-${pad(d.getFullYear() % 100)}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}-${pad(d.getHours())}-${pad(d.getMinutes())}`;
|
|
5541
|
+
})(),
|
|
5533
5542
|
cwd: input.cwd,
|
|
5534
5543
|
env: input.env,
|
|
5535
5544
|
shell: input.shell,
|
|
5536
5545
|
agentId: input.agentId
|
|
5537
5546
|
});
|
|
5538
5547
|
sessionId = sessionInfo.sessionId;
|
|
5548
|
+
isNewSession = true;
|
|
5549
|
+
}
|
|
5550
|
+
if (isNewSession) {
|
|
5551
|
+
return new Promise((resolve) => {
|
|
5552
|
+
setTimeout(async () => {
|
|
5553
|
+
const result = await executeCommand(sessionId, input, sessionManager2);
|
|
5554
|
+
resolve(result);
|
|
5555
|
+
}, 500);
|
|
5556
|
+
});
|
|
5539
5557
|
}
|
|
5558
|
+
return executeCommand(sessionId, input, sessionManager2);
|
|
5559
|
+
}
|
|
5560
|
+
async function executeCommand(sessionId, input, sessionManager2) {
|
|
5540
5561
|
const session = sessionManager2.getSession(sessionId);
|
|
5541
5562
|
if (!session) {
|
|
5542
5563
|
return {
|
|
5543
|
-
content: [
|
|
5544
|
-
{
|
|
5545
|
-
type: "text",
|
|
5546
|
-
text: "Error: Failed to get terminal session."
|
|
5547
|
-
}
|
|
5548
|
-
],
|
|
5564
|
+
content: [{ type: "text", text: "Error: Failed to get terminal session." }],
|
|
5549
5565
|
isError: true
|
|
5550
5566
|
};
|
|
5551
5567
|
}
|
|
5552
5568
|
const validation = sessionManager2.validateCommand(input.command);
|
|
5553
5569
|
if (!validation.valid) {
|
|
5554
5570
|
return {
|
|
5555
|
-
content: [
|
|
5556
|
-
{
|
|
5557
|
-
type: "text",
|
|
5558
|
-
text: `Command blocked: ${validation.reason}`
|
|
5559
|
-
}
|
|
5560
|
-
],
|
|
5571
|
+
content: [{ type: "text", text: `Command blocked: ${validation.reason}` }],
|
|
5561
5572
|
isError: true
|
|
5562
5573
|
};
|
|
5563
5574
|
}
|
|
5564
5575
|
const timeoutMs = input.timeoutMs ?? sessionManager2.getDefaultTimeout();
|
|
5565
5576
|
const waitForCompletion = input.waitForCompletion ?? true;
|
|
5566
|
-
const result = await session.execute(
|
|
5567
|
-
input.command,
|
|
5568
|
-
timeoutMs,
|
|
5569
|
-
waitForCompletion
|
|
5570
|
-
);
|
|
5577
|
+
const result = await session.execute(input.command, timeoutMs, waitForCompletion);
|
|
5571
5578
|
let cleanOutput = result.output.replace(/\x1b\[[0-9;]*[a-zA-Z]/g, "").replace(/\x1b\][^\x07]*\x07/g, "").replace(/\r\n/g, "\n").replace(/\r/g, "").trim();
|
|
5572
5579
|
const lines = cleanOutput.split("\n");
|
|
5573
5580
|
if (lines.length > 0 && lines[0].trim() === input.command.trim()) {
|
|
5574
5581
|
lines.shift();
|
|
5575
5582
|
cleanOutput = lines.join("\n").trim();
|
|
5576
5583
|
}
|
|
5577
|
-
const statusParts = [
|
|
5578
|
-
`exit: ${result.exitCode ?? "n/a"}`,
|
|
5579
|
-
`${result.durationMs}ms`,
|
|
5580
|
-
sessionId
|
|
5581
|
-
];
|
|
5584
|
+
const statusParts = [`exit: ${result.exitCode ?? "n/a"}`, `${result.durationMs}ms`, sessionId];
|
|
5582
5585
|
let text = `$ ${input.command}
|
|
5583
5586
|
${cleanOutput}
|
|
5584
5587
|
|
|
@@ -5588,12 +5591,7 @@ ${cleanOutput}
|
|
|
5588
5591
|
[TIMED OUT after ${timeoutMs}ms - session still active, use read to get more output]`;
|
|
5589
5592
|
}
|
|
5590
5593
|
return {
|
|
5591
|
-
content: [
|
|
5592
|
-
{
|
|
5593
|
-
type: "text",
|
|
5594
|
-
text
|
|
5595
|
-
}
|
|
5596
|
-
],
|
|
5594
|
+
content: [{ type: "text", text }],
|
|
5597
5595
|
isError: result.exitCode !== null && result.exitCode !== 0
|
|
5598
5596
|
};
|
|
5599
5597
|
}
|
|
@@ -5614,23 +5612,20 @@ async function handleTerminalReadOutput(params, sessionManager2) {
|
|
|
5614
5612
|
};
|
|
5615
5613
|
}
|
|
5616
5614
|
const result = session.readOutput(input.offset, input.lines);
|
|
5615
|
+
const cleanOutput = result.lines.join("\n").replace(/\x1b\[[0-9;]*[a-zA-Z]/g, "").replace(/\x1b\][^\x07]*\x07/g, "").replace(/\r\n/g, "\n").replace(/\r/g, "").trim();
|
|
5616
|
+
const status = [
|
|
5617
|
+
`lines: ${result.readFrom}-${result.readFrom + result.readCount}/${result.totalLines}`,
|
|
5618
|
+
`remaining: ${result.remaining}`,
|
|
5619
|
+
input.sessionId
|
|
5620
|
+
];
|
|
5621
|
+
const text = `${cleanOutput}
|
|
5622
|
+
|
|
5623
|
+
[${status.join(" | ")}]`;
|
|
5617
5624
|
return {
|
|
5618
5625
|
content: [
|
|
5619
5626
|
{
|
|
5620
5627
|
type: "text",
|
|
5621
|
-
text
|
|
5622
|
-
{
|
|
5623
|
-
sessionId: input.sessionId,
|
|
5624
|
-
readFrom: result.readFrom,
|
|
5625
|
-
readCount: result.readCount,
|
|
5626
|
-
totalLines: result.totalLines,
|
|
5627
|
-
remaining: result.remaining,
|
|
5628
|
-
isComplete: result.isComplete,
|
|
5629
|
-
output: result.lines.join("\n")
|
|
5630
|
-
},
|
|
5631
|
-
null,
|
|
5632
|
-
2
|
|
5633
|
-
)
|
|
5628
|
+
text
|
|
5634
5629
|
}
|
|
5635
5630
|
]
|
|
5636
5631
|
};
|
|
@@ -5640,18 +5635,23 @@ async function handleTerminalReadOutput(params, sessionManager2) {
|
|
|
5640
5635
|
async function handleTerminalList(params, sessionManager2) {
|
|
5641
5636
|
const input = terminalListSchema.parse(params ?? {});
|
|
5642
5637
|
const sessions = sessionManager2.listSessions(input.agentId);
|
|
5638
|
+
if (sessions.length === 0) {
|
|
5639
|
+
return {
|
|
5640
|
+
content: [{ type: "text", text: "No active sessions." }]
|
|
5641
|
+
};
|
|
5642
|
+
}
|
|
5643
|
+
const lines = sessions.map((s) => {
|
|
5644
|
+
const age = Math.round((Date.now() - s.createdAt) / 1e3);
|
|
5645
|
+
const parts = [s.sessionId, s.name, `cwd: ${s.cwd}`, `${age}s old`, `${s.outputLineCount} lines`];
|
|
5646
|
+
if (s.agentId) parts.push(`agent: ${s.agentId}`);
|
|
5647
|
+
return parts.join(" | ");
|
|
5648
|
+
});
|
|
5643
5649
|
return {
|
|
5644
5650
|
content: [
|
|
5645
5651
|
{
|
|
5646
5652
|
type: "text",
|
|
5647
|
-
text:
|
|
5648
|
-
|
|
5649
|
-
count: sessions.length,
|
|
5650
|
-
sessions
|
|
5651
|
-
},
|
|
5652
|
-
null,
|
|
5653
|
-
2
|
|
5654
|
-
)
|
|
5653
|
+
text: `${sessions.length} active session(s):
|
|
5654
|
+
${lines.join("\n")}`
|
|
5655
5655
|
}
|
|
5656
5656
|
]
|
|
5657
5657
|
};
|
|
@@ -5676,10 +5676,7 @@ async function handleTerminalClose(params, sessionManager2) {
|
|
|
5676
5676
|
content: [
|
|
5677
5677
|
{
|
|
5678
5678
|
type: "text",
|
|
5679
|
-
text:
|
|
5680
|
-
status: "closed",
|
|
5681
|
-
sessionId: input.sessionId
|
|
5682
|
-
})
|
|
5679
|
+
text: `Session closed: ${input.sessionId}`
|
|
5683
5680
|
}
|
|
5684
5681
|
]
|
|
5685
5682
|
};
|
|
@@ -5705,12 +5702,7 @@ async function handleTerminalSendInput(params, sessionManager2) {
|
|
|
5705
5702
|
content: [
|
|
5706
5703
|
{
|
|
5707
5704
|
type: "text",
|
|
5708
|
-
text:
|
|
5709
|
-
status: "sent",
|
|
5710
|
-
sessionId: input.sessionId,
|
|
5711
|
-
inputLength: input.input.length,
|
|
5712
|
-
pressedEnter: input.pressEnter ?? true
|
|
5713
|
-
})
|
|
5705
|
+
text: `Input sent to ${input.sessionId} (${input.input.length} chars${input.pressEnter ?? true ? " + Enter" : ""})`
|
|
5714
5706
|
}
|
|
5715
5707
|
]
|
|
5716
5708
|
};
|
|
@@ -5948,7 +5940,6 @@ var TerminalSession = class {
|
|
|
5948
5940
|
isActive = true;
|
|
5949
5941
|
lastCommandAt;
|
|
5950
5942
|
shellReady;
|
|
5951
|
-
resolveShellReady;
|
|
5952
5943
|
constructor(config, maxOutputLines) {
|
|
5953
5944
|
this.sessionId = generateSessionId();
|
|
5954
5945
|
this.name = config.name;
|
|
@@ -5964,27 +5955,12 @@ var TerminalSession = class {
|
|
|
5964
5955
|
if (config.shell) {
|
|
5965
5956
|
terminalOptions.shellPath = config.shell;
|
|
5966
5957
|
}
|
|
5967
|
-
this.shellReady = new Promise((resolve) => {
|
|
5968
|
-
this.resolveShellReady = resolve;
|
|
5969
|
-
});
|
|
5970
5958
|
this.terminal = vscode2.window.createTerminal(terminalOptions);
|
|
5971
5959
|
this.terminal.show(true);
|
|
5960
|
+
this.shellReady = new Promise((resolve) => {
|
|
5961
|
+
resolve();
|
|
5962
|
+
});
|
|
5972
5963
|
this.setupShellIntegrationCapture();
|
|
5973
|
-
if (vscode2.window.onDidChangeTerminalShellIntegration) {
|
|
5974
|
-
const disposable = vscode2.window.onDidChangeTerminalShellIntegration((e) => {
|
|
5975
|
-
if (e.terminal === this.terminal) {
|
|
5976
|
-
disposable.dispose();
|
|
5977
|
-
log(`Shell integration ready for session ${this.sessionId}`);
|
|
5978
|
-
this.resolveShellReady();
|
|
5979
|
-
}
|
|
5980
|
-
});
|
|
5981
|
-
setTimeout(() => {
|
|
5982
|
-
disposable.dispose();
|
|
5983
|
-
this.resolveShellReady();
|
|
5984
|
-
}, 3e3);
|
|
5985
|
-
} else {
|
|
5986
|
-
setTimeout(() => this.resolveShellReady(), 1500);
|
|
5987
|
-
}
|
|
5988
5964
|
log(`Session ${this.sessionId} created: ${config.name} (cwd: ${this.cwd})`);
|
|
5989
5965
|
}
|
|
5990
5966
|
setupShellIntegrationCapture() {
|
|
@@ -6016,6 +5992,7 @@ var TerminalSession = class {
|
|
|
6016
5992
|
this.commandHistory.push(this.currentCommand);
|
|
6017
5993
|
this.currentCommand = null;
|
|
6018
5994
|
}
|
|
5995
|
+
this.lastCommandAt = Date.now();
|
|
6019
5996
|
log(
|
|
6020
5997
|
`Shell execution ended in session ${this.sessionId} with exit code: ${event.exitCode}`
|
|
6021
5998
|
);
|
|
@@ -6023,11 +6000,10 @@ var TerminalSession = class {
|
|
|
6023
6000
|
}
|
|
6024
6001
|
}
|
|
6025
6002
|
}
|
|
6026
|
-
/**
|
|
6027
|
-
* Execute a command in this terminal session.
|
|
6028
|
-
*/
|
|
6029
6003
|
async execute(command, timeoutMs, waitForCompletion) {
|
|
6004
|
+
log(`Waiting for shell ready in session ${this.sessionId}...`);
|
|
6030
6005
|
await this.shellReady;
|
|
6006
|
+
log(`Shell ready, executing command in session ${this.sessionId}`);
|
|
6031
6007
|
const commandId = generateCommandId();
|
|
6032
6008
|
const startedAt = Date.now();
|
|
6033
6009
|
this.lastCommandAt = startedAt;
|
|
@@ -6114,9 +6090,6 @@ var TerminalSession = class {
|
|
|
6114
6090
|
}
|
|
6115
6091
|
});
|
|
6116
6092
|
}
|
|
6117
|
-
/**
|
|
6118
|
-
* Send text input to the terminal (for interactive commands).
|
|
6119
|
-
*/
|
|
6120
6093
|
sendInput(input, pressEnter) {
|
|
6121
6094
|
this.terminal.sendText(input, pressEnter);
|
|
6122
6095
|
this.lastCommandAt = Date.now();
|
|
@@ -6124,21 +6097,12 @@ var TerminalSession = class {
|
|
|
6124
6097
|
`Input sent to session ${this.sessionId}: ${input.slice(0, 50)}${input.length > 50 ? "..." : ""}`
|
|
6125
6098
|
);
|
|
6126
6099
|
}
|
|
6127
|
-
/**
|
|
6128
|
-
* Read output from the buffer with pagination.
|
|
6129
|
-
*/
|
|
6130
6100
|
readOutput(offset = 0, maxLines = 500) {
|
|
6131
6101
|
return readFromBuffer(this.outputBuffer, offset, maxLines);
|
|
6132
6102
|
}
|
|
6133
|
-
/**
|
|
6134
|
-
* Check if a command is currently executing.
|
|
6135
|
-
*/
|
|
6136
6103
|
get isBusy() {
|
|
6137
6104
|
return this.currentCommand !== null;
|
|
6138
6105
|
}
|
|
6139
|
-
/**
|
|
6140
|
-
* Get session info for listing.
|
|
6141
|
-
*/
|
|
6142
6106
|
getInfo() {
|
|
6143
6107
|
return {
|
|
6144
6108
|
sessionId: this.sessionId,
|
|
@@ -6151,23 +6115,14 @@ var TerminalSession = class {
|
|
|
6151
6115
|
outputLineCount: getBufferLineCount(this.outputBuffer)
|
|
6152
6116
|
};
|
|
6153
6117
|
}
|
|
6154
|
-
/**
|
|
6155
|
-
* Get the VSCode terminal instance (for matching events).
|
|
6156
|
-
*/
|
|
6157
6118
|
getTerminal() {
|
|
6158
6119
|
return this.terminal;
|
|
6159
6120
|
}
|
|
6160
|
-
/**
|
|
6161
|
-
* Check if session has been idle longer than the given duration.
|
|
6162
|
-
*/
|
|
6163
6121
|
isIdle(idleThresholdMs) {
|
|
6164
6122
|
if (idleThresholdMs <= 0) return false;
|
|
6165
6123
|
const lastActivity = this.lastCommandAt ?? this.createdAt;
|
|
6166
6124
|
return Date.now() - lastActivity > idleThresholdMs;
|
|
6167
6125
|
}
|
|
6168
|
-
/**
|
|
6169
|
-
* Close the terminal session.
|
|
6170
|
-
*/
|
|
6171
6126
|
dispose() {
|
|
6172
6127
|
this.isActive = false;
|
|
6173
6128
|
this.shellExecutionDisposable?.dispose();
|
|
@@ -6183,7 +6138,6 @@ var SessionManager = class {
|
|
|
6183
6138
|
onSessionsChanged = this.onSessionsChangedEmitter.event;
|
|
6184
6139
|
idleReaperInterval = null;
|
|
6185
6140
|
constructor() {
|
|
6186
|
-
this.startIdleReaper();
|
|
6187
6141
|
vscode3.window.onDidCloseTerminal((terminal) => {
|
|
6188
6142
|
for (const [id, session] of this.sessions) {
|
|
6189
6143
|
if (session.getTerminal() === terminal) {
|
|
@@ -6217,6 +6171,7 @@ var SessionManager = class {
|
|
|
6217
6171
|
const config = this.getConfig();
|
|
6218
6172
|
if (config.idleTimeoutMs <= 0) return;
|
|
6219
6173
|
for (const [id, session] of this.sessions) {
|
|
6174
|
+
if (session.isBusy) continue;
|
|
6220
6175
|
if (session.isIdle(config.idleTimeoutMs)) {
|
|
6221
6176
|
log(`Reaping idle session ${id}`);
|
|
6222
6177
|
session.dispose();
|
|
@@ -6344,7 +6299,16 @@ var sessionManager;
|
|
|
6344
6299
|
var statusBarItem;
|
|
6345
6300
|
function getSocketPath() {
|
|
6346
6301
|
const tmpDir = os.tmpdir();
|
|
6347
|
-
|
|
6302
|
+
const crypto3 = require("crypto");
|
|
6303
|
+
const workspace4 = vscode4.workspace.workspaceFolders?.[0]?.uri.fsPath || "";
|
|
6304
|
+
const hash = crypto3.createHash("md5").update(workspace4).digest("hex").slice(0, 8);
|
|
6305
|
+
const socketPath = path.join(tmpDir, `vscode-terminal-mcp-${hash}.sock`);
|
|
6306
|
+
const discoveryPath = path.join(tmpDir, "vscode-terminal-mcp.discovery");
|
|
6307
|
+
try {
|
|
6308
|
+
fs.writeFileSync(discoveryPath, socketPath);
|
|
6309
|
+
} catch {
|
|
6310
|
+
}
|
|
6311
|
+
return socketPath;
|
|
6348
6312
|
}
|
|
6349
6313
|
function cleanupSocket(socketPath) {
|
|
6350
6314
|
try {
|
package/dist/mcp-entry.js
CHANGED
|
@@ -27,7 +27,20 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
27
27
|
var net = __toESM(require("net"));
|
|
28
28
|
var path = __toESM(require("path"));
|
|
29
29
|
var os = __toESM(require("os"));
|
|
30
|
-
var
|
|
30
|
+
var fs = __toESM(require("fs"));
|
|
31
|
+
function getSocketPath() {
|
|
32
|
+
const tmpDir = os.tmpdir();
|
|
33
|
+
const discoveryPath = path.join(tmpDir, "vscode-terminal-mcp.discovery");
|
|
34
|
+
try {
|
|
35
|
+
const socketPath = fs.readFileSync(discoveryPath, "utf8").trim();
|
|
36
|
+
if (socketPath && fs.existsSync(socketPath)) {
|
|
37
|
+
return socketPath;
|
|
38
|
+
}
|
|
39
|
+
} catch {
|
|
40
|
+
}
|
|
41
|
+
return path.join(tmpDir, "vscode-terminal-mcp.sock");
|
|
42
|
+
}
|
|
43
|
+
var SOCKET_PATH = getSocketPath();
|
|
31
44
|
var RECONNECT_DELAY_MS = 1e3;
|
|
32
45
|
var MAX_RECONNECT_ATTEMPTS = 30;
|
|
33
46
|
var StdioToIpcBridge = class {
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "vscode-terminal-mcp",
|
|
3
3
|
"displayName": "Terminal MCP Server",
|
|
4
4
|
"description": "MCP server that provides visible terminal sessions in VSCode for Claude Code and subagents",
|
|
5
|
-
"version": "0.1.
|
|
5
|
+
"version": "0.1.6",
|
|
6
6
|
"publisher": "sirlordt",
|
|
7
7
|
"license": "MIT",
|
|
8
8
|
"author": "sirlordt",
|