deepseek-pp-shell-host 0.4.4 → 0.6.1

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.
@@ -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 { homedir, hostname, platform, arch } from 'node:os';
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 sep = platform() === 'win32' ? ';' : ':';
21
- const currentPath = process.env.PATH || (platform() === 'win32' ? '' : '/usr/bin:/bin');
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.split(sep).filter(d => d && !managedPathDirs.has(d));
32
- process.env.PATH = [...new Set([nodeBinDir, ...userBinDirs, ...existingPathDirs, ...localBinDirs])].join(sep);
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 shell command on the local system. Returns stdout, stderr, and exit code.',
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 && typeof args.env === 'object' ? { ...process.env, ...args.env } : process.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 isWin = platform() === 'win32';
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]');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "deepseek-pp-shell-host",
3
- "version": "0.4.4",
3
+ "version": "0.6.1",
4
4
  "description": "Native Messaging Shell MCP host installer for DeepSeek++",
5
5
  "type": "module",
6
6
  "private": false,