deepseek-pp-shell-host 0.6.3 → 0.6.5

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,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  import { execFileSync, spawn } from 'node:child_process';
3
- import { dirname, resolve } from 'node:path';
3
+ import { dirname, join, resolve } from 'node:path';
4
4
  import { fileURLToPath } from 'node:url';
5
5
  import {
6
6
  arch,
@@ -8,10 +8,11 @@ import {
8
8
  hostname,
9
9
  platform,
10
10
  release as osRelease,
11
+ tmpdir,
11
12
  type as osType,
12
13
  version as osVersion,
13
14
  } from 'node:os';
14
- import { existsSync } from 'node:fs';
15
+ import { existsSync, mkdtempSync, readdirSync, rmSync } from 'node:fs';
15
16
 
16
17
  // Resolve package root from this script's location (native/ -> package root).
17
18
  const __dirname = dirname(fileURLToPath(import.meta.url));
@@ -32,8 +33,15 @@ const userBinDirs = platform() === 'win32'
32
33
  ? [resolve(localAppData, 'OfficeCLI')]
33
34
  : [
34
35
  resolve(homedir(), '.local', 'bin'),
36
+ resolve(homedir(), '.pyenv', 'shims'),
37
+ resolve(homedir(), 'miniconda3', 'bin'),
38
+ resolve(homedir(), 'anaconda3', 'bin'),
39
+ resolve(homedir(), 'miniforge3', 'bin'),
40
+ resolve(homedir(), 'mambaforge', 'bin'),
35
41
  '/opt/homebrew/bin',
36
42
  '/usr/local/bin',
43
+ '/usr/bin',
44
+ '/bin',
37
45
  ];
38
46
  const managedPathDirs = new Set([nodeBinDir, ...localBinDirs, ...userBinDirs]);
39
47
  const existingPathDirs = splitPath(currentPath).filter(d => !managedPathDirs.has(d));
@@ -49,6 +57,12 @@ setEnvironmentPath(process.env, hostPath);
49
57
  const MCP_PROTOCOL_VERSION = '2025-06-18';
50
58
  const DEFAULT_TIMEOUT_MS = 120_000;
51
59
  const MAX_OUTPUT_BYTES = 128_000;
60
+ const DEFAULT_PYTHON_TIMEOUT_MS = 10_000;
61
+ const MAX_PYTHON_TIMEOUT_MS = 30_000;
62
+ const MAX_PYTHON_CODE_BYTES = 60_000;
63
+ const MAX_PYTHON_OUTPUT_BYTES = 64_000;
64
+ const PYTHON_PACKAGE_CHECKS = ['numpy', 'pandas', 'sympy'];
65
+ const PYTHON_NOT_FOUND_MESSAGE = 'No local Python interpreter found. Tried environment variables, PATH entries, common paths, and python/python3/py --version.';
52
66
  const DEFAULT_SHELL = platform() === 'win32' ? 'powershell.exe' : process.env.SHELL || '/bin/sh';
53
67
  const WINDOWS_POWERSHELL_UTF8_PREAMBLE = [
54
68
  '[Console]::InputEncoding = [System.Text.UTF8Encoding]::new($false)',
@@ -82,6 +96,28 @@ const TOOL_DEFINITIONS = [
82
96
  inputSchema: { type: 'object', properties: {}, additionalProperties: false },
83
97
  annotations: { operation: 'read', risk: 'low' },
84
98
  },
99
+ {
100
+ name: 'python_status',
101
+ title: 'Python Interpreter Status',
102
+ description: 'Report whether a local Python interpreter is available and which quick-validation packages can be imported.',
103
+ inputSchema: { type: 'object', properties: {}, additionalProperties: false },
104
+ annotations: { operation: 'read', risk: 'low' },
105
+ },
106
+ {
107
+ name: 'python_exec',
108
+ title: 'Execute Python Code',
109
+ description: 'Run short Python code for calculation, reasoning checks, and small data transformations. Do not install packages, access sensitive local files, or use network access.',
110
+ inputSchema: {
111
+ type: 'object',
112
+ properties: {
113
+ code: { type: 'string', description: 'Short Python code to execute. Keep it focused on computation or validation.' },
114
+ timeout_ms: { type: 'integer', minimum: 1000, maximum: MAX_PYTHON_TIMEOUT_MS, description: 'Timeout in milliseconds. Default 10000.' },
115
+ },
116
+ required: ['code'],
117
+ additionalProperties: false,
118
+ },
119
+ annotations: { operation: 'execute', risk: 'high' },
120
+ },
85
121
  ];
86
122
 
87
123
  // --- Native messaging framing (4-byte LE length prefix) ---
@@ -176,7 +212,7 @@ function handleInitialize(id) {
176
212
  protocolVersion: MCP_PROTOCOL_VERSION,
177
213
  capabilities: { tools: {} },
178
214
  serverInfo: { name: 'deepseek-pp-shell', version: '1.0.0' },
179
- instructions: 'General-purpose shell execution host. Use shell_exec to run any command on the local system.',
215
+ instructions: 'General-purpose shell execution host. Use shell_exec for local commands and python_exec only for short computation or validation snippets.',
180
216
  });
