@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
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import { tool, type CoreTool } from 'ai';
|
|
2
|
+
import type { McpToolInfo, McpServerConfig } from './types';
|
|
3
|
+
import { parseSafeId } from './types';
|
|
4
|
+
import { McpProcessManager } from './processManager';
|
|
5
|
+
import { McpApprovalPolicy } from './approvalPolicy';
|
|
6
|
+
import { jsonSchemaObjectToZodObject } from './schemaConverter';
|
|
7
|
+
|
|
8
|
+
function matchGlobList(name: string, patterns: string[]): boolean {
|
|
9
|
+
for (const pattern of patterns) {
|
|
10
|
+
if (pattern === '*') return true;
|
|
11
|
+
if (pattern === name) return true;
|
|
12
|
+
|
|
13
|
+
const regex = new RegExp(
|
|
14
|
+
'^' + pattern.replace(/[.+^${}()|[\]\\]/g, '\\$&').replace(/\*/g, '.*').replace(/\?/g, '.') + '$'
|
|
15
|
+
);
|
|
16
|
+
if (regex.test(name)) return true;
|
|
17
|
+
}
|
|
18
|
+
return false;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export class McpToolCatalog {
|
|
22
|
+
private processManager: McpProcessManager;
|
|
23
|
+
private approvalPolicy: McpApprovalPolicy;
|
|
24
|
+
private configs: McpServerConfig[];
|
|
25
|
+
private exposedTools = new Map<string, CoreTool>();
|
|
26
|
+
private toolInfoMap = new Map<string, McpToolInfo>();
|
|
27
|
+
private safeToCanonical = new Map<string, string>();
|
|
28
|
+
private canonicalToSafe = new Map<string, string>();
|
|
29
|
+
|
|
30
|
+
constructor(processManager: McpProcessManager, approvalPolicy: McpApprovalPolicy, configs: McpServerConfig[]) {
|
|
31
|
+
this.processManager = processManager;
|
|
32
|
+
this.approvalPolicy = approvalPolicy;
|
|
33
|
+
this.configs = configs;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
refreshTools(serverId?: string): void {
|
|
37
|
+
if (serverId) {
|
|
38
|
+
this.refreshServerTools(serverId);
|
|
39
|
+
} else {
|
|
40
|
+
for (const config of this.configs) {
|
|
41
|
+
if (config.enabled) {
|
|
42
|
+
this.refreshServerTools(config.id);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
private refreshServerTools(serverId: string): void {
|
|
49
|
+
for (const [safeId, info] of this.toolInfoMap) {
|
|
50
|
+
if (info.serverId === serverId) {
|
|
51
|
+
this.exposedTools.delete(safeId);
|
|
52
|
+
this.toolInfoMap.delete(safeId);
|
|
53
|
+
this.safeToCanonical.delete(safeId);
|
|
54
|
+
this.canonicalToSafe.delete(info.canonicalId);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const config = this.configs.find(c => c.id === serverId);
|
|
59
|
+
if (!config || !config.enabled) return;
|
|
60
|
+
|
|
61
|
+
const rawTools = this.processManager.listTools(serverId);
|
|
62
|
+
|
|
63
|
+
for (const toolInfo of rawTools) {
|
|
64
|
+
if (config.tools.deny && config.tools.deny.length > 0) {
|
|
65
|
+
if (matchGlobList(toolInfo.name, config.tools.deny)) continue;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (config.tools.allow && config.tools.allow.length > 0) {
|
|
69
|
+
if (!matchGlobList(toolInfo.name, config.tools.allow)) continue;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const coreTool = this.convertToCoreToolDef(toolInfo, config);
|
|
73
|
+
this.exposedTools.set(toolInfo.safeId, coreTool);
|
|
74
|
+
this.toolInfoMap.set(toolInfo.safeId, toolInfo);
|
|
75
|
+
this.safeToCanonical.set(toolInfo.safeId, toolInfo.canonicalId);
|
|
76
|
+
this.canonicalToSafe.set(toolInfo.canonicalId, toolInfo.safeId);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
private convertToCoreToolDef(toolInfo: McpToolInfo, config: McpServerConfig): CoreTool {
|
|
81
|
+
const schema = toolInfo.inputSchema || { type: 'object', properties: {} };
|
|
82
|
+
const zodParams = jsonSchemaObjectToZodObject(schema as Record<string, unknown>);
|
|
83
|
+
|
|
84
|
+
const pm = this.processManager;
|
|
85
|
+
const ap = this.approvalPolicy;
|
|
86
|
+
const serverConfig = config;
|
|
87
|
+
|
|
88
|
+
return tool({
|
|
89
|
+
description: toolInfo.description || `MCP tool: ${toolInfo.name} (${config.name})`,
|
|
90
|
+
parameters: zodParams,
|
|
91
|
+
execute: async (args: Record<string, unknown>) => {
|
|
92
|
+
try {
|
|
93
|
+
const approvalResult = await ap.requestMcpApproval({
|
|
94
|
+
serverId: serverConfig.id,
|
|
95
|
+
serverName: serverConfig.name,
|
|
96
|
+
toolName: toolInfo.name,
|
|
97
|
+
canonicalId: toolInfo.canonicalId,
|
|
98
|
+
args,
|
|
99
|
+
approvalMode: serverConfig.approval,
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
if (!approvalResult.approved) {
|
|
103
|
+
if (approvalResult.customResponse) {
|
|
104
|
+
return {
|
|
105
|
+
error: `OPERATION REJECTED BY USER with custom instructions: "${approvalResult.customResponse}"`,
|
|
106
|
+
userMessage: 'Operation cancelled by user',
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
return {
|
|
110
|
+
error: `OPERATION REJECTED BY USER: calling MCP tool ${toolInfo.canonicalId}`,
|
|
111
|
+
userMessage: 'Operation cancelled by user',
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const result = await pm.callTool(serverConfig.id, toolInfo.name, args);
|
|
116
|
+
|
|
117
|
+
if (result.isError) {
|
|
118
|
+
return { error: result.content };
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return result.content;
|
|
122
|
+
} catch (error) {
|
|
123
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
124
|
+
return { error: `MCP tool call failed: ${message}` };
|
|
125
|
+
}
|
|
126
|
+
},
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
getExposedTools(): Record<string, CoreTool> {
|
|
131
|
+
const result: Record<string, CoreTool> = {};
|
|
132
|
+
for (const [safeId, coreTool] of this.exposedTools) {
|
|
133
|
+
result[safeId] = coreTool;
|
|
134
|
+
}
|
|
135
|
+
return result;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
getMcpToolInfos(): McpToolInfo[] {
|
|
139
|
+
return Array.from(this.toolInfoMap.values());
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
getToolInfo(safeId: string): McpToolInfo | null {
|
|
143
|
+
return this.toolInfoMap.get(safeId) || null;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
getSafeIdFromCanonical(canonicalId: string): string | null {
|
|
147
|
+
return this.canonicalToSafe.get(canonicalId) || null;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
getCanonicalFromSafeId(safeId: string): string | null {
|
|
151
|
+
return this.safeToCanonical.get(safeId) || null;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
isMcpTool(toolName: string): boolean {
|
|
155
|
+
return toolName.startsWith('mcp__');
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
parseMcpToolName(safeId: string): { serverId: string; toolName: string } | null {
|
|
159
|
+
return parseSafeId(safeId);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
updateConfigs(configs: McpServerConfig[]): void {
|
|
163
|
+
this.configs = configs;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
static mergeTools(internal: Record<string, CoreTool>, mcp: Record<string, CoreTool>): Record<string, CoreTool> {
|
|
167
|
+
return { ...internal, ...mcp };
|
|
168
|
+
}
|
|
169
|
+
}
|
package/src/mcp/types.ts
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
export interface McpTransportConfig {
|
|
2
|
+
type: 'stdio';
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
export interface McpServerConfig {
|
|
6
|
+
id: string;
|
|
7
|
+
name: string;
|
|
8
|
+
enabled: boolean;
|
|
9
|
+
transport: McpTransportConfig;
|
|
10
|
+
command: string;
|
|
11
|
+
args: string[];
|
|
12
|
+
cwd?: string;
|
|
13
|
+
env?: Record<string, string>;
|
|
14
|
+
autostart: 'startup' | 'on-demand' | 'never';
|
|
15
|
+
timeouts: {
|
|
16
|
+
initialize: number;
|
|
17
|
+
call: number;
|
|
18
|
+
};
|
|
19
|
+
limits: {
|
|
20
|
+
maxCallsPerMinute: number;
|
|
21
|
+
maxPayloadBytes: number;
|
|
22
|
+
};
|
|
23
|
+
logs: {
|
|
24
|
+
persist: boolean;
|
|
25
|
+
path?: string;
|
|
26
|
+
bufferSize: number;
|
|
27
|
+
};
|
|
28
|
+
tools: {
|
|
29
|
+
allow?: string[];
|
|
30
|
+
deny?: string[];
|
|
31
|
+
};
|
|
32
|
+
approval: McpApprovalMode;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export type McpApprovalMode = 'always' | 'once-per-tool' | 'once-per-server' | 'never';
|
|
36
|
+
|
|
37
|
+
export type McpServerStatus = 'stopped' | 'starting' | 'running' | 'error';
|
|
38
|
+
|
|
39
|
+
export interface McpServerState {
|
|
40
|
+
status: McpServerStatus;
|
|
41
|
+
pid?: number;
|
|
42
|
+
initLatencyMs?: number;
|
|
43
|
+
toolCount: number;
|
|
44
|
+
lastError?: string;
|
|
45
|
+
lastCallAt?: number;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export interface McpToolInfo {
|
|
49
|
+
serverId: string;
|
|
50
|
+
name: string;
|
|
51
|
+
description: string;
|
|
52
|
+
inputSchema: Record<string, unknown>;
|
|
53
|
+
canonicalId: string;
|
|
54
|
+
safeId: string;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export type McpRiskHint = 'read' | 'write' | 'execute' | 'network' | 'unknown';
|
|
58
|
+
|
|
59
|
+
export type McpApprovalScope = 'toolArgs' | 'tool' | 'server';
|
|
60
|
+
|
|
61
|
+
export interface McpApprovalCacheEntry {
|
|
62
|
+
scope: McpApprovalScope;
|
|
63
|
+
key: string;
|
|
64
|
+
expiresAt: number;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export interface McpGlobalConfig {
|
|
68
|
+
servers: McpServerConfig[];
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function toCanonicalId(serverId: string, toolName: string): string {
|
|
72
|
+
return `mcp:${serverId}:${toolName}`;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function toSafeId(serverId: string, toolName: string): string {
|
|
76
|
+
return `mcp__${serverId}__${toolName}`;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function parseSafeId(safeId: string): { serverId: string; toolName: string } | null {
|
|
80
|
+
if (!safeId.startsWith('mcp__')) return null;
|
|
81
|
+
const parts = safeId.slice(5).split('__');
|
|
82
|
+
if (parts.length < 2) return null;
|
|
83
|
+
const toolName = parts.pop()!;
|
|
84
|
+
const serverId = parts.join('__');
|
|
85
|
+
return { serverId, toolName };
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function parseCanonicalId(canonicalId: string): { serverId: string; toolName: string } | null {
|
|
89
|
+
if (!canonicalId.startsWith('mcp:')) return null;
|
|
90
|
+
const parts = canonicalId.slice(4).split(':');
|
|
91
|
+
if (parts.length < 2) return null;
|
|
92
|
+
const toolName = parts.pop()!;
|
|
93
|
+
const serverId = parts.join(':');
|
|
94
|
+
return { serverId, toolName };
|
|
95
|
+
}
|
|
@@ -1,12 +1,19 @@
|
|
|
1
1
|
export interface ApprovalRequest {
|
|
2
2
|
id: string;
|
|
3
|
-
toolName:
|
|
3
|
+
toolName: string;
|
|
4
4
|
preview: {
|
|
5
5
|
title: string;
|
|
6
6
|
content: string;
|
|
7
7
|
details?: string[];
|
|
8
8
|
};
|
|
9
9
|
args: Record<string, unknown>;
|
|
10
|
+
mcpMeta?: {
|
|
11
|
+
serverId: string;
|
|
12
|
+
serverName: string;
|
|
13
|
+
canonicalId: string;
|
|
14
|
+
riskHint: string;
|
|
15
|
+
payloadSize: number;
|
|
16
|
+
};
|
|
10
17
|
}
|
|
11
18
|
|
|
12
19
|
export interface ApprovalResponse {
|
|
@@ -16,7 +23,7 @@ export interface ApprovalResponse {
|
|
|
16
23
|
}
|
|
17
24
|
|
|
18
25
|
export interface ApprovalAccepted {
|
|
19
|
-
toolName:
|
|
26
|
+
toolName: string;
|
|
20
27
|
args: Record<string, unknown>;
|
|
21
28
|
}
|
|
22
29
|
|
|
@@ -28,6 +35,11 @@ let listeners = new Set<ApprovalListener>();
|
|
|
28
35
|
let acceptedListeners = new Set<ApprovalAcceptedListener>();
|
|
29
36
|
let pendingResolve: ((response: ApprovalResponse) => void) | null = null;
|
|
30
37
|
let pendingReject: ((reason?: any) => void) | null = null;
|
|
38
|
+
let queuedRequests: {
|
|
39
|
+
request: ApprovalRequest;
|
|
40
|
+
resolve: (response: ApprovalResponse) => void;
|
|
41
|
+
reject: (reason?: any) => void;
|
|
42
|
+
}[] = [];
|
|
31
43
|
|
|
32
44
|
function notify(): void {
|
|
33
45
|
for (const listener of listeners) {
|
|
@@ -54,7 +66,7 @@ export function subscribeApprovalAccepted(listener: ApprovalAcceptedListener): (
|
|
|
54
66
|
};
|
|
55
67
|
}
|
|
56
68
|
|
|
57
|
-
function notifyApprovalAccepted(toolName:
|
|
69
|
+
function notifyApprovalAccepted(toolName: string, args: Record<string, unknown>): void {
|
|
58
70
|
for (const listener of acceptedListeners) {
|
|
59
71
|
listener({ toolName, args });
|
|
60
72
|
}
|
|
@@ -65,27 +77,32 @@ export function getCurrentApproval(): ApprovalRequest | null {
|
|
|
65
77
|
}
|
|
66
78
|
|
|
67
79
|
export async function requestApproval(
|
|
68
|
-
toolName:
|
|
80
|
+
toolName: string,
|
|
69
81
|
args: Record<string, unknown>,
|
|
70
82
|
preview: { title: string; content: string; details?: string[] }
|
|
71
83
|
): Promise<{ approved: boolean; customResponse?: string }> {
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
84
|
+
const mcpMeta = (args as any).__mcpMeta;
|
|
85
|
+
const cleanArgs = { ...args };
|
|
86
|
+
delete (cleanArgs as any).__mcpMeta;
|
|
75
87
|
|
|
76
88
|
const request: ApprovalRequest = {
|
|
77
89
|
id: createId(),
|
|
78
90
|
toolName,
|
|
79
91
|
preview,
|
|
80
|
-
args,
|
|
92
|
+
args: cleanArgs,
|
|
93
|
+
...(mcpMeta && { mcpMeta }),
|
|
81
94
|
};
|
|
82
95
|
|
|
83
|
-
currentRequest = request;
|
|
84
|
-
notify();
|
|
85
|
-
|
|
86
96
|
const response = await new Promise<ApprovalResponse>((resolve, reject) => {
|
|
97
|
+
if (pendingResolve) {
|
|
98
|
+
queuedRequests.push({ request, resolve, reject });
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
currentRequest = request;
|
|
87
103
|
pendingResolve = resolve;
|
|
88
104
|
pendingReject = reject;
|
|
105
|
+
notify();
|
|
89
106
|
});
|
|
90
107
|
|
|
91
108
|
return { approved: response.approved, customResponse: response.customResponse };
|
|
@@ -114,6 +131,14 @@ export function respondApproval(approved: boolean, customResponse?: string): voi
|
|
|
114
131
|
}
|
|
115
132
|
|
|
116
133
|
resolve(response);
|
|
134
|
+
|
|
135
|
+
const next = queuedRequests.shift();
|
|
136
|
+
if (next) {
|
|
137
|
+
currentRequest = next.request;
|
|
138
|
+
pendingResolve = next.resolve;
|
|
139
|
+
pendingReject = next.reject;
|
|
140
|
+
notify();
|
|
141
|
+
}
|
|
117
142
|
}
|
|
118
143
|
|
|
119
144
|
export function cancelApproval(): void {
|
|
@@ -126,4 +151,12 @@ export function cancelApproval(): void {
|
|
|
126
151
|
notify();
|
|
127
152
|
|
|
128
153
|
reject(new Error('Interrupted by user'));
|
|
129
|
-
|
|
154
|
+
|
|
155
|
+
const next = queuedRequests.shift();
|
|
156
|
+
if (next) {
|
|
157
|
+
currentRequest = next.request;
|
|
158
|
+
pendingResolve = next.resolve;
|
|
159
|
+
pendingReject = next.reject;
|
|
160
|
+
notify();
|
|
161
|
+
}
|
|
162
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { shouldRequireApprovals } from './config'
|
|
2
|
+
|
|
3
|
+
type ApprovalModeListener = (requireApprovals: boolean) => void
|
|
4
|
+
|
|
5
|
+
const listeners = new Set<ApprovalModeListener>()
|
|
6
|
+
|
|
7
|
+
export function subscribeApprovalMode(listener: ApprovalModeListener): () => void {
|
|
8
|
+
listeners.add(listener)
|
|
9
|
+
listener(shouldRequireApprovals())
|
|
10
|
+
return () => {
|
|
11
|
+
listeners.delete(listener)
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function emitApprovalMode(requireApprovals: boolean): void {
|
|
16
|
+
listeners.forEach((listener) => listener(requireApprovals))
|
|
17
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import type { Command } from './types'
|
|
2
|
+
import { getCurrentApproval, respondApproval } from '../approvalBridge'
|
|
3
|
+
import { shouldRequireApprovals, setRequireApprovals } from '../config'
|
|
4
|
+
import { notifyNotification } from '../notificationBridge'
|
|
5
|
+
import { emitApprovalMode } from '../approvalModeBridge'
|
|
6
|
+
|
|
7
|
+
export const approvalsCommand: Command = {
|
|
8
|
+
name: 'approvals',
|
|
9
|
+
description: 'Toggle approval prompts for agent changes',
|
|
10
|
+
usage: '/approvals on|off|toggle|status',
|
|
11
|
+
aliases: ['approval', 'autoapprove', 'auto-approve'],
|
|
12
|
+
execute: (args: string[]) => {
|
|
13
|
+
const raw = args[0]?.toLowerCase()
|
|
14
|
+
const current = shouldRequireApprovals()
|
|
15
|
+
let next = current
|
|
16
|
+
|
|
17
|
+
if (!raw || raw === 'toggle') {
|
|
18
|
+
next = !current
|
|
19
|
+
} else if (raw === 'on' || raw === 'true' || raw === 'yes') {
|
|
20
|
+
next = true
|
|
21
|
+
} else if (raw === 'off' || raw === 'false' || raw === 'no') {
|
|
22
|
+
next = false
|
|
23
|
+
} else if (raw === 'status') {
|
|
24
|
+
return {
|
|
25
|
+
success: true,
|
|
26
|
+
content: current ? 'Approvals are enabled.' : 'Auto-approve is enabled.'
|
|
27
|
+
}
|
|
28
|
+
} else {
|
|
29
|
+
return {
|
|
30
|
+
success: false,
|
|
31
|
+
content: 'Usage: /approvals on|off|toggle|status'
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
setRequireApprovals(next)
|
|
36
|
+
if (!next && getCurrentApproval()) {
|
|
37
|
+
respondApproval(true)
|
|
38
|
+
}
|
|
39
|
+
emitApprovalMode(next)
|
|
40
|
+
|
|
41
|
+
notifyNotification(next ? 'Approvals enabled.' : 'Auto-approve enabled.', 'info', 2500)
|
|
42
|
+
|
|
43
|
+
return {
|
|
44
|
+
success: true,
|
|
45
|
+
content: next ? 'Approvals enabled.' : 'Auto-approve enabled.'
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { Command, CommandResult } from './types';
|
|
2
|
+
|
|
3
|
+
export const compactCommand: Command = {
|
|
4
|
+
name: 'compact',
|
|
5
|
+
description: 'Compact the current conversation context',
|
|
6
|
+
usage: '/compact [maxTokens]',
|
|
7
|
+
execute: (args: string[]): CommandResult => {
|
|
8
|
+
let maxTokens: number | undefined;
|
|
9
|
+
if (args[0]) {
|
|
10
|
+
const parsed = Number(args[0]);
|
|
11
|
+
if (!Number.isFinite(parsed) || parsed <= 0) {
|
|
12
|
+
return {
|
|
13
|
+
success: false,
|
|
14
|
+
content: 'Invalid maxTokens. Usage: /compact [maxTokens]',
|
|
15
|
+
shouldAddToHistory: false
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
maxTokens = Math.floor(parsed);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
return {
|
|
22
|
+
success: true,
|
|
23
|
+
content: 'Conversation compacted.',
|
|
24
|
+
shouldAddToHistory: false,
|
|
25
|
+
shouldCompactMessages: true,
|
|
26
|
+
compactMaxTokens: maxTokens,
|
|
27
|
+
shouldClearMessages: true
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
};
|
|
@@ -5,7 +5,7 @@ export const echoCommand: Command = {
|
|
|
5
5
|
description: 'Echo the provided text back to the user',
|
|
6
6
|
usage: '/echo <text>',
|
|
7
7
|
aliases: ['e'],
|
|
8
|
-
execute: (args: string[],
|
|
8
|
+
execute: (args: string[], _fullCommand: string): { success: boolean; content: string } => {
|
|
9
9
|
if (args.length === 0) {
|
|
10
10
|
return {
|
|
11
11
|
success: false,
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { existsSync, readFileSync, statSync } from "fs";
|
|
2
|
+
import { basename } from "path";
|
|
3
|
+
import type { Command } from "./types";
|
|
4
|
+
import { guessImageMimeType } from "../images";
|
|
5
|
+
import { emitImageCommand, canUseImages } from "../imageBridge";
|
|
6
|
+
|
|
7
|
+
const MAX_IMAGE_BYTES = 10 * 1024 * 1024;
|
|
8
|
+
|
|
9
|
+
function parseImagePath(fullCommand: string): string {
|
|
10
|
+
const trimmed = fullCommand.trim();
|
|
11
|
+
const without = trimmed.replace(/^\/(image|img)\s+/i, "");
|
|
12
|
+
return without.trim();
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export const imageCommand: Command = {
|
|
16
|
+
name: "image",
|
|
17
|
+
description: "Attach an image for the next message",
|
|
18
|
+
usage: "/image <path> | /image clear",
|
|
19
|
+
aliases: ["img"],
|
|
20
|
+
execute: (args, fullCommand) => {
|
|
21
|
+
const first = args[0]?.toLowerCase();
|
|
22
|
+
if (!first) {
|
|
23
|
+
return {
|
|
24
|
+
success: false,
|
|
25
|
+
content: "Usage: /image <path> | /image clear",
|
|
26
|
+
shouldAddToHistory: false
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if (first === "clear") {
|
|
31
|
+
emitImageCommand({ type: "clear" });
|
|
32
|
+
return {
|
|
33
|
+
success: true,
|
|
34
|
+
content: "Image list cleared.",
|
|
35
|
+
shouldAddToHistory: false
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (!canUseImages()) {
|
|
40
|
+
return {
|
|
41
|
+
success: false,
|
|
42
|
+
content: "Images are not supported by the current model.",
|
|
43
|
+
shouldAddToHistory: false
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const path = parseImagePath(fullCommand);
|
|
48
|
+
if (!path) {
|
|
49
|
+
return {
|
|
50
|
+
success: false,
|
|
51
|
+
content: "Missing image path.",
|
|
52
|
+
shouldAddToHistory: false
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (!existsSync(path)) {
|
|
57
|
+
return {
|
|
58
|
+
success: false,
|
|
59
|
+
content: "File not found.",
|
|
60
|
+
shouldAddToHistory: false
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const stat = statSync(path);
|
|
65
|
+
if (!stat.isFile()) {
|
|
66
|
+
return {
|
|
67
|
+
success: false,
|
|
68
|
+
content: "Not a file.",
|
|
69
|
+
shouldAddToHistory: false
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (stat.size > MAX_IMAGE_BYTES) {
|
|
74
|
+
return {
|
|
75
|
+
success: false,
|
|
76
|
+
content: "Image too large (max 10 MB).",
|
|
77
|
+
shouldAddToHistory: false
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const name = basename(path);
|
|
82
|
+
const mimeType = guessImageMimeType(name);
|
|
83
|
+
if (!mimeType.startsWith("image/")) {
|
|
84
|
+
return {
|
|
85
|
+
success: false,
|
|
86
|
+
content: "Unsupported image type.",
|
|
87
|
+
shouldAddToHistory: false
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const data = readFileSync(path).toString("base64");
|
|
92
|
+
emitImageCommand({
|
|
93
|
+
type: "add",
|
|
94
|
+
image: {
|
|
95
|
+
id: `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
|
|
96
|
+
name,
|
|
97
|
+
mimeType,
|
|
98
|
+
data,
|
|
99
|
+
size: stat.size
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
return {
|
|
104
|
+
success: true,
|
|
105
|
+
content: `Image attached: ${name}`,
|
|
106
|
+
shouldAddToHistory: false
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
};
|
|
@@ -3,10 +3,11 @@ import { commandRegistry } from './registry';
|
|
|
3
3
|
import { echoCommand } from './echo';
|
|
4
4
|
import { helpCommand } from './help';
|
|
5
5
|
import { initCommand } from './init';
|
|
6
|
-
import { undoCommand } from './undo';
|
|
7
|
-
import { redoCommand } from './redo';
|
|
8
|
-
import { sessionsCommand } from './sessions';
|
|
9
6
|
import { webCommand } from './web';
|
|
7
|
+
import { imageCommand } from './image';
|
|
8
|
+
import { approvalsCommand } from './approvals';
|
|
9
|
+
import { newCommand } from './new';
|
|
10
|
+
import { compactCommand } from './compact';
|
|
10
11
|
|
|
11
12
|
export { commandRegistry } from './registry';
|
|
12
13
|
export type { Command, CommandResult, CommandRegistry } from './types';
|
|
@@ -61,8 +62,9 @@ export function initializeCommands(): void {
|
|
|
61
62
|
commandRegistry.register(echoCommand);
|
|
62
63
|
commandRegistry.register(helpCommand);
|
|
63
64
|
commandRegistry.register(initCommand);
|
|
64
|
-
commandRegistry.register(undoCommand);
|
|
65
|
-
commandRegistry.register(redoCommand);
|
|
66
|
-
commandRegistry.register(sessionsCommand);
|
|
67
65
|
commandRegistry.register(webCommand);
|
|
68
|
-
|
|
66
|
+
commandRegistry.register(imageCommand);
|
|
67
|
+
commandRegistry.register(approvalsCommand);
|
|
68
|
+
commandRegistry.register(newCommand);
|
|
69
|
+
commandRegistry.register(compactCommand);
|
|
70
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { Command, CommandResult } from './types';
|
|
2
|
+
|
|
3
|
+
export const newCommand: Command = {
|
|
4
|
+
name: 'new',
|
|
5
|
+
description: 'Start a new chat',
|
|
6
|
+
usage: '/new',
|
|
7
|
+
aliases: ['clear'],
|
|
8
|
+
execute: (_args: string[], _fullCommand: string): CommandResult => {
|
|
9
|
+
return {
|
|
10
|
+
success: true,
|
|
11
|
+
content: 'Starting a new chat...',
|
|
12
|
+
shouldClearMessages: true
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
};
|
package/src/utils/config.ts
CHANGED
|
@@ -38,6 +38,8 @@ export interface MosaicConfig {
|
|
|
38
38
|
model?: string;
|
|
39
39
|
apiKey?: string;
|
|
40
40
|
systemPrompt?: string;
|
|
41
|
+
maxSteps?: number;
|
|
42
|
+
maxContextTokens?: number;
|
|
41
43
|
customProviders?: CustomProvider[];
|
|
42
44
|
customModels?: { [providerId: string]: AIModel[] };
|
|
43
45
|
requireApprovals?: boolean;
|
|
@@ -354,4 +356,4 @@ export function clearRecentProjects(): void {
|
|
|
354
356
|
const config = readConfig();
|
|
355
357
|
config.recentProjects = [];
|
|
356
358
|
writeConfig(config);
|
|
357
|
-
}
|
|
359
|
+
}
|