@kirosnn/mosaic 0.74.0 → 0.75.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 +1 -1
- package/package.json +2 -2
- package/src/agent/prompts/systemPrompt.ts +1 -1
- package/src/agent/prompts/toolsPrompt.ts +11 -2
- package/src/agent/tools/explore.ts +4 -4
- package/src/agent/tools/exploreExecutor.ts +56 -4
- package/src/components/main/ChatPage.tsx +858 -858
- package/src/index.tsx +32 -5
- package/src/mcp/servers/navigation/index.ts +1 -1
- package/src/mcp/servers/navigation/tools.ts +5 -5
- package/src/mcp/servers/navigation/types.ts +1 -1
- package/src/mcp/servers/navigation/utils.ts +2 -2
- package/src/utils/history.ts +82 -82
package/src/index.tsx
CHANGED
|
@@ -208,14 +208,27 @@ if (parsed.directory) {
|
|
|
208
208
|
}
|
|
209
209
|
|
|
210
210
|
import { addRecentProject } from './utils/config';
|
|
211
|
+
import { appendFileSync } from 'fs';
|
|
212
|
+
import { join } from 'path';
|
|
213
|
+
import { homedir } from 'os';
|
|
214
|
+
|
|
215
|
+
const DEBUG_LOG = join(homedir(), '.mosaic', 'debug.log');
|
|
216
|
+
const debugLog = (msg: string) => {
|
|
217
|
+
try { appendFileSync(DEBUG_LOG, `[${new Date().toISOString()}] ${msg}\n`); } catch {}
|
|
218
|
+
};
|
|
219
|
+
|
|
220
|
+
debugLog('--- Mosaic starting ---');
|
|
211
221
|
addRecentProject(process.cwd());
|
|
212
222
|
|
|
223
|
+
debugLog('MCP init...');
|
|
213
224
|
const { initializeMcp } = await import('./mcp/index');
|
|
214
|
-
await initializeMcp().catch(() => { });
|
|
225
|
+
await initializeMcp().catch((e) => { debugLog(`MCP init error: ${e}`); });
|
|
226
|
+
debugLog('MCP init done');
|
|
215
227
|
|
|
216
228
|
process.title = '⁘ Mosaic';
|
|
217
229
|
|
|
218
230
|
const cleanup = async (code = 0) => {
|
|
231
|
+
debugLog(`cleanup called with code=${code}`);
|
|
219
232
|
try {
|
|
220
233
|
const { shutdownMcp } = await import('./mcp/index');
|
|
221
234
|
await shutdownMcp();
|
|
@@ -224,17 +237,31 @@ const cleanup = async (code = 0) => {
|
|
|
224
237
|
process.exit(code);
|
|
225
238
|
};
|
|
226
239
|
|
|
227
|
-
process.on('SIGINT', () => cleanup(0));
|
|
228
|
-
process.on('SIGTERM', () => cleanup(0));
|
|
229
|
-
process.on('uncaughtException', () =>
|
|
230
|
-
|
|
240
|
+
process.on('SIGINT', () => { debugLog('SIGINT received'); cleanup(0); });
|
|
241
|
+
process.on('SIGTERM', () => { debugLog('SIGTERM received'); cleanup(0); });
|
|
242
|
+
process.on('uncaughtException', (err) => {
|
|
243
|
+
const msg = `Uncaught exception: ${err?.stack ?? err}`;
|
|
244
|
+
debugLog(msg);
|
|
245
|
+
originalStderrWrite(msg + '\n');
|
|
246
|
+
cleanup(1);
|
|
247
|
+
});
|
|
248
|
+
process.on('unhandledRejection', (reason) => {
|
|
249
|
+
const msg = `Unhandled rejection: ${reason instanceof Error ? reason.stack : reason}`;
|
|
250
|
+
debugLog(msg);
|
|
251
|
+
originalStderrWrite(msg + '\n');
|
|
252
|
+
cleanup(1);
|
|
253
|
+
});
|
|
231
254
|
|
|
232
255
|
await new Promise(resolve => setTimeout(resolve, 100));
|
|
233
256
|
|
|
257
|
+
debugLog('Creating renderer...');
|
|
234
258
|
try {
|
|
235
259
|
const renderer = await createCliRenderer();
|
|
260
|
+
debugLog('Renderer created, mounting React...');
|
|
236
261
|
createRoot(renderer).render(<App initialMessage={parsed.initialMessage} />);
|
|
262
|
+
debugLog('React mounted');
|
|
237
263
|
} catch (error) {
|
|
264
|
+
debugLog(`Renderer/React error: ${error}`);
|
|
238
265
|
console.error(error);
|
|
239
266
|
cleanup(1);
|
|
240
267
|
}
|
|
@@ -29,7 +29,7 @@ async function resolveRedirects(page: Page, results: SearchResult[]): Promise<Se
|
|
|
29
29
|
if (decoded.startsWith('http')) return { ...r, href: decoded };
|
|
30
30
|
}
|
|
31
31
|
}
|
|
32
|
-
} catch {}
|
|
32
|
+
} catch { }
|
|
33
33
|
return r;
|
|
34
34
|
});
|
|
35
35
|
}, results);
|
|
@@ -160,7 +160,7 @@ async function dismissConsentDialogs(page: Page): Promise<void> {
|
|
|
160
160
|
).first();
|
|
161
161
|
if (await consentButton.isVisible({ timeout: 1500 })) {
|
|
162
162
|
await consentButton.click();
|
|
163
|
-
await page.waitForLoadState('domcontentloaded', { timeout: 3000 }).catch(() => {});
|
|
163
|
+
await page.waitForLoadState('domcontentloaded', { timeout: 3000 }).catch(() => { });
|
|
164
164
|
}
|
|
165
165
|
} catch {
|
|
166
166
|
// No consent dialog
|
|
@@ -176,7 +176,7 @@ function withTimeout<T>(promise: Promise<T>, ms: number, label: string): Promise
|
|
|
176
176
|
|
|
177
177
|
export function registerTools(server: McpServer) {
|
|
178
178
|
server.registerTool('navigation_search', {
|
|
179
|
-
description: 'Search the web and return top results (titles, links, snippets). Defaults to
|
|
179
|
+
description: 'Search the web and return top results (titles, links, snippets). Defaults to Google. Supports bing, duckduckgo, and google engines.',
|
|
180
180
|
inputSchema: {
|
|
181
181
|
query: z.string(),
|
|
182
182
|
limit: z.number().optional(),
|
|
@@ -184,9 +184,9 @@ export function registerTools(server: McpServer) {
|
|
|
184
184
|
newTab: z.boolean().optional(),
|
|
185
185
|
},
|
|
186
186
|
}, async (args) => {
|
|
187
|
-
const engineName: SearchEngine = args.engine ?? '
|
|
187
|
+
const engineName: SearchEngine = args.engine ?? 'google';
|
|
188
188
|
const engine = engines[engineName];
|
|
189
|
-
const limit = args.limit ??
|
|
189
|
+
const limit = args.limit ?? 10;
|
|
190
190
|
|
|
191
191
|
const doSearch = async () => {
|
|
192
192
|
const target = args.newTab ? await ensureContext().then(async ctx => {
|
|
@@ -14,7 +14,7 @@ export async function waitForStability(page: Page, timeout = 10000): Promise<voi
|
|
|
14
14
|
try {
|
|
15
15
|
await page.waitForLoadState('networkidle', { timeout });
|
|
16
16
|
} catch {
|
|
17
|
-
await page.waitForLoadState('domcontentloaded', { timeout: 5000 }).catch(() => {});
|
|
17
|
+
await page.waitForLoadState('domcontentloaded', { timeout: 5000 }).catch(() => { });
|
|
18
18
|
await randomDelay(300, 600);
|
|
19
19
|
}
|
|
20
|
-
}
|
|
20
|
+
}
|
package/src/utils/history.ts
CHANGED
|
@@ -1,31 +1,31 @@
|
|
|
1
|
-
import { existsSync, mkdirSync, writeFileSync, readdirSync, readFileSync, unlinkSync } from 'fs';
|
|
1
|
+
import { existsSync, mkdirSync, writeFileSync, readdirSync, readFileSync, unlinkSync } from 'fs';
|
|
2
2
|
import { join } from 'path';
|
|
3
3
|
import { homedir } from 'os';
|
|
4
4
|
|
|
5
|
-
export interface ConversationStep {
|
|
6
|
-
type: 'user' | 'assistant' | 'tool';
|
|
7
|
-
content: string;
|
|
8
|
-
images?: import("./images").ImageAttachment[];
|
|
9
|
-
toolName?: string;
|
|
10
|
-
toolArgs?: Record<string, unknown>;
|
|
11
|
-
toolResult?: unknown;
|
|
12
|
-
timestamp: number;
|
|
13
|
-
responseDuration?: number;
|
|
14
|
-
blendWord?: string;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
export interface ConversationHistory {
|
|
18
|
-
id: string;
|
|
19
|
-
timestamp: number;
|
|
20
|
-
steps: ConversationStep[];
|
|
21
|
-
totalSteps: number;
|
|
22
|
-
title?: string | null;
|
|
23
|
-
workspace?: string | null;
|
|
24
|
-
totalTokens?: {
|
|
25
|
-
prompt: number;
|
|
26
|
-
completion: number;
|
|
27
|
-
total: number;
|
|
28
|
-
};
|
|
5
|
+
export interface ConversationStep {
|
|
6
|
+
type: 'user' | 'assistant' | 'tool';
|
|
7
|
+
content: string;
|
|
8
|
+
images?: import("./images").ImageAttachment[];
|
|
9
|
+
toolName?: string;
|
|
10
|
+
toolArgs?: Record<string, unknown>;
|
|
11
|
+
toolResult?: unknown;
|
|
12
|
+
timestamp: number;
|
|
13
|
+
responseDuration?: number;
|
|
14
|
+
blendWord?: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface ConversationHistory {
|
|
18
|
+
id: string;
|
|
19
|
+
timestamp: number;
|
|
20
|
+
steps: ConversationStep[];
|
|
21
|
+
totalSteps: number;
|
|
22
|
+
title?: string | null;
|
|
23
|
+
workspace?: string | null;
|
|
24
|
+
totalTokens?: {
|
|
25
|
+
prompt: number;
|
|
26
|
+
completion: number;
|
|
27
|
+
total: number;
|
|
28
|
+
};
|
|
29
29
|
model?: string;
|
|
30
30
|
provider?: string;
|
|
31
31
|
}
|
|
@@ -41,49 +41,49 @@ export function getHistoryDir(): string {
|
|
|
41
41
|
return historyDir;
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
-
export function saveConversation(conversation: ConversationHistory): void {
|
|
45
|
-
const historyDir = getHistoryDir();
|
|
46
|
-
const filename = `${conversation.id}.json`;
|
|
47
|
-
const filepath = join(historyDir, filename);
|
|
48
|
-
|
|
49
|
-
writeFileSync(filepath, JSON.stringify(conversation, null, 2), 'utf-8');
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
export function updateConversationTitle(id: string, title: string | null): boolean {
|
|
53
|
-
const historyDir = getHistoryDir();
|
|
54
|
-
const filepath = join(historyDir, `${id}.json`);
|
|
55
|
-
|
|
56
|
-
if (!existsSync(filepath)) {
|
|
57
|
-
return false;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
try {
|
|
61
|
-
const content = readFileSync(filepath, 'utf-8');
|
|
62
|
-
const data = JSON.parse(content) as ConversationHistory;
|
|
63
|
-
data.title = title;
|
|
64
|
-
writeFileSync(filepath, JSON.stringify(data, null, 2), 'utf-8');
|
|
65
|
-
return true;
|
|
66
|
-
} catch (error) {
|
|
67
|
-
return false;
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
export function deleteConversation(id: string): boolean {
|
|
72
|
-
const historyDir = getHistoryDir();
|
|
73
|
-
const filepath = join(historyDir, `${id}.json`);
|
|
74
|
-
|
|
75
|
-
if (!existsSync(filepath)) {
|
|
76
|
-
return false;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
try {
|
|
80
|
-
unlinkSync(filepath);
|
|
81
|
-
return true;
|
|
82
|
-
} catch (error) {
|
|
83
|
-
return false;
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
|
|
44
|
+
export function saveConversation(conversation: ConversationHistory): void {
|
|
45
|
+
const historyDir = getHistoryDir();
|
|
46
|
+
const filename = `${conversation.id}.json`;
|
|
47
|
+
const filepath = join(historyDir, filename);
|
|
48
|
+
|
|
49
|
+
writeFileSync(filepath, JSON.stringify(conversation, null, 2), 'utf-8');
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function updateConversationTitle(id: string, title: string | null): boolean {
|
|
53
|
+
const historyDir = getHistoryDir();
|
|
54
|
+
const filepath = join(historyDir, `${id}.json`);
|
|
55
|
+
|
|
56
|
+
if (!existsSync(filepath)) {
|
|
57
|
+
return false;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
try {
|
|
61
|
+
const content = readFileSync(filepath, 'utf-8');
|
|
62
|
+
const data = JSON.parse(content) as ConversationHistory;
|
|
63
|
+
data.title = title;
|
|
64
|
+
writeFileSync(filepath, JSON.stringify(data, null, 2), 'utf-8');
|
|
65
|
+
return true;
|
|
66
|
+
} catch (error) {
|
|
67
|
+
return false;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function deleteConversation(id: string): boolean {
|
|
72
|
+
const historyDir = getHistoryDir();
|
|
73
|
+
const filepath = join(historyDir, `${id}.json`);
|
|
74
|
+
|
|
75
|
+
if (!existsSync(filepath)) {
|
|
76
|
+
return false;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
try {
|
|
80
|
+
unlinkSync(filepath);
|
|
81
|
+
return true;
|
|
82
|
+
} catch (error) {
|
|
83
|
+
return false;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
87
|
export function loadConversations(): ConversationHistory[] {
|
|
88
88
|
const historyDir = getHistoryDir();
|
|
89
89
|
|
|
@@ -91,19 +91,19 @@ export function loadConversations(): ConversationHistory[] {
|
|
|
91
91
|
return [];
|
|
92
92
|
}
|
|
93
93
|
|
|
94
|
-
const files = readdirSync(historyDir).filter(f => f.endsWith('.json') && f !== 'inputs.json');
|
|
95
|
-
const conversations: ConversationHistory[] = [];
|
|
96
|
-
|
|
97
|
-
for (const file of files) {
|
|
98
|
-
try {
|
|
99
|
-
const content = readFileSync(join(historyDir, file), 'utf-8');
|
|
100
|
-
const parsed = JSON.parse(content) as ConversationHistory;
|
|
101
|
-
if (!parsed || !Array.isArray(parsed.steps)) continue;
|
|
102
|
-
conversations.push(parsed);
|
|
103
|
-
} catch (error) {
|
|
104
|
-
console.error(`Failed to load ${file}:`, error);
|
|
105
|
-
}
|
|
106
|
-
}
|
|
94
|
+
const files = readdirSync(historyDir).filter(f => f.endsWith('.json') && f !== 'inputs.json');
|
|
95
|
+
const conversations: ConversationHistory[] = [];
|
|
96
|
+
|
|
97
|
+
for (const file of files) {
|
|
98
|
+
try {
|
|
99
|
+
const content = readFileSync(join(historyDir, file), 'utf-8');
|
|
100
|
+
const parsed = JSON.parse(content) as ConversationHistory;
|
|
101
|
+
if (!parsed || !Array.isArray(parsed.steps)) continue;
|
|
102
|
+
conversations.push(parsed);
|
|
103
|
+
} catch (error) {
|
|
104
|
+
console.error(`Failed to load ${file}:`, error);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
107
|
|
|
108
108
|
return conversations.sort((a, b) => b.timestamp - a.timestamp);
|
|
109
109
|
}
|
|
@@ -145,4 +145,4 @@ export function addInputToHistory(input: string): void {
|
|
|
145
145
|
|
|
146
146
|
saveInputHistory(history);
|
|
147
147
|
}
|
|
148
|
-
}
|
|
148
|
+
}
|