@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.
Files changed (99) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +2 -6
  3. package/package.json +55 -48
  4. package/src/agent/Agent.ts +353 -131
  5. package/src/agent/context.ts +4 -4
  6. package/src/agent/prompts/systemPrompt.ts +209 -70
  7. package/src/agent/prompts/toolsPrompt.ts +285 -138
  8. package/src/agent/provider/anthropic.ts +109 -105
  9. package/src/agent/provider/google.ts +111 -107
  10. package/src/agent/provider/mistral.ts +95 -95
  11. package/src/agent/provider/ollama.ts +73 -17
  12. package/src/agent/provider/openai.ts +146 -102
  13. package/src/agent/provider/rateLimit.ts +178 -0
  14. package/src/agent/provider/reasoning.ts +29 -0
  15. package/src/agent/provider/xai.ts +108 -104
  16. package/src/agent/tools/definitions.ts +15 -1
  17. package/src/agent/tools/executor.ts +717 -98
  18. package/src/agent/tools/exploreExecutor.ts +20 -22
  19. package/src/agent/tools/fetch.ts +58 -0
  20. package/src/agent/tools/glob.ts +20 -4
  21. package/src/agent/tools/grep.ts +64 -9
  22. package/src/agent/tools/plan.ts +27 -0
  23. package/src/agent/tools/question.ts +7 -1
  24. package/src/agent/tools/read.ts +2 -0
  25. package/src/agent/types.ts +15 -14
  26. package/src/components/App.tsx +50 -8
  27. package/src/components/CustomInput.tsx +461 -77
  28. package/src/components/Main.tsx +1459 -1112
  29. package/src/components/Setup.tsx +1 -1
  30. package/src/components/ShortcutsModal.tsx +11 -8
  31. package/src/components/Welcome.tsx +1 -1
  32. package/src/components/main/ApprovalPanel.tsx +4 -3
  33. package/src/components/main/ChatPage.tsx +858 -516
  34. package/src/components/main/HomePage.tsx +58 -39
  35. package/src/components/main/QuestionPanel.tsx +52 -7
  36. package/src/components/main/ThinkingIndicator.tsx +13 -2
  37. package/src/components/main/types.ts +11 -10
  38. package/src/index.tsx +53 -25
  39. package/src/mcp/approvalPolicy.ts +148 -0
  40. package/src/mcp/cli/add.ts +185 -0
  41. package/src/mcp/cli/doctor.ts +77 -0
  42. package/src/mcp/cli/index.ts +85 -0
  43. package/src/mcp/cli/list.ts +50 -0
  44. package/src/mcp/cli/logs.ts +24 -0
  45. package/src/mcp/cli/manage.ts +99 -0
  46. package/src/mcp/cli/show.ts +53 -0
  47. package/src/mcp/cli/tools.ts +77 -0
  48. package/src/mcp/config.ts +223 -0
  49. package/src/mcp/index.ts +80 -0
  50. package/src/mcp/processManager.ts +299 -0
  51. package/src/mcp/rateLimiter.ts +50 -0
  52. package/src/mcp/registry.ts +151 -0
  53. package/src/mcp/schemaConverter.ts +100 -0
  54. package/src/mcp/servers/navigation.ts +854 -0
  55. package/src/mcp/toolCatalog.ts +169 -0
  56. package/src/mcp/types.ts +95 -0
  57. package/src/utils/approvalBridge.ts +45 -12
  58. package/src/utils/approvalModeBridge.ts +17 -0
  59. package/src/utils/commands/approvals.ts +48 -0
  60. package/src/utils/commands/compact.ts +30 -0
  61. package/src/utils/commands/echo.ts +1 -1
  62. package/src/utils/commands/image.ts +109 -0
  63. package/src/utils/commands/index.ts +9 -7
  64. package/src/utils/commands/new.ts +15 -0
  65. package/src/utils/commands/types.ts +3 -0
  66. package/src/utils/config.ts +3 -1
  67. package/src/utils/diffRendering.tsx +13 -16
  68. package/src/utils/exploreBridge.ts +10 -0
  69. package/src/utils/history.ts +82 -40
  70. package/src/utils/imageBridge.ts +28 -0
  71. package/src/utils/images.ts +31 -0
  72. package/src/utils/markdown.tsx +163 -99
  73. package/src/utils/models.ts +31 -16
  74. package/src/utils/notificationBridge.ts +23 -0
  75. package/src/utils/questionBridge.ts +36 -1
  76. package/src/utils/tokenEstimator.ts +32 -0
  77. package/src/utils/toolFormatting.ts +428 -48
  78. package/src/web/app.tsx +65 -5
  79. package/src/web/assets/css/ChatPage.css +102 -30
  80. package/src/web/assets/css/MessageItem.css +26 -29
  81. package/src/web/assets/css/ThinkingIndicator.css +44 -6
  82. package/src/web/assets/css/ToolMessage.css +36 -14
  83. package/src/web/components/ChatPage.tsx +228 -105
  84. package/src/web/components/HomePage.tsx +3 -3
  85. package/src/web/components/MessageItem.tsx +80 -81
  86. package/src/web/components/QuestionPanel.tsx +72 -12
  87. package/src/web/components/Setup.tsx +1 -1
  88. package/src/web/components/Sidebar.tsx +1 -3
  89. package/src/web/components/ThinkingIndicator.tsx +41 -21
  90. package/src/web/router.ts +1 -1
  91. package/src/web/server.tsx +894 -662
  92. package/src/web/storage.ts +23 -1
  93. package/src/web/types.ts +7 -6
  94. package/src/utils/commands/redo.ts +0 -74
  95. package/src/utils/commands/sessions.ts +0 -129
  96. package/src/utils/commands/undo.ts +0 -75
  97. package/src/utils/undoRedo.ts +0 -429
  98. package/src/utils/undoRedoBridge.ts +0 -45
  99. package/src/utils/undoRedoDb.ts +0 -338
