deepseek-pp-shell-host 0.4.4

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/README.md ADDED
@@ -0,0 +1,16 @@
1
+ # deepseek-pp-shell-host
2
+
3
+ Native Messaging Shell MCP host installer for DeepSeek++.
4
+
5
+ ```bash
6
+ npx deepseek-pp-shell-host install --browser chrome --extension-id <extension-id>
7
+ ```
8
+
9
+ The installer writes the browser Native Messaging manifest, installs the Shell MCP host into the user's profile directory, and installs command-based OfficeCLI by default.
10
+
11
+ Useful commands:
12
+
13
+ ```bash
14
+ npx deepseek-pp-shell-host status --browser chrome
15
+ npx deepseek-pp-shell-host uninstall --browser chrome
16
+ ```
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env node
2
+ import { main } from '../lib/installer.mjs';
3
+
4
+ main().catch((err) => {
5
+ console.error(`\nInstall failed: ${err.message}`);
6
+ process.exit(1);
7
+ });
@@ -0,0 +1,547 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ chmodSync,
4
+ copyFileSync,
5
+ existsSync,
6
+ mkdirSync,
7
+ readFileSync,
8
+ renameSync,
9
+ rmSync,
10
+ writeFileSync,
11
+ } from 'node:fs';
12
+ import { execFileSync, execSync } from 'node:child_process';
13
+ import { createHash } from 'node:crypto';
14
+ import { dirname, resolve } from 'node:path';
15
+ import { arch, homedir, platform, tmpdir } from 'node:os';
16
+ import { fileURLToPath } from 'node:url';
17
+
18
+ export const HOST_NAME = 'com.deepseek_pp.shell';
19
+
20
+ const __dirname = dirname(fileURLToPath(import.meta.url));
21
+ const PACKAGE_ROOT = resolve(__dirname, '..');
22
+ const HOST_SOURCE = resolve(PACKAGE_ROOT, 'native', 'shell-mcp-host.mjs');
23
+ const FIREFOX_EXTENSION_ID = 'deepseek-pp@zhu1090093659.github';
24
+ const SUPPORTED_BROWSERS = new Set(['chrome', 'chromium', 'edge', 'firefox']);
25
+ const COMMANDS = new Set(['install', 'status', 'uninstall']);
26
+ const OFFICECLI_REPO = 'iOfficeAI/OfficeCLI';
27
+ const OFFICECLI_BINARY = platform() === 'win32' ? 'officecli.exe' : 'officecli';
28
+ const OFFICECLI_MIRROR_BASE = 'https://d.officecli.ai';
29
+ const OFFICECLI_GITHUB_RELEASE_BASE = `https://github.com/${OFFICECLI_REPO}/releases/latest/download`;
30
+ const OFFICECLI_REQUIRED_HELP_PATTERNS = [
31
+ /\bview\s+<file>\s+<mode>/,
32
+ /\bget\s+<file>\s+<path>/,
33
+ /\bset\s+<file>\s+<path>/,
34
+ /\bbatch\s+<file>/,
35
+ /\bvalidate\s+<file>/,
36
+ /--json\b/,
37
+ ];
38
+
39
+ function parseArgs(argv) {
40
+ const args = {
41
+ command: 'install',
42
+ extensionId: null,
43
+ browser: 'chrome',
44
+ skipOfficecli: false,
45
+ forceOfficecli: false,
46
+ };
47
+ const tokens = [...argv];
48
+
49
+ if (tokens[0] && COMMANDS.has(tokens[0])) {
50
+ args.command = tokens.shift();
51
+ }
52
+
53
+ for (let i = 0; i < tokens.length; i++) {
54
+ const token = tokens[i];
55
+ if (token === '--extension-id' && tokens[i + 1]) args.extensionId = tokens[++i];
56
+ else if (token === '--browser' && tokens[i + 1]) args.browser = tokens[++i].toLowerCase();
57
+ else if (token === '--skip-officecli') args.skipOfficecli = true;
58
+ else if (token === '--force-officecli') args.forceOfficecli = true;
59
+ else if (token === '--help' || token === '-h') {
60
+ printHelp();
61
+ process.exit(0);
62
+ } else {
63
+ throw new Error(`Unknown option: ${token}`);
64
+ }
65
+ }
66
+
67
+ if (!SUPPORTED_BROWSERS.has(args.browser)) {
68
+ throw new Error(`Unsupported browser: ${args.browser}`);
69
+ }
70
+
71
+ return args;
72
+ }
73
+
74
+ function printHelp() {
75
+ console.log(`DeepSeek++ Shell Native Host installer
76
+
77
+ Usage:
78
+ deepseek-pp-shell-host install --browser chrome --extension-id <extension-id>
79
+ deepseek-pp-shell-host status --browser chrome
80
+ deepseek-pp-shell-host uninstall --browser chrome
81
+
82
+ Commands:
83
+ install Install the Shell Native Host and OfficeCLI
84
+ status Show manifest, host, and OfficeCLI status
85
+ uninstall Remove the Shell Native Host manifest and installed host files
86
+
87
+ Options:
88
+ --extension-id <id> Chrome/Edge/Chromium extension ID
89
+ --browser <name> Target browser: chrome, chromium, edge, firefox (default: chrome)
90
+ --skip-officecli Install only the Shell Native Host
91
+ --force-officecli Reinstall OfficeCLI even if a compatible binary exists
92
+ --help Show this help
93
+
94
+ Examples:
95
+ npx deepseek-pp-shell-host install --browser chrome --extension-id abcdefghijklmnopqrstuvwxyz123456
96
+ npx deepseek-pp-shell-host install --browser firefox
97
+ `);
98
+ }
99
+
100
+ function getAppDataRoot() {
101
+ const home = homedir();
102
+ if (platform() === 'darwin') return `${home}/Library/Application Support/DeepSeek++`;
103
+ if (platform() === 'linux') return `${home}/.local/share/deepseek-pp`;
104
+ if (platform() === 'win32') {
105
+ const localAppData = process.env.LOCALAPPDATA || resolve(home, 'AppData', 'Local');
106
+ return resolve(localAppData, 'DeepSeek++');
107
+ }
108
+ throw new Error(`Unsupported platform: ${platform()}`);
109
+ }
110
+
111
+ function getHostInstallDir() {
112
+ const root = getAppDataRoot();
113
+ return platform() === 'linux' ? resolve(root, 'native-host') : resolve(root, 'NativeHost');
114
+ }
115
+
116
+ function getManifestDir(browser) {
117
+ const os = platform();
118
+ const home = homedir();
119
+
120
+ if (os === 'darwin') {
121
+ switch (browser) {
122
+ case 'chrome': return `${home}/Library/Application Support/Google/Chrome/NativeMessagingHosts`;
123
+ case 'chromium': return `${home}/Library/Application Support/Chromium/NativeMessagingHosts`;
124
+ case 'edge': return `${home}/Library/Application Support/Microsoft Edge/NativeMessagingHosts`;
125
+ case 'firefox': return `${home}/Library/Application Support/Mozilla/NativeMessagingHosts`;
126
+ default: return `${home}/Library/Application Support/Google/Chrome/NativeMessagingHosts`;
127
+ }
128
+ }
129
+
130
+ if (os === 'linux') {
131
+ switch (browser) {
132
+ case 'chrome': return `${home}/.config/google-chrome/NativeMessagingHosts`;
133
+ case 'chromium': return `${home}/.config/chromium/NativeMessagingHosts`;
134
+ case 'edge': return `${home}/.config/microsoft-edge/NativeMessagingHosts`;
135
+ case 'firefox': return `${home}/.mozilla/native-messaging-hosts`;
136
+ default: return `${home}/.config/google-chrome/NativeMessagingHosts`;
137
+ }
138
+ }
139
+
140
+ if (os === 'win32') {
141
+ return resolve(getAppDataRoot(), 'NativeMessagingHosts');
142
+ }
143
+
144
+ throw new Error(`Unsupported platform: ${os}`);
145
+ }
146
+
147
+ function getManifestPath(browser) {
148
+ return resolve(getManifestDir(browser), `${HOST_NAME}.json`);
149
+ }
150
+
151
+ function getRegistryKey(browser) {
152
+ switch (browser) {
153
+ case 'chrome': return `HKCU\\Software\\Google\\Chrome\\NativeMessagingHosts\\${HOST_NAME}`;
154
+ case 'edge': return `HKCU\\Software\\Microsoft\\Edge\\NativeMessagingHosts\\${HOST_NAME}`;
155
+ case 'chromium': return `HKCU\\Software\\Chromium\\NativeMessagingHosts\\${HOST_NAME}`;
156
+ default: return null;
157
+ }
158
+ }
159
+
160
+ function buildManifest(args, wrapperPath) {
161
+ const manifest = {
162
+ name: HOST_NAME,
163
+ description: 'DeepSeek++ Shell MCP - General purpose shell execution via Native Messaging',
164
+ path: wrapperPath,
165
+ type: 'stdio',
166
+ };
167
+
168
+ if (args.browser === 'firefox') {
169
+ manifest.allowed_extensions = [FIREFOX_EXTENSION_ID];
170
+ } else {
171
+ if (!args.extensionId) {
172
+ throw new Error('--extension-id is required for Chrome/Edge/Chromium.');
173
+ }
174
+ manifest.allowed_origins = [`chrome-extension://${args.extensionId}/`];
175
+ }
176
+
177
+ return manifest;
178
+ }
179
+
180
+ function copyHostScript(installDir) {
181
+ const hostPath = resolve(installDir, 'shell-mcp-host.mjs');
182
+ mkdirSync(installDir, { recursive: true });
183
+ copyFileSync(HOST_SOURCE, hostPath);
184
+ if (platform() !== 'win32') chmodSync(hostPath, 0o755);
185
+ return hostPath;
186
+ }
187
+
188
+ function createWrapper(hostPath) {
189
+ const installDir = dirname(hostPath);
190
+ const nodePath = process.execPath;
191
+
192
+ if (platform() === 'win32') {
193
+ const wrapperPath = resolve(installDir, 'shell-mcp-host.bat');
194
+ const content = `@echo off\r\n"${nodePath}" "${hostPath}" %*\r\n`;
195
+ writeFileSync(wrapperPath, content);
196
+ return wrapperPath;
197
+ }
198
+
199
+ const wrapperPath = resolve(installDir, 'shell-mcp-host');
200
+ const content = `#!/bin/sh\nexec "${nodePath}" "${hostPath}" "$@"\n`;
201
+ writeFileSync(wrapperPath, content, { mode: 0o755 });
202
+ return wrapperPath;
203
+ }
204
+
205
+ function writeWindowsRegistry(browser, manifestPath) {
206
+ const regKey = getRegistryKey(browser);
207
+ if (!regKey) return;
208
+
209
+ try {
210
+ execSync(`reg add "${regKey}" /ve /t REG_SZ /d "${manifestPath}" /f`, { stdio: 'pipe' });
211
+ console.log(`Registry: ${regKey}`);
212
+ } catch {
213
+ console.error('Warning: Failed to write registry key. You may need to run as Administrator.');
214
+ console.error(` Manual: reg add "${regKey}" /ve /t REG_SZ /d "${manifestPath}" /f`);
215
+ }
216
+ }
217
+
218
+ function removeWindowsRegistry(browser) {
219
+ const regKey = getRegistryKey(browser);
220
+ if (!regKey) return;
221
+
222
+ try {
223
+ execSync(`reg delete "${regKey}" /f`, { stdio: 'pipe' });
224
+ console.log(`Removed registry key: ${regKey}`);
225
+ } catch {
226
+ console.log(`Registry key not removed or was already absent: ${regKey}`);
227
+ }
228
+ }
229
+
230
+ function getOfficeCliInstallDir() {
231
+ if (platform() === 'win32') {
232
+ const localAppData = process.env.LOCALAPPDATA || resolve(homedir(), 'AppData', 'Local');
233
+ return resolve(localAppData, 'OfficeCLI');
234
+ }
235
+ return resolve(homedir(), '.local', 'bin');
236
+ }
237
+
238
+ function isProjectNodeModulesPath(path) {
239
+ const normalized = path.replaceAll('\\', '/');
240
+ return normalized.includes('/node_modules/.bin/');
241
+ }
242
+
243
+ function getPathOfficeCliCandidates() {
244
+ const command = platform() === 'win32' ? 'where.exe' : 'which';
245
+ const args = platform() === 'win32' ? [OFFICECLI_BINARY] : ['-a', 'officecli'];
246
+ try {
247
+ const out = execFileSync(command, args, { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] });
248
+ return out
249
+ .split(/\r?\n/)
250
+ .map(line => line.trim())
251
+ .filter(Boolean);
252
+ } catch {
253
+ return [];
254
+ }
255
+ }
256
+
257
+ function getOfficeCliCandidates() {
258
+ const installPath = resolve(getOfficeCliInstallDir(), OFFICECLI_BINARY);
259
+ const candidates = [installPath, ...getPathOfficeCliCandidates()]
260
+ .filter(candidate => !isProjectNodeModulesPath(candidate));
261
+ return [...new Set(candidates)];
262
+ }
263
+
264
+ function isCompatibleOfficeCli(binaryPath) {
265
+ if (!existsSync(binaryPath)) return false;
266
+ try {
267
+ const help = execFileSync(binaryPath, ['--help'], {
268
+ encoding: 'utf8',
269
+ timeout: 20_000,
270
+ env: { ...process.env, OFFICECLI_SKIP_UPDATE: '1' },
271
+ stdio: ['ignore', 'pipe', 'pipe'],
272
+ });
273
+ return OFFICECLI_REQUIRED_HELP_PATTERNS.every(pattern => pattern.test(help));
274
+ } catch {
275
+ return false;
276
+ }
277
+ }
278
+
279
+ function findCompatibleOfficeCli() {
280
+ return getOfficeCliCandidates().find(isCompatibleOfficeCli) ?? null;
281
+ }
282
+
283
+ function detectLinuxMusl() {
284
+ if (existsSync('/etc/alpine-release')) return true;
285
+ try {
286
+ const out = execFileSync('ldd', ['--version'], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] });
287
+ return /musl/i.test(out);
288
+ } catch {
289
+ return false;
290
+ }
291
+ }
292
+
293
+ function getOfficeCliAssetName() {
294
+ const os = platform();
295
+ const cpu = arch();
296
+ if (os === 'darwin') {
297
+ if (cpu === 'arm64') return 'officecli-mac-arm64';
298
+ if (cpu === 'x64') return 'officecli-mac-x64';
299
+ }
300
+ if (os === 'linux') {
301
+ const distro = detectLinuxMusl() ? 'linux-alpine' : 'linux';
302
+ if (cpu === 'x64') return `officecli-${distro}-x64`;
303
+ if (cpu === 'arm64') return `officecli-${distro}-arm64`;
304
+ }
305
+ if (os === 'win32') {
306
+ if (cpu === 'x64') return 'officecli-win-x64.exe';
307
+ if (cpu === 'arm64') return 'officecli-win-arm64.exe';
308
+ }
309
+ throw new Error(`Unsupported OfficeCLI platform: ${os}/${cpu}`);
310
+ }
311
+
312
+ async function fetchBytes(url) {
313
+ const response = await fetch(url, {
314
+ headers: { 'user-agent': 'deepseek-pp-officecli-installer' },
315
+ });
316
+ if (!response.ok) {
317
+ throw new Error(`${response.status} ${response.statusText}`);
318
+ }
319
+ return Buffer.from(await response.arrayBuffer());
320
+ }
321
+
322
+ async function downloadWithFallback(asset, outPath) {
323
+ const primary = `${OFFICECLI_MIRROR_BASE}/releases/latest/download/${asset}`;
324
+ const fallback = `${OFFICECLI_GITHUB_RELEASE_BASE}/${asset}`;
325
+ try {
326
+ writeFileSync(outPath, await fetchBytes(primary));
327
+ console.log(` downloaded ${asset} via mirror`);
328
+ } catch (primaryError) {
329
+ console.log(` mirror unavailable for ${asset}, falling back to GitHub`);
330
+ try {
331
+ writeFileSync(outPath, await fetchBytes(fallback));
332
+ } catch (fallbackError) {
333
+ throw new Error(`Failed to download ${asset}: mirror=${primaryError.message}; github=${fallbackError.message}`);
334
+ }
335
+ }
336
+ }
337
+
338
+ async function verifyOfficeCliChecksum(asset, binaryPath) {
339
+ const sumsPath = resolve(tmpdir(), `officecli-SHA256SUMS-${process.pid}`);
340
+ try {
341
+ try {
342
+ await downloadWithFallback('SHA256SUMS', sumsPath);
343
+ } catch {
344
+ console.log(' checksum file unavailable, skipping verification');
345
+ return;
346
+ }
347
+ const sums = readFileSync(sumsPath, 'utf8');
348
+ const expectedLine = sums.split(/\r?\n/).find(line => line.includes(asset));
349
+ if (!expectedLine) {
350
+ console.log(' checksum entry not found, skipping verification');
351
+ return;
352
+ }
353
+ const expected = expectedLine.trim().split(/\s+/)[0].toLowerCase();
354
+ const actual = createHash('sha256').update(readFileSync(binaryPath)).digest('hex');
355
+ if (actual !== expected) {
356
+ throw new Error(`Checksum mismatch for ${asset}: expected ${expected}, got ${actual}`);
357
+ }
358
+ console.log(' checksum verified');
359
+ } finally {
360
+ rmSync(sumsPath, { force: true });
361
+ }
362
+ }
363
+
364
+ function verifyDownloadedOfficeCli(binaryPath) {
365
+ if (platform() !== 'win32') {
366
+ chmodSync(binaryPath, 0o755);
367
+ }
368
+ execFileSync(binaryPath, ['--version'], {
369
+ timeout: 20_000,
370
+ stdio: ['ignore', 'ignore', 'pipe'],
371
+ env: { ...process.env, OFFICECLI_SKIP_UPDATE: '1' },
372
+ });
373
+ if (!isCompatibleOfficeCli(binaryPath)) {
374
+ throw new Error('Downloaded OfficeCLI does not expose the required command-based interface.');
375
+ }
376
+ }
377
+
378
+ function addOfficeCliToUserPath(installDir) {
379
+ if (platform() === 'win32') {
380
+ try {
381
+ execFileSync('powershell.exe', [
382
+ '-NoProfile',
383
+ '-ExecutionPolicy',
384
+ 'Bypass',
385
+ '-Command',
386
+ `$p=[Environment]::GetEnvironmentVariable('Path','User'); if (($p -split ';') -notcontains '${installDir.replaceAll("'", "''")}') { [Environment]::SetEnvironmentVariable('Path', (($p.TrimEnd(';') + ';${installDir.replaceAll("'", "''")}').TrimStart(';')), 'User') }`,
387
+ ], { stdio: 'pipe' });
388
+ console.log(`Added ${installDir} to the user PATH.`);
389
+ } catch {
390
+ console.log(`Could not update the user PATH automatically. Add this directory manually: ${installDir}`);
391
+ }
392
+ return;
393
+ }
394
+
395
+ const shellRc = platform() === 'darwin' || process.env.SHELL?.includes('zsh')
396
+ ? resolve(homedir(), '.zshrc')
397
+ : resolve(homedir(), '.bashrc');
398
+ const pathLine = `export PATH="${installDir}:$PATH"`;
399
+ try {
400
+ const existing = existsSync(shellRc) ? readFileSync(shellRc, 'utf8') : '';
401
+ if (!existing.includes(installDir)) {
402
+ writeFileSync(shellRc, `${existing}${existing.endsWith('\n') || existing.length === 0 ? '' : '\n'}\n${pathLine}\n`);
403
+ console.log(`Added ${installDir} to PATH in ${shellRc}.`);
404
+ }
405
+ } catch {
406
+ console.log(`Could not update shell PATH automatically. Add this line manually: ${pathLine}`);
407
+ }
408
+ }
409
+
410
+ async function ensureOfficeCliInstalled({ force }) {
411
+ if (!force) {
412
+ const existing = findCompatibleOfficeCli();
413
+ if (existing) {
414
+ console.log(`OfficeCLI command binary already available: ${existing}`);
415
+ return existing;
416
+ }
417
+ }
418
+
419
+ const asset = getOfficeCliAssetName();
420
+ const installDir = getOfficeCliInstallDir();
421
+ const targetPath = resolve(installDir, OFFICECLI_BINARY);
422
+ const tempPath = resolve(tmpdir(), `${OFFICECLI_BINARY}-${process.pid}`);
423
+ const stagedPath = `${targetPath}.new`;
424
+
425
+ console.log(`Installing OfficeCLI from ${OFFICECLI_REPO} (${asset})...`);
426
+ await downloadWithFallback(asset, tempPath);
427
+ await verifyOfficeCliChecksum(asset, tempPath);
428
+ verifyDownloadedOfficeCli(tempPath);
429
+
430
+ mkdirSync(installDir, { recursive: true });
431
+ copyFileSync(tempPath, stagedPath);
432
+ if (platform() !== 'win32') {
433
+ chmodSync(stagedPath, 0o755);
434
+ }
435
+ if (platform() === 'darwin') {
436
+ try { execFileSync('xattr', ['-d', 'com.apple.quarantine', stagedPath], { stdio: 'ignore' }); } catch {}
437
+ try { execFileSync('codesign', ['-s', '-', '-f', stagedPath], { stdio: 'ignore' }); } catch {}
438
+ }
439
+ renameSync(stagedPath, targetPath);
440
+ rmSync(tempPath, { force: true });
441
+
442
+ addOfficeCliToUserPath(installDir);
443
+ console.log(`OfficeCLI installed: ${targetPath}`);
444
+ return targetPath;
445
+ }
446
+
447
+ function readManifest(manifestPath) {
448
+ if (!existsSync(manifestPath)) return null;
449
+ return JSON.parse(readFileSync(manifestPath, 'utf8'));
450
+ }
451
+
452
+ function install(args) {
453
+ const manifestPath = getManifestPath(args.browser);
454
+ const manifestDir = dirname(manifestPath);
455
+ const hostPath = copyHostScript(getHostInstallDir());
456
+ const wrapperPath = createWrapper(hostPath);
457
+ const manifest = buildManifest(args, wrapperPath);
458
+
459
+ mkdirSync(manifestDir, { recursive: true });
460
+ writeFileSync(manifestPath, JSON.stringify(manifest, null, 2) + '\n');
461
+
462
+ if (platform() === 'win32') {
463
+ writeWindowsRegistry(args.browser, manifestPath);
464
+ }
465
+
466
+ console.log('\nInstalled native messaging host manifest:');
467
+ console.log(` ${manifestPath}\n`);
468
+ console.log(`Host script: ${hostPath}`);
469
+ console.log(`Wrapper: ${manifest.path}`);
470
+ console.log(`Host name: ${HOST_NAME}`);
471
+ console.log(`Browser: ${args.browser}`);
472
+ if (manifest.allowed_origins) console.log(`Origin: ${manifest.allowed_origins[0]}`);
473
+ if (manifest.allowed_extensions) console.log(`Extension: ${manifest.allowed_extensions[0]}`);
474
+ console.log('');
475
+ if (args.skipOfficecli) {
476
+ console.log('OfficeCLI install skipped by --skip-officecli.');
477
+ }
478
+ }
479
+
480
+ function status(args) {
481
+ const installDir = getHostInstallDir();
482
+ const hostPath = resolve(installDir, 'shell-mcp-host.mjs');
483
+ const wrapperPath = resolve(installDir, platform() === 'win32' ? 'shell-mcp-host.bat' : 'shell-mcp-host');
484
+ const manifestPath = getManifestPath(args.browser);
485
+ const manifest = readManifest(manifestPath);
486
+ const officeCli = findCompatibleOfficeCli();
487
+ const isReady = Boolean(manifest && existsSync(hostPath) && existsSync(manifest.path ?? wrapperPath));
488
+
489
+ console.log('DeepSeek++ Shell Native Host status');
490
+ console.log(`Browser: ${args.browser}`);
491
+ console.log(`Host name: ${HOST_NAME}`);
492
+ console.log(`Install dir: ${installDir}`);
493
+ console.log(`Host script: ${existsSync(hostPath) ? 'found' : 'missing'} (${hostPath})`);
494
+ console.log(`Wrapper: ${existsSync(wrapperPath) ? 'found' : 'missing'} (${wrapperPath})`);
495
+ console.log(`Manifest: ${manifest ? 'found' : 'missing'} (${manifestPath})`);
496
+ if (manifest) {
497
+ console.log(`Target path: ${manifest.path}`);
498
+ if (manifest.allowed_origins) console.log(`Origins: ${manifest.allowed_origins.join(', ')}`);
499
+ if (manifest.allowed_extensions) console.log(`Extensions: ${manifest.allowed_extensions.join(', ')}`);
500
+ }
501
+ if (platform() === 'win32') {
502
+ const regKey = getRegistryKey(args.browser);
503
+ if (regKey) console.log(`Registry: ${regKey}`);
504
+ }
505
+ console.log(`OfficeCLI: ${officeCli ? `compatible (${officeCli})` : 'missing or incompatible'}`);
506
+
507
+ if (!isReady) {
508
+ process.exitCode = 1;
509
+ }
510
+ }
511
+
512
+ function uninstall(args) {
513
+ const manifestPath = getManifestPath(args.browser);
514
+ rmSync(manifestPath, { force: true });
515
+ if (platform() === 'win32') {
516
+ removeWindowsRegistry(args.browser);
517
+ }
518
+ rmSync(getHostInstallDir(), { recursive: true, force: true });
519
+ console.log(`Removed Shell Native Host for ${args.browser}.`);
520
+ console.log('OfficeCLI was left in place because it may be shared by other tools.');
521
+ }
522
+
523
+ export async function main(argv = process.argv.slice(2)) {
524
+ const args = parseArgs(argv);
525
+ if (args.command === 'status') {
526
+ status(args);
527
+ return;
528
+ }
529
+ if (args.command === 'uninstall') {
530
+ uninstall(args);
531
+ return;
532
+ }
533
+
534
+ install(args);
535
+ if (!args.skipOfficecli) {
536
+ await ensureOfficeCliInstalled({ force: args.forceOfficecli });
537
+ }
538
+ console.log(`\nDone. Restart ${args.browser} to activate.`);
539
+ }
540
+
541
+ const isDirect = process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url);
542
+ if (isDirect) {
543
+ main().catch((err) => {
544
+ console.error(`\nInstall failed: ${err.message}`);
545
+ process.exit(1);
546
+ });
547
+ }
@@ -0,0 +1,358 @@
1
+ #!/usr/bin/env node
2
+ import { spawn } from 'node:child_process';
3
+ import { dirname, resolve } from 'node:path';
4
+ import { fileURLToPath } from 'node:url';
5
+ import { homedir, hostname, platform, arch } from 'node:os';
6
+ import { existsSync } from 'node:fs';
7
+
8
+ // Resolve package root from this script's location (native/ -> package root).
9
+ const __dirname = dirname(fileURLToPath(import.meta.url));
10
+ const PROJECT_ROOT = resolve(__dirname, '..');
11
+
12
+ // Ensure child processes can find node and project binaries via PATH.
13
+ // Chrome-launched native hosts inherit a minimal PATH that often excludes
14
+ // Homebrew/nvm/fnm directories, breaking #!/usr/bin/env node shebangs.
15
+ const nodeBinDir = dirname(process.execPath);
16
+ const localBinDirs = [
17
+ resolve(PROJECT_ROOT, 'node_modules', '.bin'),
18
+ resolve(PROJECT_ROOT, '..', '..', 'node_modules', '.bin'),
19
+ ].filter(existsSync);
20
+ const sep = platform() === 'win32' ? ';' : ':';
21
+ const currentPath = process.env.PATH || (platform() === 'win32' ? '' : '/usr/bin:/bin');
22
+ const localAppData = process.env.LOCALAPPDATA || resolve(homedir(), 'AppData', 'Local');
23
+ const userBinDirs = platform() === 'win32'
24
+ ? [resolve(localAppData, 'OfficeCLI')]
25
+ : [
26
+ resolve(homedir(), '.local', 'bin'),
27
+ '/opt/homebrew/bin',
28
+ '/usr/local/bin',
29
+ ];
30
+ 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);
33
+
34
+ const MCP_PROTOCOL_VERSION = '2025-06-18';
35
+ const DEFAULT_TIMEOUT_MS = 120_000;
36
+ const MAX_OUTPUT_BYTES = 128_000;
37
+ const DEFAULT_SHELL = platform() === 'win32' ? 'powershell.exe' : process.env.SHELL || '/bin/sh';
38
+
39
+ const TOOL_DEFINITIONS = [
40
+ {
41
+ name: 'shell_exec',
42
+ title: 'Execute Shell Command',
43
+ description: 'Execute a shell command on the local system. Returns stdout, stderr, and exit code.',
44
+ inputSchema: {
45
+ type: 'object',
46
+ properties: {
47
+ command: { type: 'string', description: 'The shell command to execute.' },
48
+ cwd: { type: 'string', description: 'Working directory. Defaults to user home.' },
49
+ env: { type: 'object', additionalProperties: { type: 'string' }, description: 'Additional environment variables to set.' },
50
+ timeout_ms: { type: 'integer', minimum: 1000, maximum: 600000, description: 'Timeout in milliseconds. Default 120000.' },
51
+ },
52
+ required: ['command'],
53
+ additionalProperties: false,
54
+ },
55
+ annotations: { operation: 'write', risk: 'high' },
56
+ },
57
+ {
58
+ name: 'shell_status',
59
+ title: 'Shell Host Status',
60
+ description: 'Report host health, platform, shell, current working directory, and Node.js version.',
61
+ inputSchema: { type: 'object', properties: {}, additionalProperties: false },
62
+ annotations: { operation: 'read', risk: 'low' },
63
+ },
64
+ ];
65
+
66
+ // --- Native messaging framing (4-byte LE length prefix) ---
67
+
68
+ let buffer = Buffer.alloc(0);
69
+ let messageResolve = null;
70
+ const messageQueue = [];
71
+
72
+ function onStdinData(chunk) {
73
+ buffer = Buffer.concat([buffer, chunk]);
74
+ drainBuffer();
75
+ }
76
+
77
+ function drainBuffer() {
78
+ while (true) {
79
+ if (buffer.length < 4) return;
80
+ const len = buffer.readUInt32LE(0);
81
+ if (len === 0 || len > 10 * 1024 * 1024) {
82
+ process.stderr.write(`[shell-mcp-host] Invalid message length: ${len}\n`);
83
+ process.exit(1);
84
+ }
85
+ if (buffer.length < 4 + len) return;
86
+ const json = buffer.subarray(4, 4 + len).toString('utf8');
87
+ buffer = buffer.subarray(4 + len);
88
+ try {
89
+ const msg = JSON.parse(json);
90
+ if (messageResolve) {
91
+ const r = messageResolve;
92
+ messageResolve = null;
93
+ r(msg);
94
+ } else {
95
+ messageQueue.push(msg);
96
+ }
97
+ } catch (err) {
98
+ process.stderr.write(`[shell-mcp-host] JSON parse error: ${err.message}\n`);
99
+ }
100
+ }
101
+ }
102
+
103
+ let stdinEnded = false;
104
+ const EOF = Symbol('EOF');
105
+
106
+ function readMessage() {
107
+ if (messageQueue.length > 0) return Promise.resolve(messageQueue.shift());
108
+ if (stdinEnded) return Promise.resolve(EOF);
109
+ return new Promise((resolve) => { messageResolve = resolve; });
110
+ }
111
+
112
+ process.stdin.on('data', onStdinData);
113
+ process.stdin.on('end', () => {
114
+ stdinEnded = true;
115
+ if (messageResolve) {
116
+ const r = messageResolve;
117
+ messageResolve = null;
118
+ r(EOF);
119
+ }
120
+ });
121
+ process.stdin.on('error', () => {
122
+ stdinEnded = true;
123
+ if (messageResolve) {
124
+ const r = messageResolve;
125
+ messageResolve = null;
126
+ r(EOF);
127
+ }
128
+ });
129
+
130
+ function writeNativeMessage(message) {
131
+ return new Promise((resolve) => {
132
+ const json = JSON.stringify(message);
133
+ const body = Buffer.from(json, 'utf8');
134
+ const header = Buffer.alloc(4);
135
+ header.writeUInt32LE(body.length, 0);
136
+ process.stdout.write(header);
137
+ process.stdout.write(body, resolve);
138
+ });
139
+ }
140
+
141
+ // --- JSON-RPC helpers ---
142
+
143
+ function jsonRpcResult(id, result) {
144
+ return { jsonrpc: '2.0', id, result };
145
+ }
146
+
147
+ function jsonRpcError(id, code, message, data) {
148
+ return { jsonrpc: '2.0', id, error: { code, message, ...(data ? { data } : {}) } };
149
+ }
150
+
151
+ // --- Request handlers ---
152
+
153
+ function handleInitialize(id) {
154
+ return jsonRpcResult(id, {
155
+ protocolVersion: MCP_PROTOCOL_VERSION,
156
+ capabilities: { tools: {} },
157
+ serverInfo: { name: 'deepseek-pp-shell', version: '1.0.0' },
158
+ instructions: 'General-purpose shell execution host. Use shell_exec to run any command on the local system.',
159
+ });
160
+ }
161
+
162
+ function handleListTools(id) {
163
+ return jsonRpcResult(id, { tools: TOOL_DEFINITIONS });
164
+ }
165
+
166
+ async function handleCallTool(id, params) {
167
+ const name = params?.name;
168
+ const args = params?.arguments ?? {};
169
+
170
+ if (name === 'shell_status') {
171
+ return jsonRpcResult(id, {
172
+ content: [{ type: 'text', text: `Shell host ready on ${platform()} ${arch()}` }],
173
+ structuredContent: {
174
+ ok: true,
175
+ data: {
176
+ platform: platform(),
177
+ arch: arch(),
178
+ shell: DEFAULT_SHELL,
179
+ cwd: homedir(),
180
+ nodeVersion: process.version,
181
+ hostname: hostname(),
182
+ },
183
+ },
184
+ });
185
+ }
186
+
187
+ if (name === 'shell_exec') {
188
+ const command = args.command;
189
+ if (typeof command !== 'string' || command.trim().length === 0) {
190
+ return jsonRpcResult(id, {
191
+ isError: true,
192
+ content: [{ type: 'text', text: 'command is required and must be a non-empty string.' }],
193
+ });
194
+ }
195
+
196
+ 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;
198
+ const timeoutMs = typeof args.timeout_ms === 'number' && args.timeout_ms >= 1000
199
+ ? Math.min(args.timeout_ms, 600_000)
200
+ : DEFAULT_TIMEOUT_MS;
201
+
202
+ try {
203
+ const result = await execCommand(command, { cwd, env, timeoutMs });
204
+ return jsonRpcResult(id, {
205
+ content: [{ type: 'text', text: formatExecSummary(result) }],
206
+ structuredContent: {
207
+ ok: result.exitCode === 0,
208
+ data: result,
209
+ },
210
+ isError: result.exitCode !== 0,
211
+ });
212
+ } catch (err) {
213
+ return jsonRpcResult(id, {
214
+ isError: true,
215
+ content: [{ type: 'text', text: err.message }],
216
+ });
217
+ }
218
+ }
219
+
220
+ return jsonRpcError(id, -32602, `Unknown tool: ${name}`);
221
+ }
222
+
223
+ // --- Shell execution ---
224
+
225
+ function execCommand(command, { cwd, env, timeoutMs }) {
226
+ 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;
230
+
231
+ const child = spawn(shellBin, shellArgs, {
232
+ cwd,
233
+ env,
234
+ shell: false,
235
+ stdio: ['ignore', 'pipe', 'pipe'],
236
+ windowsHide: true,
237
+ });
238
+
239
+ const stdout = [];
240
+ const stderr = [];
241
+ let stdoutBytes = 0;
242
+ let stderrBytes = 0;
243
+ let timedOut = false;
244
+
245
+ const timer = setTimeout(() => {
246
+ timedOut = true;
247
+ child.kill('SIGTERM');
248
+ setTimeout(() => child.kill('SIGKILL'), 3000);
249
+ }, timeoutMs);
250
+
251
+ child.stdout.on('data', (chunk) => {
252
+ if (stdoutBytes < MAX_OUTPUT_BYTES) {
253
+ const remaining = MAX_OUTPUT_BYTES - stdoutBytes;
254
+ stdout.push(chunk.length <= remaining ? chunk : chunk.subarray(0, remaining));
255
+ }
256
+ stdoutBytes += chunk.length;
257
+ });
258
+
259
+ child.stderr.on('data', (chunk) => {
260
+ if (stderrBytes < MAX_OUTPUT_BYTES) {
261
+ const remaining = MAX_OUTPUT_BYTES - stderrBytes;
262
+ stderr.push(chunk.length <= remaining ? chunk : chunk.subarray(0, remaining));
263
+ }
264
+ stderrBytes += chunk.length;
265
+ });
266
+
267
+ child.on('error', (err) => {
268
+ clearTimeout(timer);
269
+ reject(new Error(`Failed to spawn command: ${err.message}`));
270
+ });
271
+
272
+ child.on('close', (exitCode, signal) => {
273
+ clearTimeout(timer);
274
+ resolve({
275
+ command,
276
+ exitCode: timedOut ? -1 : (exitCode ?? -1),
277
+ signal: signal || (timedOut ? 'SIGTERM' : null),
278
+ stdout: Buffer.concat(stdout).toString('utf8'),
279
+ stderr: Buffer.concat(stderr).toString('utf8'),
280
+ truncated: stdoutBytes > MAX_OUTPUT_BYTES || stderrBytes > MAX_OUTPUT_BYTES,
281
+ timedOut,
282
+ });
283
+ });
284
+ });
285
+ }
286
+
287
+ function formatExecSummary(result) {
288
+ const parts = [];
289
+ if (result.timedOut) parts.push('[TIMED OUT]');
290
+ if (result.exitCode !== 0) parts.push(`[exit ${result.exitCode}]`);
291
+ if (result.truncated) parts.push('[output truncated]');
292
+ if (result.stdout) parts.push(result.stdout.slice(0, 4000));
293
+ if (result.stderr) parts.push(`STDERR: ${result.stderr.slice(0, 2000)}`);
294
+ return parts.join('\n') || '(no output)';
295
+ }
296
+
297
+ // --- Message dispatch ---
298
+
299
+ async function handleMessage(envelope) {
300
+ if (envelope.protocol !== 'deepseek-pp-mcp-native' || envelope.version !== 1) {
301
+ await writeNativeMessage(jsonRpcError(null, -32600, 'Invalid envelope: expected deepseek-pp-mcp-native v1'));
302
+ return;
303
+ }
304
+
305
+ const message = envelope.message;
306
+ if (!message || typeof message !== 'object' || message.jsonrpc !== '2.0' || typeof message.method !== 'string') {
307
+ await writeNativeMessage(jsonRpcError(null, -32600, 'Invalid JSON-RPC request.'));
308
+ return;
309
+ }
310
+
311
+ const id = message.id ?? null;
312
+
313
+ if (!('id' in message)) {
314
+ return;
315
+ }
316
+
317
+ let response;
318
+ switch (message.method) {
319
+ case 'initialize':
320
+ response = handleInitialize(id);
321
+ break;
322
+ case 'tools/list':
323
+ response = handleListTools(id);
324
+ break;
325
+ case 'tools/call':
326
+ response = await handleCallTool(id, message.params);
327
+ break;
328
+ default:
329
+ response = jsonRpcError(id, -32601, `Unsupported method: ${message.method}`);
330
+ }
331
+
332
+ await writeNativeMessage(response);
333
+ }
334
+
335
+ // --- Persistent main loop ---
336
+
337
+ async function main() {
338
+ while (true) {
339
+ let envelope;
340
+ try {
341
+ envelope = await readMessage();
342
+ } catch {
343
+ break;
344
+ }
345
+ if (envelope === EOF) break;
346
+ try {
347
+ await handleMessage(envelope);
348
+ } catch (err) {
349
+ process.stderr.write(`[shell-mcp-host] Error: ${err.message || err}\n`);
350
+ await writeNativeMessage(jsonRpcError(null, -32603, err.message || 'Internal error'));
351
+ }
352
+ }
353
+ }
354
+
355
+ main().catch((err) => {
356
+ process.stderr.write(`[shell-mcp-host] Fatal: ${err.message || err}\n`);
357
+ process.exit(1);
358
+ });
package/package.json ADDED
@@ -0,0 +1,22 @@
1
+ {
2
+ "name": "deepseek-pp-shell-host",
3
+ "version": "0.4.4",
4
+ "description": "Native Messaging Shell MCP host installer for DeepSeek++",
5
+ "type": "module",
6
+ "private": false,
7
+ "bin": {
8
+ "deepseek-pp-shell-host": "bin/deepseek-pp-shell-host.mjs"
9
+ },
10
+ "files": [
11
+ "bin/",
12
+ "lib/",
13
+ "native/",
14
+ "README.md"
15
+ ],
16
+ "engines": {
17
+ "node": ">=18.17"
18
+ },
19
+ "publishConfig": {
20
+ "access": "public"
21
+ }
22
+ }