@rawwee/interactive-mcp 1.1.0 → 1.3.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/README.md +5 -5
- package/dist/components/InteractiveInput.js +258 -10
- package/dist/components/interactive-input/keyboard.js +15 -0
- package/dist/index.js +8 -9
- package/dist/tool-definitions/intensive-chat.js +12 -7
- package/dist/tool-definitions/request-user-input.js +14 -3
- package/dist/utils/clipboard.js +184 -0
- package/package.json +8 -2
package/README.md
CHANGED
|
@@ -19,6 +19,7 @@ This server exposes the following tools via the Model Context Protocol (MCP):
|
|
|
19
19
|
- `stop_intensive_chat`: Closes an active intensive chat session.
|
|
20
20
|
|
|
21
21
|
Prompt UIs support markdown-friendly question text (including multiline prompts, fenced code blocks, and diff snippets). When useful, you can also include VS Code-style file links in prompt text (for example, `vscode://file/<absolute-path>:<line>:<column>`).
|
|
22
|
+
In TUI input mode, `Cmd/Ctrl+V` supports clipboard text plus file/image includes from pasted paths, copied file objects, and copied images (platform support varies).
|
|
22
23
|
|
|
23
24
|
## Usage Scenarios
|
|
24
25
|
|
|
@@ -131,11 +132,10 @@ This section is primarily for developers looking to modify or contribute to the
|
|
|
131
132
|
bun run start
|
|
132
133
|
```
|
|
133
134
|
|
|
134
|
-
###
|
|
135
|
+
### UI backend status
|
|
135
136
|
|
|
136
|
-
`interactive-mcp`
|
|
137
|
-
|
|
138
|
-
OpenTUI is the terminal UI renderer backend.
|
|
137
|
+
`interactive-mcp` currently runs with the OpenTUI terminal backend (`@opentui/core`, `@opentui/react`).
|
|
138
|
+
The VS Code extension and bridge runtime have been removed from the active feature set for now, and may be reconsidered in a future iteration.
|
|
139
139
|
|
|
140
140
|
#### Command-Line Options
|
|
141
141
|
|
|
@@ -143,7 +143,7 @@ The `interactive-mcp` server accepts the following command-line options. These s
|
|
|
143
143
|
|
|
144
144
|
| Option | Alias | Description |
|
|
145
145
|
| ----------------- | ----- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
146
|
-
| `--timeout` | `-t` | Sets the default timeout (in seconds) for user input prompts.
|
|
146
|
+
| `--timeout` | `-t` | Sets the default timeout (in seconds) for user input prompts. |
|
|
147
147
|
| `--disable-tools` | `-d` | Disables specific tools or groups (comma-separated list). Prevents the server from advertising or registering them. Options: `request_user_input`, `message_complete_notification`, `intensive_chat`. |
|
|
148
148
|
|
|
149
149
|
**Example:** Setting multiple options in the client config `args` array:
|
|
@@ -1,16 +1,143 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "@opentui/react/jsx-runtime";
|
|
2
2
|
import * as OpenTuiReact from '@opentui/react';
|
|
3
3
|
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
4
|
+
import crypto from 'node:crypto';
|
|
5
|
+
import fs from 'node:fs/promises';
|
|
4
6
|
import path from 'node:path';
|
|
5
7
|
import { getAutocompleteTarget, rankFileSuggestions, readRepositoryFiles, } from './interactive-input/autocomplete.js';
|
|
6
|
-
import { isPrintableCharacter, isCopyShortcut, isReverseTabShortcut, isSubmitShortcut, textareaKeyBindings, } from './interactive-input/keyboard.js';
|
|
8
|
+
import { extractPastedText, isPrintableCharacter, isCopyShortcut, isPasteShortcut, isReverseTabShortcut, isSubmitShortcut, textareaKeyBindings, } from './interactive-input/keyboard.js';
|
|
7
9
|
import { InputEditor, ModeTabs, OptionList, SuggestionsPanel, } from './interactive-input/sections.js';
|
|
8
10
|
import { getTextareaDimensions } from './interactive-input/textarea-height.js';
|
|
9
11
|
import { MarkdownText } from './MarkdownText.js';
|
|
10
|
-
import { copyTextToClipboard } from '../utils/clipboard.js';
|
|
12
|
+
import { copyTextToClipboard, readFilePathsFromClipboard, readImageDataUrlFromClipboard, readTextFromClipboard, } from '../utils/clipboard.js';
|
|
11
13
|
const { useKeyboard } = OpenTuiReact;
|
|
12
14
|
const { useTerminalDimensions } = OpenTuiReact;
|
|
13
15
|
const repositoryFileCache = new Map();
|
|
16
|
+
const IMAGE_MIME_BY_EXTENSION = {
|
|
17
|
+
'.png': 'image/png',
|
|
18
|
+
'.jpg': 'image/jpeg',
|
|
19
|
+
'.jpeg': 'image/jpeg',
|
|
20
|
+
'.gif': 'image/gif',
|
|
21
|
+
'.webp': 'image/webp',
|
|
22
|
+
'.bmp': 'image/bmp',
|
|
23
|
+
'.svg': 'image/svg+xml',
|
|
24
|
+
};
|
|
25
|
+
const IMAGE_EMBED_MAX_BYTES = 2 * 1024 * 1024;
|
|
26
|
+
const TEXT_EMBED_MAX_BYTES = 512 * 1024;
|
|
27
|
+
const TEXT_EMBED_MAX_CHARS = 20000;
|
|
28
|
+
const COLLAPSE_TEXT_PASTE_CHARS = 800;
|
|
29
|
+
const COLLAPSE_TEXT_PASTE_LINES = 12;
|
|
30
|
+
const inferCodeFenceLanguage = (fileName) => {
|
|
31
|
+
const extension = path.extname(fileName).slice(1).toLowerCase();
|
|
32
|
+
const mapping = {
|
|
33
|
+
js: 'javascript',
|
|
34
|
+
jsx: 'jsx',
|
|
35
|
+
ts: 'typescript',
|
|
36
|
+
tsx: 'tsx',
|
|
37
|
+
json: 'json',
|
|
38
|
+
md: 'markdown',
|
|
39
|
+
py: 'python',
|
|
40
|
+
sh: 'bash',
|
|
41
|
+
yml: 'yaml',
|
|
42
|
+
yaml: 'yaml',
|
|
43
|
+
html: 'html',
|
|
44
|
+
css: 'css',
|
|
45
|
+
go: 'go',
|
|
46
|
+
rs: 'rust',
|
|
47
|
+
java: 'java',
|
|
48
|
+
cs: 'csharp',
|
|
49
|
+
};
|
|
50
|
+
return mapping[extension] ?? '';
|
|
51
|
+
};
|
|
52
|
+
const normalizeClipboardPath = (clipboardText, searchRoot) => {
|
|
53
|
+
const trimmed = clipboardText.trim();
|
|
54
|
+
if (!trimmed) {
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
const lines = trimmed
|
|
58
|
+
.split(/\r?\n/)
|
|
59
|
+
.map((line) => line.trim())
|
|
60
|
+
.filter(Boolean);
|
|
61
|
+
let candidate = lines[0] ?? '';
|
|
62
|
+
if (!candidate) {
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
if ((candidate.startsWith('"') && candidate.endsWith('"')) ||
|
|
66
|
+
(candidate.startsWith("'") && candidate.endsWith("'"))) {
|
|
67
|
+
candidate = candidate.slice(1, -1);
|
|
68
|
+
}
|
|
69
|
+
if (candidate.startsWith('file://')) {
|
|
70
|
+
candidate = decodeURIComponent(candidate.replace(/^file:\/\//, ''));
|
|
71
|
+
}
|
|
72
|
+
if (path.isAbsolute(candidate)) {
|
|
73
|
+
return candidate;
|
|
74
|
+
}
|
|
75
|
+
if (!searchRoot) {
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
return path.resolve(searchRoot, candidate);
|
|
79
|
+
};
|
|
80
|
+
const isLikelyTextBuffer = (buffer) => {
|
|
81
|
+
if (buffer.includes(0)) {
|
|
82
|
+
return false;
|
|
83
|
+
}
|
|
84
|
+
const sample = buffer.subarray(0, Math.min(buffer.length, 2048));
|
|
85
|
+
let suspiciousBytes = 0;
|
|
86
|
+
for (const byte of sample) {
|
|
87
|
+
const isPrintableAscii = byte >= 32 && byte <= 126;
|
|
88
|
+
const isWhitespace = byte === 9 || byte === 10 || byte === 13;
|
|
89
|
+
const isExtendedUtf8Byte = byte >= 128;
|
|
90
|
+
if (!isPrintableAscii && !isWhitespace && !isExtendedUtf8Byte) {
|
|
91
|
+
suspiciousBytes += 1;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return suspiciousBytes < 8;
|
|
95
|
+
};
|
|
96
|
+
const shouldCollapsePastedText = (text) => text.length >= COLLAPSE_TEXT_PASTE_CHARS ||
|
|
97
|
+
text.split(/\r?\n/).length >= COLLAPSE_TEXT_PASTE_LINES;
|
|
98
|
+
const buildAttachmentFromPath = async (absolutePath) => {
|
|
99
|
+
const fileStats = await fs.stat(absolutePath);
|
|
100
|
+
if (!fileStats.isFile()) {
|
|
101
|
+
throw new Error('Clipboard path is not a file');
|
|
102
|
+
}
|
|
103
|
+
const fileName = path.basename(absolutePath);
|
|
104
|
+
const extension = path.extname(fileName).toLowerCase();
|
|
105
|
+
const imageMimeType = IMAGE_MIME_BY_EXTENSION[extension];
|
|
106
|
+
if (imageMimeType) {
|
|
107
|
+
if (fileStats.size > IMAGE_EMBED_MAX_BYTES) {
|
|
108
|
+
return {
|
|
109
|
+
id: crypto.randomUUID(),
|
|
110
|
+
label: `Image: ${fileName} (${Math.round(fileStats.size / 1024)}KB, too large to embed)`,
|
|
111
|
+
payload: `[Image attachment: ${fileName}] (${imageMimeType}, ${fileStats.size} bytes, too large to embed)`,
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
const imageBuffer = await fs.readFile(absolutePath);
|
|
115
|
+
return {
|
|
116
|
+
id: crypto.randomUUID(),
|
|
117
|
+
label: `Image: ${fileName}`,
|
|
118
|
+
payload: `})`,
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
const fileBuffer = await fs.readFile(absolutePath);
|
|
122
|
+
if (fileStats.size <= TEXT_EMBED_MAX_BYTES &&
|
|
123
|
+
isLikelyTextBuffer(fileBuffer)) {
|
|
124
|
+
const fileText = fileBuffer.toString('utf8');
|
|
125
|
+
const truncatedText = fileText.length > TEXT_EMBED_MAX_CHARS
|
|
126
|
+
? `${fileText.slice(0, TEXT_EMBED_MAX_CHARS)}\n\n...[truncated ${fileText.length - TEXT_EMBED_MAX_CHARS} chars]`
|
|
127
|
+
: fileText;
|
|
128
|
+
const language = inferCodeFenceLanguage(fileName);
|
|
129
|
+
return {
|
|
130
|
+
id: crypto.randomUUID(),
|
|
131
|
+
label: `File: ${fileName} (${truncatedText.length} chars)`,
|
|
132
|
+
payload: `Attached file: ${fileName}\n\`\`\`${language}\n${truncatedText}\n\`\`\``,
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
return {
|
|
136
|
+
id: crypto.randomUUID(),
|
|
137
|
+
label: `File: ${fileName} (${Math.round(fileStats.size / 1024)}KB binary)`,
|
|
138
|
+
payload: `[File attachment: ${fileName}] (${fileStats.size} bytes, binary content omitted)`,
|
|
139
|
+
};
|
|
140
|
+
};
|
|
14
141
|
export function InteractiveInput({ question, questionId, predefinedOptions = [], onSubmit, onInputActivity, searchRoot, }) {
|
|
15
142
|
const [mode, setMode] = useState(predefinedOptions.length > 0 ? 'option' : 'input');
|
|
16
143
|
const [selectedIndex, setSelectedIndex] = useState(0);
|
|
@@ -23,6 +150,7 @@ export function InteractiveInput({ question, questionId, predefinedOptions = [],
|
|
|
23
150
|
const [textareaRenderVersion, setTextareaRenderVersion] = useState(0);
|
|
24
151
|
const [focusRequestToken, setFocusRequestToken] = useState(0);
|
|
25
152
|
const [clipboardStatus, setClipboardStatus] = useState(null);
|
|
153
|
+
const [queuedAttachments, setQueuedAttachments] = useState([]);
|
|
26
154
|
const textareaRef = useRef(null);
|
|
27
155
|
const latestInputValueRef = useRef(inputValue);
|
|
28
156
|
const latestCaretPositionRef = useRef(caretPosition);
|
|
@@ -176,6 +304,7 @@ export function InteractiveInput({ question, questionId, predefinedOptions = [],
|
|
|
176
304
|
setSelectedIndex(0);
|
|
177
305
|
setInputValue('');
|
|
178
306
|
setCaretPosition(0);
|
|
307
|
+
setQueuedAttachments([]);
|
|
179
308
|
latestInputValueRef.current = '';
|
|
180
309
|
latestCaretPositionRef.current = 0;
|
|
181
310
|
setFileSuggestions([]);
|
|
@@ -301,18 +430,123 @@ export function InteractiveInput({ question, questionId, predefinedOptions = [],
|
|
|
301
430
|
});
|
|
302
431
|
onInputActivity?.();
|
|
303
432
|
}, [inputValue, mode, onInputActivity]);
|
|
304
|
-
const
|
|
305
|
-
if (
|
|
306
|
-
|
|
433
|
+
const insertTextAtCaret = useCallback((text) => {
|
|
434
|
+
if (!text) {
|
|
435
|
+
return;
|
|
436
|
+
}
|
|
437
|
+
const textareaState = safeReadTextarea();
|
|
438
|
+
const currentValue = textareaState?.value ?? inputValue;
|
|
439
|
+
const currentCaret = Math.max(0, Math.min(textareaState?.caret ?? caretPosition, currentValue.length));
|
|
440
|
+
const nextValue = currentValue.slice(0, currentCaret) +
|
|
441
|
+
text +
|
|
442
|
+
currentValue.slice(currentCaret);
|
|
443
|
+
setTextareaValue(nextValue, currentCaret + text.length);
|
|
444
|
+
}, [caretPosition, inputValue, safeReadTextarea, setTextareaValue]);
|
|
445
|
+
const queueAttachment = useCallback((attachment) => {
|
|
446
|
+
setQueuedAttachments((previous) => [...previous, attachment]);
|
|
447
|
+
}, []);
|
|
448
|
+
const handlePastedText = useCallback((pastedText) => {
|
|
449
|
+
if (!pastedText) {
|
|
450
|
+
return;
|
|
451
|
+
}
|
|
452
|
+
const pasteAsPlainText = () => {
|
|
453
|
+
insertTextAtCaret(pastedText);
|
|
454
|
+
setClipboardStatus('Pasted text');
|
|
455
|
+
onInputActivity?.();
|
|
456
|
+
};
|
|
457
|
+
const normalizedPath = normalizeClipboardPath(pastedText, searchRoot);
|
|
458
|
+
if (normalizedPath) {
|
|
459
|
+
void buildAttachmentFromPath(normalizedPath)
|
|
460
|
+
.then((attachment) => {
|
|
461
|
+
queueAttachment(attachment);
|
|
462
|
+
setClipboardStatus(`Queued ${attachment.label}`);
|
|
463
|
+
onInputActivity?.();
|
|
464
|
+
})
|
|
465
|
+
.catch(() => {
|
|
466
|
+
pasteAsPlainText();
|
|
467
|
+
});
|
|
468
|
+
return;
|
|
469
|
+
}
|
|
470
|
+
if (shouldCollapsePastedText(pastedText)) {
|
|
471
|
+
queueAttachment({
|
|
472
|
+
id: crypto.randomUUID(),
|
|
473
|
+
label: `Pasted text block (${pastedText.length} chars)`,
|
|
474
|
+
payload: pastedText,
|
|
475
|
+
});
|
|
476
|
+
setClipboardStatus('Queued pasted text block');
|
|
477
|
+
onInputActivity?.();
|
|
307
478
|
return;
|
|
308
479
|
}
|
|
309
|
-
|
|
310
|
-
|
|
480
|
+
pasteAsPlainText();
|
|
481
|
+
}, [insertTextAtCaret, onInputActivity, queueAttachment, searchRoot]);
|
|
482
|
+
const pasteClipboardIntoInput = useCallback(() => {
|
|
483
|
+
requestInputFocus();
|
|
484
|
+
void readTextFromClipboard()
|
|
485
|
+
.then(async (clipboardText) => {
|
|
486
|
+
if (clipboardText.trim()) {
|
|
487
|
+
handlePastedText(clipboardText);
|
|
488
|
+
return;
|
|
489
|
+
}
|
|
490
|
+
const clipboardPaths = await readFilePathsFromClipboard();
|
|
491
|
+
if (clipboardPaths.length > 0) {
|
|
492
|
+
const attachments = await Promise.all(clipboardPaths.map(async (clipboardPath) => {
|
|
493
|
+
try {
|
|
494
|
+
return await buildAttachmentFromPath(clipboardPath);
|
|
495
|
+
}
|
|
496
|
+
catch {
|
|
497
|
+
return null;
|
|
498
|
+
}
|
|
499
|
+
}));
|
|
500
|
+
const validAttachments = attachments.filter((value) => value !== null);
|
|
501
|
+
if (validAttachments.length > 0) {
|
|
502
|
+
validAttachments.forEach((attachment) => {
|
|
503
|
+
queueAttachment(attachment);
|
|
504
|
+
});
|
|
505
|
+
setClipboardStatus(validAttachments.length === 1
|
|
506
|
+
? `Queued ${validAttachments[0].label}`
|
|
507
|
+
: `Queued ${validAttachments.length} clipboard files`);
|
|
508
|
+
onInputActivity?.();
|
|
509
|
+
return;
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
const imageDataUrl = await readImageDataUrlFromClipboard();
|
|
513
|
+
if (imageDataUrl) {
|
|
514
|
+
queueAttachment({
|
|
515
|
+
id: crypto.randomUUID(),
|
|
516
|
+
label: 'Image: pasted-image.png',
|
|
517
|
+
payload: ``,
|
|
518
|
+
});
|
|
519
|
+
setClipboardStatus('Queued clipboard image');
|
|
520
|
+
onInputActivity?.();
|
|
521
|
+
return;
|
|
522
|
+
}
|
|
523
|
+
setClipboardStatus('Paste failed: clipboard is empty');
|
|
524
|
+
})
|
|
525
|
+
.catch((error) => {
|
|
526
|
+
const errorMessage = error instanceof Error ? error.message : 'unknown error';
|
|
527
|
+
setClipboardStatus(`Paste failed: ${errorMessage}`);
|
|
528
|
+
});
|
|
529
|
+
}, [handlePastedText, onInputActivity, queueAttachment, requestInputFocus]);
|
|
530
|
+
const submitCurrentSelection = useCallback(() => {
|
|
531
|
+
const baseValue = mode === 'option' && predefinedOptions.length > 0
|
|
532
|
+
? predefinedOptions[selectedIndex]
|
|
533
|
+
: (safeReadTextarea()?.value ?? inputValue);
|
|
534
|
+
const attachmentPayload = queuedAttachments
|
|
535
|
+
.map((attachment) => attachment.payload)
|
|
536
|
+
.join('\n\n');
|
|
537
|
+
const finalValue = attachmentPayload
|
|
538
|
+
? baseValue.trim().length > 0
|
|
539
|
+
? `${baseValue}\n\n${attachmentPayload}`
|
|
540
|
+
: attachmentPayload
|
|
541
|
+
: baseValue;
|
|
542
|
+
onSubmit(questionId, finalValue);
|
|
543
|
+
setQueuedAttachments([]);
|
|
311
544
|
}, [
|
|
312
545
|
inputValue,
|
|
313
546
|
mode,
|
|
314
547
|
onSubmit,
|
|
315
548
|
predefinedOptions,
|
|
549
|
+
queuedAttachments,
|
|
316
550
|
questionId,
|
|
317
551
|
safeReadTextarea,
|
|
318
552
|
selectedIndex,
|
|
@@ -383,6 +617,18 @@ export function InteractiveInput({ question, questionId, predefinedOptions = [],
|
|
|
383
617
|
submitCurrentSelection();
|
|
384
618
|
return;
|
|
385
619
|
}
|
|
620
|
+
const pastedText = extractPastedText(key);
|
|
621
|
+
if (pastedText !== null) {
|
|
622
|
+
if (mode === 'option' && hasOptions) {
|
|
623
|
+
setModeToInput();
|
|
624
|
+
}
|
|
625
|
+
handlePastedText(pastedText);
|
|
626
|
+
return;
|
|
627
|
+
}
|
|
628
|
+
if (isPasteShortcut(key)) {
|
|
629
|
+
pasteClipboardIntoInput();
|
|
630
|
+
return;
|
|
631
|
+
}
|
|
386
632
|
if (isCopyShortcut(key)) {
|
|
387
633
|
copyInputToClipboard();
|
|
388
634
|
return;
|
|
@@ -462,7 +708,9 @@ export function InteractiveInput({ question, questionId, predefinedOptions = [],
|
|
|
462
708
|
? `#search root: ${searchRoot}`
|
|
463
709
|
: '#search root: no search root' }), _jsx("text", { fg: "gray", children: isIndexingFiles
|
|
464
710
|
? '#search index: indexing...'
|
|
465
|
-
: `#search index: ${repositoryFiles.length} files indexed` })] })), _jsxs("box", { flexDirection: isNarrow ? 'column' : 'row', justifyContent: "space-between", marginBottom: 0, gap: isNarrow ? 0 : undefined, children: [_jsx("text", { fg: "gray", children: mode === 'input' ? 'Custom input' : 'Option selection' }),
|
|
466
|
-
|
|
467
|
-
|
|
711
|
+
: `#search index: ${repositoryFiles.length} files indexed` })] })), _jsxs("box", { flexDirection: isNarrow ? 'column' : 'row', justifyContent: "space-between", marginBottom: 0, gap: isNarrow ? 0 : undefined, children: [_jsx("text", { fg: "gray", children: mode === 'input' ? 'Custom input' : 'Option selection' }), _jsx("text", { fg: "gray", children: mode === 'input' && queuedAttachments.length > 0
|
|
712
|
+
? `${inputValue.length} chars + ${queuedAttachments.length} queued`
|
|
713
|
+
: `${inputValue.length} chars` })] }), mode === 'input' && clipboardStatus && (_jsx("text", { fg: clipboardStatus.startsWith('Copy failed:') ? 'red' : 'green', children: clipboardStatus })), mode === 'input' && queuedAttachments.length > 0 && (_jsxs("box", { flexDirection: "column", width: "100%", children: [_jsx("text", { fg: "yellow", children: _jsx("strong", { children: "QUEUED ATTACHMENTS" }) }), queuedAttachments.map((attachment) => (_jsxs("text", { fg: "gray", wrapMode: "word", children: ["- ", attachment.label] }, attachment.id)))] })), mode === 'input' && (_jsx("box", { backgroundColor: "cyan", paddingLeft: 1, paddingRight: 1, alignSelf: "flex-start", marginBottom: 0, onClick: submitCurrentSelection, children: _jsxs("text", { fg: "black", children: [_jsx("strong", { children: "Send" }), " \u2303S"] }) })), mode === 'input' && (_jsx("text", { fg: "gray", wrapMode: "word", children: hasOptions
|
|
714
|
+
? 'Enter/Ctrl+J newline (or #search apply) • #search nav: ↑/↓ or Ctrl+N/P • Tab mode switch • #path for repo file autocomplete • Cmd/Ctrl+C copy • Cmd/Ctrl+V paste/attach'
|
|
715
|
+
: 'Enter/Ctrl+J newline • #search nav: ↑/↓ or Ctrl+N/P • Enter/Tab #search apply • #path for repo file autocomplete • Cmd/Ctrl+C copy • Cmd/Ctrl+V paste/attach' }))] }));
|
|
468
716
|
}
|
|
@@ -24,6 +24,8 @@ export const isControlKeyShortcut = (key, letter) => {
|
|
|
24
24
|
export const isSubmitShortcut = (key) => isControlKeyShortcut(key, 's');
|
|
25
25
|
export const isCopyShortcut = (key) => isControlKeyShortcut(key, 'c') ||
|
|
26
26
|
(key.ctrl && key.shift && key.name.toLowerCase() === 'c');
|
|
27
|
+
export const isPasteShortcut = (key) => isControlKeyShortcut(key, 'v') ||
|
|
28
|
+
(key.ctrl && key.shift && key.name.toLowerCase() === 'v');
|
|
27
29
|
export const isReverseTabShortcut = (key) => key.name === 'backtab' ||
|
|
28
30
|
(key.name === 'tab' && key.shift) ||
|
|
29
31
|
key.sequence === '\u001b[Z';
|
|
@@ -42,6 +44,19 @@ export const isPrintableCharacter = (key) => {
|
|
|
42
44
|
}
|
|
43
45
|
return null;
|
|
44
46
|
};
|
|
47
|
+
export const extractPastedText = (key) => {
|
|
48
|
+
if (key.ctrl || key.meta || key.option || key.sequence.length <= 1) {
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
for (const character of key.sequence) {
|
|
52
|
+
const code = character.charCodeAt(0);
|
|
53
|
+
const isAllowedControl = code === 9 || code === 10 || code === 13;
|
|
54
|
+
if (!isAllowedControl && code < 32) {
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return key.sequence;
|
|
59
|
+
};
|
|
45
60
|
export const textareaKeyBindings = [
|
|
46
61
|
{ name: 's', ctrl: true, action: 'submit' },
|
|
47
62
|
{ name: 's', meta: true, action: 'submit' },
|
package/dist/index.js
CHANGED
|
@@ -4,8 +4,8 @@ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
|
|
|
4
4
|
import notifier from 'node-notifier';
|
|
5
5
|
import yargs from 'yargs';
|
|
6
6
|
import { hideBin } from 'yargs/helpers';
|
|
7
|
+
import { askQuestionInSession, startIntensiveChatSession, stopIntensiveChatSession, } from './commands/intensive-chat/index.js';
|
|
7
8
|
import { getCmdWindowInput } from './commands/input/index.js';
|
|
8
|
-
import { startIntensiveChatSession, askQuestionInSession, stopIntensiveChatSession, } from './commands/intensive-chat/index.js';
|
|
9
9
|
import { USER_INPUT_TIMEOUT_SECONDS, USER_INPUT_TIMEOUT_SENTINEL, } from './constants.js';
|
|
10
10
|
import logger from './utils/logger.js';
|
|
11
11
|
import { validateRepositoryBaseDirectory } from './utils/base-directory.js';
|
|
@@ -158,10 +158,11 @@ if (isToolEnabled('start_intensive_chat')) {
|
|
|
158
158
|
const { sessionTitle, baseDirectory } = args;
|
|
159
159
|
try {
|
|
160
160
|
const validatedBaseDirectory = await validateRepositoryBaseDirectory(baseDirectory);
|
|
161
|
-
// Start a new intensive chat session, passing global timeout
|
|
162
161
|
const sessionId = await startIntensiveChatSession(sessionTitle, validatedBaseDirectory, globalTimeoutSeconds);
|
|
163
162
|
// Track this session for the client
|
|
164
|
-
activeChatSessions.set(sessionId,
|
|
163
|
+
activeChatSessions.set(sessionId, {
|
|
164
|
+
title: sessionTitle,
|
|
165
|
+
});
|
|
165
166
|
return {
|
|
166
167
|
content: [
|
|
167
168
|
{
|
|
@@ -200,8 +201,8 @@ if (isToolEnabled('ask_intensive_chat')) {
|
|
|
200
201
|
async (args) => {
|
|
201
202
|
// Use inferred args type
|
|
202
203
|
const { sessionId, question, predefinedOptions, baseDirectory } = args;
|
|
203
|
-
|
|
204
|
-
if (!
|
|
204
|
+
const activeSession = activeChatSessions.get(sessionId);
|
|
205
|
+
if (!activeSession) {
|
|
205
206
|
return {
|
|
206
207
|
content: [
|
|
207
208
|
{ type: 'text', text: 'Error: Invalid or expired session ID.' },
|
|
@@ -210,7 +211,6 @@ if (isToolEnabled('ask_intensive_chat')) {
|
|
|
210
211
|
}
|
|
211
212
|
try {
|
|
212
213
|
const validatedBaseDirectory = await validateRepositoryBaseDirectory(baseDirectory);
|
|
213
|
-
// Ask the question in the session
|
|
214
214
|
const answer = await askQuestionInSession(sessionId, question, validatedBaseDirectory, predefinedOptions);
|
|
215
215
|
// Check for the specific timeout indicator
|
|
216
216
|
if (answer === USER_INPUT_TIMEOUT_SENTINEL) {
|
|
@@ -279,8 +279,8 @@ if (isToolEnabled('stop_intensive_chat')) {
|
|
|
279
279
|
async (args) => {
|
|
280
280
|
// Use inferred args type
|
|
281
281
|
const { sessionId } = args;
|
|
282
|
-
|
|
283
|
-
if (!
|
|
282
|
+
const activeSession = activeChatSessions.get(sessionId);
|
|
283
|
+
if (!activeSession) {
|
|
284
284
|
return {
|
|
285
285
|
content: [
|
|
286
286
|
{ type: 'text', text: 'Error: Invalid or expired session ID.' },
|
|
@@ -288,7 +288,6 @@ if (isToolEnabled('stop_intensive_chat')) {
|
|
|
288
288
|
};
|
|
289
289
|
}
|
|
290
290
|
try {
|
|
291
|
-
// Stop the session
|
|
292
291
|
const success = await stopIntensiveChatSession(sessionId);
|
|
293
292
|
// Remove session from map if successful
|
|
294
293
|
if (success) {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
// === Start Intensive Chat Definition ===
|
|
3
3
|
const startCapability = {
|
|
4
|
-
description: 'Start a persistent
|
|
4
|
+
description: 'Start a persistent intensive chat session for gathering multiple answers quickly with markdown-friendly prompts.',
|
|
5
5
|
parameters: {
|
|
6
6
|
type: 'object',
|
|
7
7
|
properties: {
|
|
@@ -18,17 +18,18 @@ const startCapability = {
|
|
|
18
18
|
},
|
|
19
19
|
};
|
|
20
20
|
const startDescription = (globalTimeoutSeconds) => `<description>
|
|
21
|
-
Start an intensive chat session
|
|
21
|
+
Start an intensive chat session for gathering multiple answers quickly from the user.
|
|
22
22
|
**Highly recommended** for scenarios requiring a sequence of related inputs or confirmations.
|
|
23
23
|
Very useful for gathering multiple answers from the user in a short period of time.
|
|
24
24
|
Especially useful for brainstorming ideas or discussing complex topics with the user.
|
|
25
25
|
</description>
|
|
26
26
|
|
|
27
27
|
<importantNotes>
|
|
28
|
-
- (!important!) Opens a persistent
|
|
28
|
+
- (!important!) Opens a persistent interaction session that stays active for multiple questions.
|
|
29
29
|
- (!important!) Returns a session ID that **must** be used for subsequent questions via 'ask_intensive_chat'.
|
|
30
30
|
- (!important!) **Must** be closed with 'stop_intensive_chat' when finished gathering all inputs.
|
|
31
31
|
- (!important!) After starting a session, **immediately** continue asking all necessary questions using 'ask_intensive_chat' within the **same response message**. Do not end the response until the chat is closed with 'stop_intensive_chat'. This creates a seamless conversational flow for the user.
|
|
32
|
+
- (!important!) Continue the prompt loop until the user explicitly says one of: "Stop prompting", "End session", or "Don't ask anymore".
|
|
32
33
|
</importantNotes>
|
|
33
34
|
|
|
34
35
|
<whenToUseThisTool>
|
|
@@ -41,7 +42,7 @@ Especially useful for brainstorming ideas or discussing complex topics with the
|
|
|
41
42
|
</whenToUseThisTool>
|
|
42
43
|
|
|
43
44
|
<features>
|
|
44
|
-
- Opens a persistent
|
|
45
|
+
- Opens a persistent interactive prompt surface for continuous interaction
|
|
45
46
|
- Renders markdown prompts, including code/diff snippets, for richer question context
|
|
46
47
|
- Preserves markdown links, including VS Code file links (for example: "vscode://file/<abs-path>:<line>:<column>") in prompt content
|
|
47
48
|
- Supports option mode + free-text mode while asking follow-up questions
|
|
@@ -49,6 +50,7 @@ Especially useful for brainstorming ideas or discussing complex topics with the
|
|
|
49
50
|
- Returns a session ID for subsequent interactions
|
|
50
51
|
- Keeps full chat history visible to the user
|
|
51
52
|
- Maintains state between questions
|
|
53
|
+
- Backend-agnostic contract: start/ask/stop behavior is consistent across available UI backends
|
|
52
54
|
- Requires baseDirectory and pins autocomplete/search scope to the repository root
|
|
53
55
|
</features>
|
|
54
56
|
|
|
@@ -85,7 +87,7 @@ const startToolDefinition = {
|
|
|
85
87
|
};
|
|
86
88
|
// === Ask Intensive Chat Definition ===
|
|
87
89
|
const askCapability = {
|
|
88
|
-
description: 'Ask a markdown-friendly question in an active
|
|
90
|
+
description: 'Ask a markdown-friendly question in an active intensive chat session.',
|
|
89
91
|
parameters: {
|
|
90
92
|
type: 'object',
|
|
91
93
|
properties: {
|
|
@@ -120,6 +122,8 @@ Ask a new question in an active intensive chat session previously started with '
|
|
|
120
122
|
- (!important!) Supports predefined options for quick selection.
|
|
121
123
|
- (!important!) Returns the user's answer or indicates if they didn't respond.
|
|
122
124
|
- (!important!) **Use this repeatedly within the same response message** after 'start_intensive_chat' until all questions are asked.
|
|
125
|
+
- (!important!) If response is empty or times out for required input, re-prompt and do not proceed with assumptions.
|
|
126
|
+
- (!important!) Keep the loop active until the user explicitly says one of: "Stop prompting", "End session", or "Don't ask anymore".
|
|
123
127
|
</importantNotes>
|
|
124
128
|
|
|
125
129
|
<whenToUseThisTool>
|
|
@@ -193,9 +197,10 @@ const stopDescription = `<description>
|
|
|
193
197
|
</description>
|
|
194
198
|
|
|
195
199
|
<importantNotes>
|
|
196
|
-
- (!important!) Closes the
|
|
200
|
+
- (!important!) Closes the active intensive chat session.
|
|
197
201
|
- (!important!) Frees up system resources.
|
|
198
202
|
- (!important!) **Should always be called** as the final step when finished with an intensive chat session, typically at the end of the response message where 'start_intensive_chat' was called.
|
|
203
|
+
- (!important!) Only stop the session when the user explicitly wants to end prompting, such as with "Stop prompting", "End session", or "Don't ask anymore".
|
|
199
204
|
</importantNotes>
|
|
200
205
|
|
|
201
206
|
<whenToUseThisTool>
|
|
@@ -207,7 +212,7 @@ const stopDescription = `<description>
|
|
|
207
212
|
</whenToUseThisTool>
|
|
208
213
|
|
|
209
214
|
<features>
|
|
210
|
-
- Gracefully closes the
|
|
215
|
+
- Gracefully closes the active session in the current backend
|
|
211
216
|
- Cleans up system resources
|
|
212
217
|
- Marks the session as complete
|
|
213
218
|
</features>
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
// Define capability conforming to ToolCapabilityInfo
|
|
3
3
|
const capabilityInfo = {
|
|
4
|
-
description: 'Ask the user a question in an
|
|
4
|
+
description: 'Ask the user a question in an interactive prompt surface and await their reply.',
|
|
5
5
|
parameters: {
|
|
6
6
|
type: 'object',
|
|
7
7
|
properties: {
|
|
@@ -29,7 +29,7 @@ const capabilityInfo = {
|
|
|
29
29
|
};
|
|
30
30
|
// Define description conforming to ToolRegistrationDescription
|
|
31
31
|
const registrationDescription = (globalTimeoutSeconds) => `<description>
|
|
32
|
-
Send a question to the user via
|
|
32
|
+
Send a question to the user via an interactive prompt surface. **Crucial for clarifying requirements, confirming plans, or resolving ambiguity.**
|
|
33
33
|
You should call this tool whenever it has **any** uncertainty or needs clarification or confirmation, even for trivial or silly questions.
|
|
34
34
|
Feel free to ask anything! **Proactive questioning is preferred over making assumptions.**
|
|
35
35
|
</description>
|
|
@@ -39,9 +39,15 @@ Feel free to ask anything! **Proactive questioning is preferred over making assu
|
|
|
39
39
|
- (!important!) Continue to generate existing messages after user answers.
|
|
40
40
|
- (!important!) Provide predefined options for quick selection if applicable.
|
|
41
41
|
- (!important!) **Essential for validating assumptions before proceeding with significant actions (e.g., code edits, running commands).**
|
|
42
|
+
- (!important!) **Do not exit the prompt loop** until the user explicitly says one of: "Stop prompting", "End session", or "Don't ask anymore".
|
|
43
|
+
- (!important!) Immediately before final/closing handoff, ask exactly: "Are you satisfied with this result, or would you like any changes?"
|
|
44
|
+
- (!important!) If a required prompt times out or response is empty, re-prompt indefinitely and do not proceed with assumptions.
|
|
45
|
+
- (!important!) If the user skips a requested command/script, ask why it was skipped and whether to continue with alternatives or stop.
|
|
42
46
|
</importantNotes>
|
|
43
47
|
|
|
44
48
|
<whenToUseThisTool>
|
|
49
|
+
- Before starting any task, even if requirements appear clear
|
|
50
|
+
- After completing any task, to run the mandatory satisfaction check
|
|
45
51
|
- When you need clarification on user requirements or preferences
|
|
46
52
|
- When multiple implementation approaches are possible and user input is needed
|
|
47
53
|
- **Before making potentially impactful changes (code edits, file operations, complex commands)**
|
|
@@ -51,14 +57,19 @@ Feel free to ask anything! **Proactive questioning is preferred over making assu
|
|
|
51
57
|
- When facing ambiguous instructions that require clarification
|
|
52
58
|
- When seeking feedback on generated code or solutions
|
|
53
59
|
- When needing permission to modify critical files or functionality
|
|
60
|
+
- When user instructions are conflicting or unclear
|
|
61
|
+
- When the user asks to be prompted, asks a direct question, or asks a reply question
|
|
62
|
+
- When the user skips a command you requested
|
|
63
|
+
- Immediately before any final/closing handoff
|
|
54
64
|
- **Whenever you feel even slightly unsure about the user's intent or the correct next step.**
|
|
55
65
|
</whenToUseThisTool>
|
|
56
66
|
|
|
57
67
|
<features>
|
|
58
|
-
-
|
|
68
|
+
- Interactive prompt UI with markdown rendering (including code/diff blocks)
|
|
59
69
|
- Preserves markdown links, including VS Code file links (for example: "vscode://file/<abs-path>:<line>:<column>") when provided in the prompt text
|
|
60
70
|
- Supports option mode + free-text input mode when predefinedOptions are provided
|
|
61
71
|
- Returns user response or timeout notification (timeout defaults to ${globalTimeoutSeconds} seconds)
|
|
72
|
+
- Backend-agnostic contract: same request/response behavior regardless of the active UI backend
|
|
62
73
|
- Maintains context across user interactions
|
|
63
74
|
- Handles empty responses gracefully
|
|
64
75
|
- Shows project context in the prompt header/title
|
package/dist/utils/clipboard.js
CHANGED
|
@@ -1,5 +1,40 @@
|
|
|
1
1
|
import os from 'os';
|
|
2
2
|
import { spawn } from 'child_process';
|
|
3
|
+
function decodeFileUri(value) {
|
|
4
|
+
const trimmed = value.trim();
|
|
5
|
+
if (!trimmed.startsWith('file://')) {
|
|
6
|
+
return trimmed;
|
|
7
|
+
}
|
|
8
|
+
return decodeURIComponent(trimmed.replace(/^file:\/\//, ''));
|
|
9
|
+
}
|
|
10
|
+
function normalizePathCandidate(value) {
|
|
11
|
+
const trimmed = value.trim();
|
|
12
|
+
if (!trimmed) {
|
|
13
|
+
return null;
|
|
14
|
+
}
|
|
15
|
+
const decoded = decodeFileUri(trimmed);
|
|
16
|
+
if (decoded.startsWith('/')) {
|
|
17
|
+
return decoded;
|
|
18
|
+
}
|
|
19
|
+
if (/^[a-zA-Z]:[\\/]/.test(decoded)) {
|
|
20
|
+
return decoded;
|
|
21
|
+
}
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
function parseNonEmptyLines(value) {
|
|
25
|
+
return value
|
|
26
|
+
.split(/\r?\n/)
|
|
27
|
+
.map((line) => line.trim())
|
|
28
|
+
.filter((line) => Boolean(line) && !line.startsWith('#'));
|
|
29
|
+
}
|
|
30
|
+
function parseFilePaths(value) {
|
|
31
|
+
return parseNonEmptyLines(value)
|
|
32
|
+
.map((line) => normalizePathCandidate(line))
|
|
33
|
+
.filter((line) => typeof line === 'string');
|
|
34
|
+
}
|
|
35
|
+
function uniquePaths(values) {
|
|
36
|
+
return Array.from(new Set(values));
|
|
37
|
+
}
|
|
3
38
|
function runCommand(command, args, input) {
|
|
4
39
|
return new Promise((resolve, reject) => {
|
|
5
40
|
const child = spawn(command, args, {
|
|
@@ -65,3 +100,152 @@ export async function readTextFromClipboard() {
|
|
|
65
100
|
}
|
|
66
101
|
throw new Error(`Clipboard paste is not supported on platform: ${platform}`);
|
|
67
102
|
}
|
|
103
|
+
export async function readFilePathsFromClipboard() {
|
|
104
|
+
const platform = os.platform();
|
|
105
|
+
if (platform === 'darwin') {
|
|
106
|
+
const aliasResult = await runCommand('osascript', [
|
|
107
|
+
'-e',
|
|
108
|
+
'try',
|
|
109
|
+
'-e',
|
|
110
|
+
'POSIX path of (the clipboard as alias)',
|
|
111
|
+
'-e',
|
|
112
|
+
'on error',
|
|
113
|
+
'-e',
|
|
114
|
+
'return ""',
|
|
115
|
+
'-e',
|
|
116
|
+
'end try',
|
|
117
|
+
]);
|
|
118
|
+
const aliasPaths = parseFilePaths(aliasResult.stdout);
|
|
119
|
+
if (aliasPaths.length > 0) {
|
|
120
|
+
return aliasPaths;
|
|
121
|
+
}
|
|
122
|
+
const listResult = await runCommand('osascript', [
|
|
123
|
+
'-e',
|
|
124
|
+
'try',
|
|
125
|
+
'-e',
|
|
126
|
+
'set L to the clipboard as list',
|
|
127
|
+
'-e',
|
|
128
|
+
'set out to {}',
|
|
129
|
+
'-e',
|
|
130
|
+
'repeat with i in L',
|
|
131
|
+
'-e',
|
|
132
|
+
'set end of out to POSIX path of i',
|
|
133
|
+
'-e',
|
|
134
|
+
'end repeat',
|
|
135
|
+
'-e',
|
|
136
|
+
'set text item delimiters of AppleScript to linefeed',
|
|
137
|
+
'-e',
|
|
138
|
+
'return out as text',
|
|
139
|
+
'-e',
|
|
140
|
+
'on error',
|
|
141
|
+
'-e',
|
|
142
|
+
'return ""',
|
|
143
|
+
'-e',
|
|
144
|
+
'end try',
|
|
145
|
+
]);
|
|
146
|
+
const listPaths = parseFilePaths(listResult.stdout);
|
|
147
|
+
if (listPaths.length > 0) {
|
|
148
|
+
return listPaths;
|
|
149
|
+
}
|
|
150
|
+
const jxaResult = await runCommand('osascript', [
|
|
151
|
+
'-l',
|
|
152
|
+
'JavaScript',
|
|
153
|
+
'-e',
|
|
154
|
+
'ObjC.import("AppKit")',
|
|
155
|
+
'-e',
|
|
156
|
+
'ObjC.import("Foundation")',
|
|
157
|
+
'-e',
|
|
158
|
+
'const pb = $.NSPasteboard.generalPasteboard',
|
|
159
|
+
'-e',
|
|
160
|
+
'const classes = $.NSArray.arrayWithObject($.NSURL)',
|
|
161
|
+
'-e',
|
|
162
|
+
'const urls = pb.readObjectsForClassesOptions(classes, $())',
|
|
163
|
+
'-e',
|
|
164
|
+
'if (!urls || urls.count === 0) { "" } else { const out = []; for (let i = 0; i < urls.count; i += 1) { out.push(ObjC.unwrap(urls.objectAtIndex(i).path)); } out.join("\\n"); }',
|
|
165
|
+
]);
|
|
166
|
+
const jxaPaths = parseFilePaths(jxaResult.stdout);
|
|
167
|
+
if (jxaPaths.length > 0) {
|
|
168
|
+
return uniquePaths(jxaPaths);
|
|
169
|
+
}
|
|
170
|
+
const jxaTypeProbeResult = await runCommand('osascript', [
|
|
171
|
+
'-l',
|
|
172
|
+
'JavaScript',
|
|
173
|
+
'-e',
|
|
174
|
+
'ObjC.import("AppKit")',
|
|
175
|
+
'-e',
|
|
176
|
+
'ObjC.import("Foundation")',
|
|
177
|
+
'-e',
|
|
178
|
+
'const pb = $.NSPasteboard.generalPasteboard',
|
|
179
|
+
'-e',
|
|
180
|
+
'const out = []',
|
|
181
|
+
'-e',
|
|
182
|
+
'const pushLine = (value) => { if (!value) { return; } const text = String(value).trim(); if (!text) { return; } out.push(text); }',
|
|
183
|
+
'-e',
|
|
184
|
+
'const types = ObjC.deepUnwrap(pb.types) || []',
|
|
185
|
+
'-e',
|
|
186
|
+
'for (const type of types) { const data = pb.dataForType(type); if (!data || data.isNil()) { continue; } let text = ""; const utf8 = $.NSString.alloc.initWithDataEncoding(data, $.NSUTF8StringEncoding); if (utf8 && !utf8.isNil()) { text = ObjC.unwrap(utf8); } if (!text) { const utf16 = $.NSString.alloc.initWithDataEncoding(data, $.NSUTF16StringEncoding); if (utf16 && !utf16.isNil()) { text = ObjC.unwrap(utf16); } } if (!text) { continue; } const lines = String(text).split(/\\r?\\n/); for (const line of lines) { const trimmed = line.trim(); if (!trimmed || trimmed.startsWith("#")) { continue; } pushLine(trimmed); } }',
|
|
187
|
+
'-e',
|
|
188
|
+
'out.join("\\n")',
|
|
189
|
+
]);
|
|
190
|
+
return uniquePaths(parseFilePaths(jxaTypeProbeResult.stdout));
|
|
191
|
+
}
|
|
192
|
+
if (platform === 'linux') {
|
|
193
|
+
try {
|
|
194
|
+
const uriListResult = await runCommand('xclip', [
|
|
195
|
+
'-selection',
|
|
196
|
+
'clipboard',
|
|
197
|
+
'-o',
|
|
198
|
+
'-t',
|
|
199
|
+
'text/uri-list',
|
|
200
|
+
]);
|
|
201
|
+
return parseFilePaths(uriListResult.stdout);
|
|
202
|
+
}
|
|
203
|
+
catch {
|
|
204
|
+
return [];
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
if (platform === 'win32') {
|
|
208
|
+
try {
|
|
209
|
+
const fileDropResult = await runCommand('powershell', [
|
|
210
|
+
'-NoProfile',
|
|
211
|
+
'-Command',
|
|
212
|
+
'$paths = Get-Clipboard -Format FileDropList; if ($paths) { $paths | ForEach-Object { $_.FullName } }',
|
|
213
|
+
]);
|
|
214
|
+
return parseFilePaths(fileDropResult.stdout);
|
|
215
|
+
}
|
|
216
|
+
catch {
|
|
217
|
+
return [];
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
return [];
|
|
221
|
+
}
|
|
222
|
+
export async function readImageDataUrlFromClipboard() {
|
|
223
|
+
const platform = os.platform();
|
|
224
|
+
if (platform !== 'darwin') {
|
|
225
|
+
return null;
|
|
226
|
+
}
|
|
227
|
+
try {
|
|
228
|
+
const imageResult = await runCommand('osascript', [
|
|
229
|
+
'-l',
|
|
230
|
+
'JavaScript',
|
|
231
|
+
'-e',
|
|
232
|
+
'ObjC.import("AppKit")',
|
|
233
|
+
'-e',
|
|
234
|
+
'ObjC.import("Foundation")',
|
|
235
|
+
'-e',
|
|
236
|
+
'const pb = $.NSPasteboard.generalPasteboard',
|
|
237
|
+
'-e',
|
|
238
|
+
'const image = $.NSImage.alloc.initWithPasteboard(pb)',
|
|
239
|
+
'-e',
|
|
240
|
+
'if (!image || image.isNil()) { "" } else { const tiff = image.TIFFRepresentation; if (!tiff || tiff.isNil()) { "" } else { const rep = $.NSBitmapImageRep.imageRepWithData(tiff); if (!rep || rep.isNil()) { "" } else { const pngData = rep.representationUsingTypeProperties($.NSBitmapImageFileTypePNG, $()); if (!pngData || pngData.isNil()) { "" } else { ObjC.unwrap(pngData.base64EncodedStringWithOptions(0)); } } } }',
|
|
241
|
+
]);
|
|
242
|
+
const base64Payload = imageResult.stdout.trim();
|
|
243
|
+
if (!base64Payload) {
|
|
244
|
+
return null;
|
|
245
|
+
}
|
|
246
|
+
return `data:image/png;base64,${base64Payload}`;
|
|
247
|
+
}
|
|
248
|
+
catch {
|
|
249
|
+
return null;
|
|
250
|
+
}
|
|
251
|
+
}
|
package/package.json
CHANGED
|
@@ -1,13 +1,19 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rawwee/interactive-mcp",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.0",
|
|
4
4
|
"main": "dist/index.js",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
7
|
"interactive-mcp": "dist/index.js"
|
|
8
8
|
},
|
|
9
9
|
"files": [
|
|
10
|
-
"dist",
|
|
10
|
+
"dist/constants.js",
|
|
11
|
+
"dist/index.js",
|
|
12
|
+
"dist/commands/input",
|
|
13
|
+
"dist/commands/intensive-chat",
|
|
14
|
+
"dist/components",
|
|
15
|
+
"dist/tool-definitions",
|
|
16
|
+
"dist/utils",
|
|
11
17
|
"README.md",
|
|
12
18
|
"LICENSE",
|
|
13
19
|
"package.json"
|