@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,215 +1,21 @@
1
- import fs from 'fs/promises';
2
- import path from 'path';
3
- import { existsSync } from 'fs';
4
- import { CONFIG_FILE } from '../config.js';
5
- import { logger } from './logger.js';
6
1
  class FeatureFlagManager {
7
2
  constructor() {
8
3
  this.flags = {};
9
- this.lastFetch = 0;
10
- this.cacheMaxAge = 30 * 60 * 1000;
11
- this.refreshInterval = null;
12
- // Track fresh fetch status for A/B tests that need network flags
13
- this.freshFetchPromise = null;
14
- this.resolveFreshFetch = null;
15
- this.loadedFromCache = false;
16
- const configDir = path.dirname(CONFIG_FILE);
17
- this.cachePath = path.join(configDir, 'feature-flags.json');
18
- // Use production flags (v2 supports weighted variants)
19
- this.flagUrl = process.env.DC_FLAG_URL ||
20
- 'https://desktopcommander.app/flags/v2/production.json';
21
- // Set up promise for waiting on fresh fetch
22
- this.freshFetchPromise = new Promise((resolve) => {
23
- this.resolveFreshFetch = resolve;
24
- });
25
4
  }
26
- /**
27
- * Initialize - load from cache and start background refresh
28
- */
29
- async initialize() {
30
- try {
31
- // Load from cache immediately (non-blocking)
32
- await this.loadFromCache();
33
- // Fetch in background (don't block startup)
34
- this.fetchFlags().then(() => {
35
- // Signal that fresh flags are now available
36
- if (this.resolveFreshFetch) {
37
- this.resolveFreshFetch();
38
- }
39
- }).catch(err => {
40
- logger.debug('Initial flag fetch failed:', err.message);
41
- // Still resolve the promise so waiters don't hang forever
42
- if (this.resolveFreshFetch) {
43
- this.resolveFreshFetch();
44
- }
45
- });
46
- // Start periodic refresh every 5 minutes
47
- this.refreshInterval = setInterval(() => {
48
- this.fetchFlags().catch(err => {
49
- logger.debug('Periodic flag fetch failed:', err.message);
50
- });
51
- }, this.cacheMaxAge);
52
- // Allow process to exit even if interval is pending
53
- // This is critical for proper cleanup when MCP client disconnects
54
- this.refreshInterval.unref();
55
- logger.info(`Feature flags initialized (refresh every ${this.cacheMaxAge / 1000}s)`);
56
- }
57
- catch (error) {
58
- logger.warning('Failed to initialize feature flags:', error);
59
- }
60
- }
61
- /**
62
- * Get a flag value
63
- */
5
+ async initialize() { }
64
6
  get(flagName, defaultValue = false) {
65
7
  return this.flags[flagName] !== undefined ? this.flags[flagName] : defaultValue;
66
8
  }
67
- /**
68
- * Get all flags for debugging
69
- */
70
9
  getAll() {
71
10
  return { ...this.flags };
72
11
  }
73
- /**
74
- * Manually refresh flags immediately (for testing)
75
- */
76
12
  async refresh() {
77
- try {
78
- await this.fetchFlags();
79
- return true;
80
- }
81
- catch (error) {
82
- logger.error('Manual refresh failed:', error);
83
- return false;
84
- }
13
+ return true;
85
14
  }
86
- /**
87
- * Check if flags were loaded from cache (vs fresh fetch)
88
- */
89
15
  wasLoadedFromCache() {
90
- return this.loadedFromCache;
91
- }
92
- /**
93
- * Wait for fresh flags to be fetched from network.
94
- * Use this when you need to ensure flags are loaded before making decisions
95
- * (e.g., A/B test assignments for new users who don't have a cache yet)
96
- *
97
- * Has a hard timeout to prevent blocking MCP startup if the fetch hangs.
98
- * See: https://github.com/wonderwhy-er/DesktopCommanderMCP/issues/465
99
- */
100
- async waitForFreshFlags() {
101
- if (this.freshFetchPromise) {
102
- let safetyTimeoutHandle;
103
- try {
104
- const safetyTimeout = new Promise((resolve) => {
105
- safetyTimeoutHandle = setTimeout(resolve, 5000);
106
- });
107
- await Promise.race([this.freshFetchPromise, safetyTimeout]);
108
- }
109
- finally {
110
- if (safetyTimeoutHandle) {
111
- clearTimeout(safetyTimeoutHandle);
112
- }
113
- }
114
- }
115
- }
116
- /**
117
- * Load flags from local cache
118
- */
119
- async loadFromCache() {
120
- try {
121
- if (!existsSync(this.cachePath)) {
122
- logger.debug('No feature flag cache found');
123
- this.loadedFromCache = false;
124
- return;
125
- }
126
- const data = await fs.readFile(this.cachePath, 'utf8');
127
- const config = JSON.parse(data);
128
- if (config.flags) {
129
- this.flags = config.flags;
130
- this.lastFetch = Date.now();
131
- this.loadedFromCache = true;
132
- logger.debug(`Loaded ${Object.keys(this.flags).length} feature flags from cache`);
133
- }
134
- }
135
- catch (error) {
136
- logger.warning('Failed to load feature flags from cache:', error);
137
- this.loadedFromCache = false;
138
- }
139
- }
140
- /**
141
- * Fetch flags from remote URL
142
- */
143
- async fetchFlags() {
144
- const FETCH_TIMEOUT_MS = 3000;
145
- const controller = new AbortController();
146
- const abortTimeout = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
147
- let hardTimeoutHandle;
148
- try {
149
- // Don't log here - runs async and can interfere with MCP clients
150
- // Use Promise.race as a hard timeout safety net.
151
- // On some platforms (Windows + Node 24 / undici 7.x), AbortController.abort()
152
- // fails to interrupt an in-progress TCP connect — the fetch hangs until the
153
- // OS-level TCP timeout (~30s on Windows). Promise.race guarantees we reject
154
- // at the JS level regardless of AbortController behavior.
155
- // See: https://github.com/wonderwhy-er/DesktopCommanderMCP/issues/465
156
- const fetchPromise = fetch(this.flagUrl, {
157
- signal: controller.signal,
158
- headers: {
159
- 'Cache-Control': 'no-cache',
160
- }
161
- });
162
- const hardTimeout = new Promise((_, reject) => hardTimeoutHandle = setTimeout(() => reject(new Error('Feature flags fetch timed out')), FETCH_TIMEOUT_MS));
163
- const response = await Promise.race([fetchPromise, hardTimeout]);
164
- if (!response.ok) {
165
- throw new Error(`HTTP ${response.status}: ${response.statusText}`);
166
- }
167
- const config = await response.json();
168
- // Update flags
169
- if (config.flags) {
170
- this.flags = config.flags;
171
- this.lastFetch = Date.now();
172
- // Save to cache (silently - don't log during async operations
173
- // as it can interfere with MCP clients that close quickly)
174
- await this.saveToCache(config);
175
- }
176
- }
177
- catch (error) {
178
- logger.debug('Failed to fetch feature flags:', error.message);
179
- // Continue with cached values
180
- }
181
- finally {
182
- clearTimeout(abortTimeout);
183
- if (hardTimeoutHandle) {
184
- clearTimeout(hardTimeoutHandle);
185
- }
186
- }
187
- }
188
- /**
189
- * Save flags to local cache
190
- */
191
- async saveToCache(config) {
192
- try {
193
- const configDir = path.dirname(this.cachePath);
194
- if (!existsSync(configDir)) {
195
- await fs.mkdir(configDir, { recursive: true });
196
- }
197
- await fs.writeFile(this.cachePath, JSON.stringify(config, null, 2), 'utf8');
198
- // Don't log here - this runs async and can cause issues with MCP clients
199
- }
200
- catch (error) {
201
- logger.warning('Failed to save feature flags to cache:', error);
202
- }
203
- }
204
- /**
205
- * Cleanup on shutdown
206
- */
207
- destroy() {
208
- if (this.refreshInterval) {
209
- clearInterval(this.refreshInterval);
210
- this.refreshInterval = null;
211
- }
16
+ return false;
212
17
  }
18
+ async waitForFreshFlags() { }
19
+ destroy() { }
213
20
  }
