deepseek-pp-shell-host 0.4.4 → 0.6.2
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/native/shell-mcp-host.mjs +150 -11
- package/package.json +1 -1
|
@@ -1,8 +1,16 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { spawn } from 'node:child_process';
|
|
2
|
+
import { execFileSync, spawn } from 'node:child_process';
|
|
3
3
|
import { dirname, resolve } from 'node:path';
|
|
4
4
|
import { fileURLToPath } from 'node:url';
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
arch,
|
|
7
|
+
homedir,
|
|
8
|
+
hostname,
|
|
9
|
+
platform,
|
|
10
|
+
release as osRelease,
|
|
11
|
+
type as osType,
|
|
12
|
+
version as osVersion,
|
|
13
|
+
} from 'node:os';
|
|
6
14
|
import { existsSync } from 'node:fs';
|
|
7
15
|
|
|
8
16
|
// Resolve package root from this script's location (native/ -> package root).
|
|
@@ -17,8 +25,8 @@ const localBinDirs = [
|
|
|
17
25
|
resolve(PROJECT_ROOT, 'node_modules', '.bin'),
|
|
18
26
|
resolve(PROJECT_ROOT, '..', '..', 'node_modules', '.bin'),
|
|
19
27
|
].filter(existsSync);
|
|
20
|
-
const
|
|
21
|
-
const currentPath = process.env
|
|
28
|
+
const PATH_SEPARATOR = platform() === 'win32' ? ';' : ':';
|
|
29
|
+
const currentPath = getEnvironmentPath(process.env) || (platform() === 'win32' ? '' : '/usr/bin:/bin');
|
|
22
30
|
const localAppData = process.env.LOCALAPPDATA || resolve(homedir(), 'AppData', 'Local');
|
|
23
31
|
const userBinDirs = platform() === 'win32'
|
|
24
32
|
? [resolve(localAppData, 'OfficeCLI')]
|
|
@@ -28,19 +36,32 @@ const userBinDirs = platform() === 'win32'
|
|
|
28
36
|
'/usr/local/bin',
|
|
29
37
|
];
|
|
30
38
|
const managedPathDirs = new Set([nodeBinDir, ...localBinDirs, ...userBinDirs]);
|
|
31
|
-
const existingPathDirs = currentPath
|
|
32
|
-
|
|
39
|
+
const existingPathDirs = splitPath(currentPath).filter(d => !managedPathDirs.has(d));
|
|
40
|
+
const hostPath = dedupePathDirs([
|
|
41
|
+
nodeBinDir,
|
|
42
|
+
...userBinDirs,
|
|
43
|
+
...readWindowsUserMachinePathDirs(),
|
|
44
|
+
...existingPathDirs,
|
|
45
|
+
...localBinDirs,
|
|
46
|
+
]).join(PATH_SEPARATOR);
|
|
47
|
+
setEnvironmentPath(process.env, hostPath);
|
|
33
48
|
|
|
34
49
|
const MCP_PROTOCOL_VERSION = '2025-06-18';
|
|
35
50
|
const DEFAULT_TIMEOUT_MS = 120_000;
|
|
36
51
|
const MAX_OUTPUT_BYTES = 128_000;
|
|
37
52
|
const DEFAULT_SHELL = platform() === 'win32' ? 'powershell.exe' : process.env.SHELL || '/bin/sh';
|
|
53
|
+
const WINDOWS_POWERSHELL_UTF8_PREAMBLE = [
|
|
54
|
+
'[Console]::InputEncoding = [System.Text.UTF8Encoding]::new($false)',
|
|
55
|
+
'[Console]::OutputEncoding = [System.Text.UTF8Encoding]::new($false)',
|
|
56
|
+
'$OutputEncoding = [Console]::OutputEncoding',
|
|
57
|
+
'try { chcp.com 65001 > $null } catch {}',
|
|
58
|
+
].join('; ');
|
|
38
59
|
|
|
39
60
|
const TOOL_DEFINITIONS = [
|
|
40
61
|
{
|
|
41
62
|
name: 'shell_exec',
|
|
42
63
|
title: 'Execute Shell Command',
|
|
43
|
-
description: 'Execute a
|
|
64
|
+
description: 'Execute a command in the shell reported by shell_status. Returns stdout, stderr, and exit code.',
|
|
44
65
|
inputSchema: {
|
|
45
66
|
type: 'object',
|
|
46
67
|
properties: {
|
|
@@ -175,10 +196,16 @@ async function handleCallTool(id, params) {
|
|
|
175
196
|
data: {
|
|
176
197
|
platform: platform(),
|
|
177
198
|
arch: arch(),
|
|
199
|
+
osType: osType(),
|
|
200
|
+
osRelease: osRelease(),
|
|
201
|
+
osVersion: osVersion(),
|
|
202
|
+
windowsVersion: getWindowsVersionLabel(),
|
|
178
203
|
shell: DEFAULT_SHELL,
|
|
179
204
|
cwd: homedir(),
|
|
180
205
|
nodeVersion: process.version,
|
|
181
206
|
hostname: hostname(),
|
|
207
|
+
path: getEnvironmentPath(process.env),
|
|
208
|
+
pathEntries: splitPath(getEnvironmentPath(process.env)),
|
|
182
209
|
},
|
|
183
210
|
},
|
|
184
211
|
});
|
|
@@ -194,7 +221,7 @@ async function handleCallTool(id, params) {
|
|
|
194
221
|
}
|
|
195
222
|
|
|
196
223
|
const cwd = typeof args.cwd === 'string' && args.cwd.trim() ? args.cwd.trim() : homedir();
|
|
197
|
-
const env = args.env
|
|
224
|
+
const env = createChildEnv(args.env);
|
|
198
225
|
const timeoutMs = typeof args.timeout_ms === 'number' && args.timeout_ms >= 1000
|
|
199
226
|
? Math.min(args.timeout_ms, 600_000)
|
|
200
227
|
: DEFAULT_TIMEOUT_MS;
|
|
@@ -224,9 +251,7 @@ async function handleCallTool(id, params) {
|
|
|
224
251
|
|
|
225
252
|
function execCommand(command, { cwd, env, timeoutMs }) {
|
|
226
253
|
return new Promise((resolve, reject) => {
|
|
227
|
-
const
|
|
228
|
-
const shellArgs = isWin ? ['/c', command] : ['-c', command];
|
|
229
|
-
const shellBin = isWin ? 'cmd.exe' : DEFAULT_SHELL;
|
|
254
|
+
const { shellBin, shellArgs } = createShellInvocation(command);
|
|
230
255
|
|
|
231
256
|
const child = spawn(shellBin, shellArgs, {
|
|
232
257
|
cwd,
|
|
@@ -273,6 +298,7 @@ function execCommand(command, { cwd, env, timeoutMs }) {
|
|
|
273
298
|
clearTimeout(timer);
|
|
274
299
|
resolve({
|
|
275
300
|
command,
|
|
301
|
+
shell: shellBin,
|
|
276
302
|
exitCode: timedOut ? -1 : (exitCode ?? -1),
|
|
277
303
|
signal: signal || (timedOut ? 'SIGTERM' : null),
|
|
278
304
|
stdout: Buffer.concat(stdout).toString('utf8'),
|
|
@@ -284,6 +310,119 @@ function execCommand(command, { cwd, env, timeoutMs }) {
|
|
|
284
310
|
});
|
|
285
311
|
}
|
|
286
312
|
|
|
313
|
+
function createChildEnv(extraEnv) {
|
|
314
|
+
const explicitPath = getExplicitPathOverride(extraEnv);
|
|
315
|
+
const env = extraEnv && typeof extraEnv === 'object' ? { ...process.env, ...extraEnv } : { ...process.env };
|
|
316
|
+
const pathValue = explicitPath !== null ? explicitPath : (getEnvironmentPath(env) || getEnvironmentPath(process.env));
|
|
317
|
+
setEnvironmentPath(env, pathValue);
|
|
318
|
+
if (platform() === 'win32') {
|
|
319
|
+
env.PYTHONUTF8 ??= '1';
|
|
320
|
+
env.PYTHONIOENCODING ??= 'utf-8';
|
|
321
|
+
}
|
|
322
|
+
return env;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
function createShellInvocation(command) {
|
|
326
|
+
if (platform() === 'win32') {
|
|
327
|
+
return {
|
|
328
|
+
shellBin: DEFAULT_SHELL,
|
|
329
|
+
shellArgs: [
|
|
330
|
+
'-NoLogo',
|
|
331
|
+
'-NoProfile',
|
|
332
|
+
'-NonInteractive',
|
|
333
|
+
'-Command',
|
|
334
|
+
`${WINDOWS_POWERSHELL_UTF8_PREAMBLE}; ${command}`,
|
|
335
|
+
],
|
|
336
|
+
};
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
return { shellBin: DEFAULT_SHELL, shellArgs: ['-c', command] };
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
function splitPath(value) {
|
|
343
|
+
return (value || '')
|
|
344
|
+
.split(PATH_SEPARATOR)
|
|
345
|
+
.map(entry => entry.trim())
|
|
346
|
+
.filter(Boolean);
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
function getEnvironmentPath(env) {
|
|
350
|
+
const canonicalKey = platform() === 'win32' ? 'Path' : 'PATH';
|
|
351
|
+
if (typeof env[canonicalKey] === 'string') return env[canonicalKey];
|
|
352
|
+
const key = Object.keys(env).find(name => name.toLowerCase() === 'path');
|
|
353
|
+
return key ? env[key] || '' : '';
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
function setEnvironmentPath(env, value) {
|
|
357
|
+
for (const key of Object.keys(env)) {
|
|
358
|
+
if (key.toLowerCase() === 'path') delete env[key];
|
|
359
|
+
}
|
|
360
|
+
env[platform() === 'win32' ? 'Path' : 'PATH'] = value;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
function getExplicitPathOverride(env) {
|
|
364
|
+
if (!env || typeof env !== 'object') return null;
|
|
365
|
+
let value = null;
|
|
366
|
+
for (const [key, candidate] of Object.entries(env)) {
|
|
367
|
+
if (key.toLowerCase() === 'path' && typeof candidate === 'string') {
|
|
368
|
+
value = candidate;
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
return value;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
function dedupePathDirs(dirs) {
|
|
375
|
+
const seen = new Set();
|
|
376
|
+
const result = [];
|
|
377
|
+
for (const dir of dirs) {
|
|
378
|
+
if (!dir) continue;
|
|
379
|
+
const key = platform() === 'win32'
|
|
380
|
+
? dir.replace(/[\\/]+$/, '').toLowerCase()
|
|
381
|
+
: dir.replace(/\/+$/, '');
|
|
382
|
+
if (seen.has(key)) continue;
|
|
383
|
+
seen.add(key);
|
|
384
|
+
result.push(dir);
|
|
385
|
+
}
|
|
386
|
+
return result;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
function readWindowsUserMachinePathDirs() {
|
|
390
|
+
if (platform() !== 'win32') return [];
|
|
391
|
+
const command = [
|
|
392
|
+
"[Console]::OutputEncoding = [System.Text.UTF8Encoding]::new($false)",
|
|
393
|
+
"$paths = @([Environment]::GetEnvironmentVariable('Path', 'Machine'), [Environment]::GetEnvironmentVariable('Path', 'User'))",
|
|
394
|
+
"$paths | Where-Object { $_ } | ForEach-Object { [Environment]::ExpandEnvironmentVariables($_) }",
|
|
395
|
+
].join('; ');
|
|
396
|
+
try {
|
|
397
|
+
const out = execFileSync('powershell.exe', [
|
|
398
|
+
'-NoLogo',
|
|
399
|
+
'-NoProfile',
|
|
400
|
+
'-NonInteractive',
|
|
401
|
+
'-Command',
|
|
402
|
+
command,
|
|
403
|
+
], {
|
|
404
|
+
encoding: 'utf8',
|
|
405
|
+
timeout: 10_000,
|
|
406
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
407
|
+
windowsHide: true,
|
|
408
|
+
});
|
|
409
|
+
return splitPath(out.replace(/\r?\n/g, PATH_SEPARATOR));
|
|
410
|
+
} catch (err) {
|
|
411
|
+
process.stderr.write(`[shell-mcp-host] Could not read Windows User/Machine PATH: ${err.message}\n`);
|
|
412
|
+
return [];
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
function getWindowsVersionLabel() {
|
|
417
|
+
if (platform() !== 'win32') return null;
|
|
418
|
+
const release = osRelease();
|
|
419
|
+
const parts = release.split('.').map(part => Number.parseInt(part, 10));
|
|
420
|
+
const build = parts[2] || 0;
|
|
421
|
+
if (parts[0] === 10 && build >= 22000) return `Windows 11 (${release})`;
|
|
422
|
+
if (parts[0] === 10) return `Windows 10 (${release})`;
|
|
423
|
+
return `Windows (${release})`;
|
|
424
|
+
}
|
|
425
|
+
|
|
287
426
|
function formatExecSummary(result) {
|
|
288
427
|
const parts = [];
|
|
289
428
|
if (result.timedOut) parts.push('[TIMED OUT]');
|