@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.
- package/README.md +7 -71
- package/dist/bootstrap.js +1 -1
- package/dist/config-manager.js +56 -19
- package/dist/config.js +1 -1
- package/dist/custom-stdio.js +1 -2
- package/dist/data/onboarding-prompts.json +22 -11
- package/dist/device/device-state.d.ts +2 -5
- package/dist/device/device-state.js +3 -85
- package/dist/device/device.d.ts +1 -1
- package/dist/device/device.js +69 -9
- package/dist/device/execution-engine.d.ts +11 -1
- package/dist/device/execution-engine.js +85 -126
- package/dist/device/gateway-channel.d.ts +19 -1
- package/dist/device/gateway-channel.js +107 -21
- package/dist/device/gateway-identity.d.ts +1 -0
- package/dist/device/gateway-identity.js +4 -0
- package/dist/device/gateway-tool-adapter.d.ts +3 -3
- package/dist/device/gateway-tool-adapter.js +12 -11
- package/dist/device/linux-service.d.ts +6 -0
- package/dist/device/linux-service.js +12 -5
- package/dist/device/runtime-owner.d.ts +1 -0
- package/dist/device/runtime-owner.js +6 -6
- package/dist/device/self-update.d.ts +107 -12
- package/dist/device/self-update.js +660 -62
- package/dist/device/update-helper.cjs +343 -0
- package/dist/device/windows-service.d.ts +3 -4
- package/dist/device/windows-service.js +5 -44
- package/dist/handlers/filesystem-handlers.js +3 -0
- package/dist/index.js +7 -3
- package/dist/npm-scripts/remote.d.ts +5 -0
- package/dist/npm-scripts/remote.js +50 -3
- package/dist/server.js +83 -167
- package/dist/terminal-manager.js +41 -1
- package/dist/tools/config.d.ts +1 -1
- package/dist/tools/filesystem.js +3 -3
- package/dist/tools/pdf/markdown.js +1 -1
- package/dist/tools/schemas.d.ts +2 -3
- package/dist/tools/schemas.js +0 -10
- package/dist/ui/config-editor/index.html +1 -1
- package/dist/ui/file-preview/index.html +1 -1
- package/dist/ui/file-preview/styles.css +2 -2
- package/dist/utils/capture.d.ts +5 -28
- package/dist/utils/capture.js +11 -455
- package/dist/utils/feature-flags.d.ts +0 -47
- package/dist/utils/feature-flags.js +5 -199
- package/dist/utils/files/docx.d.ts +2 -2
- package/dist/utils/files/docx.js +2 -2
- package/dist/utils/files/factory.js +20 -0
- package/dist/utils/files/text.d.ts +1 -0
- package/dist/utils/files/text.js +78 -34
- package/dist/utils/trackTools.js +23 -2
- package/dist/utils/usageTracker.js +2 -24
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +6 -7
- package/dist/tools/feedback.d.ts +0 -8
- 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
|
-
|
|
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
|
|
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
|
|
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
|
|
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.
|
package/dist/utils/files/docx.js
CHANGED
|
@@ -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
|
|
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
|
|
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,6 +5,7 @@
|
|
|
5
5
|
* Each handler implements canHandle() which can be sync (extension-based)
|
|
6
6
|
* or async (content-based like BinaryFileHandler using isBinaryFile)
|
|
7
7
|
*/
|
|
8
|
+
import fs from 'fs/promises';
|
|
8
9
|
import { TextFileHandler } from './text.js';
|
|
9
10
|
import { ImageFileHandler } from './image.js';
|
|
10
11
|
import { BinaryFileHandler } from './binary.js';
|
|
@@ -86,6 +87,25 @@ export async function getFileHandler(filePath) {
|
|
|
86
87
|
if (getImageHandler().canHandle(filePath)) {
|
|
87
88
|
return getImageHandler();
|
|
88
89
|
}
|
|
90
|
+
// UTF-16LE text contains many NUL bytes and generic binary sniffers
|
|
91
|
+
// often classify it as binary. A BOM is authoritative, so route it to the
|
|
92
|
+
// text handler before content-based binary detection.
|
|
93
|
+
try {
|
|
94
|
+
const fd = await fs.open(filePath, 'r');
|
|
95
|
+
try {
|
|
96
|
+
const bom = Buffer.alloc(2);
|
|
97
|
+
const { bytesRead } = await fd.read(bom, 0, bom.length, 0);
|
|
98
|
+
if (bytesRead === 2 && bom[0] === 0xff && bom[1] === 0xfe) {
|
|
99
|
+
return getTextHandler();
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
finally {
|
|
103
|
+
await fd.close();
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
catch {
|
|
107
|
+
// Preserve existing fallback behavior if the file cannot be sampled.
|
|
108
|
+
}
|
|
89
109
|
// Check Binary (content-based, async via isBinaryFile)
|
|
90
110
|
if (await getBinaryHandler().canHandle(filePath)) {
|
|
91
111
|
return getBinaryHandler();
|
|
@@ -19,6 +19,7 @@ import { FileHandler, ReadOptions, FileResult, FileInfo } from './base.js';
|
|
|
19
19
|
*/
|
|
20
20
|
export declare class TextFileHandler implements FileHandler {
|
|
21
21
|
canHandle(_path: string): boolean;
|
|
22
|
+
private detectTextEncoding;
|
|
22
23
|
read(filePath: string, options?: ReadOptions): Promise<FileResult>;
|
|
23
24
|
write(path: string, content: string, mode?: 'rewrite' | 'append'): Promise<void>;
|
|
24
25
|
getInfo(path: string): Promise<FileInfo>;
|
package/dist/utils/files/text.js
CHANGED
|
@@ -38,6 +38,17 @@ export class TextFileHandler {
|
|
|
38
38
|
// The factory routes binary files to BinaryFileHandler before reaching here
|
|
39
39
|
return true;
|
|
40
40
|
}
|
|
41
|
+
async detectTextEncoding(filePath) {
|
|
42
|
+
const fd = await fs.open(filePath, 'r');
|
|
43
|
+
try {
|
|
44
|
+
const bom = Buffer.alloc(2);
|
|
45
|
+
const { bytesRead } = await fd.read(bom, 0, bom.length, 0);
|
|
46
|
+
return bytesRead === 2 && bom[0] === 0xff && bom[1] === 0xfe ? 'utf16le' : 'utf8';
|
|
47
|
+
}
|
|
48
|
+
finally {
|
|
49
|
+
await fd.close();
|
|
50
|
+
}
|
|
51
|
+
}
|
|
41
52
|
async read(filePath, options) {
|
|
42
53
|
const offset = options?.offset ?? 0;
|
|
43
54
|
const length = options?.length ?? 1000; // Default from config
|
|
@@ -69,7 +80,11 @@ export class TextFileHandler {
|
|
|
69
80
|
// For text files that aren't too large, count lines
|
|
70
81
|
if (stats.isFile() && stats.size < FILE_SIZE_LIMITS.LINE_COUNT_LIMIT) {
|
|
71
82
|
try {
|
|
72
|
-
const
|
|
83
|
+
const encoding = await this.detectTextEncoding(path);
|
|
84
|
+
let content = await fs.readFile(path, encoding);
|
|
85
|
+
if (encoding === 'utf16le' && content.charCodeAt(0) === 0xfeff) {
|
|
86
|
+
content = content.slice(1);
|
|
87
|
+
}
|
|
73
88
|
const lineCount = TextFileHandler.countLines(content);
|
|
74
89
|
info.metadata.lineCount = lineCount;
|
|
75
90
|
}
|
|
@@ -100,11 +115,14 @@ export class TextFileHandler {
|
|
|
100
115
|
/**
|
|
101
116
|
* Get file line count (for files under size limit)
|
|
102
117
|
*/
|
|
103
|
-
async getFileLineCount(filePath, signal) {
|
|
118
|
+
async getFileLineCount(filePath, signal, encoding = 'utf8') {
|
|
104
119
|
try {
|
|
105
120
|
const stats = await fs.stat(filePath);
|
|
106
121
|
if (stats.size < FILE_SIZE_LIMITS.LINE_COUNT_LIMIT) {
|
|
107
|
-
|
|
122
|
+
let content = await fs.readFile(filePath, { encoding, signal });
|
|
123
|
+
if (encoding === 'utf16le' && content.charCodeAt(0) === 0xfeff) {
|
|
124
|
+
content = content.slice(1);
|
|
125
|
+
}
|
|
108
126
|
return TextFileHandler.countLines(content);
|
|
109
127
|
}
|
|
110
128
|
}
|
|
@@ -182,7 +200,16 @@ export class TextFileHandler {
|
|
|
182
200
|
async readFileWithSmartPositioning(filePath, offset, length, mimeType, includeStatusMessage = true, signal) {
|
|
183
201
|
const stats = await fs.stat(filePath);
|
|
184
202
|
const fileSize = stats.size;
|
|
185
|
-
const
|
|
203
|
+
const encoding = await this.detectTextEncoding(filePath);
|
|
204
|
+
const totalLines = await this.getFileLineCount(filePath, signal, encoding);
|
|
205
|
+
// UTF-16LE needs character-aware streaming. Skip the BOM and preserve
|
|
206
|
+
// the same line pagination/status semantics as UTF-8.
|
|
207
|
+
if (encoding === 'utf16le') {
|
|
208
|
+
if (offset < 0) {
|
|
209
|
+
return await this.readFromEndWithReadline(filePath, Math.abs(offset), mimeType, includeStatusMessage, totalLines, signal, 'utf16le', 2);
|
|
210
|
+
}
|
|
211
|
+
return await this.readFromStartWithReadline(filePath, offset, length, mimeType, includeStatusMessage, totalLines, signal, 'utf16le', 2);
|
|
212
|
+
}
|
|
186
213
|
// For negative offsets (tail behavior), use reverse reading
|
|
187
214
|
if (offset < 0) {
|
|
188
215
|
const requestedLines = Math.abs(offset);
|
|
@@ -252,9 +279,9 @@ export class TextFileHandler {
|
|
|
252
279
|
/**
|
|
253
280
|
* Read from end using readline with circular buffer
|
|
254
281
|
*/
|
|
255
|
-
async readFromEndWithReadline(filePath, requestedLines, mimeType, includeStatusMessage = true, fileTotalLines, signal) {
|
|
282
|
+
async readFromEndWithReadline(filePath, requestedLines, mimeType, includeStatusMessage = true, fileTotalLines, signal, encoding, start) {
|
|
256
283
|
const rl = createInterface({
|
|
257
|
-
input: createReadStream(filePath, { signal }),
|
|
284
|
+
input: createReadStream(filePath, { signal, encoding, start }),
|
|
258
285
|
crlfDelay: Infinity
|
|
259
286
|
});
|
|
260
287
|
const buffer = new Array(requestedLines);
|
|
@@ -284,22 +311,28 @@ export class TextFileHandler {
|
|
|
284
311
|
/**
|
|
285
312
|
* Read from start/middle using readline
|
|
286
313
|
*/
|
|
287
|
-
async readFromStartWithReadline(filePath, offset, length, mimeType, includeStatusMessage = true, fileTotalLines, signal) {
|
|
314
|
+
async readFromStartWithReadline(filePath, offset, length, mimeType, includeStatusMessage = true, fileTotalLines, signal, encoding, start) {
|
|
315
|
+
const stream = createReadStream(filePath, { signal, encoding, start });
|
|
288
316
|
const rl = createInterface({
|
|
289
|
-
input:
|
|
317
|
+
input: stream,
|
|
290
318
|
crlfDelay: Infinity
|
|
291
319
|
});
|
|
292
320
|
const result = [];
|
|
293
321
|
let lineNumber = 0;
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
result.
|
|
322
|
+
try {
|
|
323
|
+
for await (const line of rl) {
|
|
324
|
+
if (lineNumber >= offset && result.length < length) {
|
|
325
|
+
result.push(line);
|
|
326
|
+
}
|
|
327
|
+
if (result.length >= length)
|
|
328
|
+
break;
|
|
329
|
+
lineNumber++;
|
|
297
330
|
}
|
|
298
|
-
if (result.length >= length)
|
|
299
|
-
break;
|
|
300
|
-
lineNumber++;
|
|
301
331
|
}
|
|
302
|
-
|
|
332
|
+
finally {
|
|
333
|
+
rl.close();
|
|
334
|
+
stream.destroy();
|
|
335
|
+
}
|
|
303
336
|
if (includeStatusMessage) {
|
|
304
337
|
const statusMessage = this.generateEnhancedStatusMessage(result.length, offset, fileTotalLines, false);
|
|
305
338
|
const content = `${statusMessage}\n\n${result.join('\n')}`;
|
|
@@ -314,20 +347,26 @@ export class TextFileHandler {
|
|
|
314
347
|
* Read from estimated byte position for very large files
|
|
315
348
|
*/
|
|
316
349
|
async readFromEstimatedPosition(filePath, offset, length, mimeType, includeStatusMessage = true, fileTotalLines, signal) {
|
|
317
|
-
// First, do a quick scan to estimate lines per byte
|
|
350
|
+
// First, do a quick scan to estimate lines per byte.
|
|
351
|
+
const sampleStream = createReadStream(filePath, { signal });
|
|
318
352
|
const rl = createInterface({
|
|
319
|
-
input:
|
|
353
|
+
input: sampleStream,
|
|
320
354
|
crlfDelay: Infinity
|
|
321
355
|
});
|
|
322
356
|
let sampleLines = 0;
|
|
323
357
|
let bytesRead = 0;
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
358
|
+
try {
|
|
359
|
+
for await (const line of rl) {
|
|
360
|
+
bytesRead += Buffer.byteLength(line, 'utf-8') + 1;
|
|
361
|
+
sampleLines++;
|
|
362
|
+
if (bytesRead >= READ_PERFORMANCE_THRESHOLDS.SAMPLE_SIZE)
|
|
363
|
+
break;
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
finally {
|
|
367
|
+
rl.close();
|
|
368
|
+
sampleStream.destroy();
|
|
329
369
|
}
|
|
330
|
-
rl.close();
|
|
331
370
|
if (sampleLines === 0) {
|
|
332
371
|
return await this.readFromStartWithReadline(filePath, offset, length, mimeType, includeStatusMessage, fileTotalLines, signal);
|
|
333
372
|
}
|
|
@@ -345,19 +384,24 @@ export class TextFileHandler {
|
|
|
345
384
|
});
|
|
346
385
|
const result = [];
|
|
347
386
|
let firstLineSkipped = false;
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
firstLineSkipped
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
result.
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
387
|
+
try {
|
|
388
|
+
for await (const line of rl2) {
|
|
389
|
+
if (!firstLineSkipped && startPosition > 0) {
|
|
390
|
+
firstLineSkipped = true;
|
|
391
|
+
continue;
|
|
392
|
+
}
|
|
393
|
+
if (result.length < length) {
|
|
394
|
+
result.push(line);
|
|
395
|
+
}
|
|
396
|
+
else {
|
|
397
|
+
break;
|
|
398
|
+
}
|
|
358
399
|
}
|
|
359
400
|
}
|
|
360
|
-
|
|
401
|
+
finally {
|
|
402
|
+
rl2.close();
|
|
403
|
+
stream.destroy();
|
|
404
|
+
}
|
|
361
405
|
const content = includeStatusMessage
|
|
362
406
|
? `${this.generateEnhancedStatusMessage(result.length, offset, fileTotalLines, false)}\n\n${result.join('\n')}`
|
|
363
407
|
: result.join('\n');
|
package/dist/utils/trackTools.js
CHANGED
|
@@ -13,8 +13,29 @@ export async function trackToolCall(toolName, args) {
|
|
|
13
13
|
try {
|
|
14
14
|
// Get current timestamp
|
|
15
15
|
const timestamp = new Date().toISOString();
|
|
16
|
-
//
|
|
17
|
-
|
|
16
|
+
// Remote MCP Device calls may contain commands, file contents, tokens or
|
|
17
|
+
// other secrets. Persist only bounded metadata in remote mode.
|
|
18
|
+
const remote = process.env.MCP_DEVICE_REMOTE === 'true';
|
|
19
|
+
let suffix = '';
|
|
20
|
+
if (args !== undefined) {
|
|
21
|
+
if (remote) {
|
|
22
|
+
let argBytes = 0;
|
|
23
|
+
let argCount = 0;
|
|
24
|
+
try {
|
|
25
|
+
const serialized = JSON.stringify(args);
|
|
26
|
+
argBytes = Buffer.byteLength(serialized || '', 'utf8');
|
|
27
|
+
argCount = args && typeof args === 'object' && !Array.isArray(args)
|
|
28
|
+
? Object.keys(args).length
|
|
29
|
+
: 1;
|
|
30
|
+
}
|
|
31
|
+
catch { /* metadata-only logging must never affect tool execution */ }
|
|
32
|
+
suffix = `\t| Args: count=${argCount} bytes=${argBytes}`;
|
|
33
|
+
}
|
|
34
|
+
else {
|
|
35
|
+
suffix = `\t| Arguments: ${JSON.stringify(args)}`;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
const logEntry = `${timestamp} | ${toolName.padEnd(20, ' ')}${suffix}\n`;
|
|
18
39
|
// Check if file exists and get its size
|
|
19
40
|
let fileSize = 0;
|
|
20
41
|
try {
|
|
@@ -52,6 +52,8 @@ class UsageTracker {
|
|
|
52
52
|
* keeps a saturated libuv threadpool from gating tool responses on every call.
|
|
53
53
|
*/
|
|
54
54
|
async saveStats(stats) {
|
|
55
|
+
if (process.env.MCP_DEVICE_REMOTE === 'true')
|
|
56
|
+
return;
|
|
55
57
|
await configManager.setValueNonBlocking('usageStats', stats);
|
|
56
58
|
}
|
|
57
59
|
/**
|
|
@@ -160,18 +162,6 @@ class UsageTracker {
|
|
|
160
162
|
if (!feedbackEnabled) {
|
|
161
163
|
return false;
|
|
162
164
|
}
|
|
163
|
-
// Check if client is desktop-commander (disable for this client)
|
|
164
|
-
//return true;
|
|
165
|
-
try {
|
|
166
|
-
const { currentClient } = await import('../server.js');
|
|
167
|
-
if (currentClient?.name === 'desktop-commander') {
|
|
168
|
-
return false;
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
catch (error) {
|
|
172
|
-
// If we can't import server, continue with other checks
|
|
173
|
-
console.log('[FEEDBACK DEBUG] Could not check client name, continuing...');
|
|
174
|
-
}
|
|
175
165
|
const stats = await this.getStats();
|
|
176
166
|
// Don't prompt if feedback already given (check top-level config)
|
|
177
167
|
const feedbackGiven = await configManager.getValue('feedbackGiven');
|
|
@@ -337,7 +327,6 @@ class UsageTracker {
|
|
|
337
327
|
// within seconds, so when the flag is ON onboarding still fires on a
|
|
338
328
|
// later call while the user is new. Deliberately no waiting here to keep
|
|
339
329
|
// tool calls latency-free.
|
|
340
|
-
// See: https://github.com/wonderwhy-er/DesktopCommanderMCP/issues/538
|
|
341
330
|
const onboardingEnabled = featureFlagManager.get('onboarding_injection', false);
|
|
342
331
|
if (!onboardingEnabled) {
|
|
343
332
|
return false;
|
|
@@ -351,17 +340,6 @@ class UsageTracker {
|
|
|
351
340
|
if (global.disableOnboarding) {
|
|
352
341
|
return false;
|
|
353
342
|
}
|
|
354
|
-
// Check if client is desktop-commander (disable for this client)
|
|
355
|
-
try {
|
|
356
|
-
const { currentClient } = await import('../server.js');
|
|
357
|
-
if (currentClient?.name === 'desktop-commander') {
|
|
358
|
-
return false;
|
|
359
|
-
}
|
|
360
|
-
}
|
|
361
|
-
catch (error) {
|
|
362
|
-
// If we can't import server, continue with other checks
|
|
363
|
-
console.log('[ONBOARDING DEBUG] Could not check client name, continuing...');
|
|
364
|
-
}
|
|
365
343
|
const stats = await this.getStats();
|
|
366
344
|
const onboardingState = await this.getOnboardingState();
|
|
367
345
|
const now = Date.now();
|
package/dist/version.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const VERSION = "1.0.
|
|
1
|
+
export declare const VERSION = "1.0.7";
|
package/dist/version.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const VERSION = '1.0.
|
|
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.
|
|
4
|
-
"description": "MCP Device
|
|
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",
|
|
@@ -26,18 +26,18 @@
|
|
|
26
26
|
"device:start": "tsx src/device/device.ts",
|
|
27
27
|
"device:start:dev": "nodemon --watch src/device --exec tsx src/device/device.ts",
|
|
28
28
|
"sync-version": "node scripts/sync-version.js",
|
|
29
|
-
"version": "node scripts/sync-version.js && git add
|
|
29
|
+
"version": "node scripts/sync-version.js && git add src/version.ts package-lock.json",
|
|
30
30
|
"release": "node scripts/release.js patch",
|
|
31
31
|
"release:minor": "node scripts/release.js minor",
|
|
32
32
|
"release:major": "node scripts/release.js major",
|
|
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
|
|
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
|
|
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": "
|
|
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",
|
package/dist/tools/feedback.d.ts
DELETED
|
@@ -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 {};
|