@kirosnn/mosaic 0.71.0 → 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.
Files changed (75) hide show
  1. package/README.md +1 -5
  2. package/package.json +4 -2
  3. package/src/agent/Agent.ts +353 -131
  4. package/src/agent/context.ts +4 -4
  5. package/src/agent/prompts/systemPrompt.ts +15 -6
  6. package/src/agent/prompts/toolsPrompt.ts +75 -10
  7. package/src/agent/provider/anthropic.ts +100 -100
  8. package/src/agent/provider/google.ts +102 -102
  9. package/src/agent/provider/mistral.ts +95 -95
  10. package/src/agent/provider/ollama.ts +77 -60
  11. package/src/agent/provider/openai.ts +42 -38
  12. package/src/agent/provider/rateLimit.ts +178 -0
  13. package/src/agent/provider/xai.ts +99 -99
  14. package/src/agent/tools/definitions.ts +19 -9
  15. package/src/agent/tools/executor.ts +95 -85
  16. package/src/agent/tools/exploreExecutor.ts +8 -10
  17. package/src/agent/tools/grep.ts +30 -29
  18. package/src/agent/tools/question.ts +7 -1
  19. package/src/agent/types.ts +9 -8
  20. package/src/components/App.tsx +45 -45
  21. package/src/components/CustomInput.tsx +214 -36
  22. package/src/components/Main.tsx +1146 -954
  23. package/src/components/Setup.tsx +1 -1
  24. package/src/components/Welcome.tsx +1 -1
  25. package/src/components/main/ApprovalPanel.tsx +4 -3
  26. package/src/components/main/ChatPage.tsx +858 -675
  27. package/src/components/main/HomePage.tsx +53 -38
  28. package/src/components/main/QuestionPanel.tsx +52 -7
  29. package/src/components/main/ThinkingIndicator.tsx +2 -1
  30. package/src/index.tsx +50 -20
  31. package/src/mcp/approvalPolicy.ts +148 -0
  32. package/src/mcp/cli/add.ts +185 -0
  33. package/src/mcp/cli/doctor.ts +77 -0
  34. package/src/mcp/cli/index.ts +85 -0
  35. package/src/mcp/cli/list.ts +50 -0
  36. package/src/mcp/cli/logs.ts +24 -0
  37. package/src/mcp/cli/manage.ts +99 -0
  38. package/src/mcp/cli/show.ts +53 -0
  39. package/src/mcp/cli/tools.ts +77 -0
  40. package/src/mcp/config.ts +223 -0
  41. package/src/mcp/index.ts +80 -0
  42. package/src/mcp/processManager.ts +299 -0
  43. package/src/mcp/rateLimiter.ts +50 -0
  44. package/src/mcp/registry.ts +151 -0
  45. package/src/mcp/schemaConverter.ts +100 -0
  46. package/src/mcp/servers/navigation.ts +854 -0
  47. package/src/mcp/toolCatalog.ts +169 -0
  48. package/src/mcp/types.ts +95 -0
  49. package/src/utils/approvalBridge.ts +17 -5
  50. package/src/utils/commands/compact.ts +30 -0
  51. package/src/utils/commands/echo.ts +1 -1
  52. package/src/utils/commands/index.ts +4 -6
  53. package/src/utils/commands/new.ts +15 -0
  54. package/src/utils/commands/types.ts +3 -0
  55. package/src/utils/config.ts +3 -1
  56. package/src/utils/diffRendering.tsx +1 -3
  57. package/src/utils/exploreBridge.ts +10 -0
  58. package/src/utils/markdown.tsx +163 -99
  59. package/src/utils/models.ts +31 -9
  60. package/src/utils/questionBridge.ts +36 -1
  61. package/src/utils/tokenEstimator.ts +32 -0
  62. package/src/utils/toolFormatting.ts +268 -7
  63. package/src/web/app.tsx +72 -72
  64. package/src/web/components/HomePage.tsx +7 -7
  65. package/src/web/components/MessageItem.tsx +22 -22
  66. package/src/web/components/QuestionPanel.tsx +72 -12
  67. package/src/web/components/Sidebar.tsx +0 -2
  68. package/src/web/components/ThinkingIndicator.tsx +1 -0
  69. package/src/web/server.tsx +767 -683
  70. package/src/utils/commands/redo.ts +0 -74
  71. package/src/utils/commands/sessions.ts +0 -129
  72. package/src/utils/commands/undo.ts +0 -75
  73. package/src/utils/undoRedo.ts +0 -429
  74. package/src/utils/undoRedoBridge.ts +0 -45
  75. 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
+ }
@@ -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: 'write' | 'edit' | 'bash';
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: 'write' | 'edit' | 'bash';
26
+ toolName: string;
20
27
  args: Record<string, unknown>;
21
28
  }
22
29
 
@@ -59,7 +66,7 @@ export function subscribeApprovalAccepted(listener: ApprovalAcceptedListener): (
59
66
  };
