@hcu-lab.me/mcp-device 1.0.6 → 1.0.7

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.
Files changed (44) hide show
  1. package/README.md +7 -71
  2. package/dist/bootstrap.js +1 -1
  3. package/dist/config-manager.js +1 -1
  4. package/dist/config.js +1 -1
  5. package/dist/custom-stdio.js +1 -2
  6. package/dist/data/onboarding-prompts.json +22 -11
  7. package/dist/device/device-state.d.ts +0 -5
  8. package/dist/device/device-state.js +0 -84
  9. package/dist/device/device.d.ts +1 -1
  10. package/dist/device/device.js +8 -7
  11. package/dist/device/execution-engine.d.ts +10 -1
  12. package/dist/device/execution-engine.js +81 -128
  13. package/dist/device/gateway-channel.d.ts +6 -0
  14. package/dist/device/gateway-channel.js +16 -1
  15. package/dist/device/gateway-tool-adapter.d.ts +3 -3
  16. package/dist/device/gateway-tool-adapter.js +12 -11
  17. package/dist/device/linux-service.d.ts +4 -0
  18. package/dist/device/linux-service.js +10 -3
  19. package/dist/device/runtime-owner.js +2 -2
  20. package/dist/device/windows-service.d.ts +3 -4
  21. package/dist/device/windows-service.js +5 -44
  22. package/dist/npm-scripts/remote.js +0 -2
  23. package/dist/server.js +9 -99
  24. package/dist/terminal-manager.js +1 -1
  25. package/dist/tools/config.d.ts +1 -1
  26. package/dist/tools/filesystem.js +3 -3
  27. package/dist/tools/pdf/markdown.js +1 -1
  28. package/dist/tools/schemas.d.ts +2 -3
  29. package/dist/tools/schemas.js +0 -10
  30. package/dist/ui/config-editor/index.html +1 -1
  31. package/dist/ui/file-preview/index.html +1 -1
  32. package/dist/ui/file-preview/styles.css +2 -2
  33. package/dist/utils/capture.d.ts +5 -28
  34. package/dist/utils/capture.js +11 -455
  35. package/dist/utils/feature-flags.d.ts +0 -47
  36. package/dist/utils/feature-flags.js +5 -199
  37. package/dist/utils/files/docx.d.ts +2 -2
  38. package/dist/utils/files/docx.js +2 -2
  39. package/dist/utils/usageTracker.js +0 -24
  40. package/dist/version.d.ts +1 -1
  41. package/dist/version.js +1 -1
  42. package/package.json +5 -6
  43. package/dist/tools/feedback.d.ts +0 -8
  44. package/dist/tools/feedback.js +0 -132
@@ -1,4 +1,4 @@
1
- import { spawn } from 'child_process';
1
+ import { randomUUID } from 'crypto';
2
2
  import path from 'path';
3
3
  import fs from 'fs/promises';
4
4
  import { Client } from '@modelcontextprotocol/sdk/client/index.js';
@@ -8,134 +8,109 @@ import { captureRemote } from '../utils/capture.js';
8
8
  import { deviceStatePaths } from './device-state.js';
9
9
  const __filename = fileURLToPath(import.meta.url);
10
10
  const __dirname = path.dirname(__filename);
