hupilot-shell-host 1.0.0

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.
@@ -0,0 +1,39 @@
1
+ #!/usr/bin/env node
2
+ import { parseArgs, install, status, uninstall } from '../lib/installer.mjs';
3
+ import { COMMANDS } from '../lib/constants.mjs';
4
+
5
+ const args = process.argv.slice(2);
6
+
7
+ if (args.length === 0 || args.includes('--help') || args.includes('-h')) {
8
+ console.log('Hupilot Shell Native Host\n');
9
+ console.log('Usage:');
10
+ console.log(' hupilot-shell-host install --browser edge --extension-id <id>');
11
+ console.log(' hupilot-shell-host status --browser edge');
12
+ console.log(' hupilot-shell-host uninstall --browser edge\n');
13
+ console.log('Commands:');
14
+ console.log(' install Install the Shell Native Host');
15
+ console.log(' status Show installation status');
16
+ console.log(' uninstall Remove the Shell Native Host\n');
17
+ console.log('Options:');
18
+ console.log(' --extension-id <id> Extension ID for Chrome/Edge/Chromium');
19
+ console.log(' --browser <name> chrome, chromium, edge, or firefox (default: chrome)');
20
+ process.exit(0);
21
+ }
22
+
23
+ const command = COMMANDS.has(args[0]) ? args[0] : 'install';
24
+ const opts = parseArgs(args);
25
+
26
+ switch (opts.command) {
27
+ case 'install':
28
+ install(args);
29
+ break;
30
+ case 'status':
31
+ status(opts);
32
+ break;
33
+ case 'uninstall':
34
+ uninstall(opts);
35
+ break;
36
+ default:
37
+ console.error('Unknown command: ' + opts.command);
38
+ process.exit(1);
39
+ }
@@ -0,0 +1,11 @@
1
+ export const HOST_NAME = 'com.hupilot.shell';
2
+ export const MCP_PROTOCOL_VERSION = '2025-06-18';
3
+ export const DEFAULT_TIMEOUT_MS = 120_000;
4
+ export const MAX_OUTPUT_BYTES = 128_000;
5
+ export const DEFAULT_PYTHON_TIMEOUT_MS = 10_000;
6
+ export const MAX_PYTHON_TIMEOUT_MS = 30_000;
7
+ export const MAX_PYTHON_CODE_BYTES = 60_000;
8
+ export const MAX_PYTHON_OUTPUT_BYTES = 64_000;
9
+ export const SUPPORTED_BROWSERS = new Set(['chrome', 'chromium', 'edge', 'firefox']);
10
+ export const COMMANDS = new Set(['install', 'status', 'uninstall']);
11
+ export const DEFAULT_EXTENSION_ID = 'kgpeoblpookpclfcoicagocelngcaohe';
@@ -0,0 +1,267 @@
1
+ import { chmodSync, copyFileSync, existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
2
+ import { execFileSync, execSync } from 'node:child_process';
3
+ import { dirname, resolve } from 'node:path';
4
+ import { homedir, platform } from 'node:os';
5
+ import { fileURLToPath } from 'node:url';
6
+ import { HOST_NAME, SUPPORTED_BROWSERS, COMMANDS, DEFAULT_EXTENSION_ID } from './constants.mjs';
7
+
8
+ const __dirname = dirname(fileURLToPath(import.meta.url));
9
+ const PACKAGE_ROOT = resolve(__dirname, '..');
10
+ const HOST_SOURCE = resolve(PACKAGE_ROOT, 'native', 'hupilot-shell-host.mjs');
11
+
12
+ function parseArgs(argv) {
13
+ const args = { command: 'install', extensionId: null, browser: 'chrome' };
14
+ const tokens = [...argv];
15
+
16
+ if (tokens[0] && COMMANDS.has(tokens[0])) {
17
+ args.command = tokens.shift();
18
+ }
19
+
20
+ for (let i = 0; i < tokens.length; i++) {
21
+ const token = tokens[i];
22
+ if (token === '--extension-id' && tokens[i + 1]) args.extensionId = tokens[++i];
23
+ else if (token === '--browser' && tokens[i + 1]) args.browser = tokens[++i].toLowerCase();
24
+ else if (token === '--help' || token === '-h') { printHelp(); process.exit(0); }
25
+ else { throw new Error('Unknown option: ' + token); }
26
+ }
27
+
28
+ if (!SUPPORTED_BROWSERS.has(args.browser)) {
29
+ throw new Error('Unsupported browser: ' + args.browser);
30
+ }
31
+
32
+ return args;
33
+ }
34
+
35
+ function printHelp() {
36
+ console.log('Hupilot Shell Native Host installer\n');
37
+ console.log('Usage:');
38
+ console.log(' hupilot-shell-host install --browser chrome --extension-id <extension-id>');
39
+ console.log(' hupilot-shell-host status --browser chrome');
40
+ console.log(' hupilot-shell-host uninstall --browser chrome\n');
41
+ console.log('Commands:');
42
+ console.log(' install Install the Shell Native Host');
43
+ console.log(' status Show installation status');
44
+ console.log(' uninstall Remove the Shell Native Host\n');
45
+ console.log('Options:');
46
+ console.log(' --extension-id <id> Extension ID for the target browser');
47
+ console.log(' --browser <name> Target browser: chrome, chromium, edge, firefox (default: chrome)');
48
+ console.log(' --help Show this help\n');
49
+ console.log('Examples:');
50
+ console.log(' npx hupilot-shell-host install --browser edge --extension-id ' + DEFAULT_EXTENSION_ID);
51
+ }
52
+
53
+ function getAppDataRoot() {
54
+ const home = homedir();
55
+ if (platform() === 'darwin') return home + '/Library/Application Support/Hupilot';
56
+ if (platform() === 'linux') return home + '/.local/share/hupilot';
57
+ if (platform() === 'win32') {
58
+ const localAppData = process.env.LOCALAPPDATA || resolve(home, 'AppData', 'Local');
59
+ return resolve(localAppData, 'Hupilot');
60
+ }
61
+ throw new Error('Unsupported platform: ' + platform());
62
+ }
63
+
64
+ function getHostInstallDir() {
65
+ const root = getAppDataRoot();
66
+ return platform() === 'linux' ? resolve(root, 'native-host') : resolve(root, 'NativeHost');
67
+ }
68
+
69
+ function getManifestDir(browser) {
70
+ const home = homedir();
71
+ const os = platform();
72
+
73
+ if (os === 'darwin') {
74
+ const map = { chrome: 'Google/Chrome', chromium: 'Chromium', edge: 'Microsoft Edge', firefox: 'Mozilla' };
75
+ return home + '/Library/Application Support/' + (map[browser] || 'Google/Chrome') + '/NativeMessagingHosts';
76
+ }
77
+ if (os === 'linux') {
78
+ const map = { chrome: 'google-chrome', chromium: 'chromium', edge: 'microsoft-edge', firefox: 'mozilla' };
79
+ return home + '/.config/' + (map[browser] || 'google-chrome') + '/NativeMessagingHosts';
80
+ }
81
+ if (os === 'win32') {
82
+ return resolve(getAppDataRoot(), 'NativeMessagingHosts');
83
+ }
84
+ throw new Error('Unsupported platform: ' + os);
85
+ }
86
+
87
+ function getRegistryKey(browser) {
88
+ const map = { chrome: 'Google\\Chrome', edge: 'Microsoft\\Edge', chromium: 'Chromium' };
89
+ const key = map[browser];
90
+ if (!key) return null;
91
+ return 'HKCU\\Software\\' + key + '\\NativeMessagingHosts\\' + HOST_NAME;
92
+ }
93
+
94
+ function buildManifest(args, wrapperPath) {
95
+ const manifest = {
96
+ name: HOST_NAME,
97
+ description: 'Hupilot Shell Native Host - local command execution and file access',
98
+ path: wrapperPath,
99
+ type: 'stdio',
100
+ };
101
+ if (args.browser === 'firefox') {
102
+ manifest.allowed_extensions = [args.extensionId || 'hupilot@firefox'];
103
+ } else {
104
+ const extId = args.extensionId || DEFAULT_EXTENSION_ID;
105
+ manifest.allowed_origins = ['chrome-extension://' + extId + '/'];
106
+ }
107
+ return manifest;
108
+ }
109
+
110
+ function copyHostScript(installDir) {
111
+ const hostPath = resolve(installDir, 'hupilot-shell-host.mjs');
112
+ mkdirSync(installDir, { recursive: true });
113
+ copyFileSync(HOST_SOURCE, hostPath);
114
+ if (platform() !== 'win32') chmodSync(hostPath, 0o755);
115
+ return hostPath;
116
+ }
117
+
118
+ function createWrapper(hostPath) {
119
+ const installDir = dirname(hostPath);
120
+ const nodePath = process.execPath;
121
+
122
+ if (platform() === 'win32') {
123
+ const wrapperPath = resolve(installDir, 'hupilot-shell-host.bat');
124
+ const content = '@echo off\r\n"' + nodePath + '" "' + hostPath + '" %*\r\n';
125
+ writeFileSync(wrapperPath, content);
126
+ return wrapperPath;
127
+ }
128
+
129
+ const wrapperPath = resolve(installDir, 'hupilot-shell-host');
130
+ const content = '#!/bin/sh\nexec "' + nodePath + '" "' + hostPath + '" "$@"\n';
131
+ writeFileSync(wrapperPath, content, { mode: 0o755 });
132
+ return wrapperPath;
133
+ }
134
+
135
+ function writeWindowsRegistry(browser, manifestPath) {
136
+ const regKey = getRegistryKey(browser);
137
+ if (!regKey) return;
138
+ try {
139
+ execSync('reg add "' + regKey + '" /ve /t REG_SZ /d "' + manifestPath + '" /f', { stdio: 'pipe' });
140
+ console.log('Registry: ' + regKey);
141
+ } catch {
142
+ console.error('Warning: Failed to write registry key. Try running as Administrator.');
143
+ console.error(' Manual: reg add "' + regKey + '" /ve /t REG_SZ /d "' + manifestPath + '" /f');
144
+ }
145
+ }
146
+
147
+ function removeWindowsRegistry(browser) {
148
+ const regKey = getRegistryKey(browser);
149
+ if (!regKey) return;
150
+ try {
151
+ execSync('reg delete "' + regKey + '" /f', { stdio: 'pipe' });
152
+ console.log('Removed registry key: ' + regKey);
153
+ } catch {
154
+ console.log('Registry key not found or already removed: ' + regKey);
155
+ }
156
+ }
157
+
158
+ function writeManifest(args, wrapperPath) {
159
+ const manifestDir = getManifestDir(args.browser);
160
+ const manifestPath = resolve(manifestDir, HOST_NAME + '.json');
161
+ mkdirSync(manifestDir, { recursive: true });
162
+
163
+ const manifest = buildManifest(args, wrapperPath);
164
+ writeFileSync(manifestPath, JSON.stringify(manifest, null, 2), 'utf8');
165
+ console.log('Manifest: ' + manifestPath);
166
+ return manifestPath;
167
+ }
168
+
169
+ function removeManifest(browser) {
170
+ const manifestDir = getManifestDir(browser);
171
+ const manifestPath = resolve(manifestDir, HOST_NAME + '.json');
172
+ try {
173
+ rmSync(manifestPath, { force: true });
174
+ console.log('Removed manifest: ' + manifestPath);
175
+ } catch {
176
+ console.log('Manifest not found: ' + manifestPath);
177
+ }
178
+ }
179
+
180
+ function getHostStatus(browser) {
181
+ const installDir = getHostInstallDir();
182
+ const hostPath = resolve(installDir, 'hupilot-shell-host.mjs');
183
+ const manifestDir = getManifestDir(browser);
184
+ const manifestPath = resolve(manifestDir, HOST_NAME + '.json');
185
+
186
+ console.log('Platform: ' + platform());
187
+ console.log('');
188
+ console.log('Host script:');
189
+ console.log(' Path: ' + hostPath);
190
+ console.log(' Installed: ' + (existsSync(hostPath) ? 'Yes' : 'No'));
191
+ console.log('');
192
+ console.log('Native Messaging manifest:');
193
+ console.log(' Path: ' + manifestPath);
194
+ if (existsSync(manifestPath)) {
195
+ try {
196
+ const content = JSON.parse(readFileSync(manifestPath, 'utf8'));
197
+ console.log(' Name: ' + content.name);
198
+ console.log(' Path: ' + content.path);
199
+ console.log(' Allowed: ' + JSON.stringify(content.allowed_origins || content.allowed_extensions || []));
200
+ } catch (e) {
201
+ console.log(' (invalid JSON)');
202
+ }
203
+ } else {
204
+ console.log(' Installed: No');
205
+ }
206
+
207
+ if (platform() === 'win32') {
208
+ const regKey = getRegistryKey(browser);
209
+ console.log('');
210
+ console.log('Registry:');
211
+ if (regKey) {
212
+ try {
213
+ const out = execSync('reg query "' + regKey + '" /ve', { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] });
214
+ console.log(' Key: ' + regKey);
215
+ console.log(' ' + out.trim().split('\n').pop());
216
+ } catch {
217
+ console.log(' Not found: ' + regKey);
218
+ }
219
+ }
220
+ }
221
+ }
222
+
223
+ export { parseArgs };
224
+
225
+ export function install(args) {
226
+ console.log('Installing Hupilot Shell Native Host...');
227
+ args = parseArgs(args);
228
+
229
+ const installDir = getHostInstallDir();
230
+ const hostPath = copyHostScript(installDir);
231
+ console.log('Copied host script to: ' + hostPath);
232
+
233
+ const wrapperPath = createWrapper(hostPath);
234
+ console.log('Created wrapper: ' + wrapperPath);
235
+
236
+ const manifestPath = writeManifest(args, wrapperPath);
237
+
238
+ if (platform() === 'win32') {
239
+ writeWindowsRegistry(args.browser, manifestPath);
240
+ }
241
+
242
+ console.log('');
243
+ console.log('Installation complete! Restart your browser to activate.');
244
+ console.log('Then in Hupilot settings, click "Test Connection" to verify.');
245
+ }
246
+
247
+ export function status(args) {
248
+ getHostStatus(args.browser || 'chrome');
249
+ }
250
+
251
+ export function uninstall(args) {
252
+ const browser = args.browser || 'chrome';
253
+ console.log('Uninstalling Hupilot Shell Native Host...');
254
+
255
+ removeManifest(browser);
256
+ if (platform() === 'win32') removeWindowsRegistry(browser);
257
+
258
+ const installDir = getHostInstallDir();
259
+ try {
260
+ rmSync(installDir, { recursive: true, force: true });
261
+ console.log('Removed: ' + installDir);
262
+ } catch {
263
+ console.log('Not found: ' + installDir);
264
+ }
265
+
266
+ console.log('Uninstall complete.');
267
+ }
@@ -0,0 +1,426 @@
1
+ import { spawn, execFileSync } from 'node:child_process';
2
+ import { randomUUID } from 'node:crypto';
3
+ import { homedir, platform, tmpdir, hostname, arch, release as osRelease, type as osType, version as osVersion } from 'node:os';
4
+ import { mkdtempSync, rmSync, writeFileSync } from 'node:fs';
5
+ import { join } from 'node:path';
6
+
7
+ const MCP_PROTOCOL_VERSION = '2025-06-18';
8
+ const DEFAULT_TIMEOUT_MS = 120000;
9
+ const MAX_OUTPUT_BYTES = 128000;
10
+ const DEFAULT_PYTHON_TIMEOUT_MS = 10000;
11
+ const MAX_PYTHON_TIMEOUT_MS = 30000;
12
+ const MAX_OUTPUT_CHARS = 50000;
13
+
14
+ const TOOL_DEFINITIONS = [
15
+ {
16
+ name: 'shell_status',
17
+ description: 'Report host health, platform, shell, and environment information.',
18
+ inputSchema: { type: 'object', properties: {}, additionalProperties: false },
19
+ },
20
+ {
21
+ name: 'shell_exec',
22
+ description: 'Execute a command in the system shell and return stdout, stderr, and exit code.',
23
+ inputSchema: {
24
+ type: 'object',
25
+ properties: {
26
+ command: { type: 'string', description: 'The shell command to execute.' },
27
+ cwd: { type: 'string', description: 'Working directory. Defaults to user home.' },
28
+ timeout_ms: { type: 'integer', minimum: 1000, maximum: 600000, description: 'Timeout in ms. Default 120000.' },
29
+ },
30
+ required: ['command'],
31
+ additionalProperties: false,
32
+ },
33
+ },
34
+ {
35
+ name: 'python_exec',
36
+ description: 'Execute short Python code for calculation, data processing, and analysis.',
37
+ inputSchema: {
38
+ type: 'object',
39
+ properties: {
40
+ code: { type: 'string', description: 'Python code to execute.' },
41
+ timeout_ms: { type: 'integer', minimum: 1000, maximum: 30000, description: 'Timeout in ms. Default 10000.' },
42
+ },
43
+ required: ['code'],
44
+ additionalProperties: false,
45
+ },
46
+ },
47
+ {
48
+ name: 'local_folder_pick',
49
+ description: 'Open the operating system folder picker and return the selected absolute path.',
50
+ inputSchema: {
51
+ type: 'object',
52
+ properties: {
53
+ title: { type: 'string', description: 'Optional prompt shown in the native folder picker.' },
54
+ },
55
+ additionalProperties: false,
56
+ },
57
+ },
58
+ ];
59
+
60
+ let buffer = Buffer.alloc(0);
61
+ let messageResolve = null;
62
+ const messageQueue = [];
63
+
64
+ function onStdinData(chunk) {
65
+ buffer = Buffer.concat([buffer, chunk]);
66
+ drainBuffer();
67
+ }
68
+
69
+ function drainBuffer() {
70
+ while (true) {
71
+ if (buffer.length < 4) return;
72
+ const len = buffer.readUInt32LE(0);
73
+ if (len === 0 || len > 10 * 1024 * 1024) {
74
+ process.exit(1);
75
+ }
76
+ if (buffer.length < 4 + len) return;
77
+ const json = buffer.subarray(4, 4 + len).toString('utf8');
78
+ buffer = buffer.subarray(4 + len);
79
+ try {
80
+ const msg = JSON.parse(json);
81
+ if (messageResolve) {
82
+ const r = messageResolve;
83
+ messageResolve = null;
84
+ r(msg);
85
+ } else {
86
+ messageQueue.push(msg);
87
+ }
88
+ } catch (err) {
89
+ process.stderr.write('[hupilot-shell] JSON parse error: ' + err.message + '\n');
90
+ }
91
+ }
92
+ }
93
+
94
+ let stdinEnded = false;
95
+ const EOF = Symbol('EOF');
96
+
97
+ function readMessage() {
98
+ if (messageQueue.length > 0) return Promise.resolve(messageQueue.shift());
99
+ if (stdinEnded) return Promise.resolve(EOF);
100
+ return new Promise((resolve) => { messageResolve = resolve; });
101
+ }
102
+
103
+ process.stdin.on('data', onStdinData);
104
+ process.stdin.on('end', () => {
105
+ stdinEnded = true;
106
+ if (messageResolve) { messageResolve(EOF); messageResolve = null; }
107
+ });
108
+ process.stdin.on('error', () => {
109
+ stdinEnded = true;
110
+ if (messageResolve) { messageResolve(EOF); messageResolve = null; }
111
+ });
112
+
113
+ function writeMessage(message) {
114
+ return new Promise((resolve) => {
115
+ const json = JSON.stringify(message);
116
+ const body = Buffer.from(json, 'utf8');
117
+ const header = Buffer.alloc(4);
118
+ header.writeUInt32LE(body.length, 0);
119
+ process.stdout.write(header);
120
+ process.stdout.write(body, resolve);
121
+ });
122
+ }
123
+
124
+ function jsonRpcResult(id, result) {
125
+ return { jsonrpc: '2.0', id, result };
126
+ }
127
+
128
+ function jsonRpcError(id, code, message, data) {
129
+ return { jsonrpc: '2.0', id, error: { code, message, ...(data ? { data } : {}) } };
130
+ }
131
+
132
+ function handleInitialize(id) {
133
+ return jsonRpcResult(id, {
134
+ protocolVersion: MCP_PROTOCOL_VERSION,
135
+ capabilities: { tools: {} },
136
+ serverInfo: { name: 'hupilot-shell', version: '1.0.0' },
137
+ instructions: 'Hupilot shell execution host. Use shell_exec for local commands, python_exec for Python code, local_folder_pick to select folders.',
138
+ });
139
+ }
140
+
141
+ function handleListTools(id) {
142
+ return jsonRpcResult(id, { tools: TOOL_DEFINITIONS });
143
+ }
144
+
145
+ async function handleCallTool(id, params) {
146
+ const name = params?.name;
147
+ const args = params?.arguments ?? {};
148
+
149
+ if (name === 'shell_status') {
150
+ return jsonRpcResult(id, {
151
+ content: [{ type: 'text', text: 'Shell host ready on ' + platform() + ' ' + arch() }],
152
+ });
153
+ }
154
+
155
+ if (name === 'shell_exec') {
156
+ const command = args.command;
157
+ if (typeof command !== 'string' || command.trim().length === 0) {
158
+ return jsonRpcResult(id, { isError: true, content: [{ type: 'text', text: 'command is required.' }] });
159
+ }
160
+ const cwd = typeof args.cwd === 'string' && args.cwd.trim() ? args.cwd.trim() : homedir();
161
+ const timeoutMs = typeof args.timeout_ms === 'number' && args.timeout_ms >= 1000
162
+ ? Math.min(args.timeout_ms, 600000) : DEFAULT_TIMEOUT_MS;
163
+ try {
164
+ const result = await execCommand(command, cwd, timeoutMs);
165
+ const output = formatOutput(result);
166
+ return jsonRpcResult(id, {
167
+ content: [{ type: 'text', text: output }],
168
+ isError: result.exitCode !== 0,
169
+ });
170
+ } catch (err) {
171
+ return jsonRpcResult(id, { isError: true, content: [{ type: 'text', text: err.message }] });
172
+ }
173
+ }
174
+
175
+ if (name === 'python_exec') {
176
+ const code = args.code;
177
+ if (typeof code !== 'string' || code.trim().length === 0) {
178
+ return jsonRpcResult(id, { isError: true, content: [{ type: 'text', text: 'code is required.' }] });
179
+ }
180
+ const timeoutMs = typeof args.timeout_ms === 'number' && args.timeout_ms >= 1000
181
+ ? Math.min(args.timeout_ms, MAX_PYTHON_TIMEOUT_MS) : DEFAULT_PYTHON_TIMEOUT_MS;
182
+ try {
183
+ const result = await execPython(code, timeoutMs);
184
+ return jsonRpcResult(id, {
185
+ content: [{ type: 'text', text: result.output }],
186
+ isError: result.exitCode !== 0,
187
+ });
188
+ } catch (err) {
189
+ return jsonRpcResult(id, { isError: true, content: [{ type: 'text', text: err.message }] });
190
+ }
191
+ }
192
+
193
+ if (name === 'local_folder_pick') {
194
+ try {
195
+ const selectedPath = pickLocalFolder(args.title || '选择文件夹');
196
+ return jsonRpcResult(id, {
197
+ content: [{ type: 'text', text: 'Selected folder: ' + selectedPath }],
198
+ });
199
+ } catch (err) {
200
+ return jsonRpcResult(id, { isError: true, content: [{ type: 'text', text: err.message }] });
201
+ }
202
+ }
203
+
204
+ return jsonRpcError(id, -32602, 'Unknown tool: ' + name);
205
+ }
206
+
207
+ function execCommand(command, cwd, timeoutMs) {
208
+ return new Promise((resolve, reject) => {
209
+ const isWin = platform() === 'win32';
210
+ const shellBin = isWin ? 'powershell.exe' : (process.env.SHELL || '/bin/sh');
211
+ const shellArgs = isWin
212
+ ? ['-NoLogo', '-NoProfile', '-NonInteractive', '-Command', command]
213
+ : ['-c', command];
214
+
215
+ const child = spawn(shellBin, shellArgs, {
216
+ cwd,
217
+ env: process.env,
218
+ shell: false,
219
+ stdio: ['ignore', 'pipe', 'pipe'],
220
+ windowsHide: true,
221
+ });
222
+
223
+ const stdout = [];
224
+ const stderr = [];
225
+ let stdoutBytes = 0;
226
+ let stderrBytes = 0;
227
+ let timedOut = false;
228
+
229
+ const timer = setTimeout(() => {
230
+ timedOut = true;
231
+ child.kill('SIGTERM');
232
+ setTimeout(() => child.kill('SIGKILL'), 3000);
233
+ }, timeoutMs);
234
+
235
+ child.stdout.on('data', (chunk) => {
236
+ if (stdoutBytes < MAX_OUTPUT_BYTES) {
237
+ const remaining = MAX_OUTPUT_BYTES - stdoutBytes;
238
+ stdout.push(chunk.length <= remaining ? chunk : chunk.subarray(0, remaining));
239
+ }
240
+ stdoutBytes += chunk.length;
241
+ });
242
+
243
+ child.stderr.on('data', (chunk) => {
244
+ if (stderrBytes < MAX_OUTPUT_BYTES) {
245
+ const remaining = MAX_OUTPUT_BYTES - stderrBytes;
246
+ stderr.push(chunk.length <= remaining ? chunk : chunk.subarray(0, remaining));
247
+ }
248
+ stderrBytes += chunk.length;
249
+ });
250
+
251
+ child.on('error', (err) => {
252
+ clearTimeout(timer);
253
+ reject(new Error('Failed to spawn command: ' + err.message));
254
+ });
255
+
256
+ child.on('close', (exitCode, signal) => {
257
+ clearTimeout(timer);
258
+ resolve({
259
+ command,
260
+ exitCode: timedOut ? -1 : (exitCode ?? -1),
261
+ signal: signal || (timedOut ? 'SIGTERM' : null),
262
+ stdout: Buffer.concat(stdout).toString('utf8'),
263
+ stderr: Buffer.concat(stderr).toString('utf8'),
264
+ truncated: stdoutBytes > MAX_OUTPUT_BYTES || stderrBytes > MAX_OUTPUT_BYTES,
265
+ timedOut,
266
+ });
267
+ });
268
+ });
269
+ }
270
+
271
+ function formatOutput(result) {
272
+ const parts = [];
273
+ if (result.exitCode === -1 && result.timedOut) {
274
+ parts.push('[TIMEOUT] Command exceeded time limit');
275
+ }
276
+ if (result.exitCode !== 0) parts.push('[exit ' + result.exitCode + ']');
277
+ if (result.truncated) parts.push('[output truncated]');
278
+ if (result.stdout) parts.push(result.stdout.substring(0, MAX_OUTPUT_CHARS));
279
+ if (result.stderr) parts.push('STDERR: ' + result.stderr.substring(0, 2000));
280
+ return parts.join('\n') || '(no output)';
281
+ }
282
+
283
+ function execPython(code, timeoutMs) {
284
+ return new Promise((resolve, reject) => {
285
+ const pythonCmd = findPython();
286
+ if (!pythonCmd) {
287
+ return reject(new Error('No local Python interpreter found. Install Python and ensure it is in PATH.'));
288
+ }
289
+ const tmpDir = mkdtempSync(join(tmpdir(), 'hupilot-python-'));
290
+ const scriptPath = join(tmpDir, 'script.py');
291
+ writeFileSync(scriptPath, code, 'utf8');
292
+
293
+ try {
294
+ const output = execFileSync(pythonCmd, [scriptPath], {
295
+ encoding: 'utf8',
296
+ timeout: timeoutMs,
297
+ maxBuffer: MAX_OUTPUT_BYTES,
298
+ windowsHide: true,
299
+ });
300
+ rmSync(tmpDir, { recursive: true, force: true });
301
+ resolve({ exitCode: 0, output: output || '(no output)' });
302
+ } catch (err) {
303
+ rmSync(tmpDir, { recursive: true, force: true });
304
+ if (err.status !== undefined) {
305
+ resolve({ exitCode: err.status, output: err.stdout || '' + (err.stderr ? '\nSTDERR:\n' + err.stderr : '') });
306
+ } else {
307
+ reject(new Error('Python execution failed: ' + err.message));
308
+ }
309
+ }
310
+ });
311
+ }
312
+
313
+ function findPython() {
314
+ const candidates = ['python3', 'python'];
315
+ for (const cmd of candidates) {
316
+ try {
317
+ execFileSync(cmd, ['--version'], { encoding: 'utf8', timeout: 3000, windowsHide: true });
318
+ return cmd;
319
+ } catch (e) {}
320
+ }
321
+ return null;
322
+ }
323
+
324
+ function pickLocalFolder(title) {
325
+ const os = platform();
326
+ if (os === 'win32') {
327
+ const script = [
328
+ 'Add-Type -AssemblyName System.Windows.Forms',
329
+ '$dialog = New-Object System.Windows.Forms.FolderBrowserDialog',
330
+ '$dialog.Description = \'' + title.replace(/'/g, "''") + '\'',
331
+ '$dialog.ShowNewFolderButton = $false',
332
+ 'if ($dialog.ShowDialog() -eq [System.Windows.Forms.DialogResult]::OK) {',
333
+ ' [Console]::Out.Write($dialog.SelectedPath)',
334
+ '} else {',
335
+ ' [Environment]::Exit(2)',
336
+ '}',
337
+ ].join('; ');
338
+ try {
339
+ return execFileSync('powershell.exe', ['-NoProfile', '-STA', '-Command', script], {
340
+ encoding: 'utf8',
341
+ timeout: 120000,
342
+ windowsHide: false,
343
+ }).trim();
344
+ } catch (err) {
345
+ if (err.status === 2) throw new Error('Folder selection was cancelled.');
346
+ throw new Error('Failed to open folder picker: ' + err.message);
347
+ }
348
+ }
349
+ if (os === 'darwin') {
350
+ try {
351
+ return execFileSync('osascript', ['-e', 'set chosenFolder to choose folder with prompt "' + title + '"' + "\nreturn POSIX path of chosenFolder"], {
352
+ encoding: 'utf8',
353
+ timeout: 120000,
354
+ }).trim();
355
+ } catch (err) {
356
+ throw new Error('Folder selection was cancelled.');
357
+ }
358
+ }
359
+ if (os === 'linux') {
360
+ try {
361
+ return execFileSync('zenity', ['--file-selection', '--directory', '--title', title], {
362
+ encoding: 'utf8',
363
+ timeout: 120000,
364
+ }).trim();
365
+ } catch (err) {
366
+ try {
367
+ return execFileSync('kdialog', ['--getexistingdirectory', homedir(), '--title', title], {
368
+ encoding: 'utf8',
369
+ timeout: 120000,
370
+ }).trim();
371
+ } catch (e2) {
372
+ throw new Error('Folder selection cancelled or picker unavailable. Install zenity or kdialog.');
373
+ }
374
+ }
375
+ }
376
+ throw new Error('Unsupported platform: ' + os);
377
+ }
378
+
379
+ async function handleMessage(request) {
380
+ if (!request || request.jsonrpc !== '2.0' || typeof request.method !== 'string') {
381
+ await writeMessage(jsonRpcError(null, -32600, 'Invalid JSON-RPC request.'));
382
+ return;
383
+ }
384
+
385
+ const id = request.id ?? null;
386
+ let response;
387
+
388
+ switch (request.method) {
389
+ case 'initialize':
390
+ response = handleInitialize(id);
391
+ break;
392
+ case 'tools/list':
393
+ response = handleListTools(id);
394
+ break;
395
+ case 'tools/call':
396
+ response = await handleCallTool(id, request.params);
397
+ break;
398
+ default:
399
+ response = jsonRpcError(id, -32601, 'Unsupported method: ' + request.method);
400
+ }
401
+
402
+ await writeMessage(response);
403
+ }
404
+
405
+ async function main() {
406
+ while (true) {
407
+ let request;
408
+ try {
409
+ request = await readMessage();
410
+ } catch {
411
+ break;
412
+ }
413
+ if (request === EOF) break;
414
+ try {
415
+ await handleMessage(request);
416
+ } catch (err) {
417
+ process.stderr.write('[hupilot-shell] Error: ' + (err.message || err) + '\n');
418
+ await writeMessage(jsonRpcError(null, -32603, err.message || 'Internal error'));
419
+ }
420
+ }
421
+ }
422
+
423
+ main().catch((err) => {
424
+ process.stderr.write('[hupilot-shell] Fatal: ' + (err.message || err) + '\n');
425
+ process.exit(1);
426
+ });
package/package.json ADDED
@@ -0,0 +1,22 @@
1
+ {
2
+ "name": "hupilot-shell-host",
3
+ "version": "1.0.0",
4
+ "description": "Native Messaging host for Hupilot - local command execution and file access",
5
+ "type": "module",
6
+ "private": false,
7
+ "bin": {
8
+ "hupilot-shell-host": "bin/hupilot-shell-host.mjs"
9
+ },
10
+ "files": [
11
+ "bin/",
12
+ "lib/",
13
+ "native/",
14
+ "README.md"
15
+ ],
16
+ "engines": {
17
+ "node": ">=18.0"
18
+ },
19
+ "publishConfig": {
20
+ "access": "public"
21
+ }
22
+ }