214
- // Export singleton instance
215
21
  export const featureFlagManager = new FeatureFlagManager();
@@ -5,10 +5,10 @@
5
5
  *
6
6
  * READ (default): Returns a text-bearing outline — skips shapes, drawings, SVG noise.
7
7
  * Shows paragraphs with text, tables with cell content, style info, and image refs.
8
- * Each element shows its raw XML tag context so Claude can target it for editing.
8
+ * Each element shows its raw XML tag context so an agent can target it for editing.
9
9
  *
10
10
  * READ (with offset/length): Returns raw pretty-printed XML with line pagination,
11
- * so Claude can drill into specific sections when the outline isn't enough.
11
+ * so an agent can drill into specific sections when the outline isn't enough.
12
12
  *
13
13
  * EDIT (old_string/new_string): Find/replace on the pretty-printed XML, then
14
14
  * compact and repack into valid DOCX. Works exactly like text file editing.
@@ -5,10 +5,10 @@
5
5
  *
6
6
  * READ (default): Returns a text-bearing outline — skips shapes, drawings, SVG noise.
7
7
  * Shows paragraphs with text, tables with cell content, style info, and image refs.
8
- * Each element shows its raw XML tag context so Claude can target it for editing.
8
+ * Each element shows its raw XML tag context so an agent can target it for editing.
9
9
  *