11
- export class DesktopCommanderIntegration {
11
+ export class LocalExecutionEngine {
12
12
  constructor() {
13
13
  this.mcpClient = null;
14
14
  this.mcpTransport = null;
15
15
  this.isReady = false;
16
+ this.runtimeReason = 'LOCAL_EXECUTION_ENGINE_UNAVAILABLE';
17
+ this.runtimeGeneration = null;
18
+ }
19
+ markUnavailable(client) {
20
+ if (client && this.mcpClient !== client)
21
+ return;
22
+ this.isReady = false;
23
+ this.runtimeReason = 'LOCAL_EXECUTION_ENGINE_UNAVAILABLE';
24
+ this.runtimeGeneration = null;
16
25
  }
17
26
  async initialize() {
18
- console.debug('[DEBUG] DesktopCommanderIntegration.initialize() called');
19
- const config = await this.resolveMcpConfig();
20
- if (!config) {
21
- console.debug('[DEBUG] No MCP config found');
22
- throw new Error('Desktop Commander MCP not found. Please install it globally via `npm install -g @wonderwhy-er/desktop-commander` or build the local project.');
23
- }
24
- console.log(` - ⏳ Connecting to Local Desktop Commander MCP using: ${config.command} ${config.args.join(' ')}`);
25
- console.debug('[DEBUG] MCP config:', JSON.stringify(config, null, 2));
27
+ console.debug('[DEBUG] LocalExecutionEngine.initialize() called');
28
+ this.markUnavailable();
26
29
  try {
27
- console.debug('[DEBUG] Creating StdioClientTransport');
28
- // MCP_DEVICE_REMOTE tells the spawned server it is serving remote
29
- // services, so it suppresses local-only behavior like opening the
30
- // welcome page in a browser the remote user would never see.
31
- this.mcpTransport = new StdioClientTransport({
30
+ const config = await this.resolveMcpConfig();
31
+ if (!config)
32
+ throw new Error('Bundled local execution engine is unavailable. Reinstall MCP Device.');
33
+ const transport = new StdioClientTransport({
32
34
  ...config,
33
35
  env: {
34
36
  ...getDefaultEnvironment(),
35
37
  ...config.env,
36
- MCP_DEVICE_REMOTE: 'true',
37
- DESKTOP_COMMANDER_DISABLE_TELEMETRY: 'true'
38
+ MCP_DEVICE_REMOTE: 'true'
38
39
  }
39
40
  });
40
- // Create MCP client
41
- console.debug('[DEBUG] Creating MCP Client');
42
- this.mcpClient = new Client({
43
- name: "desktop-commander-client",
44
- version: "1.0.0"
45
- }, {
46
- capabilities: {}
47
- });
48
- // Connect to Desktop Commander
49
- console.debug('[DEBUG] Connecting MCP client to transport');
50
- await this.mcpClient.connect(this.mcpTransport);
41
+ const client = new Client({ name: 'mcp-device-local-engine', version: '1.0.0' }, { capabilities: {} });
42
+ this.mcpTransport = transport;
43
+ this.mcpClient = client;
44
+ client.onclose = () => this.markUnavailable(client);
45
+ await client.connect(transport);
46
+ if (this.mcpClient !== client || this.mcpTransport !== transport || transport.pid === null) {
47
+ throw new Error('Local execution engine closed during startup.');
48
+ }
49
+ this.runtimeGeneration = randomUUID();
51
50
  this.isReady = true;
52
- console.log(' - 🔌 Connected to Desktop Commander MCP');
53
- console.debug('[DEBUG] Desktop Commander MCP connection successful');
51
+ this.runtimeReason = null;
52
+ console.log(' - Local execution engine ready');
54
53
  }
55
54
  catch (error) {
56
- console.error(' - ❌ Failed to connect to Desktop Commander MCP:', error);
57
- console.debug('[DEBUG] MCP connection error:', error);
58
- await captureRemote('desktop_integration_init_failed', { error });
59
- throw error;
55
+ console.error(' - Failed to start local execution engine:', error);
56
+ await captureRemote('local_engine_init_failed', { error });
57
+ try {
58
+ await this.mcpClient?.close();
59
+ }
60
+ catch { }
61
+ try {
62
+ await this.mcpTransport?.close();
63
+ }
64
+ catch { }
65
+ this.mcpClient = null;
66
+ this.mcpTransport = null;
67
+ this.markUnavailable();
60
68
  }
61
69
  }
62
70
  async resolveMcpConfig() {
63
- console.debug('[DEBUG] Resolving MCP config...');
64
- // Option 1: Development/Local Build
65
- // Adjusting path resolution since we are now in src/device and dist is in root/dist
66
- // Original: path.resolve(__dirname, '../../dist/index.js')
67
- const devPath = path.resolve(__dirname, '../../dist/index.js');
68
- console.debug('[DEBUG] Checking local dev path:', devPath);
71
+ const localEngine = path.resolve(__dirname, '../../dist/index.js');
69
72
  try {
70
- await fs.access(devPath);
71
- console.debug(' - 🔍 Found local MCP server at:', devPath);
72
- const runtimeDir = deviceStatePaths().runtime;
73
- await fs.mkdir(runtimeDir, { recursive: true, mode: 0o700 });
74
- return {
75
- command: process.execPath, // Use the current node executable
76
- args: [devPath],
77
- cwd: runtimeDir
78
- };
73
+ await fs.access(localEngine);
79
74
  }
80
75
  catch {
81
- console.debug('[DEBUG] Local dev path not found, trying global installation');
82
- // Local file not found, continue...
76
+ return null;
83
77
  }
84
- // Option 2: Global Installation
85
- const commandName = 'desktop-commander';
86
- console.debug('[DEBUG] Checking for global command:', commandName);
87
- try {
88
- await new Promise((resolve, reject) => {
89
- // Use platform-appropriate command to check if the command exists in PATH
90
- // We can't run it directly as it's an stdio MCP server that waits for input
91
- const whichCommand = process.platform === 'win32' ? 'where' : 'which';
92
- console.debug('[DEBUG] Using platform command:', whichCommand, 'on platform:', process.platform);
93
- const check = spawn(whichCommand, [commandName], { windowsHide: true }); // Prevent visible console windows on Windows
94
- check.on('error', (err) => {
95
- console.debug('[DEBUG] Spawn error for', whichCommand, ':', err.message);
96
- reject(err);
97
- });
98
- check.on('close', (code) => {
99
- console.debug('[DEBUG]', whichCommand, 'exited with code:', code);
100
- return code === 0 ? resolve() : reject(new Error('Command not found'));
101
- });
102
- });
103
- console.debug(' - Found global desktop-commander CLI');
104
- return {
105
- command: commandName,
106
- args: []
107
- };
108
- }
109
- catch (err) {
110
- console.debug('[DEBUG] Global command not found:', err);
111
- // Global command not found
112
- }
113
- console.debug('[DEBUG] No MCP config resolved');
114
- return null;
78
+ const runtimeDir = deviceStatePaths().runtime;
79
+ await fs.mkdir(runtimeDir, { recursive: true, mode: 0o700 });
80
+ return {
81
+ command: process.execPath,
82
+ args: [localEngine],
83
+ cwd: runtimeDir
84
+ };
115
85
  }
116
86
  getChildPid() {
117
87
  return this.mcpTransport?.pid ?? null;
118
88
  }
89
+ getRuntimeState() {
90
+ return {
91
+ runtime_ready: this.isReady,
92
+ runtime_reason: this.isReady ? null : this.runtimeReason,
93
+ execution_runtime_generation: this.isReady ? this.runtimeGeneration : null
94
+ };
95
+ }
96
+ assertReady() {
97
+ if (this.isReady && this.mcpClient)
98
+ return;
99
+ const error = new Error('Local execution engine is unavailable.');
100
+ error.code = 'DEVICE_NOT_READY';
101
+ throw error;
102
+ }
119
103
  async callClientTool(toolName, args, metadata) {
120
- if (!this.isReady || !this.mcpClient) {
121
- console.debug('[DEBUG] callClientTool() failed - not ready or no client');
122
- throw new Error('DesktopIntegration not initialized');
123
- }
124
- // Proxy other tools to MCP server
104
+ this.assertReady();
125
105
  try {
126
- console.debug('[DEBUG] Calling MCP tool:', toolName, 'args:', JSON.stringify(args).substring(0, 100));
127
- const result = await this.mcpClient.callTool({
106
+ return await this.mcpClient.callTool({
128
107
  name: toolName,
129
108
  arguments: args,
130
109
  _meta: { remote: true, ...metadata || {} }
131
110
  });
132
- console.debug('[DEBUG] Tool call successful:', toolName);
133
- return result;
134
111
  }
135
112
  catch (error) {
136
- console.error(`Error executing tool ${toolName}:`, error);
137
- console.debug('[DEBUG] Tool call error details:', error);
138
- await captureRemote('desktop_integration_tool_call_failed', { error, toolName });
113
+ await captureRemote('local_engine_tool_call_failed', { error, toolName });
139
114
  throw error;
140
115
  }
141
116
  }
@@ -143,59 +118,37 @@ export class DesktopCommanderIntegration {
143
118
  if (!this.mcpClient)
144
119
  return { tools: [] };
145
120
  try {
146
- // List tools from MCP server
147
121
  const mcpTools = await this.mcpClient.listTools();
148
- // Merge tools
149
- return {
150
- tools: mcpTools.tools || []
151
- };
122
+ return { tools: mcpTools.tools || [] };
152
123
  }
153
124
  catch (error) {
154
- console.error('Error fetching capabilities:', error);
155
- await captureRemote('desktop_integration_list_tools_failed', { error });
156
- // Fallback to local tools
157
- return {
158
- tools: []
159
- };
125
+ await captureRemote('local_engine_list_tools_failed', { error });
126
+ return { tools: [] };
160
127
  }
161
128
  }
162
129
  async shutdown() {
163
- console.debug('[DEBUG] DesktopCommanderIntegration.shutdown() called');
164
- const closeWithTimeout = async (operation, name, timeoutMs = 3000) => {
165
- return Promise.race([
166
- operation(),
167
- new Promise((_, reject) => setTimeout(() => reject(new Error(`${name} timeout after ${timeoutMs}ms`)), timeoutMs))
168
- ]);
169
- };
130
+ const closeWithTimeout = async (operation, name, timeoutMs = 3000) => Promise.race([
131
+ operation(),
132
+ new Promise((_, reject) => setTimeout(() => reject(new Error(`${name} timeout after ${timeoutMs}ms`)), timeoutMs))
133
+ ]);
170
134
  if (this.mcpClient) {
171
135
  try {
172
- console.log(' → Closing MCP client...');
173
- console.debug('[DEBUG] Calling mcpClient.close() with timeout');
174
136
  await closeWithTimeout(() => this.mcpClient.close(), 'MCP client close');
175
- console.log(' ✓ MCP client closed');
176
137
  }
177
- catch (e) {
178
- console.warn(' ⚠️ MCP client close timeout or error:', e.message);
179
- console.debug('[DEBUG] MCP client close error:', e);
180
- await captureRemote('desktop_integration_shutdown_error', { error: e, component: 'client' });
138
+ catch (error) {
139
+ await captureRemote('local_engine_shutdown_error', { error, component: 'client' });
181
140
  }
182
141
  this.mcpClient = null;
183
142
  }
184
143
  if (this.mcpTransport) {
185
144
  try {
186
- console.log(' → Closing MCP transport...');
187
- console.debug('[DEBUG] Calling mcpTransport.close() with timeout');
188
145
  await closeWithTimeout(() => this.mcpTransport.close(), 'MCP transport close');
189
- console.log(' ✓ MCP transport closed');
190
146
  }
191
- catch (e) {
192
- console.warn(' ⚠️ MCP transport close timeout or error:', e.message);
193
- console.debug('[DEBUG] MCP transport close error:', e);
194
- await captureRemote('desktop_integration_shutdown_error', { error: e, component: 'transport' });
147
+ catch (error) {
148
+ await captureRemote('local_engine_shutdown_error', { error, component: 'transport' });
195
149
  }
196
150
  this.mcpTransport = null;
197
151
  }
198
- this.isReady = false;
199
- console.debug('[DEBUG] Desktop Commander integration shutdown complete');
152
+ this.markUnavailable();
200
153
  }
201
154
  }
@@ -14,6 +14,11 @@ export interface GatewayChannelOptions {
14
14
  securityProtocolFloor?: number;
15
15
  appCaPem?: string;
16
16
  adapter: GatewayToolAdapter;
17
+ runtimeState?: () => {
18
+ runtime_ready: boolean;
19
+ runtime_reason: string | null;
20
+ execution_runtime_generation: string | null;
21
+ };
17
22
  agentVersion?: string;
18
23
  onStatus?: (payload: any) => void | Promise<void>;
19
24
  onUpdateRequest?: (targetVersion: string, context: {
@@ -40,6 +45,7 @@ export declare class GatewayDeviceChannel {
40
45
  private authMode;
41
46
  private updateInFlight?;
42
47
  constructor(options: GatewayChannelOptions);
48
+ private runtimePayload;
43
49
  start(): Promise<void>;
44
50
  private startV2Transport;
45
51
  stop(): Promise<void>;
@@ -19,6 +19,19 @@ export class GatewayDeviceChannel {
19
19
  this.authMode = 'reconnect';
20
20
  this.identity = options.identity || new GatewayDeviceIdentity();
21
21
  }
22
+ runtimePayload() {
23
+ if (!this.options.runtimeState)
24
+ return {};
25
+ const state = this.options.runtimeState();
26
+ const ready = state?.runtime_ready === true;
27
+ const reason = ready ? null : String(state?.runtime_reason || 'LOCAL_EXECUTION_ENGINE_UNAVAILABLE').slice(0, 96);
28
+ const generation = String(state?.execution_runtime_generation || '').trim().slice(0, 128) || null;
29
+ return {
30
+ runtime_ready: ready,
31
+ runtime_reason: reason,
32
+ execution_runtime_generation: generation
33
+ };
34
+ }
22
35
  async start() {
23
36
  this.authenticatedDeviceId = undefined;
24
37
  this.connectionEpoch = undefined;
@@ -150,6 +163,7 @@ export class GatewayDeviceChannel {
150
163
  arch: process.arch,
151
164
  path_style: process.platform === 'win32' ? 'windows' : 'posix',
152
165
  capabilities: [...GATEWAY_CAPABILITIES],
166
+ ...this.runtimePayload(),
153
167
  ...(enrolling || pairing ? { public_key_pem: record.publicKeyPem } : {})
154
168
  }
155
169
  });
@@ -173,6 +187,7 @@ export class GatewayDeviceChannel {
173
187
  arch: process.arch,
174
188
  path_style: process.platform === 'win32' ? 'windows' : 'posix',
175
189
  capabilities: [...GATEWAY_CAPABILITIES],
190
+ ...this.runtimePayload(),
176
191
  ...(pairing || enrolling ? { public_key_pem: record.publicKeyPem, enrollment_grant: grant } : {})
177
192
  }
178
193
  });
@@ -432,7 +447,7 @@ export class GatewayDeviceChannel {
432
447
  type: 'heartbeat',
433
448
  device_id: record.deviceId,
434
449
  timestamp: Date.now(),
435
- payload: {}
450
+ payload: this.runtimePayload()
436
451
  });
437
452
  }, HEARTBEAT_MS);
438
453
  this.heartbeat.unref?.();
@@ -1,15 +1,15 @@
1
- import { DesktopCommanderIntegration } from './execution-engine.js';
1
+ import { LocalExecutionEngine } from './execution-engine.js';
2
2
  export declare const GATEWAY_CAPABILITIES: readonly ["read_text_file", "write_file", "edit_file", "shell_execute", "start_process", "read_process_output", "interact_with_process", "terminate_process", "image_preview", "project_inspect"];
3
3
  export interface GatewayToolAdapterOptions {
4
4
  allowedRoots?: string[];
5
5
  pathValidator?: (requestedPath: string) => Promise<string>;
6
6
  }
7
7
  export declare class GatewayToolAdapter {
8
- private desktop;
8
+ private engine;
9
9
  private allowedRoots;
10
10
  private pathValidator;
11
11
  private canonicalRoots?;
12
- constructor(desktop: DesktopCommanderIntegration, options?: GatewayToolAdapterOptions);
12
+ constructor(engine: LocalExecutionEngine, options?: GatewayToolAdapterOptions);
13
13
  private guardPath;
14
14
  private imagePreview;
15
15
  call(tool: string, args?: any): Promise<any>;
@@ -63,7 +63,7 @@ function parsePid(result) {
63
63
  const match = textFromResult(result).match(/PID\s+(-?\d+)/i);
64
64
  const pid = Number(match?.[1]);
65
65
  if (!Number.isInteger(pid))
66
- throw new Error('Desktop Commander did not return a process PID');
66
+ throw new Error('Local execution engine did not return a process PID');
67
67
  return pid;
68
68
  }
69
69
  async function runShell(args) {
@@ -120,8 +120,8 @@ async function runShell(args) {
120
120
  }
121
121
  }
122
122
  export class GatewayToolAdapter {
123
- constructor(desktop, options = {}) {
124
- this.desktop = desktop;
123
+ constructor(engine, options = {}) {
124
+ this.engine = engine;
125
125
  this.allowedRoots = options.allowedRoots ?? configuredGatewayRoots();
126
126
  this.pathValidator = options.pathValidator ?? validatePath;
127
127
  }
@@ -188,6 +188,7 @@ export class GatewayToolAdapter {
188
188
  throw new Error(`REMOTE_IMAGE_TOO_LARGE: unable to fit preview within ${MAX_REMOTE_IMAGE_PREVIEW_BYTES} bytes`);
189
189
  }
190
190
  async call(tool, args = {}) {
191
+ this.engine.assertReady();
191
192
  if (tool === 'read_text_file') {
192
193
  if (args.head !== undefined && args.tail !== undefined)
193
194
  throw new Error('Use either head or tail, not both');
@@ -199,10 +200,10 @@ export class GatewayToolAdapter {
199
200
  else if (args.tail !== undefined) {
200
201
  mapped.offset = -Number(args.tail);
201
202
  }
202
- return assertSuccess(await this.desktop.callClientTool('read_file', mapped), 'read_file');
203
+ return assertSuccess(await this.engine.callClientTool('read_file', mapped), 'read_file');
203
204
  }
204
205
  if (tool === 'write_file') {
205
- return assertSuccess(await this.desktop.callClientTool('write_file', {
206
+ return assertSuccess(await this.engine.callClientTool('write_file', {
206
207
  path: await this.guardPath(args.path),
207
208
  content: args.content,
208
209
  mode: 'rewrite'
@@ -214,7 +215,7 @@ export class GatewayToolAdapter {
214
215
  return await runShell({ ...args, working_directory: await this.guardPath(args.working_directory) });
215
216
  if (tool === 'start_process') {
216
217
  const workingDirectory = await this.guardPath(args.working_directory);
217
- const result = assertSuccess(await this.desktop.callClientTool('start_process', {
218
+ const result = assertSuccess(await this.engine.callClientTool('start_process', {
218
219
  command: args.command,
219
220
  timeout_ms: Number(args.timeout_ms || 10000),
220
221
  working_directory: workingDirectory
@@ -222,7 +223,7 @@ export class GatewayToolAdapter {
222
223
  return { ...result, pid: parsePid(result), session_id: String(parsePid(result)) };
223
224
  }
224
225
  if (tool === 'read_process_output') {
225
- return assertSuccess(await this.desktop.callClientTool('read_process_output', {
226
+ return assertSuccess(await this.engine.callClientTool('read_process_output', {
226
227
  pid: Number(args.session_id),
227
228
  offset: args.offset,
228
229
  length: args.length,
@@ -230,14 +231,14 @@ export class GatewayToolAdapter {
230
231
  }), 'read_process_output');
231
232
  }
232
233
  if (tool === 'interact_with_process') {
233
- return assertSuccess(await this.desktop.callClientTool('interact_with_process', {
234
+ return assertSuccess(await this.engine.callClientTool('interact_with_process', {
234
235
  pid: Number(args.session_id),
235
236
  input: String(args.input ?? ''),
236
237
  timeout_ms: Number(args.timeout_ms || 8000)
237
238
  }), 'interact_with_process');
238
239
  }
239
240
  if (tool === 'terminate_process') {
240
- return assertSuccess(await this.desktop.callClientTool('force_terminate', {
241
+ return assertSuccess(await this.engine.callClientTool('force_terminate', {
241
242
  pid: Number(args.session_id)
242
243
  }), 'force_terminate');
243
244
  }
@@ -258,7 +259,7 @@ export class GatewayToolAdapter {
258
259
  throw new Error('expected_replacements must be a positive integer');
259
260
  }
260
261
  if (args.dry_run === true) {
261
- const read = assertSuccess(await this.desktop.callClientTool('read_file', {
262
+ const read = assertSuccess(await this.engine.callClientTool('read_file', {
262
263
  path: args.path,
263
264
  offset: 0,
264
265
  length: 100000
@@ -274,7 +275,7 @@ export class GatewayToolAdapter {
274
275
  }) }]
275
276
  };
276
277
  }
277
- return assertSuccess(await this.desktop.callClientTool('edit_block', {
278
+ return assertSuccess(await this.engine.callClientTool('edit_block', {
278
279
  file_path: args.path,
279
280
  old_string: args.old_text,
280
281
  new_string: args.new_text,
@@ -22,6 +22,7 @@ export declare class SystemdUserDeviceService {
22
22
  private nodePath;
23
23
  private entrypoint;
24
24
  private pathValue;
25
+ private runtimeDir;
25
26
  constructor(options?: {
26
27
  platform?: NodeJS.Platform | string;
27
28
  execFile?: ExecFileFn;
@@ -29,6 +30,7 @@ export declare class SystemdUserDeviceService {
29
30
  nodePath?: string;
30
31
  entrypoint?: string;
31
32
  pathValue?: string;
33
+ runtimeDir?: string;
32
34
  });
33
35
  private assertLinux;
34
36
  private unitExists;
@@ -47,6 +49,7 @@ export declare class Pm2DeviceService {
47
49
  private entrypoint;
48
50
  private user;
49
51
  private pm2Home;
52
+ private runtimeDir;
50
53
  constructor(options?: {
51
54
  platform?: NodeJS.Platform | string;
52
55
  execFile?: ExecFileFn;
@@ -55,6 +58,7 @@ export declare class Pm2DeviceService {
55
58
  entrypoint?: string;
56
59
  user?: string;
57
60
  pm2Home?: string;
61
+ runtimeDir?: string;
58
62
  });
59
63
  private assertLinux;
60
64
  private processRow;
@@ -4,6 +4,7 @@ import os from 'os';
4
4
  import path from 'path';
5
5
  import readline from 'readline/promises';
6
6
  import { promisify } from 'util';
7
+ import { deviceStatePaths } from './device-state.js';
7
8
  const execFileAsync = promisify(execFileCallback);
8
9
  export const SYSTEMD_UNIT = 'mcp-device.service';
9
10
  const SYSTEMD_OWNER_MARKER = '# Managed by MCP Device';
@@ -55,6 +56,7 @@ export class SystemdUserDeviceService {
55
56
  this.nodePath = options.platform === 'linux' && options.nodePath ? String(options.nodePath) : path.resolve(options.nodePath || process.execPath);
56
57
  this.entrypoint = options.platform === 'linux' && options.entrypoint ? String(options.entrypoint) : path.resolve(options.entrypoint || process.argv[1]);
57
58
  this.pathValue = String(options.pathValue ?? process.env.PATH ?? '/usr/local/bin:/usr/bin:/bin');
59
+ this.runtimeDir = options.platform === 'linux' && options.runtimeDir ? String(options.runtimeDir) : path.resolve(options.runtimeDir || deviceStatePaths().runtime);
58
60
  }
59
61
  assertLinux() {
60
62
  if (this.platform !== 'linux')
@@ -89,6 +91,7 @@ export class SystemdUserDeviceService {
89
91
  '[Service]',
90
92
  'Type=simple',
91
93
  `Environment="PATH=${this.pathValue.replace(/["\\]/g, match => `\\${match}`)}"`,
94
+ `WorkingDirectory=${quoteSystemdArg(this.runtimeDir)}`,
92
95
  `ExecStart=${quoteSystemdArg(this.nodePath)} ${quoteSystemdArg(this.entrypoint)} --service --manager=systemd`,
93
96
  'Restart=on-failure',
94
97
  'RestartSec=3',
@@ -102,6 +105,7 @@ export class SystemdUserDeviceService {
102
105
  this.assertLinux();
103
106
  await this.assertOwnedUnitIfPresent();
104
107
  await this.execFile('systemctl', ['--user', 'show-environment']);
108
+ await fs.mkdir(this.runtimeDir, { recursive: true, mode: 0o700 });
105
109
  await fs.mkdir(path.dirname(this.unitPath), { recursive: true });
106
110
  await fs.writeFile(this.unitPath, this.unitText(), { mode: 0o600 });
107
111
  await this.execFile('systemctl', ['--user', 'daemon-reload']);
@@ -152,6 +156,7 @@ export class Pm2DeviceService {
152
156
  this.entrypoint = options.platform === 'linux' && options.entrypoint ? String(options.entrypoint) : path.resolve(options.entrypoint || process.argv[1]);
153
157
  this.user = String(options.user || process.env.USER || process.env.LOGNAME || os.userInfo().username);
154
158
  this.pm2Home = options.platform === 'linux' && options.pm2Home ? String(options.pm2Home) : path.resolve(options.pm2Home || process.env.PM2_HOME || path.join(os.homedir(), '.pm2'));
159
+ this.runtimeDir = options.platform === 'linux' && options.runtimeDir ? String(options.runtimeDir) : path.resolve(options.runtimeDir || deviceStatePaths().runtime);
155
160
  }
156
161
  assertLinux() {
157
162
  if (this.platform !== 'linux')
@@ -209,13 +214,15 @@ export class Pm2DeviceService {
209
214
  }
210
215
  async install() {
211
216
  await this.proveStartup();
217
+ await fs.mkdir(this.runtimeDir, { recursive: true, mode: 0o700 });
212
218
  const existing = await this.processRow();
213
219
  this.assertOwnedProcess(existing);
214
220
  if (!existing) {
215
- await this.execFile(this.pm2Path, ['start', this.entrypoint, '--name', PM2_NAME, '--interpreter', this.nodePath, '--', '--service', '--manager=pm2']);
221
+ await this.execFile(this.pm2Path, ['start', this.entrypoint, '--name', PM2_NAME, '--interpreter', this.nodePath, '--cwd', this.runtimeDir, '--', '--service', '--manager=pm2']);
216
222
  }
217
- else if (String(existing?.pm2_env?.status || '').toLowerCase() !== 'online') {
218
- await this.execFile(this.pm2Path, ['restart', PM2_NAME]);
223
+ else if (String(existing?.pm2_env?.status || '').toLowerCase() !== 'online' ||
224
+ String(existing?.pm2_env?.pm_cwd || '') !== this.runtimeDir) {
225
+ await this.execFile(this.pm2Path, ['restart', PM2_NAME, '--cwd', this.runtimeDir]);
219
226
  }
220
227
  await this.execFile(this.pm2Path, ['save']);
221
228
  const status = await this.status();
@@ -2,7 +2,7 @@ import crypto from 'crypto';
2
2
  import fs from 'fs/promises';
3
3
  import net from 'net';
4
4
  import path from 'path';
5
- import { deviceStatePaths, migrateLegacyDeviceState } from './device-state.js';
5
+ import { deviceStatePaths } from './device-state.js';
6
6
  function endpointFor(paths, platform = process.platform) {
7
7
  const digest = crypto.createHash('sha256').update(paths.root, 'utf8').digest('hex').slice(0, 24);
8
8
  if (platform === 'win32')
@@ -106,7 +106,7 @@ export class RuntimeOwner {
106
106
  await fs.writeFile(runtimeOwnerControlFileFor(this.paths), JSON.stringify({ endpoint: this.endpoint, nonce: this.nonce }, null, 2), { mode: 0o600 });
107
107
  }
108
108
  async bootstrap() {
109
- await migrateLegacyDeviceState(this.paths);
109
+ await fs.mkdir(this.paths.root, { recursive: true, mode: 0o700 });
110
110
  }
111
111
  setDeviceId(deviceId) {
112
112
  this.status.deviceId = String(deviceId || '').trim() || null;
@@ -9,6 +9,7 @@ export declare function buildWindowsRunnerScript(options: {
9
9
  allowedRoots?: string;
10
10
  nodePath: string;
11
11
  entrypoint: string;
12
+ runtimeDir?: string;
12
13
  }): string;
13
14
  export type WindowsDeviceServiceStatus = {
14
15
  installed: boolean;
@@ -21,24 +22,22 @@ export declare class WindowsDeviceService {
21
22
  private platform;
22
23
  private execFile;
23
24
  private runnerPath;
24
- private legacyRunnerPath;
25
25
  private nodePath;
26
26
  private entrypoint;
27
+ private runtimeDir;
27
28
  constructor(options?: {
28
29
  platform?: NodeJS.Platform | string;
29
30
  execFile?: ExecFileFn;
30
31
  runnerPath?: string;
31
- legacyRunnerPath?: string;
32
32
  nodePath?: string;
33
33
  entrypoint?: string;
34
+ runtimeDir?: string;
34
35
  });
35
36
  private assertWindows;
36
37
  private runnerExists;
37
38
  private hasUserRunEntry;
38
39
  private scheduledTaskState;
39
40
  private deleteRegistrations;
40
- private commandReferencesLegacyRunner;
41
- private deleteVerifiedLegacyRegistrations;
42
41
  private createAutostartRegistration;
43
42
  private startUserProcess;
44
43
  private userProcessRunning;