60
67
  }
61
68
 
62
- function notifyApprovalAccepted(toolName: 'write' | 'edit' | 'bash', args: Record<string, unknown>): void {
69
+ function notifyApprovalAccepted(toolName: string, args: Record<string, unknown>): void {
63
70
  for (const listener of acceptedListeners) {
64
71
  listener({ toolName, args });
65
72
  }
@@ -70,15 +77,20 @@ export function getCurrentApproval(): ApprovalRequest | null {
70
77
  }
71
78
 
72
79
  export async function requestApproval(
73
- toolName: 'write' | 'edit' | 'bash',
80
+ toolName: string,
74
81
  args: Record<string, unknown>,
75
82
  preview: { title: string; content: string; details?: string[] }
76
83
  ): Promise<{ approved: boolean; customResponse?: string }> {
84
+ const mcpMeta = (args as any).__mcpMeta;
85
+ const cleanArgs = { ...args };
86
+ delete (cleanArgs as any).__mcpMeta;
87
+
77
88
  const request: ApprovalRequest = {
78
89
  id: createId(),
79
90
  toolName,
80
91
  preview,
81
- args,
92
+ args: cleanArgs,
93
+ ...(mcpMeta && { mcpMeta }),
82
94
  };
83
95
 
84
96
  const response = await new Promise<ApprovalResponse>((resolve, reject) => {
@@ -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[], fullCommand: string): { success: boolean; content: string } => {
8
+ execute: (args: string[], _fullCommand: string): { success: boolean; content: string } => {
9
9
  if (args.length === 0) {
10
10
  return {
11
11
  success: false,
@@ -3,12 +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';
10
7
  import { imageCommand } from './image';
11
8
  import { approvalsCommand } from './approvals';
9
+ import { newCommand } from './new';
10
+ import { compactCommand } from './compact';
12
11
 
13
12
  export { commandRegistry } from './registry';
14
13
  export type { Command, CommandResult, CommandRegistry } from './types';
@@ -63,10 +62,9 @@ export function initializeCommands(): void {
63
62
  commandRegistry.register(echoCommand);
64
63
  commandRegistry.register(helpCommand);
65
64
  commandRegistry.register(initCommand);
66
- commandRegistry.register(undoCommand);
67
- commandRegistry.register(redoCommand);
68
- commandRegistry.register(sessionsCommand);
69
65
  commandRegistry.register(webCommand);
70
66
  commandRegistry.register(imageCommand);
71
67
  commandRegistry.register(approvalsCommand);
68
+ commandRegistry.register(newCommand);
69
+ commandRegistry.register(compactCommand);
72
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
+ };
@@ -2,6 +2,9 @@ export interface CommandResult {
2
2
  success: boolean;
3
3
  content: string;
4
4
  shouldAddToHistory?: boolean;
5
+ shouldClearMessages?: boolean;
6
+ shouldCompactMessages?: boolean;
7
+ compactMaxTokens?: number;
5
8
  }
6
9
 
7
10
  export interface Command {
@@ -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
+ }
@@ -34,8 +34,6 @@ export function renderDiffLine(line: string, key: string) {
34
34
 
35
35
  export function renderInlineDiffLine(content: string) {
36
36
  const parsed = parseDiffLine(content);
37
- const colors = getDiffLineColors(parsed);
38
-
39
37
  if (parsed.isDiffLine) {
40
38
  return (
41
39
  <>
@@ -56,6 +54,6 @@ export function renderInlineDiffLine(content: string) {
56
54
  return null;
57
55
  }
58
56
 
59
- export function getDiffLineBackground(content: string): string | null {
57
+ export function getDiffLineBackground(_content: string): string | null {
60
58
  return null;
61
59
  }
@@ -15,6 +15,7 @@ interface ExploreBridgeGlobal {
15
15
  toolCallback: ExploreToolCallback | null;
16
16
  totalExploreTokens: number;
17
17
  subscribers: Set<ExploreToolSubscriber>;
18
+ parentContext: string;
18
19
  }
19
20
 
20
21
  const globalKey = '__mosaic_explore_bridge__';
@@ -26,6 +27,7 @@ if (!g[globalKey]) {
26
27
  toolCallback: null,
27
28
  totalExploreTokens: 0,
28
29
  subscribers: new Set<ExploreToolSubscriber>(),
30
+ parentContext: '',
29
31
  };
30
32
  }
31
33
 
@@ -85,3 +87,11 @@ export function subscribeExploreTool(callback: ExploreToolSubscriber): () => voi
85
87
  export function getExploreTokens(): number {
86
88
  return state.totalExploreTokens;
87
89
  }
90
+
91
+ export function setExploreContext(context: string): void {
92
+ state.parentContext = context;
93
+ }
94
+
95
+ export function getExploreContext(): string {
96
+ return state.parentContext;
97
+ }