@kirosnn/mosaic 0.0.91 → 0.73.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 +1 -1
- package/README.md +2 -6
- package/package.json +55 -48
- package/src/agent/Agent.ts +353 -131
- package/src/agent/context.ts +4 -4
- package/src/agent/prompts/systemPrompt.ts +209 -70
- package/src/agent/prompts/toolsPrompt.ts +285 -138
- package/src/agent/provider/anthropic.ts +109 -105
- package/src/agent/provider/google.ts +111 -107
- package/src/agent/provider/mistral.ts +95 -95
- package/src/agent/provider/ollama.ts +73 -17
- package/src/agent/provider/openai.ts +146 -102
- package/src/agent/provider/rateLimit.ts +178 -0
- package/src/agent/provider/reasoning.ts +29 -0
- package/src/agent/provider/xai.ts +108 -104
- package/src/agent/tools/definitions.ts +15 -1
- package/src/agent/tools/executor.ts +717 -98
- package/src/agent/tools/exploreExecutor.ts +20 -22
- package/src/agent/tools/fetch.ts +58 -0
- package/src/agent/tools/glob.ts +20 -4
- package/src/agent/tools/grep.ts +64 -9
- package/src/agent/tools/plan.ts +27 -0
- package/src/agent/tools/question.ts +7 -1
- package/src/agent/tools/read.ts +2 -0
- package/src/agent/types.ts +15 -14
- package/src/components/App.tsx +50 -8
- package/src/components/CustomInput.tsx +461 -77
- package/src/components/Main.tsx +1459 -1112
- package/src/components/Setup.tsx +1 -1
- package/src/components/ShortcutsModal.tsx +11 -8
- package/src/components/Welcome.tsx +1 -1
- package/src/components/main/ApprovalPanel.tsx +4 -3
- package/src/components/main/ChatPage.tsx +858 -516
- package/src/components/main/HomePage.tsx +58 -39
- package/src/components/main/QuestionPanel.tsx +52 -7
- package/src/components/main/ThinkingIndicator.tsx +13 -2
- package/src/components/main/types.ts +11 -10
- package/src/index.tsx +53 -25
- package/src/mcp/approvalPolicy.ts +148 -0
- package/src/mcp/cli/add.ts +185 -0
- package/src/mcp/cli/doctor.ts +77 -0
- package/src/mcp/cli/index.ts +85 -0
- package/src/mcp/cli/list.ts +50 -0
- package/src/mcp/cli/logs.ts +24 -0
- package/src/mcp/cli/manage.ts +99 -0
- package/src/mcp/cli/show.ts +53 -0
- package/src/mcp/cli/tools.ts +77 -0
- package/src/mcp/config.ts +223 -0
- package/src/mcp/index.ts +80 -0
- package/src/mcp/processManager.ts +299 -0
- package/src/mcp/rateLimiter.ts +50 -0
- package/src/mcp/registry.ts +151 -0
- package/src/mcp/schemaConverter.ts +100 -0
- package/src/mcp/servers/navigation.ts +854 -0
- package/src/mcp/toolCatalog.ts +169 -0
- package/src/mcp/types.ts +95 -0
- package/src/utils/approvalBridge.ts +45 -12
- package/src/utils/approvalModeBridge.ts +17 -0
- package/src/utils/commands/approvals.ts +48 -0
- package/src/utils/commands/compact.ts +30 -0
- package/src/utils/commands/echo.ts +1 -1
- package/src/utils/commands/image.ts +109 -0
- package/src/utils/commands/index.ts +9 -7
- package/src/utils/commands/new.ts +15 -0
- package/src/utils/commands/types.ts +3 -0
- package/src/utils/config.ts +3 -1
- package/src/utils/diffRendering.tsx +13 -16
- package/src/utils/exploreBridge.ts +10 -0
- package/src/utils/history.ts +82 -40
- package/src/utils/imageBridge.ts +28 -0
- package/src/utils/images.ts +31 -0
- package/src/utils/markdown.tsx +163 -99
- package/src/utils/models.ts +31 -16
- package/src/utils/notificationBridge.ts +23 -0
- package/src/utils/questionBridge.ts +36 -1
- package/src/utils/tokenEstimator.ts +32 -0
- package/src/utils/toolFormatting.ts +428 -48
- package/src/web/app.tsx +65 -5
- package/src/web/assets/css/ChatPage.css +102 -30
- package/src/web/assets/css/MessageItem.css +26 -29
- package/src/web/assets/css/ThinkingIndicator.css +44 -6
- package/src/web/assets/css/ToolMessage.css +36 -14
- package/src/web/components/ChatPage.tsx +228 -105
- package/src/web/components/HomePage.tsx +3 -3
- package/src/web/components/MessageItem.tsx +80 -81
- package/src/web/components/QuestionPanel.tsx +72 -12
- package/src/web/components/Setup.tsx +1 -1
- package/src/web/components/Sidebar.tsx +1 -3
- package/src/web/components/ThinkingIndicator.tsx +41 -21
- package/src/web/router.ts +1 -1
- package/src/web/server.tsx +894 -662
- package/src/web/storage.ts +23 -1
- package/src/web/types.ts +7 -6
- package/src/utils/commands/redo.ts +0 -74
- package/src/utils/commands/sessions.ts +0 -129
- package/src/utils/commands/undo.ts +0 -75
- package/src/utils/undoRedo.ts +0 -429
- package/src/utils/undoRedoBridge.ts +0 -45
- package/src/utils/undoRedoDb.ts +0 -338
|
@@ -17,28 +17,43 @@ const TIPS = [
|
|
|
17
17
|
"Use /clear to reset the current chat session.",
|
|
18
18
|
"Use Tab to autocomplete commands and arguments.",
|
|
19
19
|
"Use Ctrl + K to clear the current input line.",
|
|
20
|
+
"Use Ctrl + G to edit the current input in your system editor.",
|
|
20
21
|
"Use /help to display the list of available commands.",
|
|
22
|
+
"Select text with the mouse to copy it automatically.",
|
|
23
|
+
"Attach an image with /image <path>.",
|
|
24
|
+
"Paste an image with Ctrl/Alt + V (or Cmd + V on macOS).",
|
|
21
25
|
];
|
|
22
26
|
|
|
23
|
-
export function HomePage({ onSubmit, pasteRequestId, shortcutsOpen }: HomePageProps) {
|
|
24
|
-
const [
|
|
25
|
-
const [
|
|
26
|
-
const [
|
|
27
|
-
const [
|
|
28
|
-
const [
|
|
27
|
+
export function HomePage({ onSubmit, pasteRequestId, shortcutsOpen }: HomePageProps) {
|
|
28
|
+
const [terminalWidth, setTerminalWidth] = useState(process.stdout.columns || 80);
|
|
29
|
+
const [currentTipIndex, setCurrentTipIndex] = useState(0);
|
|
30
|
+
const [displayedText, setDisplayedText] = useState("");
|
|
31
|
+
const [isDeleting, setIsDeleting] = useState(false);
|
|
32
|
+
const [isWaiting, setIsWaiting] = useState(false);
|
|
33
|
+
const [cursorVisible, setCursorVisible] = useState(true);
|
|
29
34
|
|
|
30
|
-
useEffect(() => {
|
|
31
|
-
const cursorInterval = setInterval(() => {
|
|
32
|
-
setCursorVisible((v) => !v);
|
|
33
|
-
}, 500);
|
|
34
|
-
return () => clearInterval(cursorInterval);
|
|
35
|
-
}, []);
|
|
36
|
-
|
|
37
|
-
useEffect(() => {
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
35
|
+
useEffect(() => {
|
|
36
|
+
const cursorInterval = setInterval(() => {
|
|
37
|
+
setCursorVisible((v) => !v);
|
|
38
|
+
}, 500);
|
|
39
|
+
return () => clearInterval(cursorInterval);
|
|
40
|
+
}, []);
|
|
41
|
+
|
|
42
|
+
useEffect(() => {
|
|
43
|
+
const handleResize = () => {
|
|
44
|
+
setTerminalWidth(process.stdout.columns || 80);
|
|
45
|
+
};
|
|
46
|
+
process.stdout.on('resize', handleResize);
|
|
47
|
+
return () => {
|
|
48
|
+
process.stdout.off('resize', handleResize);
|
|
49
|
+
};
|
|
50
|
+
}, []);
|
|
51
|
+
|
|
52
|
+
useEffect(() => {
|
|
53
|
+
if (isWaiting) {
|
|
54
|
+
const timeout = setTimeout(() => {
|
|
55
|
+
setIsWaiting(false);
|
|
56
|
+
setIsDeleting(true);
|
|
42
57
|
}, 5000);
|
|
43
58
|
return () => clearTimeout(timeout);
|
|
44
59
|
}
|
|
@@ -69,10 +84,13 @@ export function HomePage({ onSubmit, pasteRequestId, shortcutsOpen }: HomePagePr
|
|
|
69
84
|
return () => clearTimeout(timeout);
|
|
70
85
|
}
|
|
71
86
|
}
|
|
72
|
-
}, [displayedText, isDeleting, isWaiting, currentTipIndex]);
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
87
|
+
}, [displayedText, isDeleting, isWaiting, currentTipIndex]);
|
|
88
|
+
|
|
89
|
+
const containerWidth = Math.min(80, Math.floor(terminalWidth * 0.8));
|
|
90
|
+
const inputWidth = Math.max(10, containerWidth - 4);
|
|
91
|
+
|
|
92
|
+
return (
|
|
93
|
+
<box flexDirection="column" width="100%" height="100%" justifyContent="center" alignItems="center">
|
|
76
94
|
<box flexDirection="column" alignItems="center" marginBottom={2}>
|
|
77
95
|
<text fg="#ffca38" attributes={TextAttributes.BOLD}>███╗ ███╗</text>
|
|
78
96
|
<text fg="#ffca38" attributes={TextAttributes.BOLD}>████╗ ████║</text>
|
|
@@ -80,22 +98,23 @@ export function HomePage({ onSubmit, pasteRequestId, shortcutsOpen }: HomePagePr
|
|
|
80
98
|
</box>
|
|
81
99
|
|
|
82
100
|
<box width="80%" maxWidth={80}>
|
|
83
|
-
<box
|
|
84
|
-
flexDirection="row"
|
|
85
|
-
backgroundColor="#1a1a1a"
|
|
86
|
-
paddingLeft={2}
|
|
87
|
-
paddingRight={2}
|
|
88
|
-
paddingTop={1}
|
|
89
|
-
paddingBottom={1}
|
|
90
|
-
>
|
|
91
|
-
<CustomInput
|
|
92
|
-
onSubmit={onSubmit}
|
|
93
|
-
placeholder="Ask anything..."
|
|
94
|
-
focused={!shortcutsOpen}
|
|
95
|
-
pasteRequestId={shortcutsOpen ? 0 : pasteRequestId}
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
101
|
+
<box
|
|
102
|
+
flexDirection="row"
|
|
103
|
+
backgroundColor="#1a1a1a"
|
|
104
|
+
paddingLeft={2}
|
|
105
|
+
paddingRight={2}
|
|
106
|
+
paddingTop={1}
|
|
107
|
+
paddingBottom={1}
|
|
108
|
+
>
|
|
109
|
+
<CustomInput
|
|
110
|
+
onSubmit={onSubmit}
|
|
111
|
+
placeholder="Ask anything..."
|
|
112
|
+
focused={!shortcutsOpen}
|
|
113
|
+
pasteRequestId={shortcutsOpen ? 0 : pasteRequestId}
|
|
114
|
+
maxWidth={inputWidth}
|
|
115
|
+
/>
|
|
116
|
+
</box>
|
|
117
|
+
</box>
|
|
99
118
|
|
|
100
119
|
<box width="80%" maxWidth={80} marginTop={3} flexDirection="row" justifyContent="center">
|
|
101
120
|
<text fg="#ffca38" attributes={TextAttributes.BOLD}>ⓘ TIPS: </text>
|
|
@@ -108,4 +127,4 @@ export function HomePage({ onSubmit, pasteRequestId, shortcutsOpen }: HomePagePr
|
|
|
108
127
|
</box>
|
|
109
128
|
</box>
|
|
110
129
|
);
|
|
111
|
-
}
|
|
130
|
+
}
|
|
@@ -8,15 +8,28 @@ interface QuestionPanelProps {
|
|
|
8
8
|
request: QuestionRequest;
|
|
9
9
|
disabled?: boolean;
|
|
10
10
|
onAnswer: (index: number, customText?: string) => void;
|
|
11
|
+
maxWidth?: number;
|
|
11
12
|
}
|
|
12
13
|
|
|
13
|
-
export function QuestionPanel({ request, disabled = false, onAnswer }: QuestionPanelProps) {
|
|
14
|
+
export function QuestionPanel({ request, disabled = false, onAnswer, maxWidth }: QuestionPanelProps) {
|
|
14
15
|
const [selectedIndex, setSelectedIndex] = useState(0);
|
|
16
|
+
const [remaining, setRemaining] = useState<number | null>(request.timeout ?? null);
|
|
17
|
+
const [validationError, setValidationError] = useState<string | null>(null);
|
|
15
18
|
|
|
16
19
|
useEffect(() => {
|
|
17
20
|
setSelectedIndex(0);
|
|
21
|
+
setValidationError(null);
|
|
22
|
+
setRemaining(request.timeout ?? null);
|
|
18
23
|
}, [request.id]);
|
|
19
24
|
|
|
25
|
+
useEffect(() => {
|
|
26
|
+
if (remaining === null || remaining <= 0) return;
|
|
27
|
+
const id = setInterval(() => {
|
|
28
|
+
setRemaining(prev => (prev !== null && prev > 0 ? prev - 1 : prev));
|
|
29
|
+
}, 1000);
|
|
30
|
+
return () => clearInterval(id);
|
|
31
|
+
}, [remaining !== null]);
|
|
32
|
+
|
|
20
33
|
useKeyboard((key) => {
|
|
21
34
|
if (disabled) return;
|
|
22
35
|
|
|
@@ -47,13 +60,30 @@ export function QuestionPanel({ request, disabled = false, onAnswer }: QuestionP
|
|
|
47
60
|
if (!text || !text.trim()) {
|
|
48
61
|
return;
|
|
49
62
|
}
|
|
63
|
+
if (request.validation) {
|
|
64
|
+
try {
|
|
65
|
+
if (!new RegExp(request.validation.pattern).test(text)) {
|
|
66
|
+
setValidationError(request.validation.message || `Input must match: ${request.validation.pattern}`);
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
} catch {
|
|
70
|
+
setValidationError('Invalid validation pattern');
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
setValidationError(null);
|
|
50
75
|
onAnswer(0, text);
|
|
51
76
|
};
|
|
52
77
|
|
|
78
|
+
let lastGroup: string | undefined;
|
|
79
|
+
|
|
53
80
|
return (
|
|
54
81
|
<box flexDirection="column" width="100%" backgroundColor="#1a1a1a" paddingLeft={1} paddingRight={1} paddingTop={1} paddingBottom={1}>
|
|
55
82
|
<box flexDirection="row" marginBottom={1}>
|
|
56
83
|
<text fg="#ffca38" attributes={TextAttributes.BOLD}>Question</text>
|
|
84
|
+
{remaining !== null && (
|
|
85
|
+
<text fg={remaining <= 5 ? '#ff4444' : '#888888'}> Timeout: {remaining}s</text>
|
|
86
|
+
)}
|
|
57
87
|
</box>
|
|
58
88
|
|
|
59
89
|
<box flexDirection="column" marginBottom={1}>
|
|
@@ -67,19 +97,34 @@ export function QuestionPanel({ request, disabled = false, onAnswer }: QuestionP
|
|
|
67
97
|
const selected = index === selectedIndex;
|
|
68
98
|
const prefix = selected ? '> ' : ' ';
|
|
69
99
|
const number = index <= 8 ? `${index + 1}. ` : ' ';
|
|
100
|
+
const showGroupHeader = option.group && option.group !== lastGroup;
|
|
101
|
+
lastGroup = option.group;
|
|
70
102
|
return (
|
|
71
|
-
<box key={`${request.id}-${index}`} flexDirection="
|
|
72
|
-
|
|
73
|
-
{
|
|
74
|
-
|
|
103
|
+
<box key={`${request.id}-${index}`} flexDirection="column">
|
|
104
|
+
{showGroupHeader && (
|
|
105
|
+
<box paddingLeft={1} marginTop={index > 0 ? 1 : 0}>
|
|
106
|
+
<text fg="#888888" attributes={TextAttributes.BOLD}>{option.group}</text>
|
|
107
|
+
</box>
|
|
108
|
+
)}
|
|
109
|
+
<box flexDirection="row" backgroundColor={selected ? '#2a2a2a' : 'transparent'} paddingLeft={1} paddingRight={1}>
|
|
110
|
+
<text fg={selected ? '#ffca38' : 'white'} attributes={selected ? TextAttributes.BOLD : TextAttributes.NONE}>
|
|
111
|
+
{prefix}{number}{option.label}
|
|
112
|
+
</text>
|
|
113
|
+
</box>
|
|
75
114
|
</box>
|
|
76
115
|
);
|
|
77
116
|
})}
|
|
78
117
|
</box>
|
|
79
118
|
|
|
119
|
+
{validationError && (
|
|
120
|
+
<box marginBottom={1} paddingLeft={1}>
|
|
121
|
+
<text fg="#ff4444">{validationError}</text>
|
|
122
|
+
</box>
|
|
123
|
+
)}
|
|
124
|
+
|
|
80
125
|
<box flexDirection="row">
|
|
81
|
-
<CustomInput onSubmit={handleCustomSubmit} placeholder="Tell Mosaic what it should do and press Enter" focused={!disabled} disableHistory={true} />
|
|
126
|
+
<CustomInput onSubmit={handleCustomSubmit} placeholder="Tell Mosaic what it should do and press Enter" focused={!disabled} disableHistory={true} maxWidth={maxWidth} />
|
|
82
127
|
</box>
|
|
83
128
|
</box>
|
|
84
129
|
);
|
|
85
|
-
}
|
|
130
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { useState, useEffect } from "react";
|
|
2
2
|
import { TextAttributes } from "@opentui/core";
|
|
3
3
|
import { THINKING_WORDS } from "./types";
|
|
4
4
|
|
|
@@ -7,6 +7,8 @@ interface ThinkingIndicatorProps {
|
|
|
7
7
|
hasQuestion: boolean;
|
|
8
8
|
startTime?: number | null;
|
|
9
9
|
tokens?: number;
|
|
10
|
+
inProgressStep?: string;
|
|
11
|
+
nextStep?: string;
|
|
10
12
|
}
|
|
11
13
|
|
|
12
14
|
export function getInputBarBaseLines(): number {
|
|
@@ -22,7 +24,8 @@ export function shouldShowThinkingIndicator({ isProcessing, hasQuestion }: Think
|
|
|
22
24
|
}
|
|
23
25
|
|
|
24
26
|
export function getBottomReservedLinesForInputBar(props: ThinkingIndicatorProps): number {
|
|
25
|
-
|
|
27
|
+
const indicatorLines = shouldShowThinkingIndicator(props) ? (props.nextStep ? 3 : 2) : 0;
|
|
28
|
+
return getInputBarBaseLines() + indicatorLines + 2;
|
|
26
29
|
}
|
|
27
30
|
|
|
28
31
|
export function formatElapsedTime(ms: number | null | undefined, fromStartTime: boolean = true): string {
|
|
@@ -93,6 +96,14 @@ export function ThinkingIndicatorBlock(props: ThinkingIndicatorProps) {
|
|
|
93
96
|
return (
|
|
94
97
|
<box flexDirection="column" width="100%">
|
|
95
98
|
<ThinkingIndicator {...props} />
|
|
99
|
+
{props.nextStep ? (
|
|
100
|
+
<box flexDirection="row" width="100%" paddingLeft={2}>
|
|
101
|
+
<text fg="#ffca38" attributes={TextAttributes.BOLD}>⎿ </text>
|
|
102
|
+
<text fg="#ffca38" attributes={TextAttributes.BOLD}>Next:</text>
|
|
103
|
+
<text> </text>
|
|
104
|
+
<text fg="white" attributes={TextAttributes.DIM}>{props.nextStep}</text>
|
|
105
|
+
</box>
|
|
106
|
+
) : null}
|
|
96
107
|
<box flexDirection="row" width="100%">
|
|
97
108
|
<text> </text>
|
|
98
109
|
</box>
|
|
@@ -26,15 +26,16 @@ export const THINKING_WORDS = [
|
|
|
26
26
|
"Revolutionizing"
|
|
27
27
|
];
|
|
28
28
|
|
|
29
|
-
export interface Message {
|
|
30
|
-
id: string;
|
|
31
|
-
role: "user" | "assistant" | "tool" | "slash";
|
|
32
|
-
displayRole?: "user" | "assistant" | "tool" | "slash";
|
|
33
|
-
displayContent?: string;
|
|
34
|
-
content: string;
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
29
|
+
export interface Message {
|
|
30
|
+
id: string;
|
|
31
|
+
role: "user" | "assistant" | "tool" | "slash";
|
|
32
|
+
displayRole?: "user" | "assistant" | "tool" | "slash";
|
|
33
|
+
displayContent?: string;
|
|
34
|
+
content: string;
|
|
35
|
+
images?: import("../../utils/images").ImageAttachment[];
|
|
36
|
+
toolName?: string;
|
|
37
|
+
toolArgs?: Record<string, unknown>;
|
|
38
|
+
toolResult?: unknown;
|
|
38
39
|
success?: boolean;
|
|
39
40
|
isError?: boolean;
|
|
40
41
|
responseDuration?: number;
|
|
@@ -52,4 +53,4 @@ export interface MainProps {
|
|
|
52
53
|
shortcutsOpen?: boolean;
|
|
53
54
|
commandsOpen?: boolean;
|
|
54
55
|
initialMessage?: string;
|
|
55
|
-
}
|
|
56
|
+
}
|
package/src/index.tsx
CHANGED
|
@@ -37,6 +37,8 @@ interface ParsedArgs {
|
|
|
37
37
|
uninstall?: boolean;
|
|
38
38
|
forceUninstall?: boolean;
|
|
39
39
|
webServer?: boolean;
|
|
40
|
+
mcpCommand?: boolean;
|
|
41
|
+
mcpArgs?: string[];
|
|
40
42
|
}
|
|
41
43
|
|
|
42
44
|
class CLI {
|
|
@@ -54,8 +56,9 @@ class CLI {
|
|
|
54
56
|
parsed.directory = args[i + 1];
|
|
55
57
|
i += 2;
|
|
56
58
|
} else if (arg === 'run') {
|
|
57
|
-
|
|
58
|
-
|
|
59
|
+
const message = args.slice(i + 1).join(' ');
|
|
60
|
+
if (message) parsed.initialMessage = message;
|
|
61
|
+
i = args.length;
|
|
59
62
|
} else if (arg === 'uninstall') {
|
|
60
63
|
parsed.uninstall = true;
|
|
61
64
|
if (args[i + 1] === '--force') {
|
|
@@ -64,6 +67,10 @@ class CLI {
|
|
|
64
67
|
} else {
|
|
65
68
|
i++;
|
|
66
69
|
}
|
|
70
|
+
} else if (arg === 'mcp') {
|
|
71
|
+
parsed.mcpCommand = true;
|
|
72
|
+
parsed.mcpArgs = args.slice(i + 1);
|
|
73
|
+
i = args.length;
|
|
67
74
|
} else if (arg === 'web') {
|
|
68
75
|
parsed.webServer = true;
|
|
69
76
|
i++;
|
|
@@ -80,31 +87,39 @@ class CLI {
|
|
|
80
87
|
|
|
81
88
|
showHelp(): void {
|
|
82
89
|
const gold = (text: string) => `\x1b[38;2;255;202;56m${text}\x1b[0m`;
|
|
90
|
+
const gray = (text: string) => `\x1b[90m${text}\x1b[0m`;
|
|
83
91
|
|
|
84
92
|
console.log('');
|
|
85
|
-
console.log(gold('███╗ ███╗'));
|
|
86
|
-
console.log(gold('████╗ ████║'));
|
|
87
|
-
console.log(gold('███╔████╔███║'));
|
|
88
93
|
console.log(`
|
|
89
|
-
Mosaic
|
|
90
|
-
|
|
91
|
-
Usage
|
|
92
|
-
mosaic [options] [
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
mosaic
|
|
107
|
-
mosaic
|
|
94
|
+
${gold('Mosaic')}
|
|
95
|
+
|
|
96
|
+
${gold('Usage')}
|
|
97
|
+
$ mosaic [options] [path]
|
|
98
|
+
$ mosaic <command> [options]
|
|
99
|
+
|
|
100
|
+
${gold('Options')}
|
|
101
|
+
-h, --help ${gray('Show this help message')}
|
|
102
|
+
-d, --directory <path> ${gray('Open Mosaic in a specific directory (default: current)')}
|
|
103
|
+
|
|
104
|
+
${gold('Commands')}
|
|
105
|
+
run "<message>" ${gray('Launch Mosaic with an initial prompt to execute immediately')}
|
|
106
|
+
web ${gray('Start the Mosaic web interface server (default: http://127.0.0.1:8192)')}
|
|
107
|
+
mcp <subcommand> ${gray('Manage Model Context Protocol (MCP) servers')}
|
|
108
|
+
uninstall [--force] ${gray('Uninstall Mosaic from your system')}
|
|
109
|
+
|
|
110
|
+
${gold('MCP Subcommands')}
|
|
111
|
+
mosaic mcp list ${gray('List configured MCP servers')}
|
|
112
|
+
mosaic mcp add [name] ${gray('Add a new MCP server')}
|
|
113
|
+
mosaic mcp doctor ${gray('Run diagnostics')}
|
|
114
|
+
mosaic mcp help ${gray('View full list of MCP commands')}
|
|
115
|
+
|
|
116
|
+
${gold('Examples')}
|
|
117
|
+
${gray('mosaic')} # Start in current directory
|
|
118
|
+
${gray('mosaic ./my-project')} # Start in specific directory
|
|
119
|
+
${gray('mosaic run "Fix the bug in main.ts"')} # Launch with a specific task
|
|
120
|
+
${gray('mosaic web')} # Start the web UI
|
|
121
|
+
${gray('mosaic mcp list')} # Check connected tools
|
|
122
|
+
${gray('mosaic uninstall --force')} # Completely remove Mosaic
|
|
108
123
|
`);
|
|
109
124
|
}
|
|
110
125
|
|
|
@@ -128,6 +143,12 @@ if (parsed.uninstall) {
|
|
|
128
143
|
process.exit(0);
|
|
129
144
|
}
|
|
130
145
|
|
|
146
|
+
if (parsed.mcpCommand) {
|
|
147
|
+
const { runMcpCli } = await import('./mcp/cli/index');
|
|
148
|
+
await runMcpCli(parsed.mcpArgs ?? []);
|
|
149
|
+
process.exit(0);
|
|
150
|
+
}
|
|
151
|
+
|
|
131
152
|
if (parsed.webServer) {
|
|
132
153
|
const { spawn } = await import('child_process');
|
|
133
154
|
const path = await import('path');
|
|
@@ -189,9 +210,16 @@ if (parsed.directory) {
|
|
|
189
210
|
import { addRecentProject } from './utils/config';
|
|
190
211
|
addRecentProject(process.cwd());
|
|
191
212
|
|
|
213
|
+
const { initializeMcp } = await import('./mcp/index');
|
|
214
|
+
await initializeMcp().catch(() => { });
|
|
215
|
+
|
|
192
216
|
process.title = '⁘ Mosaic';
|
|
193
217
|
|
|
194
|
-
const cleanup = (code = 0) => {
|
|
218
|
+
const cleanup = async (code = 0) => {
|
|
219
|
+
try {
|
|
220
|
+
const { shutdownMcp } = await import('./mcp/index');
|
|
221
|
+
await shutdownMcp();
|
|
222
|
+
} catch { }
|
|
195
223
|
process.stdout.write('\x1b[?25h');
|
|
196
224
|
process.exit(code);
|
|
197
225
|
};
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import { createHash } from 'crypto';
|
|
2
|
+
import { requestApproval } from '../utils/approvalBridge';
|
|
3
|
+
import type { McpApprovalCacheEntry, McpRiskHint, McpServerConfig } from './types';
|
|
4
|
+
|
|
5
|
+
const READ_KEYWORDS = ['read', 'get', 'list', 'search', 'find', 'show', 'describe', 'query', 'fetch', 'inspect', 'view', 'ls', 'cat'];
|
|
6
|
+
const WRITE_KEYWORDS = ['write', 'set', 'create', 'update', 'put', 'save', 'modify', 'patch', 'upsert', 'insert', 'append'];
|
|
7
|
+
const EXEC_KEYWORDS = ['exec', 'run', 'execute', 'spawn', 'shell', 'eval', 'invoke', 'call', 'process'];
|
|
8
|
+
const DELETE_KEYWORDS = ['delete', 'remove', 'destroy', 'drop', 'purge', 'clean', 'wipe', 'rm', 'unlink'];
|
|
9
|
+
const NET_KEYWORDS = ['http', 'request', 'download', 'upload', 'send', 'post', 'api', 'webhook', 'socket', 'connect'];
|
|
10
|
+
|
|
11
|
+
export class McpApprovalPolicy {
|
|
12
|
+
private cache = new Map<string, McpApprovalCacheEntry>();
|
|
13
|
+
|
|
14
|
+
inferRiskHint(toolName: string, _args: Record<string, unknown>): McpRiskHint {
|
|
15
|
+
const lower = toolName.toLowerCase();
|
|
16
|
+
|
|
17
|
+
for (const kw of DELETE_KEYWORDS) {
|
|
18
|
+
if (lower.includes(kw)) return 'execute';
|
|
19
|
+
}
|
|
20
|
+
for (const kw of EXEC_KEYWORDS) {
|
|
21
|
+
if (lower.includes(kw)) return 'execute';
|
|
22
|
+
}
|
|
23
|
+
for (const kw of WRITE_KEYWORDS) {
|
|
24
|
+
if (lower.includes(kw)) return 'write';
|
|
25
|
+
}
|
|
26
|
+
for (const kw of NET_KEYWORDS) {
|
|
27
|
+
if (lower.includes(kw)) return 'network';
|
|
28
|
+
}
|
|
29
|
+
for (const kw of READ_KEYWORDS) {
|
|
30
|
+
if (lower.includes(kw)) return 'read';
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return 'unknown';
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async requestMcpApproval(request: {
|
|
37
|
+
serverId: string;
|
|
38
|
+
serverName: string;
|
|
39
|
+
toolName: string;
|
|
40
|
+
canonicalId: string;
|
|
41
|
+
args: Record<string, unknown>;
|
|
42
|
+
approvalMode: McpServerConfig['approval'];
|
|
43
|
+
}): Promise<{ approved: boolean; customResponse?: string }> {
|
|
44
|
+
if (request.approvalMode === 'never') {
|
|
45
|
+
return { approved: true };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const riskHint = this.inferRiskHint(request.toolName, request.args);
|
|
49
|
+
|
|
50
|
+
if (this.checkCache(request.serverId, request.toolName, request.args)) {
|
|
51
|
+
return { approved: true };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const argsStr = formatArgs(request.args);
|
|
55
|
+
const payloadSize = JSON.stringify(request.args).length;
|
|
56
|
+
|
|
57
|
+
const preview = {
|
|
58
|
+
title: `MCP: ${request.serverName} / ${request.toolName}`,
|
|
59
|
+
content: argsStr,
|
|
60
|
+
details: [
|
|
61
|
+
`Server: ${request.serverName} (${request.serverId})`,
|
|
62
|
+
`Tool: ${request.toolName}`,
|
|
63
|
+
`Risk: ${riskHint}`,
|
|
64
|
+
`Payload: ${payloadSize} bytes`,
|
|
65
|
+
],
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
const mcpMeta = {
|
|
69
|
+
serverId: request.serverId,
|
|
70
|
+
serverName: request.serverName,
|
|
71
|
+
canonicalId: request.canonicalId,
|
|
72
|
+
riskHint,
|
|
73
|
+
payloadSize,
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
const result = await requestApproval(
|
|
77
|
+
request.canonicalId,
|
|
78
|
+
{ ...request.args, __mcpMeta: mcpMeta },
|
|
79
|
+
preview
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
if (result.approved && request.approvalMode !== 'always') {
|
|
83
|
+
this.addToCache(request.serverId, request.toolName, request.args, request.approvalMode);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return result;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
private checkCache(serverId: string, toolName: string, args: Record<string, unknown>): boolean {
|
|
90
|
+
const now = Date.now();
|
|
91
|
+
|
|
92
|
+
const serverKey = `server:${serverId}`;
|
|
93
|
+
const serverEntry = this.cache.get(serverKey);
|
|
94
|
+
if (serverEntry && serverEntry.expiresAt > now) return true;
|
|
95
|
+
|
|
96
|
+
const toolKey = `tool:${serverId}:${toolName}`;
|
|
97
|
+
const toolEntry = this.cache.get(toolKey);
|
|
98
|
+
if (toolEntry && toolEntry.expiresAt > now) return true;
|
|
99
|
+
|
|
100
|
+
const argsHash = hashArgs(args);
|
|
101
|
+
const argsKey = `toolArgs:${serverId}:${toolName}:${argsHash}`;
|
|
102
|
+
const argsEntry = this.cache.get(argsKey);
|
|
103
|
+
if (argsEntry && argsEntry.expiresAt > now) return true;
|
|
104
|
+
|
|
105
|
+
return false;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
private addToCache(serverId: string, toolName: string, _args: Record<string, unknown>, mode: McpServerConfig['approval']): void {
|
|
109
|
+
const ttl = 300000;
|
|
110
|
+
const expiresAt = Date.now() + ttl;
|
|
111
|
+
|
|
112
|
+
switch (mode) {
|
|
113
|
+
case 'once-per-server': {
|
|
114
|
+
const key = `server:${serverId}`;
|
|
115
|
+
this.cache.set(key, { scope: 'server', key, expiresAt });
|
|
116
|
+
break;
|
|
117
|
+
}
|
|
118
|
+
case 'once-per-tool': {
|
|
119
|
+
const key = `tool:${serverId}:${toolName}`;
|
|
120
|
+
this.cache.set(key, { scope: 'tool', key, expiresAt });
|
|
121
|
+
break;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
clearCache(): void {
|
|
127
|
+
this.cache.clear();
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function hashArgs(args: Record<string, unknown>): string {
|
|
132
|
+
const str = JSON.stringify(args, Object.keys(args).sort());
|
|
133
|
+
return createHash('sha256').update(str).digest('hex').slice(0, 12);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function formatArgs(args: Record<string, unknown>): string {
|
|
137
|
+
const entries = Object.entries(args);
|
|
138
|
+
if (entries.length === 0) return '(no arguments)';
|
|
139
|
+
|
|
140
|
+
const lines: string[] = [];
|
|
141
|
+
for (const [key, value] of entries) {
|
|
142
|
+
const strValue = typeof value === 'string'
|
|
143
|
+
? (value.length > 100 ? value.slice(0, 100) + '...' : value)
|
|
144
|
+
: JSON.stringify(value);
|
|
145
|
+
lines.push(` ${key}: ${strValue}`);
|
|
146
|
+
}
|
|
147
|
+
return lines.join('\n');
|
|
148
|
+
}
|