package/LICENSE CHANGED
@@ -18,4 +18,4 @@ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
18
  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
19
  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
20
  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
- SOFTWARE.
21
+ SOFTWARE.
package/README.md CHANGED
@@ -5,7 +5,7 @@
5
5
  <h1 align="center">Mosaic CLI</h1>
6
6
 
7
7
  <p align="center">
8
- <strong>Version 0.0.91</strong>
8
+ <strong>Version 0.73.0</strong>
9
9
  </p>
10
10
 
11
11
  <p align="center">
@@ -113,9 +113,6 @@ Open http://127.0.0.1:8192 in your browser.
113
113
  |-------------|--------------------------------------|
114
114
  | `/init` | Initialize project context (MOSAIC.md) |
115
115
  | `/help` | Show available commands |
116
- | `/undo` | Undo last file change |
117
- | `/redo` | Redo undone change |
118
- | `/sessions` | Manage conversation sessions |
119
116
  | `/web` | Open web interface |
120
117
  | `/echo` | Echo a message (debug) |
121
118
 
@@ -151,7 +148,6 @@ Mosaic relies on a tool registry that exposes safe, focused capabilities to the
151
148
 
152
149
  **Safety Features:**
153
150
  - Write and edit operations require user approval before execution
154
- - Built-in undo/redo system tracks all file changes (SQLite-backed)
155
151
  - Project context via `MOSAIC.md` helps the agent understand your codebase
156
152
 
157
153
  ## AI Providers
@@ -187,5 +183,5 @@ MIT - see [LICENSE](LICENSE) for details.
187
183
  ---
188
184
 
189
185
  <p align="center">
190
- Made with Bun, React, and OpenTUI
186
+ Made with <a href="https://bun.sh">Bun</a>, <a href="https://react.dev">React</a>, and <a href="https://opentui.com">OpenTUI</a>
191
187
  </p>