181
217
  }
182
218
 
@@ -244,6 +280,14 @@ async function handleCallTool(id, params) {
244
280
  }
245
281
  }
246
282
 
283
+ if (name === 'python_status') {
284
+ return jsonRpcResult(id, await createPythonStatusResult());
285
+ }
286
+
287
+ if (name === 'python_exec') {
288
+ return jsonRpcResult(id, await executePythonTool(args));
289
+ }
290
+
247
291
  return jsonRpcError(id, -32602, `Unknown tool: ${name}`);
248
292
  }
249
293
 
@@ -310,6 +354,431 @@ function execCommand(command, { cwd, env, timeoutMs }) {
310
354
  });
311
355
  }
312
356
 
357
+ async function createPythonStatusResult() {
358
+ const status = await detectPythonStatus();
359
+ const text = status.available
360
+ ? `Python ${status.version} ready at ${status.executable}`
361
+ : PYTHON_NOT_FOUND_MESSAGE;
362
+
363
+ return {
364
+ content: [{ type: 'text', text }],
365
+ structuredContent: {
366
+ ok: true,
367
+ data: status,
368
+ },
369
+ };
370
+ }
371
+
372
+ async function executePythonTool(args) {
373
+ const code = args?.code;
374
+ if (typeof code !== 'string' || code.trim().length === 0) {
375
+ return {
376
+ isError: true,
377
+ content: [{ type: 'text', text: 'code is required and must be a non-empty string.' }],
378
+ };
379
+ }
380
+
381
+ const codeBytes = Buffer.byteLength(code, 'utf8');
382
+ if (codeBytes > MAX_PYTHON_CODE_BYTES) {
383
+ return {
384
+ isError: true,
385
+ content: [{ type: 'text', text: `code exceeds ${MAX_PYTHON_CODE_BYTES} bytes.` }],
386
+ };
387
+ }
388
+
389
+ const timeoutMs = typeof args.timeout_ms === 'number' && args.timeout_ms >= 1000
390
+ ? Math.min(Math.floor(args.timeout_ms), MAX_PYTHON_TIMEOUT_MS)
391
+ : DEFAULT_PYTHON_TIMEOUT_MS;
392
+ const status = await detectPythonStatus();
393
+
394
+ if (!status.available || !status.command) {
395
+ return {
396
+ isError: true,
397
+ content: [{ type: 'text', text: PYTHON_NOT_FOUND_MESSAGE }],
398
+ structuredContent: {
399
+ ok: false,
400
+ data: status,
401
+ },
402
+ };
403
+ }
404
+
405
+ const cwd = mkdtempSync(join(tmpdir(), 'deepseek-pp-python-'));
406
+ try {
407
+ const result = await execPythonProcess(status.command, status.commandArgs ?? [], {
408
+ code,
409
+ cwd,
410
+ timeoutMs,
411
+ });
412
+ return {
413
+ content: [{ type: 'text', text: formatPythonExecSummary(result) }],
414
+ structuredContent: {
415
+ ok: result.exitCode === 0,
416
+ data: {
417
+ ...result,
418
+ pythonPath: status.executable,
419
+ pythonVersion: status.version,
420
+ cwd: '(temporary scratch directory)',
421
+ limits: getPythonLimits(),
422
+ },
423
+ },
424
+ isError: result.exitCode !== 0,
425
+ };
426
+ } catch (err) {
427
+ return {
428
+ isError: true,
429
+ content: [{ type: 'text', text: err.message }],
430
+ };
431
+ } finally {
432
+ rmSync(cwd, { recursive: true, force: true });
433
+ }
434
+ }
435
+
436
+ async function detectPythonStatus() {
437
+ const candidates = getPythonCandidates();
438
+ const candidateLabels = candidates.map(formatPythonCandidate);
439
+
440
+ for (const candidate of candidates) {
441
+ let versionText = null;
442
+ try {
443
+ const versionProbe = await execPythonVersionProbe(candidate);
444
+ versionText = parsePythonVersionOutput(versionProbe);
445
+ if (versionProbe.exitCode !== 0 || !versionText) continue;
446
+ } catch {
447
+ // Try the next environment value, path, or command name.
448
+ continue;
449
+ }
450
+
451
+ try {
452
+ const probe = await execPythonProbe(candidate);
453
+ if (probe.exitCode !== 0 || !probe.stdout.trim()) continue;
454
+ const data = JSON.parse(probe.stdout.trim());
455
+ return {
456
+ available: true,
457
+ command: candidate.command,
458
+ commandArgs: getPythonCommandArgs(candidate),
459
+ executable: typeof data.executable === 'string' ? data.executable : candidate.command,
460
+ version: typeof data.version === 'string' ? data.version : versionText,
461
+ versionCheck: versionText,
462
+ packages: normalizePythonPackages(data.packages),
463
+ candidates: candidateLabels,
464
+ isolation: 'python -I',
465
+ policy: getPythonPolicy(),
466
+ limits: getPythonLimits(),
467
+ };
468
+ } catch {
469
+ // --version worked, but the JSON probe failed; try the next common executable name.
470
+ }
471
+ }
472
+
473
+ return {
474
+ available: false,
475
+ command: null,
476
+ commandArgs: [],
477
+ executable: null,
478
+ version: null,
479
+ versionCheck: null,
480
+ packages: Object.fromEntries(PYTHON_PACKAGE_CHECKS.map((name) => [name, false])),
481
+ candidates: candidateLabels,
482
+ isolation: 'python -I',
483
+ policy: getPythonPolicy(),
484
+ limits: getPythonLimits(),
485
+ };
486
+ }
487
+
488
+ function getPythonCandidates() {
489
+ const envCandidates = getPythonEnvCandidates();
490
+ const pathCandidates = getPythonPathCandidates();
491
+ const fallbackCandidates = platform() === 'win32'
492
+ ? [
493
+ { command: 'py', args: [], launcherArgs: ['-3'], source: 'command:py -3 --version' },
494
+ { command: 'py.exe', args: [], launcherArgs: ['-3'], source: 'command:py.exe -3 --version' },
495
+ { command: 'python', args: [], source: 'command:python --version' },
496
+ { command: 'python.exe', args: [], source: 'command:python.exe --version' },
497
+ { command: 'python3', args: [], source: 'command:python3 --version' },
498
+ { command: 'python3.exe', args: [], source: 'command:python3.exe --version' },
499
+ ]
500
+ : [
501
+ { command: 'python3', args: [], source: 'command:python3 --version' },
502
+ { command: 'python', args: [], source: 'command:python --version' },
503
+ { command: 'py', args: [], source: 'command:py --version' },
504
+ ];
505
+ return dedupePythonCandidates([...envCandidates, ...pathCandidates, ...fallbackCandidates]);
506
+ }
507
+
508
+ function getPythonEnvCandidates() {
509
+ const names = [
510
+ 'DEEPSEEK_PP_PYTHON',
511
+ 'PYTHON_EXECUTABLE',
512
+ 'PYTHON',
513
+ 'PYTHON3',
514
+ ];
515
+ const candidates = [];
516
+ for (const name of names) {
517
+ const value = process.env[name];
518
+ if (typeof value !== 'string' || value.trim().length === 0) continue;
519
+ candidates.push({ command: value.trim(), args: [], source: 'env:' + name });
520
+ }
521
+ return candidates;
522
+ }
523
+
524
+ function getPythonPathCandidates() {
525
+ return platform() === 'win32' ? getWindowsPythonPathCandidates() : getPosixPythonPathCandidates();
526
+ }
527
+
528
+ function getPosixPythonPathCandidates() {
529
+ const candidates = [];
530
+ const directPaths = [
531
+ resolve(homedir(), '.pyenv', 'shims', 'python3'),
532
+ resolve(homedir(), '.pyenv', 'shims', 'python'),
533
+ resolve(homedir(), 'miniconda3', 'bin', 'python'),
534
+ resolve(homedir(), 'anaconda3', 'bin', 'python'),
535
+ resolve(homedir(), 'miniforge3', 'bin', 'python'),
536
+ resolve(homedir(), 'mambaforge', 'bin', 'python'),
537
+ '/opt/homebrew/bin/python3',
538
+ '/opt/homebrew/bin/python',
539
+ '/usr/local/bin/python3',
540
+ '/usr/local/bin/python',
541
+ '/usr/bin/python3',
542
+ '/usr/bin/python',
543
+ '/bin/python3',
544
+ '/bin/python',
545
+ ];
546
+ for (const pythonPath of directPaths) addPythonPathCandidate(candidates, pythonPath, 'path:file');
547
+ for (const root of ['miniconda3', 'anaconda3', 'miniforge3', 'mambaforge']) {
548
+ addPythonEnvDirCandidates(candidates, resolve(homedir(), root, 'envs'));
549
+ }
550
+ addPythonEnvDirCandidates(candidates, resolve(homedir(), '.pyenv', 'versions'));
551
+ return candidates;
552
+ }
553
+
554
+ function getWindowsPythonPathCandidates() {
555
+ const candidates = [];
556
+ addWindowsPathPythonCandidates(candidates);
557
+ const dirs = [
558
+ resolve(localAppData, 'Programs', 'Python'),
559
+ process.env.ProgramFiles ? resolve(process.env.ProgramFiles) : '',
560
+ process.env['ProgramFiles(x86)'] ? resolve(process.env['ProgramFiles(x86)']) : '',
561
+ ].filter(Boolean);
562
+ for (const dir of dirs) {
563
+ for (const entry of readDirectoryEntries(dir)) {
564
+ if (!/^Python\d+/i.test(entry.name)) continue;
565
+ addPythonPathCandidate(candidates, resolve(dir, entry.name, 'python.exe'), 'path:file');
566
+ }
567
+ }
568
+ return candidates;
569
+ }
570
+
571
+ function addWindowsPathPythonCandidates(candidates) {
572
+ for (const dir of splitPath(getEnvironmentPath(process.env))) {
573
+ for (const name of ['python.exe', 'python3.exe']) {
574
+ addPythonPathCandidate(candidates, resolve(dir, name), 'path:PATH');
575
+ }
576
+ }
577
+ }
578
+
579
+ function addPythonEnvDirCandidates(candidates, envsDir) {
580
+ for (const entry of readDirectoryEntries(envsDir)) {
581
+ if (!entry.isDirectory()) continue;
582
+ const pythonPath = platform() === 'win32'
583
+ ? resolve(envsDir, entry.name, 'python.exe')
584
+ : resolve(envsDir, entry.name, 'bin', 'python');
585
+ addPythonPathCandidate(candidates, pythonPath, 'path:env');
586
+ }
587
+ }
588
+
589
+ function addPythonPathCandidate(candidates, pythonPath, source) {
590
+ if (!existsSync(pythonPath)) return;
591
+ if (platform() === 'win32' && isWindowsAppExecutionAliasPath(pythonPath)) return;
592
+ candidates.push({ command: pythonPath, args: [], source });
593
+ }
594
+
595
+ function isWindowsAppExecutionAliasPath(filePath) {
596
+ const normalized = normalizeWindowsPathForCompare(filePath);
597
+ const aliasDir = normalizeWindowsPathForCompare(resolve(localAppData, 'Microsoft', 'WindowsApps'));
598
+ return normalized === aliasDir || normalized.startsWith(aliasDir + '/');
599
+ }
600
+
601
+ function normalizeWindowsPathForCompare(filePath) {
602
+ return resolve(filePath).replace(/[\\/]+/g, '/').replace(/\/+$/, '').toLowerCase();
603
+ }
604
+
605
+ function readDirectoryEntries(dir) {
606
+ try {
607
+ return readdirSync(dir, { withFileTypes: true });
608
+ } catch {
609
+ return [];
610
+ }
611
+ }
612
+ function dedupePythonCandidates(candidates) {
613
+ const seen = new Set();
614
+ const result = [];
615
+ for (const candidate of candidates) {
616
+ const key = [candidate.command, ...(candidate.launcherArgs ?? []), ...candidate.args].join('\0');
617
+ if (seen.has(key)) continue;
618
+ seen.add(key);
619
+ result.push(candidate);
620
+ }
621
+ return result;
622
+ }
623
+
624
+ function getPythonCommandArgs(candidate) {
625
+ return [...(candidate.launcherArgs ?? []), ...candidate.args];
626
+ }
627
+
628
+ function formatPythonCandidate(candidate) {
629
+ const label = [candidate.command, ...getPythonCommandArgs(candidate)].join(' ');
630
+ return candidate.source ? label + ' (' + candidate.source + ')' : label;
631
+ }
632
+
633
+ function execPythonVersionProbe(candidate) {
634
+ return execProcess(candidate.command, [...getPythonCommandArgs(candidate), '--version'], {
635
+ cwd: homedir(),
636
+ env: createPythonChildEnv(),
637
+ timeoutMs: 2_000,
638
+ maxOutputBytes: 2_000,
639
+ });
640
+ }
641
+
642
+ function parsePythonVersionOutput(probe) {
643
+ const text = [probe.stdout, probe.stderr].join(' ').replace(/\s+/g, ' ').trim();
644
+ const match = text.match(/Python\s+([0-9]+(?:\.[0-9]+){1,2})/i);
645
+ return match ? match[1] : null;
646
+ }
647
+
648
+ function execPythonProbe(candidate) {
649
+ const code = [
650
+ 'import importlib.util, json, sys',
651
+ `packages = {name: importlib.util.find_spec(name) is not None for name in ${JSON.stringify(PYTHON_PACKAGE_CHECKS)}}`,
652
+ 'print(json.dumps({"executable": sys.executable, "version": sys.version.split()[0], "packages": packages}, ensure_ascii=False))',
653
+ ].join('\n');
654
+
655
+ return execProcess(candidate.command, [...getPythonCommandArgs(candidate), '-I', '-c', code], {
656
+ cwd: homedir(),
657
+ env: createPythonChildEnv(),
658
+ timeoutMs: 5_000,
659
+ maxOutputBytes: 16_000,
660
+ });
661
+ }
662
+
663
+ function execPythonProcess(command, commandArgs, { code, cwd, timeoutMs }) {
664
+ return execProcess(command, [...commandArgs, '-I', '-'], {
665
+ cwd,
666
+ env: createPythonChildEnv(),
667
+ input: code,
668
+ timeoutMs,
669
+ maxOutputBytes: MAX_PYTHON_OUTPUT_BYTES,
670
+ });
671
+ }
672
+
673
+ function execProcess(command, args, { cwd, env, input, timeoutMs, maxOutputBytes }) {
674
+ return new Promise((resolve, reject) => {
675
+ const startedAt = Date.now();
676
+ const child = spawn(command, args, {
677
+ cwd,
678
+ env,
679
+ shell: false,
680
+ stdio: ['pipe', 'pipe', 'pipe'],
681
+ windowsHide: true,
682
+ });
683
+
684
+ const stdout = [];
685
+ const stderr = [];
686
+ let stdoutBytes = 0;
687
+ let stderrBytes = 0;
688
+ let timedOut = false;
689
+
690
+ const timer = setTimeout(() => {
691
+ timedOut = true;
692
+ child.kill('SIGTERM');
693
+ setTimeout(() => child.kill('SIGKILL'), 3000);
694
+ }, timeoutMs);
695
+
696
+ child.stdout.on('data', (chunk) => {
697
+ if (stdoutBytes < maxOutputBytes) {
698
+ const remaining = maxOutputBytes - stdoutBytes;
699
+ stdout.push(chunk.length <= remaining ? chunk : chunk.subarray(0, remaining));
700
+ }
701
+ stdoutBytes += chunk.length;
702
+ });
703
+
704
+ child.stderr.on('data', (chunk) => {
705
+ if (stderrBytes < maxOutputBytes) {
706
+ const remaining = maxOutputBytes - stderrBytes;
707
+ stderr.push(chunk.length <= remaining ? chunk : chunk.subarray(0, remaining));
708
+ }
709
+ stderrBytes += chunk.length;
710
+ });
711
+
712
+ child.on('error', (err) => {
713
+ clearTimeout(timer);
714
+ reject(new Error(`Failed to spawn ${command}: ${err.message}`));
715
+ });
716
+
717
+ child.on('close', (exitCode, signal) => {
718
+ clearTimeout(timer);
719
+ resolve({
720
+ command: [command, ...args].join(' '),
721
+ exitCode: timedOut ? -1 : (exitCode ?? -1),
722
+ signal: signal || (timedOut ? 'SIGTERM' : null),
723
+ stdout: Buffer.concat(stdout).toString('utf8'),
724
+ stderr: Buffer.concat(stderr).toString('utf8'),
725
+ truncated: stdoutBytes > maxOutputBytes || stderrBytes > maxOutputBytes,
726
+ timedOut,
727
+ durationMs: Date.now() - startedAt,
728
+ });
729
+ });
730
+
731
+ if (input != null) {
732
+ child.stdin.end(input);
733
+ } else {
734
+ child.stdin.end();
735
+ }
736
+ });
737
+ }
738
+
739
+ function createPythonChildEnv() {
740
+ const env = {};
741
+ const keys = platform() === 'win32'
742
+ ? ['SystemRoot', 'WINDIR', 'COMSPEC', 'PATHEXT', 'TEMP', 'TMP', 'USERPROFILE', 'LOCALAPPDATA', 'APPDATA']
743
+ : ['HOME', 'TMPDIR', 'TEMP', 'TMP', 'LANG', 'LC_ALL', 'LC_CTYPE'];
744
+
745
+ for (const key of keys) {
746
+ if (typeof process.env[key] === 'string') env[key] = process.env[key];
747
+ }
748
+
749
+ setEnvironmentPath(env, getEnvironmentPath(process.env));
750
+ env.PYTHONUTF8 = '1';
751
+ env.PYTHONIOENCODING = 'utf-8';
752
+ env.PYTHONNOUSERSITE = '1';
753
+ env.PIP_DISABLE_PIP_VERSION_CHECK = '1';
754
+ return env;
755
+ }
756
+
757
+ function normalizePythonPackages(value) {
758
+ const input = value && typeof value === 'object' ? value : {};
759
+ return Object.fromEntries(
760
+ PYTHON_PACKAGE_CHECKS.map((name) => [name, input[name] === true]),
761
+ );
762
+ }
763
+
764
+ function getPythonPolicy() {
765
+ return {
766
+ purpose: 'short computation, idea validation, and small data transformations',
767
+ packageInstall: false,
768
+ networkAccess: 'not_allowed_by_policy_not_os_enforced',
769
+ filesystemAccess: 'temporary_cwd_only_by_policy_not_os_enforced',
770
+ };
771
+ }
772
+
773
+ function getPythonLimits() {
774
+ return {
775
+ timeoutMsDefault: DEFAULT_PYTHON_TIMEOUT_MS,
776
+ timeoutMsMax: MAX_PYTHON_TIMEOUT_MS,
777
+ codeBytesMax: MAX_PYTHON_CODE_BYTES,
778
+ outputBytesMax: MAX_PYTHON_OUTPUT_BYTES,
779
+ };
780
+ }
781
+
313
782
  function createChildEnv(extraEnv) {
314
783
  const explicitPath = getExplicitPathOverride(extraEnv);
315
784
  const env = extraEnv && typeof extraEnv === 'object' ? { ...process.env, ...extraEnv } : { ...process.env };
@@ -433,6 +902,16 @@ function formatExecSummary(result) {
433
902
  return parts.join('\n') || '(no output)';
434
903
  }
435
904
 
905
+ function formatPythonExecSummary(result) {
906
+ const parts = [];
907
+ if (result.timedOut) parts.push('[TIMED OUT]');
908
+ if (result.exitCode !== 0) parts.push(`[exit ${result.exitCode}]`);
909
+ if (result.truncated) parts.push('[output truncated]');
910
+ if (result.stdout) parts.push(result.stdout.slice(0, 4000));
911
+ if (result.stderr) parts.push(`STDERR: ${result.stderr.slice(0, 2000)}`);
912
+ return parts.join('\n') || '(no output)';
913
+ }
914
+
436
915
  // --- Message dispatch ---
437
916
 
438
917
  async function handleMessage(envelope) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "deepseek-pp-shell-host",
3
- "version": "0.6.3",
3
+ "version": "0.6.5",
4
4
  "description": "Native Messaging Shell MCP host installer for DeepSeek++",
5
5
  "type": "module",
6
6
  "private": false,