starmux 0.11.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.
Files changed (136) hide show
  1. package/CONTRIBUTING.md +1 -1
  2. package/README.md +169 -97
  3. package/SECURITY.md +3 -3
  4. package/dist/src/access-control.js +23 -20
  5. package/dist/src/access-control.js.map +1 -1
  6. package/dist/src/access-dashboard.js +8 -2
  7. package/dist/src/access-dashboard.js.map +1 -1
  8. package/dist/src/cli/instances.js +87 -14
  9. package/dist/src/cli/instances.js.map +1 -1
  10. package/dist/src/cli/mac-mcp-gateway.js +4 -3
  11. package/dist/src/cli/mac-mcp-gateway.js.map +1 -1
  12. package/dist/src/cli/starmux-status.js +122 -0
  13. package/dist/src/cli/starmux-status.js.map +1 -0
  14. package/dist/src/cli/starmux.js +222 -49
  15. package/dist/src/cli/starmux.js.map +1 -1
  16. package/dist/src/deny-dashboard.js +2 -2
  17. package/dist/src/deny-dashboard.js.map +1 -1
  18. package/dist/src/instance-runtime.js +11 -2
  19. package/dist/src/instance-runtime.js.map +1 -1
  20. package/dist/src/instance-store.js +3 -1
  21. package/dist/src/instance-store.js.map +1 -1
  22. package/dist/src/ops-dashboard.js +4 -4
  23. package/dist/src/ops-dashboard.js.map +1 -1
  24. package/dist/src/permissions-dashboard.js +2 -2
  25. package/dist/src/permissions-dashboard.js.map +1 -1
  26. package/dist/src/runtime-paths.js +3 -3
  27. package/dist/src/runtime-paths.js.map +1 -1
  28. package/dist/src/starmux-migration.js +1 -1
  29. package/dist/src/starmux-migration.js.map +1 -1
  30. package/dist/vendor/desktop-commander/LICENSE +21 -0
  31. package/dist/vendor/desktop-commander/dist/bootstrap.js +22 -0
  32. package/dist/vendor/desktop-commander/dist/command-manager.js +246 -0
  33. package/dist/vendor/desktop-commander/dist/config-field-definitions.js +37 -0
  34. package/dist/vendor/desktop-commander/dist/config-manager.js +634 -0
  35. package/dist/vendor/desktop-commander/dist/config-recovery.js +81 -0
  36. package/dist/vendor/desktop-commander/dist/config.js +19 -0
  37. package/dist/vendor/desktop-commander/dist/custom-stdio.js +358 -0
  38. package/dist/vendor/desktop-commander/dist/error-handlers.js +15 -0
  39. package/dist/vendor/desktop-commander/dist/handlers/edit-search-handlers.js +6 -0
  40. package/dist/vendor/desktop-commander/dist/handlers/filesystem-handlers.js +450 -0
  41. package/dist/vendor/desktop-commander/dist/handlers/history-handlers.js +67 -0
  42. package/dist/vendor/desktop-commander/dist/handlers/index.js +7 -0
  43. package/dist/vendor/desktop-commander/dist/handlers/process-handlers.js +15 -0
  44. package/dist/vendor/desktop-commander/dist/handlers/search-answers.js +172 -0
  45. package/dist/vendor/desktop-commander/dist/handlers/search-handlers.js +136 -0
  46. package/dist/vendor/desktop-commander/dist/handlers/terminal-handlers.js +35 -0
  47. package/dist/vendor/desktop-commander/dist/index.js +88 -0
  48. package/dist/vendor/desktop-commander/dist/search-manager.js +1262 -0
  49. package/dist/vendor/desktop-commander/dist/server.js +1414 -0
  50. package/dist/vendor/desktop-commander/dist/terminal-manager.js +757 -0
  51. package/dist/vendor/desktop-commander/dist/tools/config.js +211 -0
  52. package/dist/vendor/desktop-commander/dist/tools/edit.js +455 -0
  53. package/dist/vendor/desktop-commander/dist/tools/feedback.js +10 -0
  54. package/dist/vendor/desktop-commander/dist/tools/filesystem.js +1047 -0
  55. package/dist/vendor/desktop-commander/dist/tools/fuzzySearch.js +74 -0
  56. package/dist/vendor/desktop-commander/dist/tools/fuzzySearchCore.js +125 -0
  57. package/dist/vendor/desktop-commander/dist/tools/improved-process-tools.js +726 -0
  58. package/dist/vendor/desktop-commander/dist/tools/mime-types.js +28 -0
  59. package/dist/vendor/desktop-commander/dist/tools/pdf/extract-images.js +131 -0
  60. package/dist/vendor/desktop-commander/dist/tools/pdf/index.js +4 -0
  61. package/dist/vendor/desktop-commander/dist/tools/pdf/lib/pdf2md.js +78 -0
  62. package/dist/vendor/desktop-commander/dist/tools/pdf/manipulations.js +112 -0
  63. package/dist/vendor/desktop-commander/dist/tools/pdf/markdown.js +30 -0
  64. package/dist/vendor/desktop-commander/dist/tools/pdf/utils.js +45 -0
  65. package/dist/vendor/desktop-commander/dist/tools/process.js +90 -0
  66. package/dist/vendor/desktop-commander/dist/tools/prompts.js +259 -0
  67. package/dist/vendor/desktop-commander/dist/tools/schemas.js +242 -0
  68. package/dist/vendor/desktop-commander/dist/tools/usage.js +24 -0
  69. package/dist/vendor/desktop-commander/dist/types.js +1 -0
  70. package/dist/vendor/desktop-commander/dist/ui/config-editor/config-editor-runtime.js +150 -0
  71. package/dist/vendor/desktop-commander/dist/ui/config-editor/index.html +13 -0
  72. package/dist/vendor/desktop-commander/dist/ui/config-editor/styles.css +587 -0
  73. package/dist/vendor/desktop-commander/dist/ui/contracts.js +57 -0
  74. package/dist/vendor/desktop-commander/dist/ui/file-preview/index.html +16 -0
  75. package/dist/vendor/desktop-commander/dist/ui/file-preview/preview-runtime.js +471 -0
  76. package/dist/vendor/desktop-commander/dist/ui/file-preview/shared/preview-file-types.js +59 -0
  77. package/dist/vendor/desktop-commander/dist/ui/file-preview/styles.css +1631 -0
  78. package/dist/vendor/desktop-commander/dist/ui/resources.js +93 -0
  79. package/dist/vendor/desktop-commander/dist/utils/ab-test.js +122 -0
  80. package/dist/vendor/desktop-commander/dist/utils/atomic-write.js +80 -0
  81. package/dist/vendor/desktop-commander/dist/utils/capture.js +40 -0
  82. package/dist/vendor/desktop-commander/dist/utils/dockerPrompt.js +9 -0
  83. package/dist/vendor/desktop-commander/dist/utils/exit-process.js +31 -0
  84. package/dist/vendor/desktop-commander/dist/utils/feature-flags.js +18 -0
  85. package/dist/vendor/desktop-commander/dist/utils/files/base.js +5 -0
  86. package/dist/vendor/desktop-commander/dist/utils/files/binary.js +65 -0
  87. package/dist/vendor/desktop-commander/dist/utils/files/docx.js +694 -0
  88. package/dist/vendor/desktop-commander/dist/utils/files/excel.js +497 -0
  89. package/dist/vendor/desktop-commander/dist/utils/files/factory.js +168 -0
  90. package/dist/vendor/desktop-commander/dist/utils/files/image.js +89 -0
  91. package/dist/vendor/desktop-commander/dist/utils/files/index.js +14 -0
  92. package/dist/vendor/desktop-commander/dist/utils/files/pdf.js +124 -0
  93. package/dist/vendor/desktop-commander/dist/utils/files/text.js +423 -0
  94. package/dist/vendor/desktop-commander/dist/utils/fuzzySearchLogger.js +126 -0
  95. package/dist/vendor/desktop-commander/dist/utils/internal-facts.js +23 -0
  96. package/dist/vendor/desktop-commander/dist/utils/lazy-package.js +44 -0
  97. package/dist/vendor/desktop-commander/dist/utils/lineEndingHandler.js +77 -0
  98. package/dist/vendor/desktop-commander/dist/utils/logger.js +72 -0
  99. package/dist/vendor/desktop-commander/dist/utils/mcp-ui-ab-test.js +62 -0
  100. package/dist/vendor/desktop-commander/dist/utils/open-browser.js +2 -0
  101. package/dist/vendor/desktop-commander/dist/utils/process-detection.js +260 -0
  102. package/dist/vendor/desktop-commander/dist/utils/process-tree.js +223 -0
  103. package/dist/vendor/desktop-commander/dist/utils/rename.js +74 -0
  104. package/dist/vendor/desktop-commander/dist/utils/ripgrep-resolver.js +69 -0
  105. package/dist/vendor/desktop-commander/dist/utils/shell.js +187 -0
  106. package/dist/vendor/desktop-commander/dist/utils/system-info.js +727 -0
  107. package/dist/vendor/desktop-commander/dist/utils/terminal-timestamps.js +18 -0
  108. package/dist/vendor/desktop-commander/dist/utils/toolHistory.js +309 -0
  109. package/dist/vendor/desktop-commander/dist/utils/trackTools.js +55 -0
  110. package/dist/vendor/desktop-commander/dist/utils/unsupportedParams.js +66 -0
  111. package/dist/vendor/desktop-commander/dist/utils/usageTracker.js +225 -0
  112. package/dist/vendor/desktop-commander/dist/utils/welcome-onboarding.js +2 -0
  113. package/dist/vendor/desktop-commander/dist/utils/withTimeout.js +85 -0
  114. package/dist/vendor/desktop-commander/dist/version.js +1 -0
  115. package/dist/vendor/desktop-commander/package.json +15 -0
  116. package/docs/ACCESS_CONTROL.md +5 -5
  117. package/docs/CLIENTS.md +11 -19
  118. package/docs/CONNECT_CHATGPT.md +4 -6
  119. package/docs/INSTALL.md +9 -5
  120. package/docs/INSTANCES.md +5 -2
  121. package/docs/OAUTH.md +6 -14
  122. package/docs/PLATFORMS.md +4 -4
  123. package/docs/README.md +2 -11
  124. package/docs/REMOTE_SETUP.md +13 -13
  125. package/docs/SECURITY.md +5 -5
  126. package/docs/assets/starmux-architecture.svg +315 -0
  127. package/package.json +14 -2
  128. package/docs/ARCHITECTURE.md +0 -51
  129. package/docs/MIGRATION.md +0 -37
  130. package/docs/RELEASE_STATUS.md +0 -31
  131. package/docs/TYPESCRIPT_MIGRATION.md +0 -56
  132. package/docs/VALIDATION_OAUTH.md +0 -79
  133. package/docs/VALIDATION_TUNNEL.md +0 -39
  134. package/docs/VALIDATION_WORKBUDDY.md +0 -59
  135. package/docs/assets/mcp-connection-route.svg +0 -104
  136. package/docs/assets/mcp-gateway-simple.svg +0 -62
@@ -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,2 @@
1
+ export async function openBrowser() {}
2
+ export async function openWelcomePage() {}
@@ -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
+ }