package/package.json CHANGED
@@ -1,48 +1,55 @@
1
- {
2
- "name": "@kirosnn/mosaic",
3
- "version": "0.0.91",
4
- "description": "The open source coding agent.",
5
- "license": "MIT",
6
- "type": "module",
7
- "bin": {
8
- "mosaic": "bin/mosaic.cjs"
9
- },
10
- "files": [
11
- "bin",
12
- "src",
13
- "README.md",
14
- "LICENSE"
15
- ],
16
- "scripts": {
17
- "dev": "bun run --watch src/index.tsx",
18
- "mosaic": "bun run src/index.tsx",
19
- "start": "bun run src/index.tsx"
20
- },
21
- "engines": {
22
- "node": ">=18.0.0"
23
- },
24
- "devDependencies": {
25
- "@types/bun": "latest",
26
- "@types/react": "^19.0.0",
27
- "@types/react-dom": "^19.2.3"
28
- },
29
- "dependencies": {
30
- "@ai-sdk/anthropic": "^1.0.0",
31
- "@ai-sdk/google": "^1.0.0",
32
- "@ai-sdk/mistral": "^1.0.0",
33
- "@ai-sdk/openai": "^1.0.0",
34
- "@ai-sdk/xai": "^1.0.0",
35
- "@opentui/core": "^0.1.69",
36
- "@opentui/react": "^0.1.69",
37
- "@types/react-syntax-highlighter": "^15.5.13",
38
- "ai": "^4.0.0",
39
- "better-sqlite3": "^12.6.0",
40
- "ollama": "^0.5.0",
41
- "react": "^19.2.3",
42
- "react-dom": "^19.2.3",
43
- "react-markdown": "^10.1.0",
44
- "react-syntax-highlighter": "^16.1.0",
45
- "remark-gfm": "^4.0.1",
46
- "zod": "^3.23.8"
47
- }
48
- }
1
+ {
2
+ "name": "@kirosnn/mosaic",
3
+ "version": "0.73.0",
4
+ "description": "The open source coding agent.",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "bin": {
8
+ "mosaic": "bin/mosaic.cjs"
9
+ },
10
+ "files": [
11
+ "bin",
12
+ "src",
13
+ "README.md",
14
+ "LICENSE"
15
+ ],
16
+ "scripts": {
17
+ "dev": "bun run --watch src/index.tsx",
18
+ "mosaic": "bun run src/index.tsx",
19
+ "start": "bun run src/index.tsx"
20
+ },
21
+ "engines": {
22
+ "node": ">=18.0.0"
23
+ },
24
+ "devDependencies": {
25
+ "@types/bun": "latest",
26
+ "@types/react": "^19.0.0",
27
+ "@types/react-dom": "^19.2.3",
28
+ "@types/turndown": "^5.0.6"
29
+ },
30
+ "dependencies": {
31
+ "@ai-sdk/anthropic": "^1.0.0",
32
+ "@ai-sdk/google": "^1.0.0",
33
+ "@ai-sdk/mistral": "^1.0.0",
34
+ "@ai-sdk/openai": "^1.0.0",
35
+ "@ai-sdk/xai": "^1.0.0",
36
+ "@modelcontextprotocol/sdk": "^1.25.3",
37
+ "@mozilla/readability": "^0.6.0",
38
+ "@opentui/core": "^0.1.69",
39
+ "@opentui/react": "^0.1.69",
40
+ "@types/react-syntax-highlighter": "^15.5.13",
41
+ "ai": "^4.0.0",
42
+ "better-sqlite3": "^12.6.0",
43
+ "linkedom": "^0.18.12",
44
+ "ollama": "^0.5.0",
45
+ "playwright": "^1.45.0",
46
+ "react": "^19.2.3",
47
+ "react-dom": "^19.2.3",
48
+ "react-markdown": "^10.1.0",
49
+ "react-syntax-highlighter": "^16.1.0",
50
+ "remark-gfm": "^4.0.1",
51
+ "turndown": "^7.2.2",
52
+ "zod": "^3.23.8",
53
+ "zod-to-json-schema": "^3.25.1"
54
+ }
55
+ }
@@ -1,131 +1,353 @@
1
- import { CoreMessage, CoreTool } from 'ai';
2
- import {
3
- AgentEvent,
4
- AgentMessage,
5
- ProviderConfig,
6
- Provider,
7
- ProviderSendOptions,
8
- } from './types';
9
- import { readConfig } from '../utils/config';
10
- import { DEFAULT_SYSTEM_PROMPT, processSystemPrompt } from './prompts/systemPrompt';
11
- import { getTools } from './tools/definitions';
12
- import { AnthropicProvider } from './provider/anthropic';
13
- import { OpenAIProvider } from './provider/openai';
14
- import { GoogleProvider } from './provider/google';
15
- import { MistralProvider } from './provider/mistral';
16
- import { XaiProvider } from './provider/xai';
17
- import { OllamaProvider, checkAndStartOllama } from './provider/ollama';
18
-
19
- export class Agent {
20
- private messageHistory: CoreMessage[] = [];
21
- private provider: Provider;
22
- private config: ProviderConfig;
23
- private static ollamaChecked = false;
24
-
25
- static async ensureProviderReady(): Promise<{ ready: boolean; started?: boolean; error?: string }> {
26
- const userConfig = readConfig();
27
-
28
- if (userConfig.provider === 'ollama') {
29
- if (Agent.ollamaChecked) {
30
- return { ready: true };
31
- }
32
-
33
- const result = await checkAndStartOllama();
34
- Agent.ollamaChecked = true;
35
-
36
- if (!result.running) {
37
- return { ready: false, error: result.error };
38
- }
39
-
40
- return { ready: true, started: result.started };
41
- }
42
-
43
- return { ready: true };
44
- }
45
-
46
- constructor() {
47
- const userConfig = readConfig();
48
-
49
- if (!userConfig.provider || !userConfig.model) {
50
- throw new Error('No provider or model configured. Please run setup first.');
51
- }
52
-
53
- const rawSystemPrompt = userConfig.systemPrompt || DEFAULT_SYSTEM_PROMPT;
54
- const systemPrompt = processSystemPrompt(rawSystemPrompt, true);
55
- const tools = getTools();
56
-
57
- this.config = {
58
- provider: userConfig.provider,
59
- model: userConfig.model,
60
- apiKey: userConfig.apiKey,
61
- systemPrompt,
62
- tools,
63
- maxSteps: 60,
64
- };
65
-
66
- this.provider = this.createProvider(userConfig.provider);
67
- }
68
-
69
- private createProvider(providerName: string): Provider {
70
- switch (providerName) {
71
- case 'openai':
72
- return new OpenAIProvider();
73
- case 'anthropic':
74
- return new AnthropicProvider();
75
- case 'google':
76
- return new GoogleProvider();
77
- case 'mistral':
78
- return new MistralProvider();
79
- case 'xai':
80
- return new XaiProvider();
81
- case 'ollama':
82
- return new OllamaProvider();
83
- default:
84
- throw new Error(`Unknown provider: ${providerName}`);
85
- }
86
- }
87
-
88
- async *sendMessage(userMessage: string, options?: ProviderSendOptions): AsyncGenerator<AgentEvent> {
89
- this.messageHistory.push({
90
- role: 'user',
91
- content: userMessage,
92
- });
93
-
94
- try {
95
- yield* this.provider.sendMessage(this.messageHistory, this.config, options);
96
- } catch (error) {
97
- yield {
98
- type: 'error',
99
- error: error instanceof Error ? error.message : 'Unknown error occurred',
100
- };
101
- }
102
- }
103
-
104
- async *streamMessages(messages: AgentMessage[], options?: ProviderSendOptions): AsyncGenerator<AgentEvent> {
105
- this.messageHistory = messages.map(msg => ({
106
- role: msg.role,
107
- content: msg.content,
108
- }));
109
-
110
- try {
111
- yield* this.provider.sendMessage(this.messageHistory, this.config, options);
112
- } catch (error) {
113
- yield {
114
- type: 'error',
115
- error: error instanceof Error ? error.message : 'Unknown error occurred',
116
- };
117
- }
118
- }
119
-
120
- getHistory(): CoreMessage[] {
121
- return [...this.messageHistory];
122
- }
123
-
124
- clearHistory(): void {
125
- this.messageHistory = [];
126
- }
127
-
128
- updateConfig(updates: Partial<ProviderConfig>): void {
129
- this.config = { ...this.config, ...updates };
130
- }
131
- }
1
+ import { CoreMessage } from 'ai';
2
+ import {
3
+ AgentEvent,
4
+ AgentMessage,
5
+ ProviderConfig,
6
+ Provider,
7
+ ProviderSendOptions,
8
+ } from './types';
9
+ import { readConfig } from '../utils/config';
10
+ import { DEFAULT_SYSTEM_PROMPT, processSystemPrompt } from './prompts/systemPrompt';
11
+ import { getTools } from './tools/definitions';
12
+ import { AnthropicProvider } from './provider/anthropic';
13
+ import { OpenAIProvider } from './provider/openai';
14
+ import { GoogleProvider } from './provider/google';
15
+ import { MistralProvider } from './provider/mistral';
16
+ import { XaiProvider } from './provider/xai';
17
+ import { OllamaProvider, checkAndStartOllama } from './provider/ollama';
18
+ import { getModelsDevContextLimit } from '../utils/models';
19
+ import { estimateTokensFromText, estimateTokensForContent, getDefaultContextBudget } from '../utils/tokenEstimator';
20
+ import { setExploreContext } from '../utils/exploreBridge';
21
+
22
+ function contentToText(content: CoreMessage['content']): string {
23
+ if (typeof content === 'string') return content;
24
+ if (!content) return '';
25
+
26
+ if (Array.isArray(content)) {
27
+ const text = content
28
+ .map((part: any) => {
29
+ if (part && typeof part.text === 'string') return part.text;
30
+ if (typeof part === 'string') return part;
31
+ return '';
32
+ })
33
+ .filter(Boolean)
34
+ .join('');
35
+
36
+ if (text) return text;
37
+ }
38
+
39
+ try {
40
+ return JSON.stringify(content);
41
+ } catch {
42
+ return String(content);
43
+ }
44
+ }
45
+
46
+ function estimateTokensForMessages(messages: CoreMessage[]): number {
47
+ let total = 0;
48
+ for (const message of messages) {
49
+ total += estimateTokensForContent(contentToText(message.content));
50
+ }
51
+ return total;
52
+ }
53
+
54
+ function normalizeWhitespace(text: string): string {
55
+ return text.replace(/\s+/g, ' ').trim();
56
+ }
57
+
58
+ function truncateText(text: string, maxChars: number): string {
59
+ if (text.length <= maxChars) return text;
60
+ return text.slice(0, Math.max(0, maxChars - 3)) + '...';
61
+ }
62
+
63
+ function summarizeMessage(message: CoreMessage, isLastUser: boolean): string {
64
+ if (message.role === 'tool') {
65
+ const content: any = message.content;
66
+ const part = Array.isArray(content) ? content[0] : undefined;
67
+ const toolName = part?.toolName ?? part?.tool_name ?? 'tool';
68
+ let resultText = '';
69
+ if (part?.result !== undefined) {
70
+ if (typeof part.result === 'string') resultText = part.result;
71
+ else {
72
+ try {
73
+ resultText = JSON.stringify(part.result);
74
+ } catch {
75
+ resultText = String(part.result);
76
+ }
77
+ }
78
+ } else {
79
+ resultText = contentToText(message.content);
80
+ }
81
+ const isError = resultText.toLowerCase().includes('error') || resultText.toLowerCase().includes('failed');
82
+ const status = isError ? 'FAILED' : 'OK';
83
+ const cleaned = normalizeWhitespace(resultText);
84
+ return `[tool:${toolName} ${status}] ${truncateText(cleaned, 120)}`;
85
+ }
86
+
87
+ if (message.role === 'assistant') {
88
+ const text = contentToText(message.content);
89
+ const cleaned = normalizeWhitespace(text);
90
+ const sentenceMatch = cleaned.match(/^[^.!?\n]{10,}[.!?]/);
91
+ const summary = sentenceMatch ? sentenceMatch[0] : cleaned;
92
+ return `assistant: ${truncateText(summary, 200)}`;
93
+ }
94
+
95
+ const cleaned = normalizeWhitespace(contentToText(message.content));
96
+ const limit = isLastUser ? cleaned.length : 400;
97
+ return `user: ${truncateText(cleaned, limit)}`;
98
+ }
99
+
100
+ function buildSummary(messages: CoreMessage[], maxTokens: number): string {
101
+ const maxChars = Math.max(0, maxTokens * 3);
102
+ const header = 'CONVERSATION SUMMARY (auto):';
103
+ let charCount = header.length + 1;
104
+ const lines: string[] = [];
105
+
106
+ let lastUserIndex = -1;
107
+ for (let i = messages.length - 1; i >= 0; i--) {
108
+ if (messages[i]!.role === 'user') { lastUserIndex = i; break; }
109
+ }
110
+
111
+ for (let i = 0; i < messages.length; i++) {
112
+ if (charCount >= maxChars) break;
113
+ const line = `- ${summarizeMessage(messages[i]!, i === lastUserIndex)}`;
114
+ charCount += line.length + 1;
115
+ lines.push(line);
116
+ }
117
+ const body = lines.join('\n');
118
+ const full = `${header}\n${body}`.trim();
119
+ return truncateText(full, maxChars);
120
+ }
121
+
122
+ function compactMessages(
123
+ messages: CoreMessage[],
124
+ systemPrompt: string,
125
+ maxContextTokens?: number,
126
+ provider?: string
127
+ ): CoreMessage[] {
128
+ const budget = maxContextTokens ?? getDefaultContextBudget(provider);
129
+ const systemTokens = estimateTokensFromText(systemPrompt) + 8;
130
+ const messagesTokens = estimateTokensForMessages(messages);
131
+ const total = systemTokens + messagesTokens;
132
+
133
+ if (total <= budget) return messages;
134
+
135
+ const summaryTokens = Math.min(2000, Math.max(400, Math.floor(budget * 0.2)));
136
+ const recentBudget = Math.max(500, budget - summaryTokens);
137
+
138
+ let recentTokens = 0;
139
+ const recent: CoreMessage[] = [];
140
+ for (let i = messages.length - 1; i >= 0; i--) {
141
+ const message = messages[i]!;
142
+ const msgTokens = estimateTokensForContent(contentToText(message.content));
143
+ if (recentTokens + msgTokens > recentBudget && recent.length > 0) break;
144
+ recent.unshift(message);
145
+ recentTokens += msgTokens;
146
+ }
147
+
148
+ const cutoff = messages.length - recent.length;
149
+ const older = cutoff > 0 ? messages.slice(0, cutoff) : [];
150
+
151
+ if (older.length === 0) return recent;
152
+
153
+ const summary = buildSummary(older, summaryTokens);
154
+ const summaryMessage: CoreMessage = { role: 'assistant', content: summary };
155
+
156
+ return [summaryMessage, ...recent];
157
+ }
158
+
159
+ function buildExploreContext(messages: CoreMessage[]): string {
160
+ const parts: string[] = [];
161
+
162
+ const userMessages: string[] = [];
163
+ const recentFiles = new Set<string>();
164
+
165
+ for (let i = messages.length - 1; i >= 0; i--) {
166
+ const msg = messages[i]!;
167
+
168
+ if (msg.role === 'user' && userMessages.length < 2) {
169
+ const text = normalizeWhitespace(contentToText(msg.content));
170
+ if (text) userMessages.unshift(truncateText(text, 300));
171
+ }
172
+
173
+ if (msg.role === 'tool' && recentFiles.size < 15) {
174
+ const content: any = msg.content;
175
+ const part = Array.isArray(content) ? content[0] : undefined;
176
+ const toolName = part?.toolName ?? part?.tool_name;
177
+ if (toolName === 'read' || toolName === 'write' || toolName === 'edit') {
178
+ const args = part?.args ?? part?.input;
179
+ const path = args?.path;
180
+ if (typeof path === 'string') recentFiles.add(path);
181
+ }
182
+ }
183
+ }
184
+
185
+ if (userMessages.length > 0) {
186
+ parts.push(`User intent:\n${userMessages.map(m => `- ${m}`).join('\n')}`);
187
+ }
188
+
189
+ if (recentFiles.size > 0) {
190
+ parts.push(`Files recently accessed:\n${[...recentFiles].map(f => `- ${f}`).join('\n')}`);
191
+ }
192
+
193
+ return parts.join('\n\n');
194
+ }
195
+
196
+ export class Agent {
197
+ private messageHistory: CoreMessage[] = [];
198
+ private provider: Provider;
199
+ private config: ProviderConfig;
200
+ private static ollamaChecked = false;
201
+ private resolvedMaxContextTokens?: number;
202
+
203
+ static async ensureProviderReady(): Promise<{ ready: boolean; started?: boolean; error?: string }> {
204
+ const userConfig = readConfig();
205
+
206
+ if (userConfig.provider === 'ollama') {
207
+ if (Agent.ollamaChecked) {
208
+ return { ready: true };
209
+ }
210
+
211
+ const result = await checkAndStartOllama();
212
+ Agent.ollamaChecked = true;
213
+
214
+ if (!result.running) {
215
+ return { ready: false, error: result.error };
216
+ }
217
+
218
+ return { ready: true, started: result.started };
219
+ }
220
+
221
+ return { ready: true };
222
+ }
223
+
224
+ constructor() {
225
+ const userConfig = readConfig();
226
+
227
+ if (!userConfig.provider || !userConfig.model) {
228
+ throw new Error('No provider or model configured. Please run setup first.');
229
+ }
230
+
231
+ const rawSystemPrompt = userConfig.systemPrompt || DEFAULT_SYSTEM_PROMPT;
232
+
233
+ let mcpToolInfos: Array<{ serverId: string; name: string; description: string; inputSchema: Record<string, unknown>; canonicalId: string; safeId: string }> | undefined;
234
+ try {
235
+ const { getMcpCatalog, isMcpInitialized } = require('../mcp/index');
236
+ if (isMcpInitialized()) {
237
+ mcpToolInfos = getMcpCatalog().getMcpToolInfos();
238
+ }
239
+ } catch {
240
+ // MCP not available
241
+ }
242
+
243
+ const systemPrompt = processSystemPrompt(rawSystemPrompt, true, mcpToolInfos);
244
+ const tools = getTools();
245
+
246
+ this.config = {
247
+ provider: userConfig.provider,
248
+ model: userConfig.model,
249
+ apiKey: userConfig.apiKey,
250
+ systemPrompt,
251
+ tools,
252
+ maxSteps: userConfig.maxSteps ?? 100,
253
+ maxContextTokens: userConfig.maxContextTokens,
254
+ };
255
+
256
+ this.provider = this.createProvider(userConfig.provider);
257
+ }
258
+
259
+ private createProvider(providerName: string): Provider {
260
+ switch (providerName) {
261
+ case 'openai':
262
+ return new OpenAIProvider();
263
+ case 'anthropic':
264
+ return new AnthropicProvider();
265
+ case 'google':
266
+ return new GoogleProvider();
267
+ case 'mistral':
268
+ return new MistralProvider();
269
+ case 'xai':
270
+ return new XaiProvider();
271
+ case 'ollama':
272
+ return new OllamaProvider();
273
+ default:
274
+ throw new Error(`Unknown provider: ${providerName}`);
275
+ }
276
+ }
277
+
278
+ async *sendMessage(userMessage: string, options?: ProviderSendOptions): AsyncGenerator<AgentEvent> {
279
+ this.messageHistory.push({
280
+ role: 'user',
281
+ content: userMessage,
282
+ });
283
+
284
+ try {
285
+ if (this.resolvedMaxContextTokens === undefined) {
286
+ const resolved = await getModelsDevContextLimit(this.config.provider, this.config.model);
287
+ if (typeof resolved === 'number') {
288
+ this.resolvedMaxContextTokens = resolved;
289
+ if (!this.config.maxContextTokens) {
290
+ this.config = { ...this.config, maxContextTokens: resolved };
291
+ }
292
+ }
293
+ }
294
+ const compacted = compactMessages(
295
+ this.messageHistory,
296
+ this.config.systemPrompt,
297
+ this.config.maxContextTokens ?? this.resolvedMaxContextTokens,
298
+ this.config.provider
299
+ );
300
+ setExploreContext(buildExploreContext(this.messageHistory));
301
+ yield* this.provider.sendMessage(compacted, this.config, options);
302
+ } catch (error) {
303
+ yield {
304
+ type: 'error',
305
+ error: error instanceof Error ? error.message : 'Unknown error occurred',
306
+ };
307
+ }
308
+ }
309
+
310
+ async *streamMessages(messages: AgentMessage[], options?: ProviderSendOptions): AsyncGenerator<AgentEvent> {
311
+ this.messageHistory = messages.map(msg => ({
312
+ role: msg.role,
313
+ content: msg.content,
314
+ })) as CoreMessage[];
315
+
316
+ try {
317
+ if (this.resolvedMaxContextTokens === undefined) {
318
+ const resolved = await getModelsDevContextLimit(this.config.provider, this.config.model);
319
+ if (typeof resolved === 'number') {
320
+ this.resolvedMaxContextTokens = resolved;
321
+ if (!this.config.maxContextTokens) {
322
+ this.config = { ...this.config, maxContextTokens: resolved };
323
+ }
324
+ }
325
+ }
326
+ const compacted = compactMessages(
327
+ this.messageHistory,
328
+ this.config.systemPrompt,
329
+ this.config.maxContextTokens ?? this.resolvedMaxContextTokens,
330
+ this.config.provider
331
+ );
332
+ setExploreContext(buildExploreContext(this.messageHistory));
333
+ yield* this.provider.sendMessage(compacted, this.config, options);
334
+ } catch (error) {
335
+ yield {
336
+ type: 'error',
337
+ error: error instanceof Error ? error.message : 'Unknown error occurred',
338
+ };
339
+ }
340
+ }
341
+
342
+ getHistory(): CoreMessage[] {
343
+ return [...this.messageHistory];
344
+ }
345
+
346
+ clearHistory(): void {
347
+ this.messageHistory = [];
348
+ }
349
+
350
+ updateConfig(updates: Partial<ProviderConfig>): void {
351
+ this.config = { ...this.config, ...updates };
352
+ }
353
+ }
@@ -73,9 +73,9 @@ export class AgentContextManager {
73
73
  return this.currentStep;
74
74
  }
75
75
 
76
- getMaxSteps(): number {
77
- return this.config.maxSteps || 10;
78
- }
76
+ getMaxSteps(): number {
77
+ return this.config.maxSteps || 100;
78
+ }
79
79
 
80
80
  canContinue(): boolean {
81
81
  return this.currentStep < this.getMaxSteps();
@@ -93,4 +93,4 @@ export class AgentContextManager {
93
93
  getMessageCount(): number {
94
94
  return this.messages.length;
95
95
  }
96
- }
96
+ }