@rawwee/interactive-mcp 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 +210 -0
- package/dist/commands/input/index.js +216 -0
- package/dist/commands/input/ui.js +231 -0
- package/dist/commands/intensive-chat/index.js +289 -0
- package/dist/commands/intensive-chat/ui.js +301 -0
- package/dist/components/InteractiveInput.js +420 -0
- package/dist/components/MarkdownText.js +285 -0
- package/dist/components/PromptStatus.js +46 -0
- package/dist/components/TextProgressBar.js +10 -0
- package/dist/components/interactive-input/autocomplete.js +127 -0
- package/dist/components/interactive-input/keyboard.js +51 -0
- package/dist/components/interactive-input/types.js +1 -0
- package/dist/constants.js +13 -0
- package/dist/index.js +318 -0
- package/dist/tool-definitions/intensive-chat.js +236 -0
- package/dist/tool-definitions/message-complete-notification.js +66 -0
- package/dist/tool-definitions/request-user-input.js +117 -0
- package/dist/tool-definitions/types.js +1 -0
- package/dist/utils/base-directory.js +44 -0
- package/dist/utils/clipboard.js +67 -0
- package/dist/utils/logger.js +65 -0
- package/dist/utils/search-root.js +85 -0
- package/dist/utils/spawn-detached-terminal.js +101 -0
- package/package.json +74 -0
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
// Define capability conforming to ToolCapabilityInfo
|
|
3
|
+
const capabilityInfo = {
|
|
4
|
+
description: 'Ask the user a question in an OpenTUI terminal prompt and await their reply.',
|
|
5
|
+
parameters: {
|
|
6
|
+
type: 'object',
|
|
7
|
+
properties: {
|
|
8
|
+
projectName: {
|
|
9
|
+
type: 'string',
|
|
10
|
+
description: 'Identifies the context/project making the request (used in prompt formatting)',
|
|
11
|
+
},
|
|
12
|
+
message: {
|
|
13
|
+
type: 'string',
|
|
14
|
+
description: 'The specific question for the user (appears in the prompt)',
|
|
15
|
+
},
|
|
16
|
+
predefinedOptions: {
|
|
17
|
+
type: 'array',
|
|
18
|
+
items: { type: 'string' },
|
|
19
|
+
optional: true, // Mark as optional here too for consistency
|
|
20
|
+
description: 'Predefined options for the user to choose from (optional)',
|
|
21
|
+
},
|
|
22
|
+
baseDirectory: {
|
|
23
|
+
type: 'string',
|
|
24
|
+
description: 'Required absolute path to the current repository root (must be a git repo root; used as file autocomplete/search scope)',
|
|
25
|
+
},
|
|
26
|
+
},
|
|
27
|
+
required: ['projectName', 'message', 'baseDirectory'],
|
|
28
|
+
},
|
|
29
|
+
};
|
|
30
|
+
// Define description conforming to ToolRegistrationDescription
|
|
31
|
+
const registrationDescription = (globalTimeoutSeconds) => `<description>
|
|
32
|
+
Send a question to the user via the OpenTUI terminal prompt. **Crucial for clarifying requirements, confirming plans, or resolving ambiguity.**
|
|
33
|
+
You should call this tool whenever it has **any** uncertainty or needs clarification or confirmation, even for trivial or silly questions.
|
|
34
|
+
Feel free to ask anything! **Proactive questioning is preferred over making assumptions.**
|
|
35
|
+
</description>
|
|
36
|
+
|
|
37
|
+
<importantNotes>
|
|
38
|
+
- (!important!) **Use this tool FREQUENTLY** for any question that requires user input or confirmation.
|
|
39
|
+
- (!important!) Continue to generate existing messages after user answers.
|
|
40
|
+
- (!important!) Provide predefined options for quick selection if applicable.
|
|
41
|
+
- (!important!) **Essential for validating assumptions before proceeding with significant actions (e.g., code edits, running commands).**
|
|
42
|
+
</importantNotes>
|
|
43
|
+
|
|
44
|
+
<whenToUseThisTool>
|
|
45
|
+
- When you need clarification on user requirements or preferences
|
|
46
|
+
- When multiple implementation approaches are possible and user input is needed
|
|
47
|
+
- **Before making potentially impactful changes (code edits, file operations, complex commands)**
|
|
48
|
+
- When you need to confirm assumptions before proceeding
|
|
49
|
+
- When you need additional information not available in the current context
|
|
50
|
+
- When validating potential solutions before implementation
|
|
51
|
+
- When facing ambiguous instructions that require clarification
|
|
52
|
+
- When seeking feedback on generated code or solutions
|
|
53
|
+
- When needing permission to modify critical files or functionality
|
|
54
|
+
- **Whenever you feel even slightly unsure about the user's intent or the correct next step.**
|
|
55
|
+
</whenToUseThisTool>
|
|
56
|
+
|
|
57
|
+
<features>
|
|
58
|
+
- OpenTUI prompt with markdown rendering (including code/diff blocks)
|
|
59
|
+
- Supports option mode + free-text input mode when predefinedOptions are provided
|
|
60
|
+
- Returns user response or timeout notification (timeout defaults to ${globalTimeoutSeconds} seconds)
|
|
61
|
+
- Maintains context across user interactions
|
|
62
|
+
- Handles empty responses gracefully
|
|
63
|
+
- Properly formats prompt with project context
|
|
64
|
+
- baseDirectory is required, must be the current repository root, and controls file autocomplete/search scope explicitly
|
|
65
|
+
</features>
|
|
66
|
+
|
|
67
|
+
<bestPractices>
|
|
68
|
+
- Keep questions concise and specific
|
|
69
|
+
- Provide clear options when applicable
|
|
70
|
+
- Do not ask the question if you have another tool that can answer the question
|
|
71
|
+
- e.g. when you searching file in the current repository, do not ask the question "Do you want to search for a file in the current repository?"
|
|
72
|
+
- e.g. prefer to use other tools to find the answer (Cursor tools or other MCP Server tools)
|
|
73
|
+
- Limit questions to only what's necessary **to resolve the uncertainty**
|
|
74
|
+
- Format complex questions into simple choices
|
|
75
|
+
- Reference specific code or files when relevant
|
|
76
|
+
- Indicate why the information is needed
|
|
77
|
+
- Use appropriate urgency based on importance
|
|
78
|
+
</bestPractices>
|
|
79
|
+
|
|
80
|
+
<parameters>
|
|
81
|
+
- projectName: Identifies the context/project making the request (used in prompt formatting)
|
|
82
|
+
- message: The specific question for the user (appears in the prompt)
|
|
83
|
+
- predefinedOptions: Predefined options for the user to choose from (optional)
|
|
84
|
+
- baseDirectory: Required absolute path to the current repository root (must be a git repo root)
|
|
85
|
+
</parameters>
|
|
86
|
+
|
|
87
|
+
<examples>
|
|
88
|
+
- "Should I implement the authentication using JWT or OAuth?"
|
|
89
|
+
- "Do you want to use TypeScript interfaces or type aliases for this component?"
|
|
90
|
+
- "I found three potential bugs. Should I fix them all or focus on the critical one first?"
|
|
91
|
+
- "Can I refactor the database connection code to use connection pooling?"
|
|
92
|
+
- "Is it acceptable to add React Router as a dependency?"
|
|
93
|
+
- "I plan to modify function X in file Y. Is that correct?"
|
|
94
|
+
- { "projectName": "web-app", "message": "Which file should I edit?", "baseDirectory": "/workspace/web-app" }
|
|
95
|
+
</examples>`;
|
|
96
|
+
// Define the Zod schema (as a raw shape object)
|
|
97
|
+
const rawSchema = {
|
|
98
|
+
projectName: z
|
|
99
|
+
.string()
|
|
100
|
+
.describe('Identifies the context/project making the request (used in prompt formatting)'),
|
|
101
|
+
message: z
|
|
102
|
+
.string()
|
|
103
|
+
.describe('The specific question for the user (appears in the prompt)'),
|
|
104
|
+
predefinedOptions: z
|
|
105
|
+
.array(z.string())
|
|
106
|
+
.optional()
|
|
107
|
+
.describe('Predefined options for the user to choose from (optional)'),
|
|
108
|
+
baseDirectory: z
|
|
109
|
+
.string()
|
|
110
|
+
.describe('Required absolute path to the current repository root (must be a git repo root; used as file autocomplete/search scope)'),
|
|
111
|
+
};
|
|
112
|
+
// Combine into a single ToolDefinition object
|
|
113
|
+
export const requestUserInputTool = {
|
|
114
|
+
capability: capabilityInfo,
|
|
115
|
+
description: registrationDescription,
|
|
116
|
+
schema: rawSchema, // Use the raw shape here
|
|
117
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import fs from 'fs/promises';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
/**
|
|
4
|
+
* Validate that the provided base directory is an absolute path pointing to a
|
|
5
|
+
* git repository root (contains a `.git` entry).
|
|
6
|
+
*/
|
|
7
|
+
export async function validateRepositoryBaseDirectory(baseDirectory) {
|
|
8
|
+
if (baseDirectory.trim().length === 0) {
|
|
9
|
+
throw new Error('Invalid baseDirectory: value cannot be empty. Provide an absolute path to the current repository root.');
|
|
10
|
+
}
|
|
11
|
+
if (!path.isAbsolute(baseDirectory)) {
|
|
12
|
+
throw new Error('Invalid baseDirectory: path must be absolute and point to the current repository root.');
|
|
13
|
+
}
|
|
14
|
+
const normalizedPath = path.resolve(baseDirectory);
|
|
15
|
+
let baseDirectoryStats;
|
|
16
|
+
try {
|
|
17
|
+
baseDirectoryStats = await fs.stat(normalizedPath);
|
|
18
|
+
}
|
|
19
|
+
catch (error) {
|
|
20
|
+
if (error &&
|
|
21
|
+
typeof error === 'object' &&
|
|
22
|
+
'code' in error &&
|
|
23
|
+
error.code === 'ENOENT') {
|
|
24
|
+
throw new Error(`Invalid baseDirectory: "${normalizedPath}" does not exist. Provide an absolute path to the current repository root.`);
|
|
25
|
+
}
|
|
26
|
+
throw new Error(`Invalid baseDirectory: unable to access "${normalizedPath}". Provide an absolute path to the current repository root.`);
|
|
27
|
+
}
|
|
28
|
+
if (!baseDirectoryStats.isDirectory()) {
|
|
29
|
+
throw new Error(`Invalid baseDirectory: "${normalizedPath}" is not a directory. Provide an absolute path to the current repository root.`);
|
|
30
|
+
}
|
|
31
|
+
try {
|
|
32
|
+
await fs.access(path.join(normalizedPath, '.git'));
|
|
33
|
+
}
|
|
34
|
+
catch (error) {
|
|
35
|
+
if (error &&
|
|
36
|
+
typeof error === 'object' &&
|
|
37
|
+
'code' in error &&
|
|
38
|
+
error.code === 'ENOENT') {
|
|
39
|
+
throw new Error(`Invalid baseDirectory: "${normalizedPath}" is not a git repository root (missing ".git").`);
|
|
40
|
+
}
|
|
41
|
+
throw new Error(`Invalid baseDirectory: unable to verify ".git" in "${normalizedPath}".`);
|
|
42
|
+
}
|
|
43
|
+
return normalizedPath;
|
|
44
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import os from 'os';
|
|
2
|
+
import { spawn } from 'child_process';
|
|
3
|
+
function runCommand(command, args, input) {
|
|
4
|
+
return new Promise((resolve, reject) => {
|
|
5
|
+
const child = spawn(command, args, {
|
|
6
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
7
|
+
});
|
|
8
|
+
let stdout = '';
|
|
9
|
+
let stderr = '';
|
|
10
|
+
child.stdout.on('data', (chunk) => {
|
|
11
|
+
stdout += chunk.toString();
|
|
12
|
+
});
|
|
13
|
+
child.stderr.on('data', (chunk) => {
|
|
14
|
+
stderr += chunk.toString();
|
|
15
|
+
});
|
|
16
|
+
child.on('error', (error) => {
|
|
17
|
+
reject(error);
|
|
18
|
+
});
|
|
19
|
+
child.on('close', (code) => {
|
|
20
|
+
if (code === 0) {
|
|
21
|
+
resolve({ stdout });
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
reject(new Error(`Command "${command} ${args.join(' ')}" failed with code ${code}: ${stderr || 'no stderr output'}`));
|
|
25
|
+
});
|
|
26
|
+
if (typeof input === 'string') {
|
|
27
|
+
child.stdin.write(input);
|
|
28
|
+
}
|
|
29
|
+
child.stdin.end();
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
export async function copyTextToClipboard(text) {
|
|
33
|
+
const platform = os.platform();
|
|
34
|
+
if (platform === 'darwin') {
|
|
35
|
+
await runCommand('pbcopy', [], text);
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
if (platform === 'linux') {
|
|
39
|
+
await runCommand('xclip', ['-selection', 'clipboard'], text);
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
if (platform === 'win32') {
|
|
43
|
+
await runCommand('clip', [], text);
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
throw new Error(`Clipboard copy is not supported on platform: ${platform}`);
|
|
47
|
+
}
|
|
48
|
+
export async function readTextFromClipboard() {
|
|
49
|
+
const platform = os.platform();
|
|
50
|
+
if (platform === 'darwin') {
|
|
51
|
+
const result = await runCommand('pbpaste', []);
|
|
52
|
+
return result.stdout;
|
|
53
|
+
}
|
|
54
|
+
if (platform === 'linux') {
|
|
55
|
+
const result = await runCommand('xclip', ['-selection', 'clipboard', '-o']);
|
|
56
|
+
return result.stdout;
|
|
57
|
+
}
|
|
58
|
+
if (platform === 'win32') {
|
|
59
|
+
const result = await runCommand('powershell', [
|
|
60
|
+
'-NoProfile',
|
|
61
|
+
'-Command',
|
|
62
|
+
'Get-Clipboard -Raw',
|
|
63
|
+
]);
|
|
64
|
+
return result.stdout;
|
|
65
|
+
}
|
|
66
|
+
throw new Error(`Clipboard paste is not supported on platform: ${platform}`);
|
|
67
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { pino, } from 'pino';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import fs from 'fs';
|
|
4
|
+
import os from 'os';
|
|
5
|
+
const logDir = path.resolve(os.tmpdir(), 'interactive-mcp-logs');
|
|
6
|
+
const logFile = path.join(logDir, 'dev.log');
|
|
7
|
+
// Ensure log directory exists
|
|
8
|
+
if (process.env.NODE_ENV === 'development' && !fs.existsSync(logDir)) {
|
|
9
|
+
try {
|
|
10
|
+
fs.mkdirSync(logDir, { recursive: true });
|
|
11
|
+
}
|
|
12
|
+
catch (error) {
|
|
13
|
+
console.error('Failed to create log directory:', error); // Use console here as logger isn't initialized yet
|
|
14
|
+
// Consider fallback behavior or exiting if logging is critical
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
const isDevelopment = process.env.NODE_ENV === 'development';
|
|
18
|
+
const loggerOptions = {
|
|
19
|
+
level: isDevelopment ? 'trace' : 'silent', // Default level
|
|
20
|
+
};
|
|
21
|
+
if (isDevelopment) {
|
|
22
|
+
let devTransportConfig;
|
|
23
|
+
try {
|
|
24
|
+
// Attempt to open the file in append mode to check writability before setting up transport
|
|
25
|
+
const fd = fs.openSync(logFile, 'a');
|
|
26
|
+
fs.closeSync(fd);
|
|
27
|
+
devTransportConfig = {
|
|
28
|
+
targets: [
|
|
29
|
+
{
|
|
30
|
+
target: 'pino-pretty', // Log to console with pretty printing
|
|
31
|
+
options: {
|
|
32
|
+
colorize: true,
|
|
33
|
+
sync: false, // Use async logging
|
|
34
|
+
translateTime: 'SYS:yyyy-mm-dd HH:MM:ss',
|
|
35
|
+
ignore: 'pid,hostname',
|
|
36
|
+
},
|
|
37
|
+
level: 'trace', // Log all levels to the console in dev
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
target: 'pino/file', // Log to file
|
|
41
|
+
options: { destination: logFile, mkdir: true }, // Specify file path and ensure directory exists
|
|
42
|
+
level: 'trace', // Log all levels to the file in dev
|
|
43
|
+
},
|
|
44
|
+
],
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
catch (error) {
|
|
48
|
+
console.error(`Failed to setup file transport for ${logFile}. Falling back to console-only logging. Error:`, error);
|
|
49
|
+
// Fallback transport to console only using pino-pretty if file access fails
|
|
50
|
+
devTransportConfig = {
|
|
51
|
+
target: 'pino-pretty',
|
|
52
|
+
options: {
|
|
53
|
+
colorize: true,
|
|
54
|
+
sync: false,
|
|
55
|
+
translateTime: 'SYS:yyyy-mm-dd HH:MM:ss',
|
|
56
|
+
ignore: 'pid,hostname',
|
|
57
|
+
},
|
|
58
|
+
level: 'trace',
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
// Add transport to logger options only in development
|
|
62
|
+
loggerOptions.transport = devTransportConfig;
|
|
63
|
+
}
|
|
64
|
+
const logger = pino(loggerOptions);
|
|
65
|
+
export default logger;
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import fs from 'fs/promises';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
export const SEARCH_ROOT_ENV_KEY = 'INTERACTIVE_MCP_SEARCH_ROOT';
|
|
4
|
+
const normalizeAbsolutePath = (value) => {
|
|
5
|
+
if (!value) {
|
|
6
|
+
return null;
|
|
7
|
+
}
|
|
8
|
+
const trimmed = value.trim();
|
|
9
|
+
if (!trimmed || !path.isAbsolute(trimmed)) {
|
|
10
|
+
return null;
|
|
11
|
+
}
|
|
12
|
+
return path.resolve(trimmed);
|
|
13
|
+
};
|
|
14
|
+
const existsAsDirectory = async (targetPath) => {
|
|
15
|
+
try {
|
|
16
|
+
const stats = await fs.stat(targetPath);
|
|
17
|
+
return stats.isDirectory();
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
const isGitRepositoryRoot = async (targetPath) => {
|
|
24
|
+
if (!(await existsAsDirectory(targetPath))) {
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
try {
|
|
28
|
+
await fs.access(path.join(targetPath, '.git'));
|
|
29
|
+
return true;
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
const findRepositoryRootFrom = async (startPath) => {
|
|
36
|
+
let current = path.resolve(startPath);
|
|
37
|
+
while (true) {
|
|
38
|
+
if (await isGitRepositoryRoot(current)) {
|
|
39
|
+
return current;
|
|
40
|
+
}
|
|
41
|
+
const parent = path.dirname(current);
|
|
42
|
+
if (parent === current) {
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
current = parent;
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
const toDirectoryPath = async (targetPath) => {
|
|
49
|
+
try {
|
|
50
|
+
const stats = await fs.stat(targetPath);
|
|
51
|
+
return stats.isDirectory() ? targetPath : path.dirname(targetPath);
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
const findRepositoryRootFromScriptEntry = async (argvEntry) => {
|
|
58
|
+
if (!argvEntry) {
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
const resolvedArgvEntry = path.isAbsolute(argvEntry)
|
|
62
|
+
? path.resolve(argvEntry)
|
|
63
|
+
: path.resolve(process.cwd(), argvEntry);
|
|
64
|
+
const startDirectory = await toDirectoryPath(resolvedArgvEntry);
|
|
65
|
+
if (!startDirectory) {
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
return findRepositoryRootFrom(startDirectory);
|
|
69
|
+
};
|
|
70
|
+
export async function resolveSearchRoot(preferredSearchRoot, runtimeHints = {}) {
|
|
71
|
+
const preferredCandidate = normalizeAbsolutePath(preferredSearchRoot);
|
|
72
|
+
if (preferredCandidate && (await isGitRepositoryRoot(preferredCandidate))) {
|
|
73
|
+
return preferredCandidate;
|
|
74
|
+
}
|
|
75
|
+
const envCandidate = normalizeAbsolutePath(process.env[SEARCH_ROOT_ENV_KEY]);
|
|
76
|
+
if (envCandidate && (await isGitRepositoryRoot(envCandidate))) {
|
|
77
|
+
return envCandidate;
|
|
78
|
+
}
|
|
79
|
+
const argvEntryRoot = await findRepositoryRootFromScriptEntry(runtimeHints.argvEntry ?? process.argv[1]);
|
|
80
|
+
if (argvEntryRoot) {
|
|
81
|
+
return argvEntryRoot;
|
|
82
|
+
}
|
|
83
|
+
const cwdRoot = await findRepositoryRootFrom(runtimeHints.cwd ?? process.cwd());
|
|
84
|
+
return cwdRoot ?? undefined;
|
|
85
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import fs from 'node:fs/promises';
|
|
3
|
+
import os from 'node:os';
|
|
4
|
+
import logger from './logger.js';
|
|
5
|
+
function resolveRuntimeExecutable() {
|
|
6
|
+
if (process.versions.bun) {
|
|
7
|
+
return process.execPath;
|
|
8
|
+
}
|
|
9
|
+
return process.env.INTERACTIVE_MCP_BUN_PATH || 'bun';
|
|
10
|
+
}
|
|
11
|
+
function createEscapedRuntimeCommand(executable, scriptPath, args, env) {
|
|
12
|
+
const runtimeArgs = [scriptPath, ...args].map((arg) => `"${arg}"`).join(' ');
|
|
13
|
+
const envPrefix = createShellEnvPrefix(env);
|
|
14
|
+
return `${envPrefix}exec "${executable}" ${runtimeArgs}; exit 0`
|
|
15
|
+
.replace(/\\/g, '\\\\')
|
|
16
|
+
.replace(/"/g, '\\"');
|
|
17
|
+
}
|
|
18
|
+
function createLauncherScript(executable, scriptPath, args, env) {
|
|
19
|
+
const runtimeArgs = [scriptPath, ...args].map((arg) => `"${arg}"`).join(' ');
|
|
20
|
+
const envPrefix = createShellEnvPrefix(env);
|
|
21
|
+
return `#!/bin/bash\n${envPrefix}exec "${executable}" ${runtimeArgs}\n`;
|
|
22
|
+
}
|
|
23
|
+
function escapeForDoubleQuotedShellString(value) {
|
|
24
|
+
return value
|
|
25
|
+
.replace(/\\/g, '\\\\')
|
|
26
|
+
.replace(/"/g, '\\"')
|
|
27
|
+
.replace(/\$/g, '\\$')
|
|
28
|
+
.replace(/`/g, '\\`');
|
|
29
|
+
}
|
|
30
|
+
function createShellEnvPrefix(env) {
|
|
31
|
+
if (!env) {
|
|
32
|
+
return '';
|
|
33
|
+
}
|
|
34
|
+
return Object.entries(env)
|
|
35
|
+
.filter(([key, value]) => typeof value === 'string' &&
|
|
36
|
+
key.length > 0 &&
|
|
37
|
+
/^[A-Za-z_][A-Za-z0-9_]*$/.test(key))
|
|
38
|
+
.map(([key, value]) => {
|
|
39
|
+
const escapedValue = escapeForDoubleQuotedShellString(value);
|
|
40
|
+
return `export ${key}="${escapedValue}"; `;
|
|
41
|
+
})
|
|
42
|
+
.join('');
|
|
43
|
+
}
|
|
44
|
+
export function spawnDetachedTerminal(options) {
|
|
45
|
+
const platform = os.platform();
|
|
46
|
+
const runtimeExecutable = resolveRuntimeExecutable();
|
|
47
|
+
const spawnEnv = {
|
|
48
|
+
...process.env,
|
|
49
|
+
...options.env,
|
|
50
|
+
};
|
|
51
|
+
if (platform === 'darwin') {
|
|
52
|
+
const darwinArgs = options.darwinArgs ?? options.args;
|
|
53
|
+
const escapedRuntimeCommand = createEscapedRuntimeCommand(runtimeExecutable, options.scriptPath, darwinArgs, options.env);
|
|
54
|
+
const command = `osascript -e 'tell application "Terminal" to activate' -e 'tell application "Terminal" to do script "${escapedRuntimeCommand}"'`;
|
|
55
|
+
const childProcess = spawn(command, [], {
|
|
56
|
+
stdio: ['ignore', 'ignore', 'ignore'],
|
|
57
|
+
shell: true,
|
|
58
|
+
detached: true,
|
|
59
|
+
env: spawnEnv,
|
|
60
|
+
});
|
|
61
|
+
const launchViaOpenCommand = async () => {
|
|
62
|
+
try {
|
|
63
|
+
await fs.writeFile(options.macLauncherPath, createLauncherScript(runtimeExecutable, options.scriptPath, darwinArgs, options.env), 'utf8');
|
|
64
|
+
await fs.chmod(options.macLauncherPath, 0o755);
|
|
65
|
+
const openProc = spawn('open', ['-a', 'Terminal', options.macLauncherPath], {
|
|
66
|
+
stdio: ['ignore', 'ignore', 'ignore'],
|
|
67
|
+
detached: true,
|
|
68
|
+
env: spawnEnv,
|
|
69
|
+
});
|
|
70
|
+
openProc.unref();
|
|
71
|
+
}
|
|
72
|
+
catch (error) {
|
|
73
|
+
logger.error({ error }, options.macFallbackLogMessage);
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
childProcess.on('error', () => {
|
|
77
|
+
void launchViaOpenCommand();
|
|
78
|
+
});
|
|
79
|
+
childProcess.on('close', (code) => {
|
|
80
|
+
if (code !== null && code !== 0) {
|
|
81
|
+
void launchViaOpenCommand();
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
return childProcess;
|
|
85
|
+
}
|
|
86
|
+
if (platform === 'win32') {
|
|
87
|
+
return spawn(runtimeExecutable, [options.scriptPath, ...options.args], {
|
|
88
|
+
stdio: ['ignore', 'ignore', 'ignore'],
|
|
89
|
+
shell: true,
|
|
90
|
+
detached: true,
|
|
91
|
+
windowsHide: false,
|
|
92
|
+
env: spawnEnv,
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
return spawn(runtimeExecutable, [options.scriptPath, ...options.args], {
|
|
96
|
+
stdio: ['ignore', 'ignore', 'ignore'],
|
|
97
|
+
shell: true,
|
|
98
|
+
detached: true,
|
|
99
|
+
env: spawnEnv,
|
|
100
|
+
});
|
|
101
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@rawwee/interactive-mcp",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"main": "dist/index.js",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"interactive-mcp": "dist/index.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"dist",
|
|
11
|
+
"README.md",
|
|
12
|
+
"LICENSE",
|
|
13
|
+
"package.json"
|
|
14
|
+
],
|
|
15
|
+
"scripts": {
|
|
16
|
+
"build": "tsc --outDir dist && tsc-alias",
|
|
17
|
+
"start": "bun dist/index.js",
|
|
18
|
+
"lint": "eslint \"src/**/*.{js,ts,jsx,tsx}\"",
|
|
19
|
+
"format": "prettier --write \"src/**/*.{js,ts,jsx,tsx,json,md}\"",
|
|
20
|
+
"check-types": "tsc --noEmit",
|
|
21
|
+
"prepare": "husky"
|
|
22
|
+
},
|
|
23
|
+
"keywords": [],
|
|
24
|
+
"author": "",
|
|
25
|
+
"license": "MIT",
|
|
26
|
+
"description": "",
|
|
27
|
+
"devDependencies": {
|
|
28
|
+
"@eslint/js": "^9.25.1",
|
|
29
|
+
"@semantic-release/commit-analyzer": "^13.0.1",
|
|
30
|
+
"@semantic-release/git": "^10.0.1",
|
|
31
|
+
"@semantic-release/github": "^11.0.2",
|
|
32
|
+
"@semantic-release/npm": "^13.1.5",
|
|
33
|
+
"@semantic-release/release-notes-generator": "^14.0.3",
|
|
34
|
+
"@types/bun": "^1.3.10",
|
|
35
|
+
"@types/node": "^22.15.2",
|
|
36
|
+
"@types/node-notifier": "^8.0.5",
|
|
37
|
+
"@types/pino": "^7.0.5",
|
|
38
|
+
"@types/react": "^19.1.2",
|
|
39
|
+
"conventional-changelog-conventionalcommits": "^8.0.0",
|
|
40
|
+
"eslint": "^9.25.1",
|
|
41
|
+
"eslint-config-prettier": "^10.1.2",
|
|
42
|
+
"eslint-plugin-prettier": "^5.2.6",
|
|
43
|
+
"globals": "^16.0.0",
|
|
44
|
+
"husky": "^9.1.7",
|
|
45
|
+
"jiti": "^2.4.2",
|
|
46
|
+
"lint-staged": "^15.5.1",
|
|
47
|
+
"pino-pretty": "^13.0.0",
|
|
48
|
+
"prettier": "^3.5.3",
|
|
49
|
+
"semantic-release": "^24.2.3",
|
|
50
|
+
"tsc-alias": "^1.8.15",
|
|
51
|
+
"typescript": "^5.8.3",
|
|
52
|
+
"typescript-eslint": "^8.31.0"
|
|
53
|
+
},
|
|
54
|
+
"dependencies": {
|
|
55
|
+
"@modelcontextprotocol/sdk": "^1.10.2",
|
|
56
|
+
"@opentui/core": "^0.1.86",
|
|
57
|
+
"@opentui/react": "^0.1.86",
|
|
58
|
+
"@types/yargs": "^17.0.33",
|
|
59
|
+
"node-notifier": "^10.0.1",
|
|
60
|
+
"pino": "^9.6.0",
|
|
61
|
+
"react": "^19.2.0",
|
|
62
|
+
"yargs": "^17.7.2",
|
|
63
|
+
"zod": "^3.24.3"
|
|
64
|
+
},
|
|
65
|
+
"lint-staged": {
|
|
66
|
+
"*.{js,ts,jsx,tsx}": [
|
|
67
|
+
"eslint --fix"
|
|
68
|
+
],
|
|
69
|
+
"*.{js,ts,jsx,tsx,json,md}": [
|
|
70
|
+
"prettier --write"
|
|
71
|
+
]
|
|
72
|
+
},
|
|
73
|
+
"packageManager": "bun@1.3.10"
|
|
74
|
+
}
|