@hcu-lab.me/mcp-device 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +51 -0
- package/dist/bootstrap.d.ts +1 -0
- package/dist/bootstrap.js +22 -0
- package/dist/command-manager.d.ts +8 -0
- package/dist/command-manager.js +241 -0
- package/dist/config-field-definitions.d.ts +41 -0
- package/dist/config-field-definitions.js +37 -0
- package/dist/config-manager.d.ts +102 -0
- package/dist/config-manager.js +291 -0
- package/dist/config.d.ts +6 -0
- package/dist/config.js +10 -0
- package/dist/custom-stdio.d.ts +51 -0
- package/dist/custom-stdio.js +358 -0
- package/dist/data/official-app-ca.pem +12 -0
- package/dist/data/onboarding-prompts.json +70 -0
- package/dist/device/device-state.d.ts +13 -0
- package/dist/device/device-state.js +100 -0
- package/dist/device/device-status.d.ts +81 -0
- package/dist/device/device-status.js +156 -0
- package/dist/device/device.d.ts +16 -0
- package/dist/device/device.js +190 -0
- package/dist/device/execution-engine.d.ts +144 -0
- package/dist/device/execution-engine.js +195 -0
- package/dist/device/gateway-channel.d.ts +48 -0
- package/dist/device/gateway-channel.js +384 -0
- package/dist/device/gateway-config.d.ts +40 -0
- package/dist/device/gateway-config.js +207 -0
- package/dist/device/gateway-identity.d.ts +35 -0
- package/dist/device/gateway-identity.js +178 -0
- package/dist/device/gateway-pairing.d.ts +25 -0
- package/dist/device/gateway-pairing.js +105 -0
- package/dist/device/gateway-secure-transport.d.ts +25 -0
- package/dist/device/gateway-secure-transport.js +117 -0
- package/dist/device/gateway-tool-adapter.d.ts +17 -0
- package/dist/device/gateway-tool-adapter.js +284 -0
- package/dist/device/gateway-url-policy.d.ts +3 -0
- package/dist/device/gateway-url-policy.js +43 -0
- package/dist/device/linux-service.d.ts +70 -0
- package/dist/device/linux-service.js +268 -0
- package/dist/device/official-trust.d.ts +7 -0
- package/dist/device/official-trust.js +49 -0
- package/dist/device/project-inspection.d.ts +1 -0
- package/dist/device/project-inspection.js +180 -0
- package/dist/device/runtime-owner.d.ts +41 -0
- package/dist/device/runtime-owner.js +177 -0
- package/dist/device/windows-dpapi.d.ts +11 -0
- package/dist/device/windows-dpapi.js +67 -0
- package/dist/device/windows-service.d.ts +56 -0
- package/dist/device/windows-service.js +238 -0
- package/dist/error-handlers.d.ts +7 -0
- package/dist/error-handlers.js +15 -0
- package/dist/handlers/edit-search-handlers.d.ts +6 -0
- package/dist/handlers/edit-search-handlers.js +6 -0
- package/dist/handlers/filesystem-handlers.d.ts +38 -0
- package/dist/handlers/filesystem-handlers.js +440 -0
- package/dist/handlers/history-handlers.d.ts +12 -0
- package/dist/handlers/history-handlers.js +67 -0
- package/dist/handlers/index.d.ts +6 -0
- package/dist/handlers/index.js +7 -0
- package/dist/handlers/process-handlers.d.ts +9 -0
- package/dist/handlers/process-handlers.js +15 -0
- package/dist/handlers/search-handlers.d.ts +17 -0
- package/dist/handlers/search-handlers.js +224 -0
- package/dist/handlers/terminal-handlers.d.ts +21 -0
- package/dist/handlers/terminal-handlers.js +35 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +138 -0
- package/dist/mcp-device.d.ts +2 -0
- package/dist/mcp-device.js +7 -0
- package/dist/npm-scripts/remote.d.ts +3 -0
- package/dist/npm-scripts/remote.js +314 -0
- package/dist/search-manager.d.ts +147 -0
- package/dist/search-manager.js +828 -0
- package/dist/server.d.ts +46 -0
- package/dist/server.js +1563 -0
- package/dist/terminal-manager.d.ts +104 -0
- package/dist/terminal-manager.js +635 -0
- package/dist/tools/config.d.ts +96 -0
- package/dist/tools/config.js +260 -0
- package/dist/tools/edit.d.ts +38 -0
- package/dist/tools/edit.js +435 -0
- package/dist/tools/feedback.d.ts +8 -0
- package/dist/tools/feedback.js +132 -0
- package/dist/tools/filesystem.d.ts +79 -0
- package/dist/tools/filesystem.js +947 -0
- package/dist/tools/fuzzySearch.d.ts +12 -0
- package/dist/tools/fuzzySearch.js +74 -0
- package/dist/tools/fuzzySearchCore.d.ts +52 -0
- package/dist/tools/fuzzySearchCore.js +125 -0
- package/dist/tools/improved-process-tools.d.ts +24 -0
- package/dist/tools/improved-process-tools.js +627 -0
- package/dist/tools/mime-types.d.ts +3 -0
- package/dist/tools/mime-types.js +28 -0
- package/dist/tools/pdf/extract-images.d.ts +34 -0
- package/dist/tools/pdf/extract-images.js +132 -0
- package/dist/tools/pdf/index.d.ts +6 -0
- package/dist/tools/pdf/index.js +3 -0
- package/dist/tools/pdf/lib/pdf2md.d.ts +36 -0
- package/dist/tools/pdf/lib/pdf2md.js +76 -0
- package/dist/tools/pdf/manipulations.d.ts +13 -0
- package/dist/tools/pdf/manipulations.js +99 -0
- package/dist/tools/pdf/markdown.d.ts +26 -0
- package/dist/tools/pdf/markdown.js +275 -0
- package/dist/tools/pdf/utils.d.ts +12 -0
- package/dist/tools/pdf/utils.js +34 -0
- package/dist/tools/process.d.ts +3 -0
- package/dist/tools/process.js +56 -0
- package/dist/tools/prompts.d.ts +28 -0
- package/dist/tools/prompts.js +259 -0
- package/dist/tools/schemas.d.ts +454 -0
- package/dist/tools/schemas.js +242 -0
- package/dist/tools/usage.d.ts +5 -0
- package/dist/tools/usage.js +24 -0
- package/dist/types.d.ts +83 -0
- package/dist/types.js +1 -0
- package/dist/ui/config-editor/config-editor-runtime.js +150 -0
- package/dist/ui/config-editor/index.html +13 -0
- package/dist/ui/config-editor/src/app.d.ts +43 -0
- package/dist/ui/config-editor/src/app.js +831 -0
- package/dist/ui/config-editor/src/array-modal.d.ts +19 -0
- package/dist/ui/config-editor/src/array-modal.js +185 -0
- package/dist/ui/config-editor/src/main.d.ts +1 -0
- package/dist/ui/config-editor/src/main.js +2 -0
- package/dist/ui/config-editor/styles.css +587 -0
- package/dist/ui/contracts.d.ts +14 -0
- package/dist/ui/contracts.js +21 -0
- package/dist/ui/file-preview/index.html +16 -0
- package/dist/ui/file-preview/preview-runtime.js +469 -0
- package/dist/ui/file-preview/shared/preview-file-types.d.ts +5 -0
- package/dist/ui/file-preview/shared/preview-file-types.js +59 -0
- package/dist/ui/file-preview/src/app.d.ts +4 -0
- package/dist/ui/file-preview/src/app.js +680 -0
- package/dist/ui/file-preview/src/components/code-viewer.d.ts +6 -0
- package/dist/ui/file-preview/src/components/code-viewer.js +73 -0
- package/dist/ui/file-preview/src/components/highlighting.d.ts +2 -0
- package/dist/ui/file-preview/src/components/highlighting.js +54 -0
- package/dist/ui/file-preview/src/components/html-renderer.d.ts +5 -0
- package/dist/ui/file-preview/src/components/html-renderer.js +47 -0
- package/dist/ui/file-preview/src/components/markdown-renderer.d.ts +1 -0
- package/dist/ui/file-preview/src/components/markdown-renderer.js +59 -0
- package/dist/ui/file-preview/src/directory-controller.d.ts +8 -0
- package/dist/ui/file-preview/src/directory-controller.js +240 -0
- package/dist/ui/file-preview/src/document-layout.d.ts +20 -0
- package/dist/ui/file-preview/src/document-layout.js +109 -0
- package/dist/ui/file-preview/src/document-outline.d.ts +17 -0
- package/dist/ui/file-preview/src/document-outline.js +97 -0
- package/dist/ui/file-preview/src/document-workspace.d.ts +19 -0
- package/dist/ui/file-preview/src/document-workspace.js +33 -0
- package/dist/ui/file-preview/src/file-type-handlers.d.ts +10 -0
- package/dist/ui/file-preview/src/file-type-handlers.js +109 -0
- package/dist/ui/file-preview/src/host/external-actions.d.ts +8 -0
- package/dist/ui/file-preview/src/host/external-actions.js +55 -0
- package/dist/ui/file-preview/src/host/selection-context.d.ts +9 -0
- package/dist/ui/file-preview/src/host/selection-context.js +106 -0
- package/dist/ui/file-preview/src/image-preview.d.ts +3 -0
- package/dist/ui/file-preview/src/image-preview.js +21 -0
- package/dist/ui/file-preview/src/main.d.ts +1 -0
- package/dist/ui/file-preview/src/main.js +5 -0
- package/dist/ui/file-preview/src/markdown/conflict-dialog.d.ts +40 -0
- package/dist/ui/file-preview/src/markdown/conflict-dialog.js +163 -0
- package/dist/ui/file-preview/src/markdown/controller.d.ts +44 -0
- package/dist/ui/file-preview/src/markdown/controller.js +1043 -0
- package/dist/ui/file-preview/src/markdown/editor.d.ts +131 -0
- package/dist/ui/file-preview/src/markdown/editor.js +1479 -0
- package/dist/ui/file-preview/src/markdown/linking.d.ts +16 -0
- package/dist/ui/file-preview/src/markdown/linking.js +228 -0
- package/dist/ui/file-preview/src/markdown/outline.d.ts +2 -0
- package/dist/ui/file-preview/src/markdown/outline.js +16 -0
- package/dist/ui/file-preview/src/markdown/parser.d.ts +30 -0
- package/dist/ui/file-preview/src/markdown/parser.js +38 -0
- package/dist/ui/file-preview/src/markdown/preview.d.ts +1 -0
- package/dist/ui/file-preview/src/markdown/preview.js +20 -0
- package/dist/ui/file-preview/src/markdown/slugify.d.ts +3 -0
- package/dist/ui/file-preview/src/markdown/slugify.js +31 -0
- package/dist/ui/file-preview/src/markdown/utils.d.ts +1 -0
- package/dist/ui/file-preview/src/markdown/utils.js +15 -0
- package/dist/ui/file-preview/src/model.d.ts +35 -0
- package/dist/ui/file-preview/src/model.js +1 -0
- package/dist/ui/file-preview/src/panel-actions.d.ts +17 -0
- package/dist/ui/file-preview/src/panel-actions.js +182 -0
- package/dist/ui/file-preview/src/path-utils.d.ts +6 -0
- package/dist/ui/file-preview/src/path-utils.js +64 -0
- package/dist/ui/file-preview/src/payload-utils.d.ts +11 -0
- package/dist/ui/file-preview/src/payload-utils.js +119 -0
- package/dist/ui/file-preview/src/types.d.ts +1 -0
- package/dist/ui/file-preview/src/types.js +1 -0
- package/dist/ui/file-preview/styles.css +1631 -0
- package/dist/ui/resources.d.ts +28 -0
- package/dist/ui/resources.js +86 -0
- package/dist/ui/shared/compact-row.d.ts +11 -0
- package/dist/ui/shared/compact-row.js +18 -0
- package/dist/ui/shared/escape-html.d.ts +4 -0
- package/dist/ui/shared/escape-html.js +11 -0
- package/dist/ui/shared/host-context.d.ts +15 -0
- package/dist/ui/shared/host-context.js +51 -0
- package/dist/ui/shared/tool-bridge.d.ts +30 -0
- package/dist/ui/shared/tool-bridge.js +137 -0
- package/dist/ui/shared/tool-shell.d.ts +25 -0
- package/dist/ui/shared/tool-shell.js +107 -0
- package/dist/ui/shared/ui-event-tracker.d.ts +9 -0
- package/dist/ui/shared/ui-event-tracker.js +27 -0
- package/dist/ui/shared/widget-state.d.ts +33 -0
- package/dist/ui/shared/widget-state.js +158 -0
- package/dist/utils/ab-test.d.ts +12 -0
- package/dist/utils/ab-test.js +104 -0
- package/dist/utils/capture.d.ts +35 -0
- package/dist/utils/capture.js +481 -0
- package/dist/utils/dockerPrompt.d.ts +17 -0
- package/dist/utils/dockerPrompt.js +53 -0
- package/dist/utils/feature-flags.d.ts +59 -0
- package/dist/utils/feature-flags.js +215 -0
- package/dist/utils/files/base.d.ts +173 -0
- package/dist/utils/files/base.js +5 -0
- package/dist/utils/files/binary.d.ts +21 -0
- package/dist/utils/files/binary.js +65 -0
- package/dist/utils/files/docx.d.ts +47 -0
- package/dist/utils/files/docx.js +679 -0
- package/dist/utils/files/excel.d.ts +24 -0
- package/dist/utils/files/excel.js +437 -0
- package/dist/utils/files/factory.d.ts +41 -0
- package/dist/utils/files/factory.js +113 -0
- package/dist/utils/files/image.d.ts +21 -0
- package/dist/utils/files/image.js +78 -0
- package/dist/utils/files/index.d.ts +10 -0
- package/dist/utils/files/index.js +13 -0
- package/dist/utils/files/pdf.d.ts +32 -0
- package/dist/utils/files/pdf.js +142 -0
- package/dist/utils/files/text.d.ts +63 -0
- package/dist/utils/files/text.js +370 -0
- package/dist/utils/fuzzySearchLogger.d.ts +30 -0
- package/dist/utils/fuzzySearchLogger.js +126 -0
- package/dist/utils/lineEndingHandler.d.ts +21 -0
- package/dist/utils/lineEndingHandler.js +77 -0
- package/dist/utils/logger.d.ts +32 -0
- package/dist/utils/logger.js +72 -0
- package/dist/utils/mcp-ui-ab-test.d.ts +13 -0
- package/dist/utils/mcp-ui-ab-test.js +62 -0
- package/dist/utils/open-browser.d.ts +9 -0
- package/dist/utils/open-browser.js +42 -0
- package/dist/utils/process-detection.d.ts +23 -0
- package/dist/utils/process-detection.js +150 -0
- package/dist/utils/ripgrep-resolver.d.ts +9 -0
- package/dist/utils/ripgrep-resolver.js +74 -0
- package/dist/utils/system-info.d.ts +71 -0
- package/dist/utils/system-info.js +712 -0
- package/dist/utils/toolHistory.d.ts +86 -0
- package/dist/utils/toolHistory.js +257 -0
- package/dist/utils/trackTools.d.ts +6 -0
- package/dist/utils/trackTools.js +54 -0
- package/dist/utils/unsupportedParams.d.ts +24 -0
- package/dist/utils/unsupportedParams.js +66 -0
- package/dist/utils/usageTracker.d.ts +133 -0
- package/dist/utils/usageTracker.js +477 -0
- package/dist/utils/welcome-onboarding.d.ts +11 -0
- package/dist/utils/welcome-onboarding.js +99 -0
- package/dist/utils/withTimeout.d.ts +28 -0
- package/dist/utils/withTimeout.js +85 -0
- package/dist/version.d.ts +1 -0
- package/dist/version.js +1 -0
- package/package.json +131 -0
|
@@ -0,0 +1,215 @@
|
|
|
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
|
+
class FeatureFlagManager {
|
|
7
|
+
constructor() {
|
|
8
|
+
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
|
+
}
|
|
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
|
+
*/
|
|
64
|
+
get(flagName, defaultValue = false) {
|
|
65
|
+
return this.flags[flagName] !== undefined ? this.flags[flagName] : defaultValue;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Get all flags for debugging
|
|
69
|
+
*/
|
|
70
|
+
getAll() {
|
|
71
|
+
return { ...this.flags };
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Manually refresh flags immediately (for testing)
|
|
75
|
+
*/
|
|
76
|
+
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
|
+
}
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Check if flags were loaded from cache (vs fresh fetch)
|
|
88
|
+
*/
|
|
89
|
+
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
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
// Export singleton instance
|
|
215
|
+
export const featureFlagManager = new FeatureFlagManager();
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Base interfaces and types for file handling system
|
|
3
|
+
* All file handlers implement the FileHandler interface
|
|
4
|
+
*/
|
|
5
|
+
/**
|
|
6
|
+
* Base interface that all file handlers must implement
|
|
7
|
+
*/
|
|
8
|
+
export interface FileHandler {
|
|
9
|
+
/**
|
|
10
|
+
* Read file content
|
|
11
|
+
* @param path Validated file path
|
|
12
|
+
* @param options Read options (offset, length, sheet, etc.)
|
|
13
|
+
* @returns File result with content and metadata
|
|
14
|
+
*/
|
|
15
|
+
read(path: string, options?: ReadOptions): Promise<FileResult>;
|
|
16
|
+
/**
|
|
17
|
+
* Write file (complete rewrite or append)
|
|
18
|
+
* @param path Validated file path
|
|
19
|
+
* @param content Content to write
|
|
20
|
+
* @param mode Write mode: 'rewrite' (default) or 'append'
|
|
21
|
+
*/
|
|
22
|
+
write(path: string, content: any, mode?: 'rewrite' | 'append'): Promise<void>;
|
|
23
|
+
/**
|
|
24
|
+
* Edit a specific range (bulk rewrite)
|
|
25
|
+
* PRIMARY METHOD for structured file editing (Excel, etc.)
|
|
26
|
+
* Simpler and more powerful than location-based edits
|
|
27
|
+
* Supports:
|
|
28
|
+
* - Cell ranges: "Sheet1!A1:C10" with 2D array content
|
|
29
|
+
* - Whole sheets: "Sheet1" to replace entire sheet
|
|
30
|
+
* - Chunking: Update 1000 rows at a time for large files
|
|
31
|
+
*
|
|
32
|
+
* Currently implemented by: ExcelFileHandler
|
|
33
|
+
* TECHNICAL DEBT: TextFileHandler should also implement this for search/replace
|
|
34
|
+
* (logic currently in src/tools/edit.ts - see comments there)
|
|
35
|
+
*
|
|
36
|
+
* @param path Validated file path
|
|
37
|
+
* @param range Range identifier (e.g., "Sheet1!A1:C10" or "Sheet1")
|
|
38
|
+
* @param content New content for the range (2D array for Excel)
|
|
39
|
+
* @param options Additional format-specific options
|
|
40
|
+
* @returns Result with success status
|
|
41
|
+
*/
|
|
42
|
+
editRange?(path: string, range: string, content: any, options?: Record<string, any>): Promise<EditResult>;
|
|
43
|
+
/**
|
|
44
|
+
* Get file metadata
|
|
45
|
+
* @param path Validated file path
|
|
46
|
+
* @returns File information including type-specific metadata
|
|
47
|
+
*/
|
|
48
|
+
getInfo(path: string): Promise<FileInfo>;
|
|
49
|
+
/**
|
|
50
|
+
* Check if this handler can handle the given file
|
|
51
|
+
* @param path File path
|
|
52
|
+
* @returns true if this handler supports this file type (can be async for content-based checks)
|
|
53
|
+
*/
|
|
54
|
+
canHandle(path: string): boolean | Promise<boolean>;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Options for reading files
|
|
58
|
+
*/
|
|
59
|
+
export interface ReadOptions {
|
|
60
|
+
/** Whether the path is a URL */
|
|
61
|
+
isUrl?: boolean;
|
|
62
|
+
/** Starting line/row number (for text/excel) */
|
|
63
|
+
offset?: number;
|
|
64
|
+
/** Maximum number of lines/rows to read */
|
|
65
|
+
length?: number;
|
|
66
|
+
/** Excel-specific: Sheet name or index */
|
|
67
|
+
sheet?: string | number;
|
|
68
|
+
/** Excel-specific: Cell range (e.g., "A1:C10") */
|
|
69
|
+
range?: string;
|
|
70
|
+
/** Whether to include status messages (default: true) */
|
|
71
|
+
includeStatusMessage?: boolean;
|
|
72
|
+
/** Optional AbortSignal to cancel an in-flight read (frees fd/thread on timeout). */
|
|
73
|
+
signal?: AbortSignal;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Result from reading a file
|
|
77
|
+
*/
|
|
78
|
+
export interface FileResult {
|
|
79
|
+
/** File content (string for text/csv, Buffer for binary, base64 string for images) */
|
|
80
|
+
content: string | Buffer;
|
|
81
|
+
/** MIME type of the content */
|
|
82
|
+
mimeType: string;
|
|
83
|
+
/** Type-specific metadata */
|
|
84
|
+
metadata?: FileMetadata;
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* File-type specific metadata
|
|
88
|
+
*/
|
|
89
|
+
export interface FileMetadata {
|
|
90
|
+
/** For images */
|
|
91
|
+
isImage?: boolean;
|
|
92
|
+
/** For directories (read_file fallback to listDirectory) */
|
|
93
|
+
isDirectory?: boolean;
|
|
94
|
+
/** For binary files */
|
|
95
|
+
isBinary?: boolean;
|
|
96
|
+
/** For Excel files */
|
|
97
|
+
isExcelFile?: boolean;
|
|
98
|
+
sheets?: ExcelSheet[];
|
|
99
|
+
fileSize?: number;
|
|
100
|
+
isLargeFile?: boolean;
|
|
101
|
+
/** For text files */
|
|
102
|
+
lineCount?: number;
|
|
103
|
+
/** For PDF files */
|
|
104
|
+
isPdf?: boolean;
|
|
105
|
+
author?: string;
|
|
106
|
+
title?: string;
|
|
107
|
+
totalPages?: number;
|
|
108
|
+
pages?: PdfPageItem[];
|
|
109
|
+
/** For DOCX files */
|
|
110
|
+
isDocx?: boolean;
|
|
111
|
+
/** Error information if operation failed */
|
|
112
|
+
error?: boolean;
|
|
113
|
+
errorMessage?: string;
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* PDF page content item
|
|
117
|
+
*/
|
|
118
|
+
export interface PdfPageItem {
|
|
119
|
+
pageNumber: number;
|
|
120
|
+
text: string;
|
|
121
|
+
images: Array<{
|
|
122
|
+
data: string;
|
|
123
|
+
mimeType: string;
|
|
124
|
+
}>;
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Excel sheet metadata
|
|
128
|
+
*/
|
|
129
|
+
export interface ExcelSheet {
|
|
130
|
+
/** Sheet name */
|
|
131
|
+
name: string;
|
|
132
|
+
/** Number of rows in sheet */
|
|
133
|
+
rowCount: number;
|
|
134
|
+
/** Number of columns in sheet */
|
|
135
|
+
colCount: number;
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Result from edit operation (used by editRange)
|
|
139
|
+
*/
|
|
140
|
+
export interface EditResult {
|
|
141
|
+
/** Whether all edits succeeded */
|
|
142
|
+
success: boolean;
|
|
143
|
+
/** Number of edits successfully applied */
|
|
144
|
+
editsApplied: number;
|
|
145
|
+
/** Errors that occurred during editing */
|
|
146
|
+
errors?: Array<{
|
|
147
|
+
location: string;
|
|
148
|
+
error: string;
|
|
149
|
+
}>;
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* File information and metadata
|
|
153
|
+
*/
|
|
154
|
+
export interface FileInfo {
|
|
155
|
+
/** File size in bytes */
|
|
156
|
+
size: number;
|
|
157
|
+
/** Creation time */
|
|
158
|
+
created: Date;
|
|
159
|
+
/** Last modification time */
|
|
160
|
+
modified: Date;
|
|
161
|
+
/** Last access time */
|
|
162
|
+
accessed: Date;
|
|
163
|
+
/** Is this a directory */
|
|
164
|
+
isDirectory: boolean;
|
|
165
|
+
/** Is this a regular file */
|
|
166
|
+
isFile: boolean;
|
|
167
|
+
/** File permissions (octal string) */
|
|
168
|
+
permissions: string;
|
|
169
|
+
/** File type classification */
|
|
170
|
+
fileType: 'text' | 'excel' | 'image' | 'binary' | 'docx';
|
|
171
|
+
/** Type-specific metadata */
|
|
172
|
+
metadata?: FileMetadata;
|
|
173
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Binary file handler
|
|
3
|
+
* Handles binary files that aren't supported by other handlers (Excel, Image)
|
|
4
|
+
* Uses isBinaryFile for content-based detection
|
|
5
|
+
* Returns instructions to use start_process with appropriate tools
|
|
6
|
+
*/
|
|
7
|
+
import { FileHandler, ReadOptions, FileResult, FileInfo } from './base.js';
|
|
8
|
+
/**
|
|
9
|
+
* Binary file handler implementation
|
|
10
|
+
* Uses content-based detection via isBinaryFile
|
|
11
|
+
*/
|
|
12
|
+
export declare class BinaryFileHandler implements FileHandler {
|
|
13
|
+
canHandle(filePath: string): Promise<boolean>;
|
|
14
|
+
read(filePath: string, options?: ReadOptions): Promise<FileResult>;
|
|
15
|
+
write(path: string, content: any): Promise<void>;
|
|
16
|
+
getInfo(path: string): Promise<FileInfo>;
|
|
17
|
+
/**
|
|
18
|
+
* Generate instructions for handling binary files
|
|
19
|
+
*/
|
|
20
|
+
private getBinaryInstructions;
|
|
21
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Binary file handler
|
|
3
|
+
* Handles binary files that aren't supported by other handlers (Excel, Image)
|
|
4
|
+
* Uses isBinaryFile for content-based detection
|
|
5
|
+
* Returns instructions to use start_process with appropriate tools
|
|
6
|
+
*/
|
|
7
|
+
import fs from "fs/promises";
|
|
8
|
+
import path from "path";
|
|
9
|
+
import { isBinaryFile } from 'isbinaryfile';
|
|
10
|
+
/**
|
|
11
|
+
* Binary file handler implementation
|
|
12
|
+
* Uses content-based detection via isBinaryFile
|
|
13
|
+
*/
|
|
14
|
+
export class BinaryFileHandler {
|
|
15
|
+
async canHandle(filePath) {
|
|
16
|
+
// Content-based binary detection using isBinaryFile
|
|
17
|
+
try {
|
|
18
|
+
return await isBinaryFile(filePath);
|
|
19
|
+
}
|
|
20
|
+
catch (error) {
|
|
21
|
+
// If we can't check (file doesn't exist, etc.), don't handle it
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
async read(filePath, options) {
|
|
26
|
+
const instructions = this.getBinaryInstructions(filePath);
|
|
27
|
+
return {
|
|
28
|
+
content: instructions,
|
|
29
|
+
mimeType: 'text/plain',
|
|
30
|
+
metadata: {
|
|
31
|
+
isBinary: true
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
async write(path, content) {
|
|
36
|
+
throw new Error('Cannot write binary files directly. Use start_process with appropriate tools (Python, Node.js libraries, command-line utilities).');
|
|
37
|
+
}
|
|
38
|
+
async getInfo(path) {
|
|
39
|
+
const stats = await fs.stat(path);
|
|
40
|
+
return {
|
|
41
|
+
size: stats.size,
|
|
42
|
+
created: stats.birthtime,
|
|
43
|
+
modified: stats.mtime,
|
|
44
|
+
accessed: stats.atime,
|
|
45
|
+
isDirectory: stats.isDirectory(),
|
|
46
|
+
isFile: stats.isFile(),
|
|
47
|
+
permissions: stats.mode.toString(8).slice(-3),
|
|
48
|
+
fileType: 'binary',
|
|
49
|
+
metadata: {
|
|
50
|
+
isBinary: true
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Generate instructions for handling binary files
|
|
56
|
+
*/
|
|
57
|
+
getBinaryInstructions(filePath) {
|
|
58
|
+
const fileName = path.basename(filePath);
|
|
59
|
+
return `Cannot read binary file as text: ${fileName}
|
|
60
|
+
|
|
61
|
+
Use start_process + interact_with_process to analyze binary files with appropriate tools (Node.js or Python libraries, command-line utilities, etc.).
|
|
62
|
+
|
|
63
|
+
The read_file tool only handles text files, images, and Excel files.`;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DOCX File Handler
|
|
3
|
+
*
|
|
4
|
+
* Approach: expose DOCX as filtered/raw XML through existing read_file + edit_block.
|
|
5
|
+
*
|
|
6
|
+
* READ (default): Returns a text-bearing outline — skips shapes, drawings, SVG noise.
|
|
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.
|
|
9
|
+
*
|
|
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.
|
|
12
|
+
*
|
|
13
|
+
* EDIT (old_string/new_string): Find/replace on the pretty-printed XML, then
|
|
14
|
+
* compact and repack into valid DOCX. Works exactly like text file editing.
|
|
15
|
+
*
|
|
16
|
+
* Round-trip: DOCX → unzip → pretty-print → [outline or raw] → edit → compact → repack
|
|
17
|
+
*/
|
|
18
|
+
import { FileHandler, FileResult, FileInfo, ReadOptions, EditResult } from './base.js';
|
|
19
|
+
export declare class DocxFileHandler implements FileHandler {
|
|
20
|
+
private readonly extensions;
|
|
21
|
+
canHandle(path: string): boolean;
|
|
22
|
+
/**
|
|
23
|
+
* Read DOCX content.
|
|
24
|
+
*
|
|
25
|
+
* Default (offset=0, no explicit length or default length): returns outline
|
|
26
|
+
* With offset/length: returns raw pretty-printed XML with line pagination
|
|
27
|
+
*/
|
|
28
|
+
read(path: string, options?: ReadOptions): Promise<FileResult>;
|
|
29
|
+
/**
|
|
30
|
+
* Write/create a DOCX file.
|
|
31
|
+
* Content is plain text — each line becomes a paragraph.
|
|
32
|
+
* Lines starting with # become headings (# = Heading1, ## = Heading2, etc.)
|
|
33
|
+
*/
|
|
34
|
+
write(path: string, content: any, mode?: 'rewrite' | 'append'): Promise<void>;
|
|
35
|
+
/**
|
|
36
|
+
* Edit DOCX via find/replace on pretty-printed XML.
|
|
37
|
+
*
|
|
38
|
+
* Works on the same representation that read() returns when using offset/length,
|
|
39
|
+
* so XML fragments copied from read output work as search strings.
|
|
40
|
+
* After editing, XML is compacted and repacked into the DOCX.
|
|
41
|
+
*/
|
|
42
|
+
editRange(path: string, _range: string, content: any, options?: Record<string, any>): Promise<EditResult>;
|
|
43
|
+
/**
|
|
44
|
+
* Get DOCX file info
|
|
45
|
+
*/
|
|
46
|
+
getInfo(path: string): Promise<FileInfo>;
|
|
47
|
+
}
|