starmux 0.12.0 → 0.13.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/dist/src/cli/mac-mcp-gateway.js +4 -3
- package/dist/src/cli/mac-mcp-gateway.js.map +1 -1
- package/dist/src/cli/starmux.js +1 -1
- package/dist/src/cli/starmux.js.map +1 -1
- package/dist/src/runtime-paths.js +3 -3
- package/dist/src/runtime-paths.js.map +1 -1
- package/dist/vendor/desktop-commander/LICENSE +21 -0
- package/dist/vendor/desktop-commander/dist/bootstrap.js +22 -0
- package/dist/vendor/desktop-commander/dist/command-manager.js +246 -0
- package/dist/vendor/desktop-commander/dist/config-field-definitions.js +37 -0
- package/dist/vendor/desktop-commander/dist/config-manager.js +634 -0
- package/dist/vendor/desktop-commander/dist/config-recovery.js +81 -0
- package/dist/vendor/desktop-commander/dist/config.js +19 -0
- package/dist/vendor/desktop-commander/dist/custom-stdio.js +358 -0
- package/dist/vendor/desktop-commander/dist/error-handlers.js +15 -0
- package/dist/vendor/desktop-commander/dist/handlers/edit-search-handlers.js +6 -0
- package/dist/vendor/desktop-commander/dist/handlers/filesystem-handlers.js +450 -0
- package/dist/vendor/desktop-commander/dist/handlers/history-handlers.js +67 -0
- package/dist/vendor/desktop-commander/dist/handlers/index.js +7 -0
- package/dist/vendor/desktop-commander/dist/handlers/process-handlers.js +15 -0
- package/dist/vendor/desktop-commander/dist/handlers/search-answers.js +172 -0
- package/dist/vendor/desktop-commander/dist/handlers/search-handlers.js +136 -0
- package/dist/vendor/desktop-commander/dist/handlers/terminal-handlers.js +35 -0
- package/dist/vendor/desktop-commander/dist/index.js +88 -0
- package/dist/vendor/desktop-commander/dist/search-manager.js +1262 -0
- package/dist/vendor/desktop-commander/dist/server.js +1414 -0
- package/dist/vendor/desktop-commander/dist/terminal-manager.js +757 -0
- package/dist/vendor/desktop-commander/dist/tools/config.js +211 -0
- package/dist/vendor/desktop-commander/dist/tools/edit.js +455 -0
- package/dist/vendor/desktop-commander/dist/tools/feedback.js +10 -0
- package/dist/vendor/desktop-commander/dist/tools/filesystem.js +1047 -0
- package/dist/vendor/desktop-commander/dist/tools/fuzzySearch.js +74 -0
- package/dist/vendor/desktop-commander/dist/tools/fuzzySearchCore.js +125 -0
- package/dist/vendor/desktop-commander/dist/tools/improved-process-tools.js +726 -0
- package/dist/vendor/desktop-commander/dist/tools/mime-types.js +28 -0
- package/dist/vendor/desktop-commander/dist/tools/pdf/extract-images.js +131 -0
- package/dist/vendor/desktop-commander/dist/tools/pdf/index.js +4 -0
- package/dist/vendor/desktop-commander/dist/tools/pdf/lib/pdf2md.js +78 -0
- package/dist/vendor/desktop-commander/dist/tools/pdf/manipulations.js +112 -0
- package/dist/vendor/desktop-commander/dist/tools/pdf/markdown.js +30 -0
- package/dist/vendor/desktop-commander/dist/tools/pdf/utils.js +45 -0
- package/dist/vendor/desktop-commander/dist/tools/process.js +90 -0
- package/dist/vendor/desktop-commander/dist/tools/prompts.js +259 -0
- package/dist/vendor/desktop-commander/dist/tools/schemas.js +242 -0
- package/dist/vendor/desktop-commander/dist/tools/usage.js +24 -0
- package/dist/vendor/desktop-commander/dist/types.js +1 -0
- package/dist/vendor/desktop-commander/dist/ui/config-editor/config-editor-runtime.js +150 -0
- package/dist/vendor/desktop-commander/dist/ui/config-editor/index.html +13 -0
- package/dist/vendor/desktop-commander/dist/ui/config-editor/styles.css +587 -0
- package/dist/vendor/desktop-commander/dist/ui/contracts.js +57 -0
- package/dist/vendor/desktop-commander/dist/ui/file-preview/index.html +16 -0
- package/dist/vendor/desktop-commander/dist/ui/file-preview/preview-runtime.js +471 -0
- package/dist/vendor/desktop-commander/dist/ui/file-preview/shared/preview-file-types.js +59 -0
- package/dist/vendor/desktop-commander/dist/ui/file-preview/styles.css +1631 -0
- package/dist/vendor/desktop-commander/dist/ui/resources.js +93 -0
- package/dist/vendor/desktop-commander/dist/utils/ab-test.js +122 -0
- package/dist/vendor/desktop-commander/dist/utils/atomic-write.js +80 -0
- package/dist/vendor/desktop-commander/dist/utils/capture.js +40 -0
- package/dist/vendor/desktop-commander/dist/utils/dockerPrompt.js +9 -0
- package/dist/vendor/desktop-commander/dist/utils/exit-process.js +31 -0
- package/dist/vendor/desktop-commander/dist/utils/feature-flags.js +18 -0
- package/dist/vendor/desktop-commander/dist/utils/files/base.js +5 -0
- package/dist/vendor/desktop-commander/dist/utils/files/binary.js +65 -0
- package/dist/vendor/desktop-commander/dist/utils/files/docx.js +694 -0
- package/dist/vendor/desktop-commander/dist/utils/files/excel.js +497 -0
- package/dist/vendor/desktop-commander/dist/utils/files/factory.js +168 -0
- package/dist/vendor/desktop-commander/dist/utils/files/image.js +89 -0
- package/dist/vendor/desktop-commander/dist/utils/files/index.js +14 -0
- package/dist/vendor/desktop-commander/dist/utils/files/pdf.js +124 -0
- package/dist/vendor/desktop-commander/dist/utils/files/text.js +423 -0
- package/dist/vendor/desktop-commander/dist/utils/fuzzySearchLogger.js +126 -0
- package/dist/vendor/desktop-commander/dist/utils/internal-facts.js +23 -0
- package/dist/vendor/desktop-commander/dist/utils/lazy-package.js +44 -0
- package/dist/vendor/desktop-commander/dist/utils/lineEndingHandler.js +77 -0
- package/dist/vendor/desktop-commander/dist/utils/logger.js +72 -0
- package/dist/vendor/desktop-commander/dist/utils/mcp-ui-ab-test.js +62 -0
- package/dist/vendor/desktop-commander/dist/utils/open-browser.js +2 -0
- package/dist/vendor/desktop-commander/dist/utils/process-detection.js +260 -0
- package/dist/vendor/desktop-commander/dist/utils/process-tree.js +223 -0
- package/dist/vendor/desktop-commander/dist/utils/rename.js +74 -0
- package/dist/vendor/desktop-commander/dist/utils/ripgrep-resolver.js +69 -0
- package/dist/vendor/desktop-commander/dist/utils/shell.js +187 -0
- package/dist/vendor/desktop-commander/dist/utils/system-info.js +727 -0
- package/dist/vendor/desktop-commander/dist/utils/terminal-timestamps.js +18 -0
- package/dist/vendor/desktop-commander/dist/utils/toolHistory.js +309 -0
- package/dist/vendor/desktop-commander/dist/utils/trackTools.js +55 -0
- package/dist/vendor/desktop-commander/dist/utils/unsupportedParams.js +66 -0
- package/dist/vendor/desktop-commander/dist/utils/usageTracker.js +225 -0
- package/dist/vendor/desktop-commander/dist/utils/welcome-onboarding.js +2 -0
- package/dist/vendor/desktop-commander/dist/utils/withTimeout.js +85 -0
- package/dist/vendor/desktop-commander/dist/version.js +1 -0
- package/dist/vendor/desktop-commander/package.json +15 -0
- package/package.json +14 -2
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import fs from 'fs/promises';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import os from 'os';
|
|
4
|
+
class FuzzySearchLogger {
|
|
5
|
+
constructor() {
|
|
6
|
+
this.initialized = false;
|
|
7
|
+
// Create log file in a dedicated directory
|
|
8
|
+
const logDir = path.join(os.homedir(), '.claude-server-commander-logs');
|
|
9
|
+
this.logPath = path.join(logDir, 'fuzzy-search.log');
|
|
10
|
+
}
|
|
11
|
+
async ensureLogFile() {
|
|
12
|
+
if (this.initialized)
|
|
13
|
+
return;
|
|
14
|
+
try {
|
|
15
|
+
// Create log directory if it doesn't exist
|
|
16
|
+
const logDir = path.dirname(this.logPath);
|
|
17
|
+
await fs.mkdir(logDir, { recursive: true });
|
|
18
|
+
// Check if log file exists, create with headers if not
|
|
19
|
+
try {
|
|
20
|
+
await fs.access(this.logPath);
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
// File doesn't exist, create with headers
|
|
24
|
+
const headers = [
|
|
25
|
+
'timestamp',
|
|
26
|
+
'searchText',
|
|
27
|
+
'foundText',
|
|
28
|
+
'similarity',
|
|
29
|
+
'executionTime',
|
|
30
|
+
'exactMatchCount',
|
|
31
|
+
'expectedReplacements',
|
|
32
|
+
'fuzzyThreshold',
|
|
33
|
+
'belowThreshold',
|
|
34
|
+
'diff',
|
|
35
|
+
'searchLength',
|
|
36
|
+
'foundLength',
|
|
37
|
+
'fileExtension',
|
|
38
|
+
'characterCodes',
|
|
39
|
+
'uniqueCharacterCount',
|
|
40
|
+
'diffLength'
|
|
41
|
+
].join('\t');
|
|
42
|
+
await fs.writeFile(this.logPath, headers + '\n');
|
|
43
|
+
}
|
|
44
|
+
this.initialized = true;
|
|
45
|
+
}
|
|
46
|
+
catch (error) {
|
|
47
|
+
console.error('Failed to initialize fuzzy search log file:', error);
|
|
48
|
+
throw error;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
async log(entry) {
|
|
52
|
+
try {
|
|
53
|
+
await this.ensureLogFile();
|
|
54
|
+
// Convert entry to tab-separated string
|
|
55
|
+
const logLine = [
|
|
56
|
+
entry.timestamp.toISOString(),
|
|
57
|
+
entry.searchText.replace(/\n/g, '\\n').replace(/\t/g, '\\t'),
|
|
58
|
+
entry.foundText.replace(/\n/g, '\\n').replace(/\t/g, '\\t'),
|
|
59
|
+
entry.similarity.toString(),
|
|
60
|
+
entry.executionTime.toString(),
|
|
61
|
+
entry.exactMatchCount.toString(),
|
|
62
|
+
entry.expectedReplacements.toString(),
|
|
63
|
+
entry.fuzzyThreshold.toString(),
|
|
64
|
+
entry.belowThreshold.toString(),
|
|
65
|
+
entry.diff.replace(/\n/g, '\\n').replace(/\t/g, '\\t'),
|
|
66
|
+
entry.searchLength.toString(),
|
|
67
|
+
entry.foundLength.toString(),
|
|
68
|
+
entry.fileExtension,
|
|
69
|
+
entry.characterCodes,
|
|
70
|
+
entry.uniqueCharacterCount.toString(),
|
|
71
|
+
entry.diffLength.toString()
|
|
72
|
+
].join('\t');
|
|
73
|
+
await fs.appendFile(this.logPath, logLine + '\n');
|
|
74
|
+
}
|
|
75
|
+
catch (error) {
|
|
76
|
+
console.error('Failed to write to fuzzy search log:', error);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
async getLogPath() {
|
|
80
|
+
await this.ensureLogFile();
|
|
81
|
+
return this.logPath;
|
|
82
|
+
}
|
|
83
|
+
async getRecentLogs(count = 10) {
|
|
84
|
+
try {
|
|
85
|
+
await this.ensureLogFile();
|
|
86
|
+
const content = await fs.readFile(this.logPath, 'utf-8');
|
|
87
|
+
const lines = content.split('\n').filter(line => line.trim());
|
|
88
|
+
// Return last N lines (excluding header)
|
|
89
|
+
return lines.slice(-count - 1, -1);
|
|
90
|
+
}
|
|
91
|
+
catch (error) {
|
|
92
|
+
console.error('Failed to read fuzzy search logs:', error);
|
|
93
|
+
return [];
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
async clearLog() {
|
|
97
|
+
try {
|
|
98
|
+
// Recreate with just headers
|
|
99
|
+
const headers = [
|
|
100
|
+
'timestamp',
|
|
101
|
+
'searchText',
|
|
102
|
+
'foundText',
|
|
103
|
+
'similarity',
|
|
104
|
+
'executionTime',
|
|
105
|
+
'exactMatchCount',
|
|
106
|
+
'expectedReplacements',
|
|
107
|
+
'fuzzyThreshold',
|
|
108
|
+
'belowThreshold',
|
|
109
|
+
'diff',
|
|
110
|
+
'searchLength',
|
|
111
|
+
'foundLength',
|
|
112
|
+
'fileExtension',
|
|
113
|
+
'characterCodes',
|
|
114
|
+
'uniqueCharacterCount',
|
|
115
|
+
'diffLength'
|
|
116
|
+
].join('\t');
|
|
117
|
+
await fs.writeFile(this.logPath, headers + '\n');
|
|
118
|
+
console.log('Fuzzy search log cleared');
|
|
119
|
+
}
|
|
120
|
+
catch (error) {
|
|
121
|
+
console.error('Failed to clear fuzzy search log:', error);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
// Singleton instance
|
|
126
|
+
export const fuzzySearchLogger = new FuzzySearchLogger();
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tools whose structuredContent is for Desktop Commander's own code and tests
|
|
3
|
+
* only: fixes added it next to the text, and a client must keep receiving what
|
|
4
|
+
* it received before (no new information in tool results). It is dropped here,
|
|
5
|
+
* before the result is recorded in the tool-call history or sent. Tools that
|
|
6
|
+
* already sent structuredContent (the read_file/edit widgets, get_config) are
|
|
7
|
+
* not listed and keep it.
|
|
8
|
+
*/
|
|
9
|
+
const INTERNAL_STRUCTURED_CONTENT_TOOLS = new Set([
|
|
10
|
+
'write_pdf',
|
|
11
|
+
'start_process',
|
|
12
|
+
'interact_with_process',
|
|
13
|
+
'list_sessions',
|
|
14
|
+
'start_search',
|
|
15
|
+
'get_more_search_results',
|
|
16
|
+
]);
|
|
17
|
+
/** The result as the client receives it: without structuredContent kept internal */
|
|
18
|
+
export function withoutInternalFacts(toolName, result) {
|
|
19
|
+
if (!INTERNAL_STRUCTURED_CONTENT_TOOLS.has(toolName) || !('structuredContent' in result))
|
|
20
|
+
return result;
|
|
21
|
+
const { structuredContent: _internal, ...sent } = result;
|
|
22
|
+
return sent;
|
|
23
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { logger } from './logger.js';
|
|
2
|
+
/**
|
|
3
|
+
* A package only some files need, declared next to the code that uses it,
|
|
4
|
+
* whose load() loads it on first use (#715). Loading them before initialize
|
|
5
|
+
* took 25-90 s on one reporter's machine and timed the client out, and loading
|
|
6
|
+
* one inside the tool call that first needs it can take longer than a client
|
|
7
|
+
* waits. So the server preloads them right after initialize, one at a time
|
|
8
|
+
* (preloadFileSupport() in utils/files/factory.ts), and until one is loaded the
|
|
9
|
+
* calls that need it answer at once that it is still loading. preload() loads
|
|
10
|
+
* it on a later turn of the event loop, so a call that starts it answers
|
|
11
|
+
* first. `error` says why the last preload failed. Callers that load with
|
|
12
|
+
* import() can mark failures as requiring a restart, because Node caches a
|
|
13
|
+
* failed dynamic import.
|
|
14
|
+
*/
|
|
15
|
+
export class LazyPackage {
|
|
16
|
+
constructor(name, support, load, restartAfterFailure = false) {
|
|
17
|
+
this.name = name;
|
|
18
|
+
this.support = support;
|
|
19
|
+
this.load = load;
|
|
20
|
+
this.restartAfterFailure = restartAfterFailure;
|
|
21
|
+
this.loaded = false;
|
|
22
|
+
this.needsRestart = false;
|
|
23
|
+
}
|
|
24
|
+
preload() {
|
|
25
|
+
this.running ?? (this.running = new Promise((resolve) => setImmediate(resolve)).then(async () => {
|
|
26
|
+
let loading;
|
|
27
|
+
try {
|
|
28
|
+
loading = this.load();
|
|
29
|
+
await loading;
|
|
30
|
+
this.loaded = true;
|
|
31
|
+
this.error = undefined;
|
|
32
|
+
}
|
|
33
|
+
catch (error) {
|
|
34
|
+
this.error = error instanceof Error ? error.message : String(error);
|
|
35
|
+
this.needsRestart = this.restartAfterFailure;
|
|
36
|
+
logger.error(`Loading ${this.name} failed: ${this.error}`);
|
|
37
|
+
}
|
|
38
|
+
finally {
|
|
39
|
+
this.running = undefined;
|
|
40
|
+
}
|
|
41
|
+
}));
|
|
42
|
+
return this.running;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Detect the line ending style used in a file - Optimized version
|
|
3
|
+
* This algorithm uses early termination for maximum performance
|
|
4
|
+
*/
|
|
5
|
+
export function detectLineEnding(content) {
|
|
6
|
+
for (let i = 0; i < content.length; i++) {
|
|
7
|
+
if (content[i] === '\r') {
|
|
8
|
+
if (i + 1 < content.length && content[i + 1] === '\n') {
|
|
9
|
+
return '\r\n';
|
|
10
|
+
}
|
|
11
|
+
return '\r';
|
|
12
|
+
}
|
|
13
|
+
if (content[i] === '\n') {
|
|
14
|
+
return '\n';
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
// Default to system line ending if no line endings found
|
|
18
|
+
return process.platform === 'win32' ? '\r\n' : '\n';
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Normalize line endings to match the target style
|
|
22
|
+
*/
|
|
23
|
+
export function normalizeLineEndings(text, targetLineEnding) {
|
|
24
|
+
// First normalize to LF
|
|
25
|
+
let normalized = text.replace(/\r\n/g, '\n').replace(/\r/g, '\n');
|
|
26
|
+
// Then convert to target
|
|
27
|
+
if (targetLineEnding === '\r\n') {
|
|
28
|
+
return normalized.replace(/\n/g, '\r\n');
|
|
29
|
+
}
|
|
30
|
+
else if (targetLineEnding === '\r') {
|
|
31
|
+
return normalized.replace(/\n/g, '\r');
|
|
32
|
+
}
|
|
33
|
+
return normalized;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Analyze line ending usage in content
|
|
37
|
+
*/
|
|
38
|
+
export function analyzeLineEndings(content) {
|
|
39
|
+
let crlfCount = 0;
|
|
40
|
+
let lfCount = 0;
|
|
41
|
+
let crCount = 0;
|
|
42
|
+
// Count line endings
|
|
43
|
+
for (let i = 0; i < content.length; i++) {
|
|
44
|
+
if (content[i] === '\r') {
|
|
45
|
+
if (i + 1 < content.length && content[i + 1] === '\n') {
|
|
46
|
+
crlfCount++;
|
|
47
|
+
i++; // Skip the LF
|
|
48
|
+
}
|
|
49
|
+
else {
|
|
50
|
+
crCount++;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
else if (content[i] === '\n') {
|
|
54
|
+
lfCount++;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
// Determine predominant style
|
|
58
|
+
const total = crlfCount + lfCount + crCount;
|
|
59
|
+
let style;
|
|
60
|
+
if (crlfCount > lfCount && crlfCount > crCount) {
|
|
61
|
+
style = '\r\n';
|
|
62
|
+
}
|
|
63
|
+
else if (lfCount > crCount) {
|
|
64
|
+
style = '\n';
|
|
65
|
+
}
|
|
66
|
+
else {
|
|
67
|
+
style = '\r';
|
|
68
|
+
}
|
|
69
|
+
// Check for mixed line endings
|
|
70
|
+
const usedStyles = [crlfCount > 0, lfCount > 0, crCount > 0].filter(Boolean).length;
|
|
71
|
+
const hasMixed = usedStyles > 1;
|
|
72
|
+
return {
|
|
73
|
+
style,
|
|
74
|
+
count: total,
|
|
75
|
+
hasMixed
|
|
76
|
+
};
|
|
77
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Centralized logging utility for Desktop Commander
|
|
3
|
+
* Ensures all logging goes through proper channels based on initialization state
|
|
4
|
+
*/
|
|
5
|
+
/**
|
|
6
|
+
* Log a message using the appropriate method based on MCP initialization state
|
|
7
|
+
*/
|
|
8
|
+
export function log(level, message, data) {
|
|
9
|
+
try {
|
|
10
|
+
// Check if MCP transport is available
|
|
11
|
+
if (global.mcpTransport) {
|
|
12
|
+
// Always use MCP logging (will buffer if not initialized yet)
|
|
13
|
+
global.mcpTransport.sendLog(level, message, data);
|
|
14
|
+
}
|
|
15
|
+
else {
|
|
16
|
+
// This should rarely happen, but fallback to create a JSON-RPC notification manually
|
|
17
|
+
const notification = {
|
|
18
|
+
jsonrpc: "2.0",
|
|
19
|
+
method: "notifications/message",
|
|
20
|
+
params: {
|
|
21
|
+
level: level,
|
|
22
|
+
logger: "desktop-commander",
|
|
23
|
+
data: data ? { message, ...data } : message
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
process.stdout.write(JSON.stringify(notification) + '\n');
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
catch (error) {
|
|
30
|
+
// Ultimate fallback - but this should be JSON-RPC too
|
|
31
|
+
const notification = {
|
|
32
|
+
jsonrpc: "2.0",
|
|
33
|
+
method: "notifications/message",
|
|
34
|
+
params: {
|
|
35
|
+
level: "error",
|
|
36
|
+
logger: "desktop-commander",
|
|
37
|
+
data: `[LOG-ERROR] Failed to log message: ${message}`
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
process.stdout.write(JSON.stringify(notification) + '\n');
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Convenience functions for different log levels
|
|
45
|
+
*/
|
|
46
|
+
export const logger = {
|
|
47
|
+
emergency: (message, data) => log('emergency', message, data),
|
|
48
|
+
alert: (message, data) => log('alert', message, data),
|
|
49
|
+
critical: (message, data) => log('critical', message, data),
|
|
50
|
+
error: (message, data) => log('error', message, data),
|
|
51
|
+
warning: (message, data) => log('warning', message, data),
|
|
52
|
+
notice: (message, data) => log('notice', message, data),
|
|
53
|
+
info: (message, data) => log('info', message, data),
|
|
54
|
+
debug: (message, data) => log('debug', message, data),
|
|
55
|
+
};
|
|
56
|
+
/**
|
|
57
|
+
* Log to stderr during early initialization (before MCP is ready)
|
|
58
|
+
* Use this for critical startup messages that must be visible
|
|
59
|
+
* NOTE: This should also be JSON-RPC format
|
|
60
|
+
*/
|
|
61
|
+
export function logToStderr(level, message) {
|
|
62
|
+
const notification = {
|
|
63
|
+
jsonrpc: "2.0",
|
|
64
|
+
method: "notifications/message",
|
|
65
|
+
params: {
|
|
66
|
+
level: level,
|
|
67
|
+
logger: "desktop-commander",
|
|
68
|
+
data: message
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
process.stdout.write(JSON.stringify(notification) + '\n');
|
|
72
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { configManager } from '../config-manager.js';
|
|
2
|
+
import { getABTestVariant } from './ab-test.js';
|
|
3
|
+
import { capture } from './capture.js';
|
|
4
|
+
import { featureFlagManager } from './feature-flags.js';
|
|
5
|
+
export const MCP_UI_EXPERIMENT_NAME = 'McpUiPreviews';
|
|
6
|
+
export const MCP_UI_SHOW_VARIANT = 'showMCPUi';
|
|
7
|
+
export const MCP_UI_HIDE_VARIANT = 'notShowMCPUi';
|
|
8
|
+
function variantEnablesMcpUi(variant) {
|
|
9
|
+
if (variant === MCP_UI_HIDE_VARIANT)
|
|
10
|
+
return false;
|
|
11
|
+
if (variant === MCP_UI_SHOW_VARIANT)
|
|
12
|
+
return true;
|
|
13
|
+
return null;
|
|
14
|
+
}
|
|
15
|
+
export async function resolveMcpUiPreviewDecision(deps) {
|
|
16
|
+
try {
|
|
17
|
+
const existingAssignment = await deps.getExistingAssignment();
|
|
18
|
+
const existingDecision = variantEnablesMcpUi(existingAssignment);
|
|
19
|
+
if (existingDecision !== null) {
|
|
20
|
+
if (!deps.wasLoadedFromCache()) {
|
|
21
|
+
await deps.waitForFreshFlags();
|
|
22
|
+
}
|
|
23
|
+
const currentVariant = await deps.getABTestVariant(MCP_UI_EXPERIMENT_NAME);
|
|
24
|
+
return variantEnablesMcpUi(currentVariant) ?? existingDecision;
|
|
25
|
+
}
|
|
26
|
+
if (!deps.isFirstRun()) {
|
|
27
|
+
return true;
|
|
28
|
+
}
|
|
29
|
+
if (!deps.wasLoadedFromCache()) {
|
|
30
|
+
await deps.waitForFreshFlags();
|
|
31
|
+
}
|
|
32
|
+
const variant = await deps.getABTestVariant(MCP_UI_EXPERIMENT_NAME);
|
|
33
|
+
const decision = variantEnablesMcpUi(variant);
|
|
34
|
+
if (decision === null) {
|
|
35
|
+
return true;
|
|
36
|
+
}
|
|
37
|
+
try {
|
|
38
|
+
await deps.capture('server_mcp_ui_ab_decision', {
|
|
39
|
+
experiment: MCP_UI_EXPERIMENT_NAME,
|
|
40
|
+
variant,
|
|
41
|
+
mcp_ui_enabled: decision,
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
// Telemetry must not change the assigned product experience.
|
|
46
|
+
}
|
|
47
|
+
return decision;
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
return true;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
export async function shouldShowMcpUiPreviews() {
|
|
54
|
+
return resolveMcpUiPreviewDecision({
|
|
55
|
+
getExistingAssignment: () => configManager.getValue(`abTest_${MCP_UI_EXPERIMENT_NAME}`),
|
|
56
|
+
isFirstRun: () => configManager.isFirstRun(),
|
|
57
|
+
wasLoadedFromCache: () => featureFlagManager.wasLoadedFromCache(),
|
|
58
|
+
waitForFreshFlags: () => featureFlagManager.waitForFreshFlags(),
|
|
59
|
+
getABTestVariant,
|
|
60
|
+
capture,
|
|
61
|
+
});
|
|
62
|
+
}
|
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* REPL and Process State Detection Utilities
|
|
3
|
+
* Detects from its output whether a running process is waiting for input.
|
|
4
|
+
* Whether it has finished is known from its exit, never from its output
|
|
5
|
+
* (see TerminalManager.getProcessState).
|
|
6
|
+
*/
|
|
7
|
+
// Common REPL prompts. Most are generic ("... ", "> ", "+ ", "| "): inside or
|
|
8
|
+
// at the end of a longer line they are ordinary text (pytest -v's "collecting
|
|
9
|
+
// ... ", #196; a markdown table's "| a | b |"), so they count only in a last
|
|
10
|
+
// line made of prompts alone: the prompt itself, the prompts a REPL that
|
|
11
|
+
// doesn't echo input printed one after another ("> | " from node -i), or,
|
|
12
|
+
// from a REPL that writes its prompts to stderr (python -i, bash -i), the
|
|
13
|
+
// prompts it wrote there (">>> ... ").
|
|
14
|
+
const REPL_PROMPTS = {
|
|
15
|
+
python: ['>>> ', '... '],
|
|
16
|
+
node: ['> ', '... ', '| '], // "| ": Node 24's continuation, "... " before it
|
|
17
|
+
r: ['> ', '+ '],
|
|
18
|
+
julia: ['julia> ', ' '], // julia continuation is spaces
|
|
19
|
+
shell: ['$ ', '# ', '% '],
|
|
20
|
+
mysql: ['mysql> ', ' -> '],
|
|
21
|
+
postgres: ['=# ', '-# '],
|
|
22
|
+
redis: ['redis> '],
|
|
23
|
+
mongo: ['> ', '... '],
|
|
24
|
+
powershell: ['>> '] // continuation; after "> ", so it is reported as "> ", as before
|
|
25
|
+
};
|
|
26
|
+
const PROMPTS = [...new Set(Object.values(REPL_PROMPTS).flat())];
|
|
27
|
+
const PROMPTS_ONLY = new RegExp(`^(?:${PROMPTS.map(escapeRegExp).join('|')})+$`);
|
|
28
|
+
// Prompts that also count at the end of a longer line: named ones, after
|
|
29
|
+
// output that didn't end in a newline ("done>>> "), and a shell or psql
|
|
30
|
+
// prompt's end ("bash-5.2$ ", "user@host dir % ", "postgres=# ")
|
|
31
|
+
const LINE_END_PROMPTS = ['>>> ', 'julia> ', 'mysql> ', 'redis> ', '$ ', '# ', '% '];
|
|
32
|
+
// PowerShell's prompt (powershell.exe, pwsh), at the end of a line: "PS ", a
|
|
33
|
+
// location (a drive, provider or path: "C:\Users\me", "/Users/me", "HKLM:\"),
|
|
34
|
+
// then "> " (">> " in a nested or debugger prompt, "[DBG]: PS C:\>> ")
|
|
35
|
+
const POWERSHELL_PROMPT_TEXT = String.raw `PS [^<>|\n]*[\\/:][^<>|\n]*>>? `;
|
|
36
|
+
const POWERSHELL_PROMPT = new RegExp(`${POWERSHELL_PROMPT_TEXT}$`);
|
|
37
|
+
// cmd.exe's prompt: the whole last line is a path and ">", with nothing after
|
|
38
|
+
// it ("C:\Users\me>"); output lines end in a newline, a prompt doesn't
|
|
39
|
+
const CMD_PROMPT_TEXT = String.raw `[A-Za-z]:\\[^<>|"*?\n]*>`;
|
|
40
|
+
const CMD_PROMPT = new RegExp(`^${CMD_PROMPT_TEXT}$`);
|
|
41
|
+
// The same shell prompts at the start of a line, where the shell echoes the
|
|
42
|
+
// next input line after them ("PS C:\Users\me> echo b", "C:\Users\me>echo b")
|
|
43
|
+
const SHELL_PROMPTS_AT_START = [new RegExp(`^${POWERSHELL_PROMPT_TEXT}`), new RegExp(`^${CMD_PROMPT_TEXT}`)];
|
|
44
|
+
// The REPL prompts output cleaning removes, one or more in a row at the start
|
|
45
|
+
// of a line (">>> ... " from a REPL that reads several lines at once)
|
|
46
|
+
const REPL_PROMPT = String.raw `(?:>>> |\.\.\. |>> |> |\| |\+ )`;
|
|
47
|
+
const REPL_PROMPT_RUN = new RegExp(`^${REPL_PROMPT}+`);
|
|
48
|
+
const REPL_PROMPTS_ALONE = new RegExp(`^${REPL_PROMPT}+$`);
|
|
49
|
+
const REPL_PROMPT_TOKEN = new RegExp(REPL_PROMPT, 'g');
|
|
50
|
+
// The prompts of a REPL waiting for more of a statement (Python, Node, PowerShell, R)
|
|
51
|
+
const CONTINUATION_PROMPTS = ['... ', '| ', '>> ', '+ '];
|
|
52
|
+
/** The prompt the last line of output is, or ends in, if any */
|
|
53
|
+
function findPrompt(lastLine) {
|
|
54
|
+
if (PROMPTS_ONLY.test(lastLine)) {
|
|
55
|
+
return PROMPTS.find(prompt => lastLine.endsWith(prompt));
|
|
56
|
+
}
|
|
57
|
+
const lineEnd = LINE_END_PROMPTS.find(prompt => lastLine.endsWith(prompt));
|
|
58
|
+
if (lineEnd)
|
|
59
|
+
return lineEnd;
|
|
60
|
+
// Reported as "> ", which a PowerShell prompt was reported as before #196
|
|
61
|
+
if (POWERSHELL_PROMPT.test(lastLine) || CMD_PROMPT.test(lastLine))
|
|
62
|
+
return '> ';
|
|
63
|
+
return undefined;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* How much of the end of the output state detection examines. A prompt is
|
|
67
|
+
* the last thing a process writes before it waits for input, so the end of
|
|
68
|
+
* the output decides the state; examining only this much keeps detection's
|
|
69
|
+
* cost the same however much output there is.
|
|
70
|
+
*/
|
|
71
|
+
export const STATE_DETECTION_TAIL_CHARS = 4096;
|
|
72
|
+
/**
|
|
73
|
+
* Analyze a running process's output to determine whether it is waiting for
|
|
74
|
+
* input. Output text never makes a process finished: a line such as
|
|
75
|
+
* "Error: retrying" doesn't end it.
|
|
76
|
+
*
|
|
77
|
+
* `output` is what the process wrote most recently. For a process writing to
|
|
78
|
+
* both stdout and stderr it can be the recent output of each stream that may
|
|
79
|
+
* have written last (see TerminalManager.getProcessState): their relative
|
|
80
|
+
* order is unknown, so the process is waiting for input if any of them ends
|
|
81
|
+
* in a prompt. Only the last STATE_DETECTION_TAIL_CHARS of each are examined.
|
|
82
|
+
*/
|
|
83
|
+
export function analyzeProcessState(output, pid) {
|
|
84
|
+
const tails = (typeof output === 'string' ? [output] : output)
|
|
85
|
+
.map(text => text.slice(-STATE_DETECTION_TAIL_CHARS))
|
|
86
|
+
.filter(text => text.trim().length > 0);
|
|
87
|
+
const states = tails.map(analyzeOutputTail);
|
|
88
|
+
return states.find(state => state.isWaitingForInput)
|
|
89
|
+
?? {
|
|
90
|
+
isWaitingForInput: false,
|
|
91
|
+
isFinished: false,
|
|
92
|
+
isRunning: true,
|
|
93
|
+
lastOutput: tails.join('\n')
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* The state the end of one stream's output shows (see analyzeProcessState).
|
|
98
|
+
*/
|
|
99
|
+
function analyzeOutputTail(output) {
|
|
100
|
+
const lines = output.split('\n');
|
|
101
|
+
const lastLine = lines[lines.length - 1] || '';
|
|
102
|
+
// Check for REPL prompts (waiting for input)
|
|
103
|
+
const detectedPrompt = findPrompt(lastLine);
|
|
104
|
+
if (detectedPrompt) {
|
|
105
|
+
return {
|
|
106
|
+
isWaitingForInput: true,
|
|
107
|
+
isFinished: false,
|
|
108
|
+
isRunning: true,
|
|
109
|
+
detectedPrompt,
|
|
110
|
+
lastOutput: output
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
// Default: process is running, not waiting for input
|
|
114
|
+
return {
|
|
115
|
+
isWaitingForInput: false,
|
|
116
|
+
isFinished: false,
|
|
117
|
+
isRunning: true,
|
|
118
|
+
lastOutput: output
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Clean output by removing prompts and input echoes. `readAtPrompt`: whether
|
|
123
|
+
* the process was waiting at a prompt when the input was sent; one that
|
|
124
|
+
* wasn't (a shell reading commands without a prompt) prints none for it.
|
|
125
|
+
*/
|
|
126
|
+
export function cleanProcessOutput(output, inputSent, readAtPrompt = true) {
|
|
127
|
+
// A process at a prompt prints one prompt for each input line it reads
|
|
128
|
+
const printed = readAtPrompt && inputSent !== undefined ? inputLinesSent(inputSent).length : 0;
|
|
129
|
+
const withoutEcho = removeInputEcho(output, inputSent);
|
|
130
|
+
const cleaned = removePrompts(withoutEcho.output, printed - withoutEcho.prompts);
|
|
131
|
+
// A line repeating the input is the answer, not an echo, when nothing else
|
|
132
|
+
// came back: node -i answers `10` with "10", which was removed as the echo.
|
|
133
|
+
// Not at a continuation prompt: the process waits for more of the input,
|
|
134
|
+
// so it hasn't answered it
|
|
135
|
+
if (!cleaned && withoutEcho.output !== output && !waitsAtContinuationPrompt(output)) {
|
|
136
|
+
return removePrompts(output, printed);
|
|
137
|
+
}
|
|
138
|
+
return cleaned;
|
|
139
|
+
}
|
|
140
|
+
/** Whether the output ends at a continuation prompt (">> " after "if ($true) {") */
|
|
141
|
+
function waitsAtContinuationPrompt(output) {
|
|
142
|
+
const lastLine = output.slice(output.lastIndexOf('\n') + 1);
|
|
143
|
+
if (!REPL_PROMPTS_ALONE.test(lastLine))
|
|
144
|
+
return false;
|
|
145
|
+
const prompts = lastLine.match(REPL_PROMPT_TOKEN) ?? [];
|
|
146
|
+
return CONTINUATION_PROMPTS.includes(prompts[prompts.length - 1]);
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* The output without the process's echo of the input, if it echoed it. A
|
|
150
|
+
* process that echoes (powershell.exe, cmd.exe, a terminal over ssh -t)
|
|
151
|
+
* repeats each input line where the output for it starts: the first line at
|
|
152
|
+
* the start of the output, each later one after the prompt it was read at
|
|
153
|
+
* ("PS C:\project> echo b", "C:\project>echo b", ">> }"). A line the process
|
|
154
|
+
* has read (it printed a prompt for it) that isn't repeated there means the
|
|
155
|
+
* process doesn't echo, and a line equal to an input line is output (node -i
|
|
156
|
+
* answers `1` with "1"): the output is kept as it is. Lines not read yet (the
|
|
157
|
+
* call answered at an earlier prompt) have no echo yet. Also returns how many
|
|
158
|
+
* prompts went with the echoes.
|
|
159
|
+
*/
|
|
160
|
+
function removeInputEcho(output, inputSent) {
|
|
161
|
+
if (!inputSent)
|
|
162
|
+
return { output, prompts: 0 };
|
|
163
|
+
const lines = output.split('\n');
|
|
164
|
+
const echoes = new Set();
|
|
165
|
+
let next = 0;
|
|
166
|
+
for (const [i, inputLine] of inputLinesSent(inputSent).entries()) {
|
|
167
|
+
const echo = inputLine.trim();
|
|
168
|
+
if (!echo)
|
|
169
|
+
continue; // An empty line's echo can't be told from an empty output line
|
|
170
|
+
const at = i === 0
|
|
171
|
+
? (isEchoLine(lines, 0, echo, true) ? 0 : -1)
|
|
172
|
+
: lines.findIndex((_, index) => index >= next && isEchoLine(lines, index, echo, false));
|
|
173
|
+
if (at < 0) {
|
|
174
|
+
// A process prints one prompt for each line it reads
|
|
175
|
+
const linesRead = lines.reduce((count, line) => count + promptsAtStart(line), 0);
|
|
176
|
+
if (linesRead > i)
|
|
177
|
+
return { output, prompts: 0 };
|
|
178
|
+
break;
|
|
179
|
+
}
|
|
180
|
+
echoes.add(at);
|
|
181
|
+
next = at + 1;
|
|
182
|
+
}
|
|
183
|
+
return {
|
|
184
|
+
output: lines.filter((_, index) => !echoes.has(index)).join('\n'),
|
|
185
|
+
prompts: [...echoes].reduce((count, index) => count + promptsAtStart(lines[index]), 0),
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
/** How many prompts `line` starts with: REPL prompts, one or more in a row, or a shell's prompt */
|
|
189
|
+
function promptsAtStart(line) {
|
|
190
|
+
const run = REPL_PROMPT_RUN.exec(line)?.[0];
|
|
191
|
+
if (run)
|
|
192
|
+
return run.match(REPL_PROMPT_TOKEN)?.length ?? 0;
|
|
193
|
+
return SHELL_PROMPTS_AT_START.some(prompt => prompt.test(line)) ? 1 : 0;
|
|
194
|
+
}
|
|
195
|
+
/** The lines of an input as the process reads them (sendInputToProcess ends it with a newline) */
|
|
196
|
+
function inputLinesSent(input) {
|
|
197
|
+
return (input.endsWith('\n') ? input.slice(0, -1) : input).split('\n');
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* Whether lines[index] is the echo of an input line: alone on the line (the
|
|
201
|
+
* first input line's, at the start of the output) or after a prompt (a later
|
|
202
|
+
* line's), and ended by the newline that was sent with it
|
|
203
|
+
*/
|
|
204
|
+
function isEchoLine(lines, index, echo, alone) {
|
|
205
|
+
if (index >= lines.length - 1)
|
|
206
|
+
return false;
|
|
207
|
+
const line = lines[index].trimEnd();
|
|
208
|
+
if (!line.endsWith(echo))
|
|
209
|
+
return false;
|
|
210
|
+
const before = line.slice(0, line.length - echo.length).trimEnd();
|
|
211
|
+
if (alone)
|
|
212
|
+
return before === '';
|
|
213
|
+
return before !== '' && (findPrompt(before) !== undefined || findPrompt(`${before} `) !== undefined);
|
|
214
|
+
}
|
|
215
|
+
/**
|
|
216
|
+
* The output without the REPL prompts the process printed for the input (at
|
|
217
|
+
* most `printed`), and nothing that only looks like one. The last line, when
|
|
218
|
+
* it is a prompt, is where the process waits now (a REPL's is removed; a
|
|
219
|
+
* shell's, "PS C:\project> ", stays in the output as before). The others
|
|
220
|
+
* start a line: the one where the output for the next input line begins
|
|
221
|
+
* ("1\n> 2\n> " for `1` and `2` sent to node -i). More prompt-like line starts
|
|
222
|
+
* than that means some are output ("+ added" in a diff, "> quoted"), and
|
|
223
|
+
* those lines are all kept as they are.
|
|
224
|
+
*/
|
|
225
|
+
function removePrompts(output, printed) {
|
|
226
|
+
const lines = output.split('\n');
|
|
227
|
+
const last = lines.length - 1;
|
|
228
|
+
let left = printed;
|
|
229
|
+
if (findPrompt(lines[last]) !== undefined) {
|
|
230
|
+
left -= Math.max(1, promptsAtStart(lines[last]));
|
|
231
|
+
if (REPL_PROMPTS_ALONE.test(lines[last]))
|
|
232
|
+
lines[last] = '';
|
|
233
|
+
}
|
|
234
|
+
const runs = lines.map((line, index) => (index < last ? REPL_PROMPT_RUN.exec(line)?.[0] ?? '' : ''));
|
|
235
|
+
const found = runs.reduce((count, run) => count + (run.match(REPL_PROMPT_TOKEN)?.length ?? 0), 0);
|
|
236
|
+
if (found <= left) {
|
|
237
|
+
runs.forEach((run, index) => { lines[index] = lines[index].slice(run.length); });
|
|
238
|
+
}
|
|
239
|
+
return lines.join('\n').trim();
|
|
240
|
+
}
|
|
241
|
+
/**
|
|
242
|
+
* Escape special regex characters
|
|
243
|
+
*/
|
|
244
|
+
function escapeRegExp(string) {
|
|
245
|
+
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
246
|
+
}
|
|
247
|
+
/**
|
|
248
|
+
* Format process state for user display
|
|
249
|
+
*/
|
|
250
|
+
export function formatProcessStateMessage(state, pid) {
|
|
251
|
+
if (state.isWaitingForInput) {
|
|
252
|
+
return `Process ${pid} is waiting for input${state.detectedPrompt ? ` (detected: "${state.detectedPrompt.trim()}")` : ''}`;
|
|
253
|
+
}
|
|
254
|
+
else if (state.isFinished) {
|
|
255
|
+
return `Process ${pid} has finished execution`;
|
|
256
|
+
}
|
|
257
|
+
else {
|
|
258
|
+
return `Process ${pid} is running`;
|
|
259
|
+
}
|
|
260
|
+
}
|