@hcu-lab.me/mcp-device 1.0.5 → 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 (57) hide show
  1. package/README.md +7 -71
  2. package/dist/bootstrap.js +1 -1
  3. package/dist/config-manager.js +56 -19
  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 +2 -5
  8. package/dist/device/device-state.js +3 -85
  9. package/dist/device/device.d.ts +1 -1
  10. package/dist/device/device.js +69 -9
  11. package/dist/device/execution-engine.d.ts +11 -1
  12. package/dist/device/execution-engine.js +85 -126
  13. package/dist/device/gateway-channel.d.ts +19 -1
  14. package/dist/device/gateway-channel.js +107 -21
  15. package/dist/device/gateway-identity.d.ts +1 -0
  16. package/dist/device/gateway-identity.js +4 -0
  17. package/dist/device/gateway-tool-adapter.d.ts +3 -3
  18. package/dist/device/gateway-tool-adapter.js +12 -11
  19. package/dist/device/linux-service.d.ts +6 -0
  20. package/dist/device/linux-service.js +12 -5
  21. package/dist/device/runtime-owner.d.ts +1 -0
  22. package/dist/device/runtime-owner.js +6 -6
  23. package/dist/device/self-update.d.ts +107 -12
  24. package/dist/device/self-update.js +660 -62
  25. package/dist/device/update-helper.cjs +343 -0
  26. package/dist/device/windows-service.d.ts +3 -4
  27. package/dist/device/windows-service.js +5 -44
  28. package/dist/handlers/filesystem-handlers.js +3 -0
  29. package/dist/index.js +7 -3
  30. package/dist/npm-scripts/remote.d.ts +5 -0
  31. package/dist/npm-scripts/remote.js +50 -3
  32. package/dist/server.js +83 -167
  33. package/dist/terminal-manager.js +41 -1
  34. package/dist/tools/config.d.ts +1 -1
  35. package/dist/tools/filesystem.js +3 -3
  36. package/dist/tools/pdf/markdown.js +1 -1
  37. package/dist/tools/schemas.d.ts +2 -3
  38. package/dist/tools/schemas.js +0 -10
  39. package/dist/ui/config-editor/index.html +1 -1
  40. package/dist/ui/file-preview/index.html +1 -1
  41. package/dist/ui/file-preview/styles.css +2 -2
  42. package/dist/utils/capture.d.ts +5 -28
  43. package/dist/utils/capture.js +11 -455
  44. package/dist/utils/feature-flags.d.ts +0 -47
  45. package/dist/utils/feature-flags.js +5 -199
  46. package/dist/utils/files/docx.d.ts +2 -2
  47. package/dist/utils/files/docx.js +2 -2
  48. package/dist/utils/files/factory.js +20 -0
  49. package/dist/utils/files/text.d.ts +1 -0
  50. package/dist/utils/files/text.js +78 -34
  51. package/dist/utils/trackTools.js +23 -2
  52. package/dist/utils/usageTracker.js +2 -24
  53. package/dist/version.d.ts +1 -1
  54. package/dist/version.js +1 -1
  55. package/package.json +6 -7
  56. package/dist/tools/feedback.d.ts +0 -8
  57. package/dist/tools/feedback.js +0 -132