10
10
  * READ (with offset/length): Returns raw pretty-printed XML with line pagination,
11
- * so Claude can drill into specific sections when the outline isn't enough.
11
+ * so an agent can drill into specific sections when the outline isn't enough.
12
12
  *
13
13
  * EDIT (old_string/new_string): Find/replace on the pretty-printed XML, then
14
14
  * compact and repack into valid DOCX. Works exactly like text file editing.
@@ -162,18 +162,6 @@ class UsageTracker {
162
162
  if (!feedbackEnabled) {
163
163
  return false;
164
164
  }
165
- // Check if client is desktop-commander (disable for this client)
166
- //return true;
167
- try {
168
- const { currentClient } = await import('../server.js');
169
- if (currentClient?.name === 'desktop-commander') {
170
- return false;
171
- }
172
- }
173
- catch (error) {
174
- // If we can't import server, continue with other checks
175
- console.log('[FEEDBACK DEBUG] Could not check client name, continuing...');
176
- }
177
165
  const stats = await this.getStats();
178
166
  // Don't prompt if feedback already given (check top-level config)
179
167
  const feedbackGiven = await configManager.getValue('feedbackGiven');
@@ -339,7 +327,6 @@ class UsageTracker {
339
327
  // within seconds, so when the flag is ON onboarding still fires on a
340
328
  // later call while the user is new. Deliberately no waiting here to keep
341
329
  // tool calls latency-free.
342
- // See: https://github.com/wonderwhy-er/DesktopCommanderMCP/issues/538
343
330
  const onboardingEnabled = featureFlagManager.get('onboarding_injection', false);
344
331
  if (!onboardingEnabled) {
345
332
  return false;
@@ -353,17 +340,6 @@ class UsageTracker {
353
340
  if (global.disableOnboarding) {
354
341
  return false;
355
342
  }
356
- // Check if client is desktop-commander (disable for this client)
357
- try {
358
- const { currentClient } = await import('../server.js');
359
- if (currentClient?.name === 'desktop-commander') {
360
- return false;
361
- }
362
- }
363
- catch (error) {
364
- // If we can't import server, continue with other checks
365
- console.log('[ONBOARDING DEBUG] Could not check client name, continuing...');
366
- }
367
343
  const stats = await this.getStats();
368
344
  const onboardingState = await this.getOnboardingState();
369
345
  const now = Date.now();
package/dist/version.d.ts CHANGED
@@ -1 +1 @@
1
- export declare const VERSION = "1.0.6";
1
+ export declare const VERSION = "1.0.7";
package/dist/version.js CHANGED
@@ -1 +1 @@
1
- export const VERSION = '1.0.6';
1
+ export const VERSION = '1.0.7';
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@hcu-lab.me/mcp-device",
3
- "version": "1.0.6",
4
- "description": "MCP Device securely connects a user-owned computer to an authenticated Model Context Protocol gateway.",
3
+ "version": "1.0.7",
4
+ "description": "MCP Device runtime.",
5
5
  "license": "MIT",
6
6
  "author": "Hirohimeow",
7
7
  "homepage": "https://github.com/Hiroshimeow/agent-mcp-device",
@@ -33,11 +33,11 @@
33
33
  "bump": "node scripts/sync-version.js --bump",
34
34
  "bump:minor": "node scripts/sync-version.js --bump --minor",
35
35
  "bump:major": "node scripts/sync-version.js --bump --major",
36
- "build": "shx rm -rf dist/remote-device && shx rm -f dist/device/desktop-commander-integration.js dist/device/desktop-commander-integration.d.ts dist/device/windows-dpapi.js dist/device/windows-dpapi.d.ts && tsc && shx chmod +x dist/*.js && shx cp src/device/update-helper.cjs dist/device/update-helper.cjs && shx mkdir -p dist/data && shx cp src/data/onboarding-prompts.json dist/data/ && node scripts/copy-official-ca.cjs && node scripts/build-ui-runtime.cjs",
36
+ "build": "shx rm -rf dist && tsc && shx chmod +x dist/*.js && shx cp src/device/update-helper.cjs dist/device/update-helper.cjs && shx mkdir -p dist/data && shx cp src/data/onboarding-prompts.json dist/data/ && node scripts/copy-official-ca.cjs && node scripts/build-ui-runtime.cjs",
37
37
  "watch": "tsc --watch",
38
38
  "start": "node dist/mcp-device.js",
39
39
  "start:debug": "node --inspect-brk=9229 dist/mcp-device.js",
40
- "prepack": "node scripts/verify-production-trust.cjs && shx rm -rf dist/remote-device && shx rm -f dist/device/desktop-commander-integration.js dist/device/desktop-commander-integration.d.ts dist/device/windows-dpapi.js dist/device/windows-dpapi.d.ts dist/hcu-device.js dist/hcu-device.d.ts dist/track-installation.js dist/setup-claude-server.js dist/uninstall-claude-server.js dist/npm-scripts/setup.js dist/npm-scripts/setup.d.ts dist/npm-scripts/uninstall.js dist/npm-scripts/uninstall.d.ts dist/npm-scripts/verify-ripgrep.js dist/npm-scripts/verify-ripgrep.d.ts",
40
+ "prepack": "node scripts/verify-production-trust.cjs",
41
41
  "prepare": "npm run build",
42
42
  "clean": "shx rm -rf dist",
43
43
  "test": "npm run build && node test/run-all-tests.js",
@@ -57,7 +57,6 @@
57
57
  "mcp",
58
58
  "model-context-protocol",
59
59
  "terminal",
60
- "claude",
61
60
  "ai",
62
61
  "command-line",
63
62
  "process-management",
@@ -73,7 +72,7 @@
73
72
  "file-operations"
74
73
  ],
75
74
  "dependencies": {
76
- "@modelcontextprotocol/sdk": "^1.9.0",
75
+ "@modelcontextprotocol/sdk": "1.30.0",
77
76
  "@opendocsg/pdf2md": "^0.2.2",
78
77
  "@tiptap/core": "^3.31.3",
79
78
  "@tiptap/extension-image": "^3.31.3",
@@ -1,8 +0,0 @@
1
- import { ServerResult } from '../types.js';
2
- interface FeedbackParams {
3
- }
4
- /**
5
- * Open feedback form in browser with optional pre-filled data
6
- */
7
- export declare function giveFeedbackToDesktopCommander(params?: FeedbackParams): Promise<ServerResult>;
8
- export {};
@@ -1,132 +0,0 @@
1
- import { usageTracker } from '../utils/usageTracker.js';
2
- import { capture } from '../utils/capture.js';
3
- import { configManager } from '../config-manager.js';
4
- import { exec } from 'child_process';
5
- import { promisify } from 'util';
6
- import * as os from 'os';
7
- const execAsync = promisify(exec);
8
- /**
9
- * Open feedback form in browser with optional pre-filled data
10
- */
11
- export async function giveFeedbackToDesktopCommander(params = {}) {
12
- try {
13
- // Get usage stats for context
14
- const stats = await usageTracker.getStats();
15
- // Capture feedback tool usage event
16
- await capture('feedback_tool_called', {
17
- total_calls: stats.totalToolCalls,
18
- successful_calls: stats.successfulCalls,
19
- failed_calls: stats.failedCalls,
20
- days_since_first_use: Math.floor((Date.now() - stats.firstUsed) / (1000 * 60 * 60 * 24)),
21
- total_sessions: stats.totalSessions,
22
- platform: os.platform(),
23
- });
24
- // Build Tally.so URL with pre-filled parameters
25
- const tallyUrl = await buildTallyUrl(params, stats);
26
- // Open URL in default browser
27
- const success = await openUrlInBrowser(tallyUrl);
28
- if (success) {
29
- // Capture successful browser opening
30
- await capture('feedback_form_opened_successfully', {
31
- total_calls: stats.totalToolCalls,
32
- platform: os.platform()
33
- });
34
- // Mark that user has given feedback (or at least opened the form)
35
- await usageTracker.markFeedbackGiven();
36
- return {
37
- content: [{
38
- type: "text",
39
- text: `🎉 **Feedback form opened in your browser!**\n\n` +
40
- `Thank you for taking the time to share your experience with MCP Device. ` +
41
- `Your feedback helps us build better features and improve the tool for everyone.\n\n` +
42
- `The form has been pre-filled with the information you provided. ` +
43
- `You can modify or add any additional details before submitting.\n\n` +
44
- `**Form URL**: ${tallyUrl.length > 100 ? tallyUrl.substring(0, 100) + '...' : tallyUrl}`
45
- }]
46
- };
47
- }
48
- else {
49
- // Capture browser opening failure
50
- await capture('feedback_form_open_failed', {
51
- total_calls: stats.totalToolCalls,
52
- platform: os.platform(),
53
- error_type: 'browser_open_failed'
54
- });
55
- return {
56
- content: [{
57
- type: "text",
58
- text: `⚠️ **Couldn't open browser automatically**\n\n` +
59
- `Please copy and paste this URL into your browser to access the feedback form:\n\n` +
60
- `${tallyUrl}\n\n` +
61
- `The form has been pre-filled with your information. Thank you for your feedback!`
62
- }]
63
- };
64
- }
65
- }
66
- catch (error) {
67
- // Capture error event
68
- await capture('feedback_tool_error', {
69
- error_message: error instanceof Error ? error.message : String(error),
70
- error_type: error instanceof Error ? error.constructor.name : 'unknown'
71
- });
72
- return {
73
- content: [{
74
- type: "text",
75
- text: `❌ **Error opening feedback form**: ${error instanceof Error ? error.message : String(error)}\n\n` +
76
- `You can still access our feedback form directly at: https://tally.so/r/mKqoKg\n\n` +
77
- `We appreciate your willingness to provide feedback!`
78
- }],
79
- isError: true
80
- };
81
- }
82
- }
83
- /**
84
- * Build Tally.so URL with pre-filled parameters
85
- */
86
- async function buildTallyUrl(params, stats) {
87
- const baseUrl = 'https://tally.so/r/mKqoKg';
88
- const urlParams = new URLSearchParams();
89
- // Only auto-filled hidden fields remain
90
- urlParams.set('tool_call_count', stats.totalToolCalls.toString());
91
- // Calculate days using
92
- const daysUsing = Math.floor((Date.now() - stats.firstUsed) / (1000 * 60 * 60 * 24));
93
- urlParams.set('days_using', daysUsing.toString());
94
- // Add platform info
95
- urlParams.set('platform', os.platform());
96
- // Add client_id from analytics config
97
- try {
98
- const clientId = await configManager.getValue('clientId') || 'unknown';
99
- urlParams.set('client_id', clientId);
100
- }
101
- catch (error) {
102
- // Fallback if config read fails
103
- urlParams.set('client_id', 'unknown');
104
- }
105
- return `${baseUrl}?${urlParams.toString()}`;
106
- }
107
- /**
108
- * Open URL in default browser (cross-platform)
109
- */
110
- async function openUrlInBrowser(url) {
111
- try {
112
- const platform = os.platform();
113
- let command;
114
- switch (platform) {
115
- case 'darwin': // macOS
116
- command = `open "${url}"`;
117
- break;
118
- case 'win32': // Windows
119
- command = `start "" "${url}"`;
120
- break;
121
- default: // Linux and others
122
- command = `xdg-open "${url}"`;
123
- break;
124
- }
125
- await execAsync(command);
126
- return true;
127
- }
128
- catch (error) {
129
- console.error('Failed to open browser:', error);
130
- return false;
131
- }
132
- }