@@ -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,
@@ -1,3 +1,5 @@
1
+ export declare const SYSTEMD_UNIT = "mcp-device.service";
2
+ export declare const PM2_NAME = "mcp-device";
1
3
  type ExecFileResult = {
2
4
  stdout: string;
3
5
  stderr: string;
@@ -20,6 +22,7 @@ export declare class SystemdUserDeviceService {
20
22
  private nodePath;
21
23
  private entrypoint;
22
24
  private pathValue;
25
+ private runtimeDir;
23
26
  constructor(options?: {
24
27
  platform?: NodeJS.Platform | string;
25
28
  execFile?: ExecFileFn;
@@ -27,6 +30,7 @@ export declare class SystemdUserDeviceService {
27
30
  nodePath?: string;
28
31
  entrypoint?: string;
29
32
  pathValue?: string;
33
+ runtimeDir?: string;
30
34
  });
31
35
  private assertLinux;
32
36
  private unitExists;
@@ -45,6 +49,7 @@ export declare class Pm2DeviceService {
45
49
  private entrypoint;
46
50
  private user;
47
51
  private pm2Home;
52
+ private runtimeDir;
48
53
  constructor(options?: {
49
54
  platform?: NodeJS.Platform | string;
50
55
  execFile?: ExecFileFn;
@@ -53,6 +58,7 @@ export declare class Pm2DeviceService {
53
58
  entrypoint?: string;
54
59
  user?: string;
55
60
  pm2Home?: string;
61
+ runtimeDir?: string;
56
62
  });
57
63
  private assertLinux;
58
64
  private processRow;
@@ -4,10 +4,11 @@ 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
- const SYSTEMD_UNIT = 'mcp-device.service';
9
+ export const SYSTEMD_UNIT = 'mcp-device.service';
9
10
  const SYSTEMD_OWNER_MARKER = '# Managed by MCP Device';
10
- const PM2_NAME = 'mcp-device';
11
+ export const PM2_NAME = 'mcp-device';
11
12
  async function defaultExecFile(file, args) {
12
13
  const result = await execFileAsync(file, args, { windowsHide: true, encoding: 'utf8' });
13
14
  return { stdout: String(result.stdout || ''), stderr: String(result.stderr || '') };
@@ -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();
@@ -9,6 +9,7 @@ export interface RuntimeOwnerStatus {
9
9
  entrypoint: string;
10
10
  deviceId: string | null;
11
11
  }
12
+ export declare function runtimeOwnerControlFileFor(paths: DeviceStatePaths): string;
12
13
  export declare function probeRuntimeOwner(paths?: DeviceStatePaths, platform?: NodeJS.Platform | string): Promise<RuntimeOwnerStatus | null>;
13
14
  export declare class RuntimeOwner {
14
15
  readonly paths: DeviceStatePaths;
@@ -2,14 +2,14 @@ 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')
9
9
  return `\\\\.\\pipe\\mcp-device-${digest}`;
10
10
  return path.join(paths.root, `runtime-${digest}.sock`);
11
11
  }
12
- function controlFileFor(paths) {
12
+ export function runtimeOwnerControlFileFor(paths) {
13
13
  return path.join(paths.root, 'runtime-control.json');
14
14
  }
15
15
  async function request(endpoint, message, timeoutMs = 800) {
@@ -103,10 +103,10 @@ export class RuntimeOwner {
103
103
  this.server.once('error', reject);
104
104
  this.server.listen(this.endpoint, resolve);
105
105
  });
106
- await fs.writeFile(controlFileFor(this.paths), JSON.stringify({ endpoint: this.endpoint, nonce: this.nonce }, null, 2), { mode: 0o600 });
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;
@@ -118,7 +118,7 @@ export class RuntimeOwner {
118
118
  await new Promise(resolve => server.close(() => resolve()));
119
119
  }
120
120
  try {
121
- await fs.unlink(controlFileFor(this.paths));
121
+ await fs.unlink(runtimeOwnerControlFileFor(this.paths));
122
122
  }
123
123
  catch (error) {
124
124
  if (error?.code !== 'ENOENT')
@@ -138,7 +138,7 @@ export class RuntimeOwner {
138
138
  export async function stopRuntimeOwner(paths = deviceStatePaths(), platform = process.platform) {
139
139
  let control;
140
140
  try {
141
- control = JSON.parse(await fs.readFile(controlFileFor(paths), 'utf8'));
141
+ control = JSON.parse(await fs.readFile(runtimeOwnerControlFileFor(paths), 'utf8'));
142
142
  }
143
143
  catch (error) {
144
144
  if (error?.code === 'ENOENT')
@@ -1,20 +1,115 @@
1
1
  import { spawn as spawnCallback } from 'child_process';
2
+ import { type DeviceStatePaths } from './device-state.js';
2
3
  export declare const MCP_DEVICE_PACKAGE = "@hcu-lab.me/mcp-device";
4
+ export type DeviceUpdateManagerKind = 'windows-task' | 'windows-run' | 'windows-manual' | 'systemd' | 'pm2';
5
+ export type DeviceUpdatePhase = 'prepared' | 'helper_started' | 'handoff_ready' | 'quiescing' | 'installing' | 'installed_waiting_reconnect' | 'rollback' | 'failed';
6
+ export interface DeviceUpdateState {
7
+ schema: number;
8
+ requestId: string;
9
+ fromVersion: string;
10
+ targetVersion: string;
11
+ state: DeviceUpdatePhase;
12
+ startedAt: number;
13
+ updatedAt?: number;
14
+ completedAt: number | null;
15
+ errorCode: string | null;
16
+ message: string | null;
17
+ deviceId: string;
18
+ managerKind: DeviceUpdateManagerKind;
19
+ parentPid: number;
20
+ childPid: number | null;
21
+ packageRoot: string;
22
+ globalPrefix: string;
23
+ nodePath: string;
24
+ npmCliPath: string;
25
+ targetTarball: string;
26
+ cacheDir: string;
27
+ rollbackSnapshot: string;
28
+ rollbackPath: string;
29
+ helperPath: string;
30
+ handoffPath: string;
31
+ runtimeOwnerControlPath: string;
32
+ windowsRunnerPath?: string;
33
+ windowsTaskName?: string;
34
+ windowsUpdateTaskName?: string;
35
+ powershellPath?: string;
36
+ schtasksPath?: string;
37
+ systemctlPath?: string;
38
+ systemdRunPath?: string;
39
+ systemdUnit?: string;
40
+ systemdUpdateUnit?: string;
41
+ pm2Path?: string;
42
+ pm2Name?: string;
43
+ }
44
+ export interface PreparedDeviceUpdate {
45
+ state: DeviceUpdateState;
46
+ statePath: string;
47
+ helperRunnerPath?: string;
48
+ }
49
+ type ExecFileFn = (file: string, args: string[]) => Promise<{
50
+ stdout?: string;
51
+ stderr?: string;
52
+ }>;
53
+ export interface DeviceUpdateManagerOverride {
54
+ managerKind: DeviceUpdateManagerKind;
55
+ windowsRunnerPath?: string;
56
+ windowsTaskName?: string;
57
+ powershellPath?: string;
58
+ schtasksPath?: string;
59
+ systemctlPath?: string;
60
+ systemdRunPath?: string;
61
+ systemdUnit?: string;
62
+ pm2Path?: string;
63
+ pm2Name?: string;
64
+ }
3
65
  export declare function normalizeUpdateVersion(value: string): string;
4
- export declare function installDevicePackageUpdate(targetVersion: string, options?: {
66
+ export declare function readDeviceUpdateState(paths?: DeviceStatePaths): Promise<DeviceUpdateState | null>;
67
+ export declare function assertDeviceStartupAllowedDuringUpdate(currentVersion?: string, paths?: DeviceStatePaths): Promise<void>;
68
+ export declare function resolveNpmCliPath(options?: {
5
69
  platform?: NodeJS.Platform | string;
6
- execFile?: (file: string, args: string[]) => Promise<{
7
- stdout?: string;
8
- stderr?: string;
9
- }>;
10
- npmPath?: string;
70
+ execFile?: ExecFileFn;
71
+ nodePath?: string;
72
+ npmExecPath?: string;
11
73
  }): Promise<string>;
12
- export declare function buildWindowsRestartHelper(deviceId: string, currentPid?: number): string;
13
- export declare function scheduleDeviceRuntimeRestart(options: {
74
+ export declare function findCurrentPackageRoot(entrypoint?: string): Promise<string>;
75
+ export declare function prepareDevicePackageUpdate(options: {
76
+ targetVersion: string;
77
+ requestId: string;
14
78
  deviceId: string;
15
- platform?: NodeJS.Platform | string;
79
+ childPid?: number | null;
80
+ fromVersion?: string;
16
81
  argv?: string[];
17
- delayMs?: number;
82
+ platform?: NodeJS.Platform | string;
83
+ execFile?: ExecFileFn;
84
+ paths?: DeviceStatePaths;
85
+ entrypoint?: string;
86
+ nodePath?: string;
87
+ packageRoot?: string;
88
+ npmCliPath?: string;
89
+ globalPrefix?: string;
90
+ globalRoot?: string;
91
+ managerOverride?: DeviceUpdateManagerOverride;
92
+ helperSourcePath?: string;
93
+ }): Promise<PreparedDeviceUpdate>;
94
+ export declare function launchDeviceUpdateHelper(prepared: PreparedDeviceUpdate, options?: {
95
+ execFile?: ExecFileFn;
18
96
  spawn?: typeof spawnCallback;
19
- exit?: (code?: number) => never | void;
20
- }): void;
97
+ platform?: NodeJS.Platform | string;
98
+ paths?: DeviceStatePaths;
99
+ }): Promise<void>;
100
+ export declare function signalDeviceUpdateHandoff(handoff?: {
101
+ childPid?: number | null;
102
+ }, paths?: DeviceStatePaths): Promise<void>;
103
+ export declare function markPreparedUpdateFailed(code: string, message: string, paths?: DeviceStatePaths): Promise<void>;
104
+ export type UpdateReconciliation = {
105
+ action: 'none';
106
+ } | {
107
+ action: 'success';
108
+ state: DeviceUpdateState;
109
+ } | {
110
+ action: 'report-failed';
111
+ state: DeviceUpdateState;
112
+ };
113
+ export declare function reconcileDeviceUpdateAfterStart(currentVersion?: string, paths?: DeviceStatePaths): Promise<UpdateReconciliation>;
114
+ export declare function retireDeviceUpdateState(state: DeviceUpdateState, paths?: DeviceStatePaths): Promise<void>;
115
+ export {};