@zds-ai/cli 0.1.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 (204) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +497 -0
  3. package/dist/agent/grok-agent.d.ts +250 -0
  4. package/dist/agent/grok-agent.js +2480 -0
  5. package/dist/agent/grok-agent.js.map +1 -0
  6. package/dist/agent/index.d.ts +14 -0
  7. package/dist/agent/index.js +136 -0
  8. package/dist/agent/index.js.map +1 -0
  9. package/dist/commands/mcp.d.ts +2 -0
  10. package/dist/commands/mcp.js +239 -0
  11. package/dist/commands/mcp.js.map +1 -0
  12. package/dist/grok/client.d.ts +55 -0
  13. package/dist/grok/client.js +276 -0
  14. package/dist/grok/client.js.map +1 -0
  15. package/dist/grok/tools.d.ts +8 -0
  16. package/dist/grok/tools.js +878 -0
  17. package/dist/grok/tools.js.map +1 -0
  18. package/dist/hooks/use-enhanced-input.d.ts +38 -0
  19. package/dist/hooks/use-enhanced-input.js +228 -0
  20. package/dist/hooks/use-enhanced-input.js.map +1 -0
  21. package/dist/hooks/use-input-handler.d.ts +36 -0
  22. package/dist/hooks/use-input-handler.js +1099 -0
  23. package/dist/hooks/use-input-handler.js.map +1 -0
  24. package/dist/hooks/use-input-history.d.ts +9 -0
  25. package/dist/hooks/use-input-history.js +61 -0
  26. package/dist/hooks/use-input-history.js.map +1 -0
  27. package/dist/index.d.ts +2 -0
  28. package/dist/index.js +869 -0
  29. package/dist/index.js.map +1 -0
  30. package/dist/mcp/client.d.ts +41 -0
  31. package/dist/mcp/client.js +224 -0
  32. package/dist/mcp/client.js.map +1 -0
  33. package/dist/mcp/config.d.ts +13 -0
  34. package/dist/mcp/config.js +56 -0
  35. package/dist/mcp/config.js.map +1 -0
  36. package/dist/mcp/transports.d.ts +53 -0
  37. package/dist/mcp/transports.js +256 -0
  38. package/dist/mcp/transports.js.map +1 -0
  39. package/dist/tools/character-tool.d.ts +27 -0
  40. package/dist/tools/character-tool.js +194 -0
  41. package/dist/tools/character-tool.js.map +1 -0
  42. package/dist/tools/clear-cache-tool.d.ts +14 -0
  43. package/dist/tools/clear-cache-tool.js +82 -0
  44. package/dist/tools/clear-cache-tool.js.map +1 -0
  45. package/dist/tools/confirmation-tool.d.ts +16 -0
  46. package/dist/tools/confirmation-tool.js +72 -0
  47. package/dist/tools/confirmation-tool.js.map +1 -0
  48. package/dist/tools/env-tool.d.ts +17 -0
  49. package/dist/tools/env-tool.js +89 -0
  50. package/dist/tools/env-tool.js.map +1 -0
  51. package/dist/tools/file-conversion-tool.d.ts +16 -0
  52. package/dist/tools/file-conversion-tool.js +181 -0
  53. package/dist/tools/file-conversion-tool.js.map +1 -0
  54. package/dist/tools/image-tool.d.ts +22 -0
  55. package/dist/tools/image-tool.js +268 -0
  56. package/dist/tools/image-tool.js.map +1 -0
  57. package/dist/tools/index.d.ts +14 -0
  58. package/dist/tools/index.js +15 -0
  59. package/dist/tools/index.js.map +1 -0
  60. package/dist/tools/internet-tool.d.ts +11 -0
  61. package/dist/tools/internet-tool.js +108 -0
  62. package/dist/tools/internet-tool.js.map +1 -0
  63. package/dist/tools/introspect-tool.d.ts +11 -0
  64. package/dist/tools/introspect-tool.js +243 -0
  65. package/dist/tools/introspect-tool.js.map +1 -0
  66. package/dist/tools/morph-editor.d.ts +38 -0
  67. package/dist/tools/morph-editor.js +318 -0
  68. package/dist/tools/morph-editor.js.map +1 -0
  69. package/dist/tools/restart-tool.d.ts +7 -0
  70. package/dist/tools/restart-tool.js +24 -0
  71. package/dist/tools/restart-tool.js.map +1 -0
  72. package/dist/tools/search.d.ts +71 -0
  73. package/dist/tools/search.js +340 -0
  74. package/dist/tools/search.js.map +1 -0
  75. package/dist/tools/task-tool.d.ts +19 -0
  76. package/dist/tools/task-tool.js +115 -0
  77. package/dist/tools/task-tool.js.map +1 -0
  78. package/dist/tools/text-editor.d.ts +35 -0
  79. package/dist/tools/text-editor.js +669 -0
  80. package/dist/tools/text-editor.js.map +1 -0
  81. package/dist/tools/tool-discovery.d.ts +20 -0
  82. package/dist/tools/tool-discovery.js +45 -0
  83. package/dist/tools/tool-discovery.js.map +1 -0
  84. package/dist/tools/zsh.d.ts +13 -0
  85. package/dist/tools/zsh.js +168 -0
  86. package/dist/tools/zsh.js.map +1 -0
  87. package/dist/types/index.d.ts +31 -0
  88. package/dist/types/index.js +2 -0
  89. package/dist/types/index.js.map +1 -0
  90. package/dist/ui/app.d.ts +7 -0
  91. package/dist/ui/app.js +99 -0
  92. package/dist/ui/app.js.map +1 -0
  93. package/dist/ui/components/active-task-status.d.ts +7 -0
  94. package/dist/ui/components/active-task-status.js +37 -0
  95. package/dist/ui/components/active-task-status.js.map +1 -0
  96. package/dist/ui/components/api-key-input.d.ts +7 -0
  97. package/dist/ui/components/api-key-input.js +80 -0
  98. package/dist/ui/components/api-key-input.js.map +1 -0
  99. package/dist/ui/components/backend-status.d.ts +7 -0
  100. package/dist/ui/components/backend-status.js +85 -0
  101. package/dist/ui/components/backend-status.js.map +1 -0
  102. package/dist/ui/components/chat-history.d.ts +8 -0
  103. package/dist/ui/components/chat-history.js +187 -0
  104. package/dist/ui/components/chat-history.js.map +1 -0
  105. package/dist/ui/components/chat-input.d.ts +9 -0
  106. package/dist/ui/components/chat-input.js +63 -0
  107. package/dist/ui/components/chat-input.js.map +1 -0
  108. package/dist/ui/components/chat-interface.d.ts +9 -0
  109. package/dist/ui/components/chat-interface.js +389 -0
  110. package/dist/ui/components/chat-interface.js.map +1 -0
  111. package/dist/ui/components/command-suggestions.d.ts +17 -0
  112. package/dist/ui/components/command-suggestions.js +22 -0
  113. package/dist/ui/components/command-suggestions.js.map +1 -0
  114. package/dist/ui/components/confirmation-dialog.d.ts +11 -0
  115. package/dist/ui/components/confirmation-dialog.js +105 -0
  116. package/dist/ui/components/confirmation-dialog.js.map +1 -0
  117. package/dist/ui/components/context-status.d.ts +7 -0
  118. package/dist/ui/components/context-status.js +36 -0
  119. package/dist/ui/components/context-status.js.map +1 -0
  120. package/dist/ui/components/diff-renderer.d.ts +13 -0
  121. package/dist/ui/components/diff-renderer.js +206 -0
  122. package/dist/ui/components/diff-renderer.js.map +1 -0
  123. package/dist/ui/components/loading-spinner.d.ts +8 -0
  124. package/dist/ui/components/loading-spinner.js +64 -0
  125. package/dist/ui/components/loading-spinner.js.map +1 -0
  126. package/dist/ui/components/mcp-status.d.ts +5 -0
  127. package/dist/ui/components/mcp-status.js +57 -0
  128. package/dist/ui/components/mcp-status.js.map +1 -0
  129. package/dist/ui/components/model-selection.d.ts +12 -0
  130. package/dist/ui/components/model-selection.js +17 -0
  131. package/dist/ui/components/model-selection.js.map +1 -0
  132. package/dist/ui/components/mood-status.d.ts +7 -0
  133. package/dist/ui/components/mood-status.js +34 -0
  134. package/dist/ui/components/mood-status.js.map +1 -0
  135. package/dist/ui/components/persona-status.d.ts +7 -0
  136. package/dist/ui/components/persona-status.js +34 -0
  137. package/dist/ui/components/persona-status.js.map +1 -0
  138. package/dist/ui/shared/max-sized-box.d.ts +8 -0
  139. package/dist/ui/shared/max-sized-box.js +6 -0
  140. package/dist/ui/shared/max-sized-box.js.map +1 -0
  141. package/dist/ui/utils/code-colorizer.d.ts +2 -0
  142. package/dist/ui/utils/code-colorizer.js +7 -0
  143. package/dist/ui/utils/code-colorizer.js.map +1 -0
  144. package/dist/ui/utils/colors.d.ts +14 -0
  145. package/dist/ui/utils/colors.js +15 -0
  146. package/dist/ui/utils/colors.js.map +1 -0
  147. package/dist/ui/utils/markdown-renderer.d.ts +4 -0
  148. package/dist/ui/utils/markdown-renderer.js +40 -0
  149. package/dist/ui/utils/markdown-renderer.js.map +1 -0
  150. package/dist/utils/auth-helper.d.ts +63 -0
  151. package/dist/utils/auth-helper.js +129 -0
  152. package/dist/utils/auth-helper.js.map +1 -0
  153. package/dist/utils/chat-history-manager-sqlite.d.ts +92 -0
  154. package/dist/utils/chat-history-manager-sqlite.js +334 -0
  155. package/dist/utils/chat-history-manager-sqlite.js.map +1 -0
  156. package/dist/utils/chat-history-manager.d.ts +87 -0
  157. package/dist/utils/chat-history-manager.js +273 -0
  158. package/dist/utils/chat-history-manager.js.map +1 -0
  159. package/dist/utils/chat-history-manager.json-backup.d.ts +69 -0
  160. package/dist/utils/chat-history-manager.json-backup.js +215 -0
  161. package/dist/utils/chat-history-manager.json-backup.js.map +1 -0
  162. package/dist/utils/confirmation-service.d.ts +46 -0
  163. package/dist/utils/confirmation-service.js +165 -0
  164. package/dist/utils/confirmation-service.js.map +1 -0
  165. package/dist/utils/custom-instructions.d.ts +1 -0
  166. package/dist/utils/custom-instructions.js +30 -0
  167. package/dist/utils/custom-instructions.js.map +1 -0
  168. package/dist/utils/database-connection.d.ts +27 -0
  169. package/dist/utils/database-connection.js +81 -0
  170. package/dist/utils/database-connection.js.map +1 -0
  171. package/dist/utils/database-schema.d.ts +17 -0
  172. package/dist/utils/database-schema.js +93 -0
  173. package/dist/utils/database-schema.js.map +1 -0
  174. package/dist/utils/error-logger.d.ts +13 -0
  175. package/dist/utils/error-logger.js +56 -0
  176. package/dist/utils/error-logger.js.map +1 -0
  177. package/dist/utils/hook-executor.d.ts +59 -0
  178. package/dist/utils/hook-executor.js +351 -0
  179. package/dist/utils/hook-executor.js.map +1 -0
  180. package/dist/utils/model-config.d.ts +28 -0
  181. package/dist/utils/model-config.js +42 -0
  182. package/dist/utils/model-config.js.map +1 -0
  183. package/dist/utils/path-utils.d.ts +4 -0
  184. package/dist/utils/path-utils.js +12 -0
  185. package/dist/utils/path-utils.js.map +1 -0
  186. package/dist/utils/settings-manager.d.ts +169 -0
  187. package/dist/utils/settings-manager.js +403 -0
  188. package/dist/utils/settings-manager.js.map +1 -0
  189. package/dist/utils/settings.d.ts +1 -0
  190. package/dist/utils/settings.js +4 -0
  191. package/dist/utils/settings.js.map +1 -0
  192. package/dist/utils/slash-commands.d.ts +25 -0
  193. package/dist/utils/slash-commands.js +454 -0
  194. package/dist/utils/slash-commands.js.map +1 -0
  195. package/dist/utils/startup-hook.d.ts +13 -0
  196. package/dist/utils/startup-hook.js +44 -0
  197. package/dist/utils/startup-hook.js.map +1 -0
  198. package/dist/utils/text-utils.d.ts +80 -0
  199. package/dist/utils/text-utils.js +182 -0
  200. package/dist/utils/text-utils.js.map +1 -0
  201. package/dist/utils/token-counter.d.ts +33 -0
  202. package/dist/utils/token-counter.js +78 -0
  203. package/dist/utils/token-counter.js.map +1 -0
  204. package/package.json +102 -0
@@ -0,0 +1,2480 @@
1
+ import { GrokClient } from "../grok/client.js";
2
+ import { getAllGrokTools, getMCPManager, initializeMCPServers, } from "../grok/tools.js";
3
+ import { loadMCPConfig } from "../mcp/config.js";
4
+ import { ChatHistoryManager } from "../utils/chat-history-manager.js";
5
+ import { logApiError } from "../utils/error-logger.js";
6
+ import fs from "fs";
7
+ import { TextEditorTool, MorphEditorTool, ZshTool, ConfirmationTool, SearchTool, EnvTool, IntrospectTool, ClearCacheTool, CharacterTool, TaskTool, InternetTool, ImageTool, FileConversionTool, RestartTool } from "../tools/index.js";
8
+ import { EventEmitter } from "events";
9
+ import { createTokenCounter } from "../utils/token-counter.js";
10
+ import { getSettingsManager } from "../utils/settings-manager.js";
11
+ import { executeOperationHook, executeToolApprovalHook, applyHookCommands } from "../utils/hook-executor.js";
12
+ export class GrokAgent extends EventEmitter {
13
+ grokClient;
14
+ textEditor;
15
+ morphEditor;
16
+ zsh;
17
+ confirmationTool;
18
+ search;
19
+ env;
20
+ introspect;
21
+ clearCacheTool;
22
+ characterTool;
23
+ taskTool;
24
+ internetTool;
25
+ imageTool;
26
+ fileConversionTool;
27
+ restartTool;
28
+ chatHistory = [];
29
+ messages = [];
30
+ tokenCounter;
31
+ abortController = null;
32
+ mcpInitialized = false;
33
+ maxToolRounds;
34
+ temperature;
35
+ maxTokens;
36
+ firstMessageProcessed = false;
37
+ contextWarningAt80 = false;
38
+ contextWarningAt90 = false;
39
+ persona = "";
40
+ personaColor = "white";
41
+ mood = "";
42
+ moodColor = "white";
43
+ activeTask = "";
44
+ activeTaskAction = "";
45
+ activeTaskColor = "white";
46
+ apiKeyEnvVar = "GROK_API_KEY";
47
+ pendingContextEdit = null;
48
+ constructor(apiKey, baseURL, model, maxToolRounds, debugLogFile, startupHookOutput, temperature, maxTokens) {
49
+ super();
50
+ const manager = getSettingsManager();
51
+ const savedModel = manager.getCurrentModel();
52
+ const modelToUse = model || savedModel || "grok-code-fast-1";
53
+ this.maxToolRounds = maxToolRounds || 400;
54
+ this.temperature = temperature ?? manager.getTemperature();
55
+ this.maxTokens = maxTokens ?? manager.getMaxTokens();
56
+ // Get display name from environment (set by zai/helpers)
57
+ const displayName = process.env.GROK_BACKEND_DISPLAY_NAME;
58
+ this.grokClient = new GrokClient(apiKey, modelToUse, baseURL, displayName);
59
+ // Set apiKeyEnvVar based on backend name
60
+ const backendName = this.grokClient.getBackendName().toUpperCase();
61
+ this.apiKeyEnvVar = `${backendName}_API_KEY`;
62
+ this.textEditor = new TextEditorTool();
63
+ this.morphEditor = process.env.MORPH_API_KEY ? new MorphEditorTool() : null;
64
+ this.zsh = new ZshTool();
65
+ this.confirmationTool = new ConfirmationTool();
66
+ this.search = new SearchTool();
67
+ this.env = new EnvTool();
68
+ this.introspect = new IntrospectTool();
69
+ this.clearCacheTool = new ClearCacheTool();
70
+ this.restartTool = new RestartTool();
71
+ this.characterTool = new CharacterTool();
72
+ this.taskTool = new TaskTool();
73
+ this.internetTool = new InternetTool();
74
+ this.imageTool = new ImageTool();
75
+ this.fileConversionTool = new FileConversionTool();
76
+ this.textEditor.setAgent(this); // Give text editor access to agent for context awareness
77
+ this.introspect.setAgent(this); // Give introspect access to agent for tool class info
78
+ this.clearCacheTool.setAgent(this); // Give clearCache access to agent
79
+ this.characterTool.setAgent(this); // Give character tool access to agent
80
+ this.taskTool.setAgent(this); // Give task tool access to agent
81
+ this.internetTool.setAgent(this); // Give internet tool access to agent
82
+ this.imageTool.setAgent(this); // Give image tool access to agent
83
+ this.zsh.setAgent(this); // Give zsh tool access to agent for CWD tracking
84
+ this.tokenCounter = createTokenCounter(modelToUse);
85
+ // Initialize MCP servers if configured
86
+ this.initializeMCP(debugLogFile);
87
+ // System message will be set after async initialization
88
+ this.messages.push({
89
+ role: "system",
90
+ content: "Initializing...", // Temporary, will be replaced in initialize()
91
+ });
92
+ // Note: THE system prompt is NOT added to chatHistory
93
+ // Only conversational system messages go in chatHistory
94
+ // Store startup hook output for later use
95
+ this.startupHookOutput = startupHookOutput;
96
+ }
97
+ startupHookOutput;
98
+ systemPrompt = "Initializing..."; // THE system prompt (always at messages[0])
99
+ /**
100
+ * Initialize the agent with dynamic system prompt
101
+ * Must be called after construction
102
+ */
103
+ async initialize() {
104
+ // Build system message
105
+ await this.buildSystemMessage();
106
+ // Execute instance hook on every startup (fresh or not)
107
+ const settings = getSettingsManager();
108
+ const instanceHookPath = settings.getInstanceHook();
109
+ if (instanceHookPath) {
110
+ const hookResult = await executeOperationHook(instanceHookPath, "instance", {}, 30000, false, // Instance hook is not mandatory
111
+ this.getCurrentTokenCount(), this.getMaxContextSize());
112
+ if (hookResult.approved && hookResult.commands && hookResult.commands.length > 0) {
113
+ // Apply hook commands (ENV, TOOL_RESULT, MODEL, SYSTEM)
114
+ await this.processHookResult(hookResult);
115
+ }
116
+ }
117
+ }
118
+ /**
119
+ * Build/rebuild the system message with current tool availability
120
+ * Updates this.systemPrompt which is always used for messages[0]
121
+ */
122
+ async buildSystemMessage() {
123
+ // Add startup hook output if provided
124
+ const startupHookSection = this.startupHookOutput
125
+ ? `${this.startupHookOutput}\n`
126
+ : "";
127
+ // Generate dynamic tool list using introspect tool
128
+ const toolsResult = await this.introspect.introspect("tools");
129
+ const toolsSection = toolsResult.success ? toolsResult.output : "Tools: Unknown";
130
+ // Build THE system prompt
131
+ this.systemPrompt = `${startupHookSection}
132
+ ${toolsSection}
133
+
134
+ Current working directory: ${process.cwd()}`;
135
+ // Update messages[0] with the system prompt
136
+ this.messages[0] = {
137
+ role: "system",
138
+ content: this.systemPrompt,
139
+ };
140
+ // Note: chatHistory no longer contains THE system prompt
141
+ // Only conversational system messages (persona, mood, etc.) go in chatHistory
142
+ }
143
+ async loadInitialHistory(history, systemPrompt) {
144
+ // Load chatHistory (no system messages in new architecture)
145
+ this.chatHistory = history;
146
+ // Set system prompt if provided, otherwise generate one
147
+ if (systemPrompt) {
148
+ this.setSystemPrompt(systemPrompt);
149
+ }
150
+ else {
151
+ await this.buildSystemMessage();
152
+ }
153
+ // Instance hook now runs in initialize() for both fresh and existing sessions
154
+ // Convert history to messages format for API calls
155
+ const historyMessages = [];
156
+ // Track which tool_call_ids we've seen in assistant messages
157
+ const seenToolCallIds = new Set();
158
+ // First pass: collect all tool_call_ids from assistant messages
159
+ for (const entry of history) {
160
+ if (entry.type === "assistant" && entry.tool_calls) {
161
+ entry.tool_calls.forEach(tc => seenToolCallIds.add(tc.id));
162
+ }
163
+ }
164
+ // Second pass: build history messages, only including tool_results that have matching tool_calls
165
+ const toolResultMessages = [];
166
+ const toolCallIdToMessage = new Map();
167
+ for (const entry of history) {
168
+ switch (entry.type) {
169
+ case "system":
170
+ // All system messages from chatHistory go into conversation (persona, mood, etc.)
171
+ historyMessages.push({
172
+ role: "system",
173
+ content: entry.content,
174
+ });
175
+ break;
176
+ case "user":
177
+ historyMessages.push({
178
+ role: "user",
179
+ content: entry.content,
180
+ });
181
+ break;
182
+ case "assistant":
183
+ const assistantMessage = {
184
+ role: "assistant",
185
+ content: entry.content || "", // Ensure content is never null/undefined
186
+ };
187
+ if (entry.tool_calls && entry.tool_calls.length > 0) {
188
+ // For assistant messages with tool calls, collect the tool results that correspond to them
189
+ const correspondingToolResults = [];
190
+ const toolCallsWithResults = [];
191
+ entry.tool_calls.forEach(tc => {
192
+ // Find the tool_result entry for this tool_call
193
+ const toolResultEntry = history.find(h => h.type === "tool_result" && h.toolCall?.id === tc.id);
194
+ if (toolResultEntry) {
195
+ // Only include this tool_call if we have its result
196
+ toolCallsWithResults.push(tc);
197
+ correspondingToolResults.push({
198
+ role: "tool",
199
+ content: toolResultEntry.toolResult?.output || toolResultEntry.toolResult?.error || "",
200
+ tool_call_id: tc.id,
201
+ });
202
+ }
203
+ });
204
+ // Only add tool_calls if we have at least one with a result
205
+ if (toolCallsWithResults.length > 0) {
206
+ assistantMessage.tool_calls = toolCallsWithResults;
207
+ // Add assistant message
208
+ historyMessages.push(assistantMessage);
209
+ // Add corresponding tool results immediately after
210
+ historyMessages.push(...correspondingToolResults);
211
+ }
212
+ else {
213
+ // No tool results found, just add the assistant message without tool_calls
214
+ historyMessages.push(assistantMessage);
215
+ }
216
+ }
217
+ else {
218
+ historyMessages.push(assistantMessage);
219
+ }
220
+ break;
221
+ case "tool_result":
222
+ // Skip tool_result entries here - they're handled when processing assistant messages with tool_calls
223
+ break;
224
+ // Skip tool_call entries as they are included with assistant
225
+ }
226
+ }
227
+ // Insert history messages after the system message
228
+ this.messages.splice(1, 0, ...historyMessages);
229
+ // Update token count in system message
230
+ const currentTokens = this.tokenCounter.countTokens(this.messages.map(m => typeof m.content === 'string' ? m.content : '').join(''));
231
+ if (this.messages.length > 0 && this.messages[0].role === 'system' && typeof this.messages[0].content === 'string') {
232
+ this.messages[0].content = this.messages[0].content.replace(/Current conversation token usage: .*/, `Current conversation token usage: ${currentTokens}`);
233
+ }
234
+ }
235
+ async initializeMCP(debugLogFile) {
236
+ // Initialize MCP in the background without blocking
237
+ Promise.resolve().then(async () => {
238
+ try {
239
+ const config = loadMCPConfig();
240
+ if (config.servers.length > 0) {
241
+ await initializeMCPServers(debugLogFile);
242
+ }
243
+ }
244
+ catch (error) {
245
+ console.warn("MCP initialization failed:", error);
246
+ }
247
+ finally {
248
+ this.mcpInitialized = true;
249
+ }
250
+ });
251
+ }
252
+ isGrokModel() {
253
+ const currentModel = this.grokClient.getCurrentModel();
254
+ return currentModel.toLowerCase().includes("grok");
255
+ }
256
+ // Heuristic: enable web search only when likely needed
257
+ shouldUseSearchFor(message) {
258
+ const q = message.toLowerCase();
259
+ const keywords = [
260
+ "today",
261
+ "latest",
262
+ "news",
263
+ "trending",
264
+ "breaking",
265
+ "current",
266
+ "now",
267
+ "recent",
268
+ "x.com",
269
+ "twitter",
270
+ "tweet",
271
+ "what happened",
272
+ "as of",
273
+ "update on",
274
+ "release notes",
275
+ "changelog",
276
+ "price",
277
+ ];
278
+ if (keywords.some((k) => q.includes(k)))
279
+ return true;
280
+ // crude date pattern (e.g., 2024/2025) may imply recency
281
+ if (/(20\d{2})/.test(q))
282
+ return true;
283
+ return false;
284
+ }
285
+ async processUserMessage(message) {
286
+ // Before adding the new user message, check if there are incomplete tool calls
287
+ // from a previous interrupted turn. This prevents malformed message sequences
288
+ // that cause Ollama 500 errors.
289
+ const lastMessage = this.messages[this.messages.length - 1];
290
+ if (lastMessage?.role === "assistant" && lastMessage.tool_calls) {
291
+ // Find tool_call_ids that don't have corresponding tool result messages
292
+ const toolCallIds = new Set(lastMessage.tool_calls.map((tc) => tc.id));
293
+ const completedToolCallIds = new Set();
294
+ // Check which tool calls have results
295
+ for (let i = this.messages.length - 1; i >= 0; i--) {
296
+ const msg = this.messages[i];
297
+ if (msg.role === "tool" && msg.tool_call_id) {
298
+ completedToolCallIds.add(msg.tool_call_id);
299
+ }
300
+ // Stop when we hit the assistant message with tool_calls
301
+ if (this.messages[i] === lastMessage)
302
+ break;
303
+ }
304
+ // Add cancelled results for any incomplete tool calls
305
+ for (const toolCallId of toolCallIds) {
306
+ if (!completedToolCallIds.has(toolCallId)) {
307
+ console.error(`Adding cancelled result for incomplete tool call: ${toolCallId}`);
308
+ this.messages.push({
309
+ role: "tool",
310
+ content: "[Cancelled by user]",
311
+ tool_call_id: toolCallId,
312
+ });
313
+ }
314
+ }
315
+ }
316
+ // Add user message to conversation
317
+ const userEntry = {
318
+ type: "user",
319
+ content: message,
320
+ timestamp: new Date(),
321
+ };
322
+ this.chatHistory.push(userEntry);
323
+ this.messages.push({ role: "user", content: message });
324
+ await this.emitContextChange();
325
+ const newEntries = [userEntry];
326
+ const maxToolRounds = this.maxToolRounds; // Prevent infinite loops
327
+ let toolRounds = 0;
328
+ let consecutiveNonToolResponses = 0;
329
+ try {
330
+ // Always fetch tools fresh - getAllGrokTools() handles lazy refresh internally
331
+ const supportsTools = this.grokClient.getSupportsTools();
332
+ let currentResponse = await this.grokClient.chat(this.messages, supportsTools ? await getAllGrokTools() : [], undefined, this.isGrokModel() && this.shouldUseSearchFor(message)
333
+ ? { search_parameters: { mode: "auto" } }
334
+ : { search_parameters: { mode: "off" } }, this.temperature, this.abortController?.signal, this.maxTokens);
335
+ // Parse XML tool calls from response if present
336
+ if (currentResponse.choices?.[0]?.message) {
337
+ currentResponse.choices[0].message = this.parseXMLToolCalls(currentResponse.choices[0].message);
338
+ }
339
+ // Agent loop - continue until no more tool calls or max rounds reached
340
+ while (toolRounds < maxToolRounds) {
341
+ const assistantMessage = currentResponse.choices?.[0]?.message;
342
+ if (!assistantMessage) {
343
+ throw new Error("No response from Grok");
344
+ }
345
+ // Handle tool calls
346
+ if (assistantMessage.tool_calls &&
347
+ assistantMessage.tool_calls.length > 0) {
348
+ toolRounds++;
349
+ consecutiveNonToolResponses = 0; // Reset counter when AI makes tool calls
350
+ // Clean up tool call arguments before adding to conversation history
351
+ // This prevents Ollama from rejecting malformed tool calls on subsequent API calls
352
+ const cleanedToolCalls = assistantMessage.tool_calls.map(toolCall => {
353
+ let argsString = toolCall.function.arguments?.trim() || "{}";
354
+ // Handle duplicate/concatenated JSON objects (LLM bug)
355
+ if (argsString.includes('}{')) {
356
+ try {
357
+ let depth = 0;
358
+ let firstObjEnd = -1;
359
+ for (let i = 0; i < argsString.length; i++) {
360
+ if (argsString[i] === '{')
361
+ depth++;
362
+ if (argsString[i] === '}') {
363
+ depth--;
364
+ if (depth === 0) {
365
+ firstObjEnd = i + 1;
366
+ break;
367
+ }
368
+ }
369
+ }
370
+ if (firstObjEnd > 0 && firstObjEnd < argsString.length) {
371
+ const firstObj = argsString.substring(0, firstObjEnd);
372
+ JSON.parse(firstObj); // Validate
373
+ argsString = firstObj; // Use cleaned version
374
+ }
375
+ }
376
+ catch (e) {
377
+ // Keep original if cleaning fails
378
+ }
379
+ }
380
+ return {
381
+ ...toolCall,
382
+ function: {
383
+ ...toolCall.function,
384
+ arguments: argsString
385
+ }
386
+ };
387
+ });
388
+ // Add assistant message to conversation
389
+ this.messages.push({
390
+ role: "assistant",
391
+ content: assistantMessage.content || "(Calling tools to perform this request)",
392
+ tool_calls: cleanedToolCalls,
393
+ });
394
+ // Add assistant message to chat history
395
+ const assistantToolCallEntry = {
396
+ type: "assistant",
397
+ content: assistantMessage.content || "(Calling tools to perform this request)",
398
+ timestamp: new Date(),
399
+ tool_calls: assistantMessage.tool_calls,
400
+ };
401
+ this.chatHistory.push(assistantToolCallEntry);
402
+ newEntries.push(assistantToolCallEntry);
403
+ await this.emitContextChange();
404
+ // Create initial tool call entries to show tools are being executed
405
+ // Use cleanedToolCalls to preserve arguments in chatHistory
406
+ cleanedToolCalls.forEach((toolCall) => {
407
+ const toolCallEntry = {
408
+ type: "tool_call",
409
+ content: "Executing...",
410
+ timestamp: new Date(),
411
+ toolCall: toolCall,
412
+ };
413
+ this.chatHistory.push(toolCallEntry);
414
+ newEntries.push(toolCallEntry);
415
+ });
416
+ // Execute tool calls and update the entries
417
+ let toolIndex = 0;
418
+ const completedToolCallIds = new Set();
419
+ try {
420
+ for (const toolCall of cleanedToolCalls) {
421
+ // Check for cancellation before executing each tool
422
+ if (this.abortController?.signal.aborted) {
423
+ console.error(`Tool execution cancelled after ${toolIndex}/${cleanedToolCalls.length} tools`);
424
+ // Add cancelled responses for remaining uncompleted tools
425
+ for (let i = toolIndex; i < cleanedToolCalls.length; i++) {
426
+ const remainingToolCall = cleanedToolCalls[i];
427
+ this.messages.push({
428
+ role: "tool",
429
+ content: "[Cancelled by user]",
430
+ tool_call_id: remainingToolCall.id,
431
+ });
432
+ completedToolCallIds.add(remainingToolCall.id);
433
+ }
434
+ throw new Error("Operation cancelled by user");
435
+ }
436
+ const result = await this.executeTool(toolCall);
437
+ // Update the existing tool_call entry with the result
438
+ const entryIndex = this.chatHistory.findIndex((entry) => entry.type === "tool_call" && entry.toolCall?.id === toolCall.id);
439
+ if (entryIndex !== -1) {
440
+ const updatedEntry = {
441
+ ...this.chatHistory[entryIndex],
442
+ type: "tool_result",
443
+ content: result.success
444
+ ? result.output || "Success"
445
+ : result.error || "Error occurred",
446
+ toolResult: result,
447
+ };
448
+ this.chatHistory[entryIndex] = updatedEntry;
449
+ // Also update in newEntries for return value
450
+ const newEntryIndex = newEntries.findIndex((entry) => entry.type === "tool_call" &&
451
+ entry.toolCall?.id === toolCall.id);
452
+ if (newEntryIndex !== -1) {
453
+ newEntries[newEntryIndex] = updatedEntry;
454
+ }
455
+ }
456
+ // Add tool result to messages with proper format (needed for AI context)
457
+ this.messages.push({
458
+ role: "tool",
459
+ content: result.success
460
+ ? result.output || "Success"
461
+ : result.error || "Error",
462
+ tool_call_id: toolCall.id,
463
+ });
464
+ completedToolCallIds.add(toolCall.id);
465
+ await this.emitContextChange();
466
+ toolIndex++;
467
+ }
468
+ }
469
+ finally {
470
+ // Ensure ALL tool calls in this.messages have results, even if we crashed/errored
471
+ for (const toolCall of cleanedToolCalls) {
472
+ if (!completedToolCallIds.has(toolCall.id)) {
473
+ this.messages.push({
474
+ role: "tool",
475
+ content: "[Error: Tool execution interrupted]",
476
+ tool_call_id: toolCall.id,
477
+ });
478
+ }
479
+ }
480
+ }
481
+ // After all tool results are added, add any system messages from this tool round
482
+ // System messages are added to chatHistory during tool execution (for display)
483
+ // Now we add them to this.messages in the same order (after all tool results)
484
+ // Find the most recent assistant message with tool_calls in chatHistory (search backwards)
485
+ let assistantIndex = -1;
486
+ for (let i = this.chatHistory.length - 1; i >= 0; i--) {
487
+ const entry = this.chatHistory[i];
488
+ if (entry.type === "assistant" && entry.tool_calls && entry.tool_calls.length > 0) {
489
+ assistantIndex = i;
490
+ break;
491
+ }
492
+ }
493
+ if (assistantIndex !== -1) {
494
+ // Collect system messages that appeared after this assistant message
495
+ for (let i = assistantIndex + 1; i < this.chatHistory.length; i++) {
496
+ const entry = this.chatHistory[i];
497
+ if (entry.type === 'system' && entry.content && entry.content.trim()) {
498
+ this.messages.push({
499
+ role: 'system',
500
+ content: entry.content
501
+ });
502
+ }
503
+ // Stop if we hit another assistant or user message (next turn)
504
+ if (entry.type === 'assistant' || entry.type === 'user') {
505
+ break;
506
+ }
507
+ }
508
+ }
509
+ // Get next response - this might contain more tool calls
510
+ // Debug logging to diagnose tool_call/tool_result mismatch
511
+ const debugLogPath = ChatHistoryManager.getDebugLogPath();
512
+ const timestamp = new Date().toISOString();
513
+ fs.appendFileSync(debugLogPath, `\n${timestamp} - [DEBUG] Messages before API call (${this.messages.length} messages):\n`);
514
+ this.messages.forEach((msg, idx) => {
515
+ const msgSummary = { idx, role: msg.role };
516
+ if (msg.tool_calls)
517
+ msgSummary.tool_calls = msg.tool_calls.map((tc) => tc.id);
518
+ if (msg.tool_call_id)
519
+ msgSummary.tool_call_id = msg.tool_call_id;
520
+ fs.appendFileSync(debugLogPath, ` ${JSON.stringify(msgSummary)}\n`);
521
+ });
522
+ currentResponse = await this.grokClient.chat(this.messages, supportsTools ? await getAllGrokTools() : [], undefined, this.isGrokModel() && this.shouldUseSearchFor(message)
523
+ ? { search_parameters: { mode: "auto" } }
524
+ : { search_parameters: { mode: "off" } }, this.temperature, this.abortController?.signal, this.maxTokens);
525
+ }
526
+ else {
527
+ // No tool calls in this response - only add it if there's actual content
528
+ const trimmedContent = assistantMessage.content?.trim();
529
+ if (trimmedContent) {
530
+ const responseEntry = {
531
+ type: "assistant",
532
+ content: trimmedContent,
533
+ timestamp: new Date(),
534
+ };
535
+ this.chatHistory.push(responseEntry);
536
+ this.messages.push({
537
+ role: "assistant",
538
+ content: trimmedContent,
539
+ });
540
+ newEntries.push(responseEntry);
541
+ }
542
+ // TODO: HACK - This is a temporary fix to prevent duplicate responses.
543
+ // We need a proper way for the bot to signal task completion, such as:
544
+ // - A special tool call like "taskComplete()"
545
+ // - A finish_reason indicator in the API response
546
+ // - A structured response format that explicitly marks completion
547
+ // For now, we break immediately after a substantial response to avoid
548
+ // the cascade of duplicate responses caused by "give it one more chance" logic.
549
+ // If the AI provided a substantial response (>50 chars), task is complete
550
+ if (assistantMessage.content && assistantMessage.content.trim().length > 50) {
551
+ break; // Task complete - bot gave a full response
552
+ }
553
+ // Short/empty response, give AI another chance
554
+ currentResponse = await this.grokClient.chat(this.messages, supportsTools ? await getAllGrokTools() : [], undefined, this.isGrokModel() && this.shouldUseSearchFor(message)
555
+ ? { search_parameters: { mode: "auto" } }
556
+ : { search_parameters: { mode: "off" } }, this.temperature, this.abortController?.signal, this.maxTokens);
557
+ // Parse XML tool calls from followup response if present
558
+ if (currentResponse.choices?.[0]?.message) {
559
+ currentResponse.choices[0].message = this.parseXMLToolCalls(currentResponse.choices[0].message);
560
+ }
561
+ const followupMessage = currentResponse.choices?.[0]?.message;
562
+ if (!followupMessage?.tool_calls || followupMessage.tool_calls.length === 0) {
563
+ break; // AI doesn't want to continue
564
+ }
565
+ }
566
+ }
567
+ if (toolRounds >= maxToolRounds) {
568
+ const warningEntry = {
569
+ type: "assistant",
570
+ content: "Maximum tool execution rounds reached. Stopping to prevent infinite loops.",
571
+ timestamp: new Date(),
572
+ };
573
+ this.chatHistory.push(warningEntry);
574
+ newEntries.push(warningEntry);
575
+ }
576
+ // Mark first message as processed so subsequent messages use cached tools
577
+ this.firstMessageProcessed = true;
578
+ // Check if tool support changed during first message processing
579
+ // If model doesn't support tools, regenerate system message without tool list
580
+ const supportsToolsAfter = this.grokClient.getSupportsTools();
581
+ if (!supportsToolsAfter && supportsTools) {
582
+ // Tool support was disabled during first message - regenerate system message
583
+ await this.buildSystemMessage();
584
+ }
585
+ return newEntries;
586
+ }
587
+ catch (error) {
588
+ const errorEntry = {
589
+ type: "assistant",
590
+ content: `Sorry, I encountered an error: ${error.message}`,
591
+ timestamp: new Date(),
592
+ };
593
+ this.chatHistory.push(errorEntry);
594
+ // Mark first message as processed even on error
595
+ this.firstMessageProcessed = true;
596
+ return [userEntry, errorEntry];
597
+ }
598
+ }
599
+ /**
600
+ * Parse XML-formatted tool calls from message content (x.ai format)
601
+ * Converts <xai:function_call> elements to standard GrokToolCall format
602
+ */
603
+ parseXMLToolCalls(message) {
604
+ if (!message.content || typeof message.content !== 'string') {
605
+ return message;
606
+ }
607
+ const content = message.content;
608
+ const xmlToolCallRegex = /<xai:function_call\s+name="([^"]+)">([\s\S]*?)<\/xai:function_call>/g;
609
+ const matches = Array.from(content.matchAll(xmlToolCallRegex));
610
+ if (matches.length === 0) {
611
+ return message;
612
+ }
613
+ // Parse each XML tool call
614
+ const toolCalls = [];
615
+ let cleanedContent = content;
616
+ for (const match of matches) {
617
+ const functionName = match[1];
618
+ const paramsXML = match[2];
619
+ // Parse parameters
620
+ const paramRegex = /<parameter\s+name="([^"]+)">([^<]*)<\/parameter>/g;
621
+ const paramMatches = Array.from(paramsXML.matchAll(paramRegex));
622
+ const args = {};
623
+ for (const paramMatch of paramMatches) {
624
+ args[paramMatch[1]] = paramMatch[2];
625
+ }
626
+ // Generate a unique ID for this tool call
627
+ const toolCallId = `call_xml_${Date.now()}_${Math.random().toString(36).substring(7)}`;
628
+ toolCalls.push({
629
+ id: toolCallId,
630
+ type: "function",
631
+ function: {
632
+ name: functionName,
633
+ arguments: JSON.stringify(args)
634
+ }
635
+ });
636
+ // Remove this XML block from content
637
+ cleanedContent = cleanedContent.replace(match[0], '');
638
+ }
639
+ // Trim any extra whitespace
640
+ cleanedContent = cleanedContent.trim();
641
+ // Return modified message with tool_calls and cleaned content
642
+ return {
643
+ ...message,
644
+ content: cleanedContent || null,
645
+ tool_calls: [...(message.tool_calls || []), ...toolCalls]
646
+ };
647
+ }
648
+ messageReducer(previous, item) {
649
+ const reduce = (acc, delta) => {
650
+ // Ensure acc is always an object before spreading (handles null/undefined)
651
+ acc = { ...(acc || {}) };
652
+ for (const [key, value] of Object.entries(delta)) {
653
+ // Skip null values in delta (Venice sends tool_calls: null which breaks Object.entries)
654
+ if (value === null)
655
+ continue;
656
+ if (acc[key] === undefined || acc[key] === null) {
657
+ acc[key] = value;
658
+ // Clean up index properties from tool calls
659
+ if (Array.isArray(acc[key])) {
660
+ for (const arr of acc[key]) {
661
+ delete arr.index;
662
+ }
663
+ }
664
+ }
665
+ else if (typeof acc[key] === "string" && typeof value === "string") {
666
+ // Don't concatenate certain properties that should remain separate
667
+ const nonConcatenableProps = ['id', 'type', 'name'];
668
+ if (nonConcatenableProps.includes(key)) {
669
+ // For non-concatenable properties, keep the new value
670
+ acc[key] = value;
671
+ }
672
+ else {
673
+ // For content, arguments, and other text properties, concatenate
674
+ acc[key] += value;
675
+ }
676
+ }
677
+ else if (Array.isArray(acc[key]) && Array.isArray(value)) {
678
+ const accArray = acc[key];
679
+ for (let i = 0; i < value.length; i++) {
680
+ if (!accArray[i])
681
+ accArray[i] = {};
682
+ accArray[i] = reduce(accArray[i], value[i]);
683
+ }
684
+ }
685
+ else if (typeof acc[key] === "object" && typeof value === "object") {
686
+ acc[key] = reduce(acc[key], value);
687
+ }
688
+ }
689
+ return acc;
690
+ };
691
+ return reduce(previous, item.choices?.[0]?.delta || {});
692
+ }
693
+ async *processUserMessageStream(message) {
694
+ // Create new abort controller for this request
695
+ this.abortController = new AbortController();
696
+ // Before adding the new user message, check if there are incomplete tool calls
697
+ // from a previous interrupted turn. This prevents malformed message sequences
698
+ // that cause Ollama 500 errors.
699
+ const lastMessage = this.messages[this.messages.length - 1];
700
+ if (lastMessage?.role === "assistant" && lastMessage.tool_calls) {
701
+ // Find tool_call_ids that don't have corresponding tool result messages
702
+ const toolCallIds = new Set(lastMessage.tool_calls.map((tc) => tc.id));
703
+ const completedToolCallIds = new Set();
704
+ // Check which tool calls have results
705
+ for (let i = this.messages.length - 1; i >= 0; i--) {
706
+ const msg = this.messages[i];
707
+ if (msg.role === "tool" && msg.tool_call_id) {
708
+ completedToolCallIds.add(msg.tool_call_id);
709
+ }
710
+ // Stop when we hit the assistant message with tool_calls
711
+ if (this.messages[i] === lastMessage)
712
+ break;
713
+ }
714
+ // Add cancelled results for any incomplete tool calls
715
+ for (const toolCallId of toolCallIds) {
716
+ if (!completedToolCallIds.has(toolCallId)) {
717
+ console.error(`Adding cancelled result for incomplete tool call: ${toolCallId}`);
718
+ this.messages.push({
719
+ role: "tool",
720
+ content: "[Cancelled by user]",
721
+ tool_call_id: toolCallId,
722
+ });
723
+ }
724
+ }
725
+ }
726
+ // Add user message to both API conversation and chat history
727
+ const userEntry = {
728
+ type: "user",
729
+ content: message,
730
+ timestamp: new Date(),
731
+ };
732
+ this.chatHistory.push(userEntry);
733
+ this.messages.push({ role: "user", content: message });
734
+ await this.emitContextChange();
735
+ // Yield user message so UI can display it immediately
736
+ yield {
737
+ type: "user_message",
738
+ userEntry: userEntry,
739
+ };
740
+ // Calculate input tokens
741
+ let inputTokens = this.tokenCounter.countMessageTokens(this.messages);
742
+ yield {
743
+ type: "token_count",
744
+ tokenCount: inputTokens,
745
+ };
746
+ const maxToolRounds = this.maxToolRounds; // Prevent infinite loops
747
+ let toolRounds = 0;
748
+ let totalOutputTokens = 0;
749
+ let lastTokenUpdate = 0;
750
+ let consecutiveNonToolResponses = 0;
751
+ try {
752
+ // Always fetch tools fresh - getAllGrokTools() handles lazy refresh internally
753
+ const supportsTools = this.grokClient.getSupportsTools();
754
+ // Agent loop - continue until no more tool calls or max rounds reached
755
+ while (toolRounds < maxToolRounds) {
756
+ // Check if operation was cancelled
757
+ if (this.abortController?.signal.aborted) {
758
+ yield {
759
+ type: "content",
760
+ content: "\n\n[Operation cancelled by user]",
761
+ };
762
+ yield { type: "done" };
763
+ return;
764
+ }
765
+ // Update system message with current token count
766
+ if (this.messages.length > 0 && this.messages[0].role === 'system' && typeof this.messages[0].content === 'string') {
767
+ this.messages[0].content = this.messages[0].content.replace(/Current conversation token usage: .*/, `Current conversation token usage: ${inputTokens}`);
768
+ }
769
+ // Stream response and accumulate
770
+ const stream = this.grokClient.chatStream(this.messages, supportsTools ? await getAllGrokTools() : [], undefined, this.isGrokModel() && this.shouldUseSearchFor(message)
771
+ ? { search_parameters: { mode: "auto" } }
772
+ : { search_parameters: { mode: "off" } }, this.temperature, this.abortController?.signal, this.maxTokens);
773
+ let accumulatedMessage = {};
774
+ let accumulatedContent = "";
775
+ let tool_calls_yielded = false;
776
+ let streamFinished = false;
777
+ let insideThinkTag = false;
778
+ try {
779
+ for await (const chunk of stream) {
780
+ // Check for cancellation in the streaming loop
781
+ if (this.abortController?.signal.aborted) {
782
+ yield {
783
+ type: "content",
784
+ content: "\n\n[Operation cancelled by user]",
785
+ };
786
+ yield { type: "done" };
787
+ return;
788
+ }
789
+ if (!chunk.choices?.[0])
790
+ continue;
791
+ // Check if stream is finished (Venice sends garbage after this)
792
+ if (chunk.choices?.[0]?.finish_reason === "stop" || chunk.choices?.[0]?.finish_reason === "tool_calls") {
793
+ streamFinished = true;
794
+ }
795
+ // Accumulate the message using reducer
796
+ accumulatedMessage = this.messageReducer(accumulatedMessage, chunk);
797
+ // Check for tool calls - yield when we have complete tool calls with function names
798
+ if (!tool_calls_yielded && accumulatedMessage.tool_calls?.length > 0) {
799
+ // Check if we have at least one complete tool call with a function name
800
+ const hasCompleteTool = accumulatedMessage.tool_calls.some((tc) => tc.function?.name);
801
+ if (hasCompleteTool) {
802
+ yield {
803
+ type: "tool_calls",
804
+ tool_calls: accumulatedMessage.tool_calls,
805
+ };
806
+ tool_calls_yielded = true;
807
+ }
808
+ }
809
+ // Stream content as it comes (but ignore content after stream is finished to avoid Venice garbage)
810
+ if (chunk.choices[0].delta?.content && !streamFinished) {
811
+ let deltaContent = chunk.choices[0].delta.content;
812
+ // Handle thinking tags that may span multiple chunks
813
+ // First, remove complete <think>...</think> blocks within this chunk
814
+ deltaContent = deltaContent.replace(/<think>[\s\S]*?<\/think>/g, '');
815
+ // Check for opening <think> tag
816
+ if (deltaContent.includes('<think>')) {
817
+ insideThinkTag = true;
818
+ // Remove everything from <think> onwards in this chunk
819
+ deltaContent = deltaContent.substring(0, deltaContent.indexOf('<think>'));
820
+ }
821
+ // If we're inside a think tag, remove everything up to and including </think>
822
+ if (insideThinkTag) {
823
+ if (deltaContent.includes('</think>')) {
824
+ // Found closing tag - remove everything up to and including it
825
+ const closeIndex = deltaContent.indexOf('</think>');
826
+ deltaContent = deltaContent.substring(closeIndex + 8); // 8 = length of '</think>'
827
+ insideThinkTag = false;
828
+ }
829
+ else {
830
+ // Still inside think block - remove entire chunk
831
+ deltaContent = '';
832
+ }
833
+ }
834
+ // Skip completely empty chunks after filtering (but keep spaces!)
835
+ if (deltaContent === '')
836
+ continue;
837
+ accumulatedContent += deltaContent;
838
+ // Update token count in real-time including accumulated content and any tool calls
839
+ const currentOutputTokens = this.tokenCounter.estimateStreamingTokens(accumulatedContent) +
840
+ (accumulatedMessage.tool_calls
841
+ ? this.tokenCounter.countTokens(JSON.stringify(accumulatedMessage.tool_calls))
842
+ : 0);
843
+ totalOutputTokens = currentOutputTokens;
844
+ yield {
845
+ type: "content",
846
+ content: deltaContent,
847
+ };
848
+ // Emit token count update
849
+ const now = Date.now();
850
+ if (now - lastTokenUpdate > 250) {
851
+ lastTokenUpdate = now;
852
+ yield {
853
+ type: "token_count",
854
+ tokenCount: inputTokens + totalOutputTokens,
855
+ };
856
+ }
857
+ }
858
+ }
859
+ }
860
+ catch (streamError) {
861
+ // Check if stream was aborted
862
+ if (this.abortController?.signal.aborted || streamError.name === 'AbortError' || streamError.code === 'ABORT_ERR') {
863
+ yield {
864
+ type: "content",
865
+ content: "\n\n[Operation cancelled by user]",
866
+ };
867
+ yield { type: "done" };
868
+ return;
869
+ }
870
+ // Re-throw other errors to be caught by outer catch
871
+ throw streamError;
872
+ }
873
+ // Parse XML tool calls from accumulated message if present
874
+ accumulatedMessage = this.parseXMLToolCalls(accumulatedMessage);
875
+ // Clean up tool call arguments before adding to conversation history
876
+ // This prevents Ollama from rejecting malformed tool calls on subsequent API calls
877
+ const cleanedToolCalls = accumulatedMessage.tool_calls?.map(toolCall => {
878
+ let argsString = toolCall.function.arguments?.trim() || "{}";
879
+ // Handle duplicate/concatenated JSON objects (LLM bug)
880
+ if (argsString.includes('}{')) {
881
+ try {
882
+ let depth = 0;
883
+ let firstObjEnd = -1;
884
+ for (let i = 0; i < argsString.length; i++) {
885
+ if (argsString[i] === '{')
886
+ depth++;
887
+ if (argsString[i] === '}') {
888
+ depth--;
889
+ if (depth === 0) {
890
+ firstObjEnd = i + 1;
891
+ break;
892
+ }
893
+ }
894
+ }
895
+ if (firstObjEnd > 0 && firstObjEnd < argsString.length) {
896
+ const firstObj = argsString.substring(0, firstObjEnd);
897
+ JSON.parse(firstObj); // Validate
898
+ argsString = firstObj; // Use cleaned version
899
+ }
900
+ }
901
+ catch (e) {
902
+ // Keep original if cleaning fails
903
+ }
904
+ }
905
+ return {
906
+ ...toolCall,
907
+ function: {
908
+ ...toolCall.function,
909
+ arguments: argsString
910
+ }
911
+ };
912
+ });
913
+ // Add accumulated message to conversation for API context
914
+ this.messages.push({
915
+ role: "assistant",
916
+ content: accumulatedMessage.content || "(Calling tools to perform this request)",
917
+ tool_calls: cleanedToolCalls,
918
+ });
919
+ // Add assistant message to chat history
920
+ const assistantEntry = {
921
+ type: "assistant",
922
+ content: accumulatedMessage.content || "(Calling tools to perform this request)",
923
+ timestamp: new Date(),
924
+ tool_calls: accumulatedMessage.tool_calls,
925
+ };
926
+ this.chatHistory.push(assistantEntry);
927
+ await this.emitContextChange();
928
+ // Handle tool calls if present
929
+ if (accumulatedMessage.tool_calls?.length > 0) {
930
+ toolRounds++;
931
+ // Only yield tool_calls if we haven't already yielded them during streaming
932
+ if (!tool_calls_yielded) {
933
+ yield {
934
+ type: "tool_calls",
935
+ tool_calls: accumulatedMessage.tool_calls,
936
+ };
937
+ }
938
+ // Add tool_call entries to chatHistory so they persist through UI sync
939
+ // Use cleanedToolCalls to preserve arguments in chatHistory
940
+ cleanedToolCalls.forEach((toolCall) => {
941
+ const toolCallEntry = {
942
+ type: "tool_call",
943
+ content: "Executing...",
944
+ timestamp: new Date(),
945
+ toolCall: toolCall,
946
+ };
947
+ this.chatHistory.push(toolCallEntry);
948
+ });
949
+ // Execute tools
950
+ let toolIndex = 0;
951
+ const completedToolCallIds = new Set();
952
+ try {
953
+ for (const toolCall of cleanedToolCalls) {
954
+ // Check for cancellation before executing each tool
955
+ if (this.abortController?.signal.aborted) {
956
+ console.error(`Tool execution cancelled after ${toolIndex}/${cleanedToolCalls.length} tools`);
957
+ // Add cancelled responses for remaining uncompleted tools
958
+ for (let i = toolIndex; i < cleanedToolCalls.length; i++) {
959
+ const remainingToolCall = cleanedToolCalls[i];
960
+ this.messages.push({
961
+ role: "tool",
962
+ content: "[Cancelled by user]",
963
+ tool_call_id: remainingToolCall.id,
964
+ });
965
+ completedToolCallIds.add(remainingToolCall.id);
966
+ }
967
+ yield {
968
+ type: "content",
969
+ content: "\n\n[Operation cancelled by user]",
970
+ };
971
+ yield { type: "done" };
972
+ return;
973
+ }
974
+ // Capture chatHistory length before tool execution to detect new system messages
975
+ const chatHistoryLengthBefore = this.chatHistory.length;
976
+ const result = await this.executeTool(toolCall);
977
+ // Collect any new system messages added during tool execution (from hooks)
978
+ const newSystemMessages = [];
979
+ for (let i = chatHistoryLengthBefore; i < this.chatHistory.length; i++) {
980
+ if (this.chatHistory[i].type === "system") {
981
+ newSystemMessages.push(this.chatHistory[i]);
982
+ }
983
+ }
984
+ yield {
985
+ type: "tool_result",
986
+ toolCall,
987
+ toolResult: result,
988
+ systemMessages: newSystemMessages.length > 0 ? newSystemMessages : undefined,
989
+ };
990
+ // Update the tool_call entry in chatHistory to tool_result
991
+ const entryIndex = this.chatHistory.findIndex((entry) => entry.type === "tool_call" && entry.toolCall?.id === toolCall.id);
992
+ if (entryIndex !== -1) {
993
+ this.chatHistory[entryIndex] = {
994
+ ...this.chatHistory[entryIndex],
995
+ type: "tool_result",
996
+ content: result.success
997
+ ? (result.output?.trim() || "Success")
998
+ : (result.error?.trim() || "Error occurred"),
999
+ toolResult: result,
1000
+ };
1001
+ }
1002
+ // Add tool result with proper format (needed for AI context)
1003
+ this.messages.push({
1004
+ role: "tool",
1005
+ content: result.success
1006
+ ? result.output || "Success"
1007
+ : result.error || "Error",
1008
+ tool_call_id: toolCall.id,
1009
+ });
1010
+ completedToolCallIds.add(toolCall.id);
1011
+ toolIndex++;
1012
+ }
1013
+ }
1014
+ finally {
1015
+ // Ensure ALL tool calls in this.messages have results, even if we crashed/errored
1016
+ for (const toolCall of cleanedToolCalls) {
1017
+ if (!completedToolCallIds.has(toolCall.id)) {
1018
+ this.messages.push({
1019
+ role: "tool",
1020
+ content: "[Error: Tool execution interrupted]",
1021
+ tool_call_id: toolCall.id,
1022
+ });
1023
+ }
1024
+ }
1025
+ }
1026
+ // After all tool results are added, add any system messages from this tool round
1027
+ // System messages are added to chatHistory during tool execution (for display)
1028
+ // Now we add them to this.messages in the same order (after all tool results)
1029
+ // Find the most recent assistant message with tool_calls in chatHistory (search backwards)
1030
+ let assistantIndex = -1;
1031
+ for (let i = this.chatHistory.length - 1; i >= 0; i--) {
1032
+ const entry = this.chatHistory[i];
1033
+ if (entry.type === "assistant" && entry.tool_calls && entry.tool_calls.length > 0) {
1034
+ assistantIndex = i;
1035
+ break;
1036
+ }
1037
+ }
1038
+ if (assistantIndex !== -1) {
1039
+ // Collect system messages that appeared after this assistant message
1040
+ for (let i = assistantIndex + 1; i < this.chatHistory.length; i++) {
1041
+ const entry = this.chatHistory[i];
1042
+ if (entry.type === 'system' && entry.content && entry.content.trim()) {
1043
+ this.messages.push({
1044
+ role: 'system',
1045
+ content: entry.content
1046
+ });
1047
+ }
1048
+ // Stop if we hit another assistant or user message (next turn)
1049
+ if (entry.type === 'assistant' || entry.type === 'user') {
1050
+ break;
1051
+ }
1052
+ }
1053
+ }
1054
+ // Update token count after processing all tool calls to include tool results
1055
+ inputTokens = this.tokenCounter.countMessageTokens(this.messages);
1056
+ // Final token update after tools processed
1057
+ yield {
1058
+ type: "token_count",
1059
+ tokenCount: inputTokens + totalOutputTokens,
1060
+ };
1061
+ // Continue the loop to get the next response (which might have more tool calls)
1062
+ }
1063
+ else {
1064
+ // No tool calls, we're done
1065
+ break;
1066
+ }
1067
+ }
1068
+ if (toolRounds >= maxToolRounds) {
1069
+ yield {
1070
+ type: "content",
1071
+ content: "\n\nMaximum tool execution rounds reached. Stopping to prevent infinite loops.",
1072
+ };
1073
+ }
1074
+ // Mark first message as processed so subsequent messages use cached tools
1075
+ this.firstMessageProcessed = true;
1076
+ // Check if tool support changed during first message processing
1077
+ // If model doesn't support tools, regenerate system message without tool list
1078
+ const supportsToolsAfter = this.grokClient.getSupportsTools();
1079
+ if (!supportsToolsAfter && supportsTools) {
1080
+ // Tool support was disabled during first message - regenerate system message
1081
+ await this.buildSystemMessage();
1082
+ }
1083
+ yield { type: "done" };
1084
+ }
1085
+ catch (error) {
1086
+ // Check if this was a cancellation (check both abort signal and error name)
1087
+ if (this.abortController?.signal.aborted || error.name === 'AbortError' || error.code === 'ABORT_ERR') {
1088
+ yield {
1089
+ type: "content",
1090
+ content: "\n\n[Operation cancelled by user]",
1091
+ };
1092
+ yield { type: "done" };
1093
+ return;
1094
+ }
1095
+ const errorEntry = {
1096
+ type: "assistant",
1097
+ content: `Sorry, I encountered an error: ${error.message}`,
1098
+ timestamp: new Date(),
1099
+ };
1100
+ this.chatHistory.push(errorEntry);
1101
+ yield {
1102
+ type: "content",
1103
+ content: errorEntry.content,
1104
+ };
1105
+ // Mark first message as processed even on error
1106
+ this.firstMessageProcessed = true;
1107
+ yield { type: "done" };
1108
+ }
1109
+ finally {
1110
+ // Clean up abort controller
1111
+ this.abortController = null;
1112
+ }
1113
+ }
1114
+ /**
1115
+ * Apply default parameter values for tools
1116
+ * This ensures the approval hook sees the same parameters that will be used during execution
1117
+ */
1118
+ applyToolParameterDefaults(toolName, params) {
1119
+ // Handle null/undefined params (can happen if API sends "null" as arguments string)
1120
+ const result = { ...(params || {}) };
1121
+ switch (toolName) {
1122
+ case "listFiles":
1123
+ // dirname defaults to current directory
1124
+ if (!result.dirname) {
1125
+ result.dirname = ".";
1126
+ }
1127
+ break;
1128
+ // Add other tools with defaults here as needed
1129
+ }
1130
+ return result;
1131
+ }
1132
+ /**
1133
+ * Validate tool arguments against the tool's schema
1134
+ * Returns null if valid, or an error message if invalid
1135
+ */
1136
+ async validateToolArguments(toolName, args) {
1137
+ try {
1138
+ // Get all tools (including MCP tools)
1139
+ const supportsTools = this.grokClient.getSupportsTools();
1140
+ const allTools = supportsTools ? await getAllGrokTools() : [];
1141
+ // Find the tool schema
1142
+ const toolSchema = allTools.find(t => t.function.name === toolName);
1143
+ if (!toolSchema) {
1144
+ return `Unknown tool: ${toolName}`;
1145
+ }
1146
+ const schema = toolSchema.function.parameters;
1147
+ const properties = schema.properties || {};
1148
+ const required = schema.required || [];
1149
+ // Check if tool accepts no parameters
1150
+ const acceptsNoParams = Object.keys(properties).length === 0;
1151
+ const hasArgs = args && typeof args === 'object' && Object.keys(args).length > 0;
1152
+ if (acceptsNoParams && hasArgs) {
1153
+ return `Tool ${toolName} accepts no parameters, but received: ${JSON.stringify(args)}`;
1154
+ }
1155
+ // Check for unknown parameters
1156
+ for (const argKey of Object.keys(args || {})) {
1157
+ if (!properties[argKey]) {
1158
+ return `Tool ${toolName} does not accept parameter '${argKey}'. Valid parameters: ${Object.keys(properties).join(', ') || 'none'}`;
1159
+ }
1160
+ }
1161
+ // Check for missing required parameters
1162
+ for (const requiredParam of required) {
1163
+ if (!(requiredParam in (args || {}))) {
1164
+ return `Tool ${toolName} missing required parameter '${requiredParam}'`;
1165
+ }
1166
+ }
1167
+ return null; // Valid
1168
+ }
1169
+ catch (error) {
1170
+ console.error(`Error validating tool arguments for ${toolName}:`, error);
1171
+ return null; // Allow execution if validation itself fails
1172
+ }
1173
+ }
1174
+ async executeTool(toolCall) {
1175
+ try {
1176
+ // Parse arguments - handle empty string as empty object for parameter-less tools
1177
+ let argsString = toolCall.function.arguments?.trim() || "{}";
1178
+ // Handle duplicate/concatenated JSON objects (LLM bug)
1179
+ // Pattern: {"key":"val"}{"key":"val"}
1180
+ let hadDuplicateJson = false;
1181
+ if (argsString.includes('}{')) {
1182
+ try {
1183
+ // Find the end of the first complete JSON object
1184
+ let depth = 0;
1185
+ let firstObjEnd = -1;
1186
+ for (let i = 0; i < argsString.length; i++) {
1187
+ if (argsString[i] === '{')
1188
+ depth++;
1189
+ if (argsString[i] === '}') {
1190
+ depth--;
1191
+ if (depth === 0) {
1192
+ firstObjEnd = i + 1;
1193
+ break;
1194
+ }
1195
+ }
1196
+ }
1197
+ if (firstObjEnd > 0 && firstObjEnd < argsString.length) {
1198
+ // Extract and validate first object
1199
+ const firstObj = argsString.substring(0, firstObjEnd);
1200
+ JSON.parse(firstObj); // Validate it's valid JSON
1201
+ // Use only the first object
1202
+ hadDuplicateJson = true;
1203
+ argsString = firstObj;
1204
+ }
1205
+ }
1206
+ catch (e) {
1207
+ // If extraction fails, continue with original string
1208
+ // The error will be caught by the main JSON.parse below
1209
+ }
1210
+ }
1211
+ let args = JSON.parse(argsString);
1212
+ // Handle multiple layers of JSON encoding (API bug)
1213
+ // Keep parsing until we get an object, not a string
1214
+ let parseCount = 0;
1215
+ while (typeof args === 'string' && parseCount < 5) {
1216
+ parseCount++;
1217
+ try {
1218
+ args = JSON.parse(args);
1219
+ }
1220
+ catch (e) {
1221
+ // If parse fails, the string isn't valid JSON - stop trying
1222
+ break;
1223
+ }
1224
+ }
1225
+ // Log if we had to fix encoding
1226
+ if (parseCount > 0) {
1227
+ const bugMsg = `[BUG] Tool ${toolCall.function.name} had ${parseCount} extra layer(s) of JSON encoding`;
1228
+ console.warn(bugMsg);
1229
+ const systemMsg = `Warning: Tool arguments for ${toolCall.function.name} had ${parseCount} extra encoding layer(s) - this is an API bug`;
1230
+ this.messages.push({
1231
+ role: 'system',
1232
+ content: systemMsg
1233
+ });
1234
+ this.chatHistory.push({
1235
+ type: 'system',
1236
+ content: systemMsg,
1237
+ timestamp: new Date()
1238
+ });
1239
+ }
1240
+ // Log if we had to fix duplicate JSON
1241
+ if (hadDuplicateJson) {
1242
+ const bugMsg = `[BUG] Tool ${toolCall.function.name} had duplicate/concatenated JSON objects`;
1243
+ console.warn(bugMsg);
1244
+ const systemMsg = `Warning: Tool arguments for ${toolCall.function.name} had duplicate JSON objects (used first object only) - this is an LLM bug`;
1245
+ this.messages.push({
1246
+ role: 'system',
1247
+ content: systemMsg
1248
+ });
1249
+ this.chatHistory.push({
1250
+ type: 'system',
1251
+ content: systemMsg,
1252
+ timestamp: new Date()
1253
+ });
1254
+ }
1255
+ // Ensure args is always an object (API might send null)
1256
+ if (!args || typeof args !== 'object' || Array.isArray(args)) {
1257
+ args = {};
1258
+ }
1259
+ // Apply parameter defaults before validation and execution
1260
+ args = this.applyToolParameterDefaults(toolCall.function.name, args);
1261
+ // Validate tool arguments against schema
1262
+ const validationError = await this.validateToolArguments(toolCall.function.name, args);
1263
+ if (validationError) {
1264
+ // Add system message explaining the validation error
1265
+ const systemMsg = `Tool call validation failed: ${validationError}. Please try again with correct parameters.`;
1266
+ console.warn(`[VALIDATION ERROR] ${systemMsg}`);
1267
+ this.messages.push({
1268
+ role: 'system',
1269
+ content: systemMsg
1270
+ });
1271
+ this.chatHistory.push({
1272
+ type: 'system',
1273
+ content: systemMsg,
1274
+ timestamp: new Date()
1275
+ });
1276
+ return {
1277
+ success: false,
1278
+ error: validationError
1279
+ };
1280
+ }
1281
+ // Task tools (startActiveTask, transitionActiveTaskStatus, stopActiveTask) have their own
1282
+ // dedicated task approval hook, so skip the general tool approval hook for them
1283
+ const isTaskTool = ['startActiveTask', 'transitionActiveTaskStatus', 'stopActiveTask'].includes(toolCall.function.name);
1284
+ // Check tool approval hook if configured (skip for task tools)
1285
+ const settings = getSettingsManager();
1286
+ const toolApprovalHook = settings.getToolApprovalHook();
1287
+ if (toolApprovalHook && !isTaskTool) {
1288
+ const approvalResult = await executeToolApprovalHook(toolApprovalHook, toolCall.function.name, args, 30000, // 30 second timeout
1289
+ this.getCurrentTokenCount(), this.getMaxContextSize());
1290
+ if (!approvalResult.approved) {
1291
+ const reason = approvalResult.reason || "Tool execution denied by approval hook";
1292
+ // Process rejection commands (MODEL, SYSTEM, BACKEND, etc.)
1293
+ await this.processHookResult(approvalResult);
1294
+ return {
1295
+ success: false,
1296
+ error: `Tool execution blocked: ${reason}`,
1297
+ };
1298
+ }
1299
+ if (approvalResult.timedOut) {
1300
+ // Log timeout for debugging (don't block)
1301
+ console.warn(`Tool approval hook timed out for ${toolCall.function.name} (auto-approved)`);
1302
+ }
1303
+ // Process hook commands (ENV, TOOL_RESULT, MODEL, SYSTEM, BACKEND, etc.)
1304
+ // TOOL_RESULT is for tool return values, not used by approval hook
1305
+ // ENV variables can affect tool behavior if tools read from process.env
1306
+ await this.processHookResult(approvalResult);
1307
+ }
1308
+ switch (toolCall.function.name) {
1309
+ case "viewFile":
1310
+ {
1311
+ let range;
1312
+ range = args.start_line && args.end_line
1313
+ ? [args.start_line, args.end_line]
1314
+ : undefined;
1315
+ return await this.textEditor.viewFile(args.filename, range);
1316
+ }
1317
+ case "createNewFile":
1318
+ return await this.textEditor.createNewFile(args.filename, args.content);
1319
+ case "strReplace":
1320
+ return await this.textEditor.strReplace(args.filename, args.old_str, args.new_str, args.replace_all);
1321
+ case "editFile":
1322
+ if (!this.morphEditor) {
1323
+ return {
1324
+ success: false,
1325
+ error: "Morph Fast Apply not available. Please set MORPH_API_KEY environment variable to use this feature.",
1326
+ };
1327
+ }
1328
+ return await this.morphEditor.editFile(args.filename, args.instructions, args.code_edit);
1329
+ case "execute":
1330
+ return await this.zsh.execute(args.command);
1331
+ case "listFiles":
1332
+ return await this.zsh.listFiles(args.dirname);
1333
+ case "universalSearch":
1334
+ return await this.search.universalSearch(args.query, {
1335
+ searchType: args.search_type,
1336
+ includePattern: args.include_pattern,
1337
+ excludePattern: args.exclude_pattern,
1338
+ caseSensitive: args.case_sensitive,
1339
+ wholeWord: args.whole_word,
1340
+ regex: args.regex,
1341
+ maxResults: args.max_results,
1342
+ fileTypes: args.file_types,
1343
+ includeHidden: args.include_hidden,
1344
+ });
1345
+ case "getEnv":
1346
+ return await this.env.getEnv(args.variable);
1347
+ case "getAllEnv":
1348
+ return await this.env.getAllEnv();
1349
+ case "searchEnv":
1350
+ return await this.env.searchEnv(args.pattern);
1351
+ case "introspect":
1352
+ return await this.introspect.introspect(args.target);
1353
+ case "clearCache":
1354
+ return await this.clearCacheTool.clearCache(args.confirmationCode);
1355
+ case "restart":
1356
+ return await this.restartTool.restart();
1357
+ case "setPersona":
1358
+ return await this.characterTool.setPersona(args.persona, args.color);
1359
+ case "setMood":
1360
+ return await this.characterTool.setMood(args.mood, args.color);
1361
+ case "getPersona":
1362
+ return await this.characterTool.getPersona();
1363
+ case "getMood":
1364
+ return await this.characterTool.getMood();
1365
+ case "getAvailablePersonas":
1366
+ return await this.characterTool.getAvailablePersonas();
1367
+ case "startActiveTask":
1368
+ return await this.taskTool.startActiveTask(args.activeTask, args.action, args.color);
1369
+ case "transitionActiveTaskStatus":
1370
+ return await this.taskTool.transitionActiveTaskStatus(args.action, args.color);
1371
+ case "stopActiveTask":
1372
+ return await this.taskTool.stopActiveTask(args.reason, args.documentationFile, args.color);
1373
+ case "insertLines":
1374
+ return await this.textEditor.insertLines(args.filename, args.insert_line, args.new_str);
1375
+ case "replaceLines":
1376
+ return await this.textEditor.replaceLines(args.filename, args.start_line, args.end_line, args.new_str);
1377
+ case "undoEdit":
1378
+ return await this.textEditor.undoEdit();
1379
+ case "chdir":
1380
+ return this.zsh.chdir(args.dirname);
1381
+ case "pwdir":
1382
+ return this.zsh.pwdir();
1383
+ case "downloadFile":
1384
+ return await this.internetTool.downloadFile(args.url);
1385
+ case "generateImage":
1386
+ return await this.imageTool.generateImage(args.prompt, args.negativePrompt, args.width, args.height, args.model, args.sampler, args.configScale, args.numSteps, args.nsfw, args.name, args.move, args.seed);
1387
+ case "captionImage":
1388
+ return await this.imageTool.captionImage(args.filename, args.prompt);
1389
+ case "pngInfo":
1390
+ return await this.imageTool.pngInfo(args.filename);
1391
+ case "readXlsx":
1392
+ return await this.fileConversionTool.readXlsx(args.filename, args.sheetName, args.outputFormat, args.output);
1393
+ case "listXlsxSheets":
1394
+ return await this.fileConversionTool.listXlsxSheets(args.filename);
1395
+ default:
1396
+ // Check if this is an MCP tool
1397
+ if (toolCall.function.name.startsWith("mcp__")) {
1398
+ return await this.executeMCPTool(toolCall.function.name, args);
1399
+ }
1400
+ return {
1401
+ success: false,
1402
+ error: `Unknown tool: ${toolCall.function.name}`,
1403
+ };
1404
+ }
1405
+ }
1406
+ catch (error) {
1407
+ return {
1408
+ success: false,
1409
+ error: `Tool execution error: ${error.message}`,
1410
+ };
1411
+ }
1412
+ }
1413
+ async executeMCPTool(toolName, args) {
1414
+ try {
1415
+ const mcpManager = getMCPManager();
1416
+ const result = await mcpManager.callTool(toolName, args);
1417
+ if (result.isError) {
1418
+ return {
1419
+ success: false,
1420
+ error: result.content[0]?.text || "MCP tool error",
1421
+ };
1422
+ }
1423
+ // Extract content from result
1424
+ const output = result.content
1425
+ .map((item) => {
1426
+ if (item.type === "text") {
1427
+ return item.text;
1428
+ }
1429
+ else if (item.type === "resource") {
1430
+ return `Resource: ${item.resource?.uri || "Unknown"}`;
1431
+ }
1432
+ return String(item);
1433
+ })
1434
+ .join("\n");
1435
+ // After successful MCP tool execution, invalidate cache for that server
1436
+ // Next call to getAllGrokTools() will lazy-refresh this server
1437
+ const serverNameMatch = toolName.match(/^mcp__(.+?)__/);
1438
+ if (serverNameMatch) {
1439
+ const serverName = serverNameMatch[1];
1440
+ mcpManager.invalidateCache(serverName);
1441
+ }
1442
+ return {
1443
+ success: true,
1444
+ output: output || "Success",
1445
+ };
1446
+ }
1447
+ catch (error) {
1448
+ return {
1449
+ success: false,
1450
+ error: `MCP tool execution error: ${error.message}`,
1451
+ };
1452
+ }
1453
+ }
1454
+ getChatHistory() {
1455
+ return [...this.chatHistory];
1456
+ }
1457
+ setChatHistory(history) {
1458
+ this.chatHistory = [...history];
1459
+ }
1460
+ getSystemPrompt() {
1461
+ return this.systemPrompt;
1462
+ }
1463
+ setSystemPrompt(prompt) {
1464
+ this.systemPrompt = prompt;
1465
+ this.messages[0] = {
1466
+ role: "system",
1467
+ content: prompt,
1468
+ };
1469
+ }
1470
+ getMessages() {
1471
+ return [...this.messages];
1472
+ }
1473
+ getCurrentTokenCount() {
1474
+ return this.tokenCounter.countMessageTokens(this.messages);
1475
+ }
1476
+ getMaxContextSize() {
1477
+ // TODO: Make this model-specific when different models have different context windows
1478
+ // For now, return the standard Grok context window size
1479
+ return 128000;
1480
+ }
1481
+ getContextUsagePercent() {
1482
+ const current = this.getCurrentTokenCount();
1483
+ const max = this.getMaxContextSize();
1484
+ return (current / max) * 100;
1485
+ }
1486
+ /**
1487
+ * Convert context messages to markdown format for viewing
1488
+ * Format: (N) Name (role) - timestamp
1489
+ */
1490
+ async convertContextToMarkdown() {
1491
+ const lines = [];
1492
+ // Header
1493
+ const { ChatHistoryManager } = await import("../utils/chat-history-manager.js");
1494
+ const historyManager = ChatHistoryManager.getInstance();
1495
+ const contextFilePath = historyManager.getContextFilePath();
1496
+ lines.push("# Conversation Context");
1497
+ lines.push(`Context File: ${contextFilePath}`);
1498
+ lines.push(`Session: ${process.env.ZDS_AI_AGENT_SESSION || "N/A"}`);
1499
+ lines.push(`Tokens: ${this.getCurrentTokenCount()} / ${this.getMaxContextSize()} (${this.getContextUsagePercent().toFixed(1)}%)`);
1500
+ lines.push("");
1501
+ lines.push("---");
1502
+ lines.push("");
1503
+ // Get agent name from environment or default
1504
+ const agentName = process.env.ZDS_AI_AGENT_BOT_NAME || "Assistant";
1505
+ const userName = process.env.ZDS_AI_AGENT_MESSAGE_AUTHOR || "User";
1506
+ // Process messages
1507
+ this.chatHistory.forEach((entry, index) => {
1508
+ const msgNum = index + 1;
1509
+ const timestamp = entry.timestamp.toLocaleTimeString();
1510
+ if (entry.type === 'user') {
1511
+ lines.push(`(${msgNum}) ${userName} (user) - ${timestamp}`);
1512
+ lines.push(entry.content || "");
1513
+ lines.push("");
1514
+ }
1515
+ else if (entry.type === 'assistant') {
1516
+ lines.push(`(${msgNum}) ${agentName} (assistant) - ${timestamp}`);
1517
+ lines.push(entry.content || "");
1518
+ lines.push("");
1519
+ }
1520
+ else if (entry.type === 'system') {
1521
+ lines.push(`(${msgNum}) System (system) - ${timestamp}`);
1522
+ lines.push(entry.content || "");
1523
+ lines.push("");
1524
+ }
1525
+ else if (entry.type === 'tool_call') {
1526
+ const toolCall = entry.toolCall;
1527
+ const toolName = toolCall?.function?.name || "unknown";
1528
+ const params = toolCall?.function?.arguments ? JSON.parse(toolCall.function.arguments) : {};
1529
+ lines.push(`(${msgNum}) ${agentName} (tool_call: ${toolName}) - ${timestamp}`);
1530
+ lines.push(`Parameters: ${JSON.stringify(params, null, 2)}`);
1531
+ lines.push("");
1532
+ }
1533
+ else if (entry.type === 'tool_result') {
1534
+ const toolCall = entry.toolCall;
1535
+ const toolName = toolCall?.function?.name || "unknown";
1536
+ lines.push(`(${msgNum}) System (tool_result: ${toolName}) - ${timestamp}`);
1537
+ lines.push(entry.content || "");
1538
+ lines.push("");
1539
+ }
1540
+ });
1541
+ return lines.join("\n");
1542
+ }
1543
+ getPersona() {
1544
+ return this.persona;
1545
+ }
1546
+ getPersonaColor() {
1547
+ return this.personaColor;
1548
+ }
1549
+ getMood() {
1550
+ return this.mood;
1551
+ }
1552
+ getMoodColor() {
1553
+ return this.moodColor;
1554
+ }
1555
+ getActiveTask() {
1556
+ return this.activeTask;
1557
+ }
1558
+ getActiveTaskAction() {
1559
+ return this.activeTaskAction;
1560
+ }
1561
+ getActiveTaskColor() {
1562
+ return this.activeTaskColor;
1563
+ }
1564
+ setPendingContextEdit(tmpJsonPath, contextFilePath) {
1565
+ this.pendingContextEdit = { tmpJsonPath, contextFilePath };
1566
+ }
1567
+ getPendingContextEdit() {
1568
+ return this.pendingContextEdit;
1569
+ }
1570
+ clearPendingContextEdit() {
1571
+ this.pendingContextEdit = null;
1572
+ }
1573
+ async setPersona(persona, color) {
1574
+ // Execute hook if configured
1575
+ const settings = getSettingsManager();
1576
+ const hookPath = settings.getPersonaHook();
1577
+ const hookMandatory = settings.isPersonaHookMandatory();
1578
+ if (!hookPath && hookMandatory) {
1579
+ const reason = "Persona hook is mandatory but not configured";
1580
+ // Note: Don't add to this.messages during tool execution - only chatHistory
1581
+ this.chatHistory.push({
1582
+ type: 'system',
1583
+ content: `Failed to change persona to "${persona}": ${reason}`,
1584
+ timestamp: new Date()
1585
+ });
1586
+ return {
1587
+ success: false,
1588
+ error: reason
1589
+ };
1590
+ }
1591
+ if (hookPath) {
1592
+ const hookResult = await executeOperationHook(hookPath, "setPersona", {
1593
+ persona_old: this.persona || "",
1594
+ persona_new: persona,
1595
+ persona_color: color || "white"
1596
+ }, 30000, hookMandatory, this.getCurrentTokenCount(), this.getMaxContextSize());
1597
+ if (!hookResult.approved) {
1598
+ const reason = hookResult.reason || "Hook rejected persona change";
1599
+ // Process rejection commands (MODEL, SYSTEM)
1600
+ // Even in rejection, we process commands (might have MODEL change)
1601
+ await this.processHookResult(hookResult);
1602
+ // Note: We ignore the return value here since we're already rejecting the persona
1603
+ // Note: Don't add to this.messages during tool execution - only chatHistory
1604
+ this.chatHistory.push({
1605
+ type: 'system',
1606
+ content: `Failed to change persona to "${persona}": ${reason}`,
1607
+ timestamp: new Date()
1608
+ });
1609
+ return {
1610
+ success: false,
1611
+ error: reason
1612
+ };
1613
+ }
1614
+ if (hookResult.timedOut) {
1615
+ // Note: Don't add to this.messages during tool execution - only chatHistory
1616
+ this.chatHistory.push({
1617
+ type: 'system',
1618
+ content: `Persona hook timed out (auto-approved)`,
1619
+ timestamp: new Date()
1620
+ });
1621
+ }
1622
+ // Process hook commands (ENV, MODEL, SYSTEM)
1623
+ const result = await this.processHookResult(hookResult, 'ZDS_AI_AGENT_PERSONA');
1624
+ if (!result.success) {
1625
+ // Model/backend test failed - don't apply persona change
1626
+ return {
1627
+ success: false,
1628
+ error: "Persona change rejected due to failed model/backend test"
1629
+ };
1630
+ }
1631
+ // Apply persona transformation if present
1632
+ if (result.transformedValue) {
1633
+ persona = result.transformedValue;
1634
+ }
1635
+ }
1636
+ const oldPersona = this.persona;
1637
+ const oldColor = this.personaColor;
1638
+ this.persona = persona;
1639
+ this.personaColor = color || "white";
1640
+ process.env.ZDS_AI_AGENT_PERSONA = persona;
1641
+ // Add system message for recordkeeping
1642
+ let systemContent;
1643
+ if (oldPersona) {
1644
+ const oldColorStr = oldColor && oldColor !== "white" ? ` (${oldColor})` : "";
1645
+ const newColorStr = this.personaColor && this.personaColor !== "white" ? ` (${this.personaColor})` : "";
1646
+ systemContent = `Assistant changed the persona from "${oldPersona}"${oldColorStr} to "${this.persona}"${newColorStr}`;
1647
+ }
1648
+ else {
1649
+ const colorStr = this.personaColor && this.personaColor !== "white" ? ` (${this.personaColor})` : "";
1650
+ systemContent = `Assistant set the persona to "${this.persona}"${colorStr}`;
1651
+ }
1652
+ // Note: Don't add to this.messages during tool execution - only chatHistory
1653
+ // System messages added during tool execution create invalid message sequences
1654
+ // because they get inserted between tool_calls and tool_results
1655
+ this.chatHistory.push({
1656
+ type: 'system',
1657
+ content: systemContent,
1658
+ timestamp: new Date()
1659
+ });
1660
+ this.emit('personaChange', {
1661
+ persona: this.persona,
1662
+ color: this.personaColor
1663
+ });
1664
+ return { success: true };
1665
+ }
1666
+ async setMood(mood, color) {
1667
+ // Execute hook if configured
1668
+ const settings = getSettingsManager();
1669
+ const hookPath = settings.getMoodHook();
1670
+ const hookMandatory = settings.isMoodHookMandatory();
1671
+ if (!hookPath && hookMandatory) {
1672
+ const reason = "Mood hook is mandatory but not configured";
1673
+ // Note: Don't add to this.messages during tool execution - only chatHistory
1674
+ this.chatHistory.push({
1675
+ type: 'system',
1676
+ content: `Failed to change mood to "${mood}": ${reason}`,
1677
+ timestamp: new Date()
1678
+ });
1679
+ return {
1680
+ success: false,
1681
+ error: reason
1682
+ };
1683
+ }
1684
+ if (hookPath) {
1685
+ const hookResult = await executeOperationHook(hookPath, "setMood", {
1686
+ mood_old: this.mood || "",
1687
+ mood_new: mood,
1688
+ mood_color: color || "white"
1689
+ }, 30000, hookMandatory, this.getCurrentTokenCount(), this.getMaxContextSize());
1690
+ if (!hookResult.approved) {
1691
+ const reason = hookResult.reason || "Hook rejected mood change";
1692
+ // Process rejection commands (MODEL, SYSTEM)
1693
+ await this.processHookResult(hookResult);
1694
+ // Note: Don't add to this.messages during tool execution - only chatHistory
1695
+ this.chatHistory.push({
1696
+ type: 'system',
1697
+ content: `Failed to change mood to "${mood}": ${reason}`,
1698
+ timestamp: new Date()
1699
+ });
1700
+ return {
1701
+ success: false,
1702
+ error: reason
1703
+ };
1704
+ }
1705
+ if (hookResult.timedOut) {
1706
+ // Note: Don't add to this.messages during tool execution - only chatHistory
1707
+ this.chatHistory.push({
1708
+ type: 'system',
1709
+ content: `Mood hook timed out (auto-approved)`,
1710
+ timestamp: new Date()
1711
+ });
1712
+ }
1713
+ // Process hook commands (ENV, MODEL, SYSTEM)
1714
+ const result = await this.processHookResult(hookResult, 'ZDS_AI_AGENT_MOOD');
1715
+ if (!result.success) {
1716
+ // Model/backend test failed - don't apply mood change
1717
+ return {
1718
+ success: false,
1719
+ error: "Mood change rejected due to failed model/backend test"
1720
+ };
1721
+ }
1722
+ // Apply mood transformation if present
1723
+ if (result.transformedValue) {
1724
+ mood = result.transformedValue;
1725
+ }
1726
+ }
1727
+ const oldMood = this.mood;
1728
+ const oldColor = this.moodColor;
1729
+ this.mood = mood;
1730
+ this.moodColor = color || "white";
1731
+ process.env.ZDS_AI_AGENT_MOOD = mood;
1732
+ // Add system message for recordkeeping
1733
+ let systemContent;
1734
+ if (oldMood) {
1735
+ const oldColorStr = oldColor && oldColor !== "white" ? ` (${oldColor})` : "";
1736
+ const newColorStr = this.moodColor && this.moodColor !== "white" ? ` (${this.moodColor})` : "";
1737
+ systemContent = `Assistant changed the mood from "${oldMood}"${oldColorStr} to "${this.mood}"${newColorStr}`;
1738
+ }
1739
+ else {
1740
+ const colorStr = this.moodColor && this.moodColor !== "white" ? ` (${this.moodColor})` : "";
1741
+ systemContent = `Assistant set the mood to "${this.mood}"${colorStr}`;
1742
+ }
1743
+ // Note: Don't add to this.messages during tool execution - only chatHistory
1744
+ // System messages added during tool execution create invalid message sequences
1745
+ // because they get inserted between tool_calls and tool_results
1746
+ this.chatHistory.push({
1747
+ type: 'system',
1748
+ content: systemContent,
1749
+ timestamp: new Date()
1750
+ });
1751
+ this.emit('moodChange', {
1752
+ mood: this.mood,
1753
+ color: this.moodColor
1754
+ });
1755
+ return { success: true };
1756
+ }
1757
+ async startActiveTask(activeTask, action, color) {
1758
+ // Cannot start new task if one already exists
1759
+ if (this.activeTask) {
1760
+ return {
1761
+ success: false,
1762
+ error: `Cannot start new task "${activeTask}". Active task "${this.activeTask}" must be stopped first.`
1763
+ };
1764
+ }
1765
+ // Execute hook if configured
1766
+ const settings = getSettingsManager();
1767
+ const hookPath = settings.getTaskApprovalHook();
1768
+ if (hookPath) {
1769
+ const hookResult = await executeOperationHook(hookPath, "startActiveTask", {
1770
+ activetask: activeTask,
1771
+ action: action,
1772
+ color: color || "white"
1773
+ }, 30000, false, // Task hook is not mandatory
1774
+ this.getCurrentTokenCount(), this.getMaxContextSize());
1775
+ // Process hook commands (MODEL, SYSTEM, ENV, BACKEND, etc.) for both approval and rejection
1776
+ await this.processHookResult(hookResult);
1777
+ if (!hookResult.approved) {
1778
+ const reason = hookResult.reason || "Hook rejected task start";
1779
+ this.messages.push({
1780
+ role: 'system',
1781
+ content: `Failed to start task "${activeTask}": ${reason}`
1782
+ });
1783
+ return {
1784
+ success: false,
1785
+ error: reason
1786
+ };
1787
+ }
1788
+ if (hookResult.timedOut) {
1789
+ this.messages.push({
1790
+ role: 'system',
1791
+ content: `Task start hook timed out (auto-approved)`
1792
+ });
1793
+ }
1794
+ }
1795
+ // Set the task
1796
+ this.activeTask = activeTask;
1797
+ this.activeTaskAction = action;
1798
+ this.activeTaskColor = color || "white";
1799
+ // Add system message
1800
+ const colorStr = this.activeTaskColor && this.activeTaskColor !== "white" ? ` (${this.activeTaskColor})` : "";
1801
+ this.messages.push({
1802
+ role: 'system',
1803
+ content: `Assistant changed task status for "${this.activeTask}" to ${this.activeTaskAction}${colorStr}`
1804
+ });
1805
+ // Emit event
1806
+ this.emit('activeTaskChange', {
1807
+ activeTask: this.activeTask,
1808
+ action: this.activeTaskAction,
1809
+ color: this.activeTaskColor
1810
+ });
1811
+ return { success: true };
1812
+ }
1813
+ async transitionActiveTaskStatus(action, color) {
1814
+ // Cannot transition if no active task
1815
+ if (!this.activeTask) {
1816
+ return {
1817
+ success: false,
1818
+ error: "Cannot transition task status. No active task is currently running."
1819
+ };
1820
+ }
1821
+ // Execute hook if configured
1822
+ const settings = getSettingsManager();
1823
+ const hookPath = settings.getTaskApprovalHook();
1824
+ if (hookPath) {
1825
+ const hookResult = await executeOperationHook(hookPath, "transitionActiveTaskStatus", {
1826
+ action: action,
1827
+ color: color || "white"
1828
+ }, 30000, false, // Task hook is not mandatory
1829
+ this.getCurrentTokenCount(), this.getMaxContextSize());
1830
+ // Process hook commands (MODEL, SYSTEM, ENV, BACKEND, etc.) for both approval and rejection
1831
+ await this.processHookResult(hookResult);
1832
+ if (!hookResult.approved) {
1833
+ const reason = hookResult.reason || "Hook rejected task status transition";
1834
+ this.messages.push({
1835
+ role: 'system',
1836
+ content: `Failed to transition task "${this.activeTask}" from ${this.activeTaskAction} to ${action}: ${reason}`
1837
+ });
1838
+ return {
1839
+ success: false,
1840
+ error: reason
1841
+ };
1842
+ }
1843
+ if (hookResult.timedOut) {
1844
+ this.messages.push({
1845
+ role: 'system',
1846
+ content: `Task transition hook timed out (auto-approved)`
1847
+ });
1848
+ }
1849
+ }
1850
+ // Store old action for system message
1851
+ const oldAction = this.activeTaskAction;
1852
+ // Update the action and color
1853
+ this.activeTaskAction = action;
1854
+ this.activeTaskColor = color || this.activeTaskColor;
1855
+ // Add system message
1856
+ const colorStr = this.activeTaskColor && this.activeTaskColor !== "white" ? ` (${this.activeTaskColor})` : "";
1857
+ this.messages.push({
1858
+ role: 'system',
1859
+ content: `Assistant changed task status for "${this.activeTask}" from ${oldAction} to ${this.activeTaskAction}${colorStr}`
1860
+ });
1861
+ // Emit event
1862
+ this.emit('activeTaskChange', {
1863
+ activeTask: this.activeTask,
1864
+ action: this.activeTaskAction,
1865
+ color: this.activeTaskColor
1866
+ });
1867
+ return { success: true };
1868
+ }
1869
+ async stopActiveTask(reason, documentationFile, color) {
1870
+ // Cannot stop if no active task
1871
+ if (!this.activeTask) {
1872
+ return {
1873
+ success: false,
1874
+ error: "Cannot stop task. No active task is currently running."
1875
+ };
1876
+ }
1877
+ // Record the start time for 3-second minimum
1878
+ const startTime = Date.now();
1879
+ // Execute hook if configured
1880
+ const settings = getSettingsManager();
1881
+ const hookPath = settings.getTaskApprovalHook();
1882
+ if (hookPath) {
1883
+ const hookResult = await executeOperationHook(hookPath, "stopActiveTask", {
1884
+ reason: reason,
1885
+ documentation_file: documentationFile,
1886
+ color: color || "white"
1887
+ }, 30000, false, // Task hook is not mandatory
1888
+ this.getCurrentTokenCount(), this.getMaxContextSize());
1889
+ // Process hook commands (MODEL, SYSTEM, ENV, BACKEND, etc.) for both approval and rejection
1890
+ await this.processHookResult(hookResult);
1891
+ if (!hookResult.approved) {
1892
+ const hookReason = hookResult.reason || "Hook rejected task stop";
1893
+ this.messages.push({
1894
+ role: 'system',
1895
+ content: `Failed to stop task "${this.activeTask}": ${hookReason}`
1896
+ });
1897
+ return {
1898
+ success: false,
1899
+ error: hookReason
1900
+ };
1901
+ }
1902
+ if (hookResult.timedOut) {
1903
+ this.messages.push({
1904
+ role: 'system',
1905
+ content: `Task stop hook timed out (auto-approved)`
1906
+ });
1907
+ }
1908
+ }
1909
+ // Calculate remaining time to meet 3-second minimum
1910
+ const elapsed = Date.now() - startTime;
1911
+ const minimumDelay = 3000;
1912
+ const remainingDelay = Math.max(0, minimumDelay - elapsed);
1913
+ // Wait for remaining time if needed
1914
+ if (remainingDelay > 0) {
1915
+ await new Promise(resolve => setTimeout(resolve, remainingDelay));
1916
+ }
1917
+ // Store task info for system message before clearing
1918
+ const stoppedTask = this.activeTask;
1919
+ const stoppedAction = this.activeTaskAction;
1920
+ // Clear the task
1921
+ this.activeTask = "";
1922
+ this.activeTaskAction = "";
1923
+ this.activeTaskColor = "white";
1924
+ // Add system message
1925
+ const colorStr = color && color !== "white" ? ` (${color})` : "";
1926
+ this.messages.push({
1927
+ role: 'system',
1928
+ content: `Assistant stopped task "${stoppedTask}" (was ${stoppedAction}) with reason: ${reason}${colorStr}`
1929
+ });
1930
+ // Emit event to clear widget
1931
+ this.emit('activeTaskChange', {
1932
+ activeTask: "",
1933
+ action: "",
1934
+ color: "white"
1935
+ });
1936
+ return { success: true };
1937
+ }
1938
+ async emitContextChange() {
1939
+ const percent = this.getContextUsagePercent();
1940
+ this.emit('contextChange', {
1941
+ current: this.getCurrentTokenCount(),
1942
+ max: this.getMaxContextSize(),
1943
+ percent
1944
+ });
1945
+ // Add system warnings based on context usage (may auto-clear at 100%)
1946
+ await this.addContextWarningIfNeeded(percent);
1947
+ }
1948
+ async addContextWarningIfNeeded(percent) {
1949
+ let warning = null;
1950
+ const roundedPercent = Math.round(percent);
1951
+ if (percent >= 100) {
1952
+ // Auto-clear at 100%+ to prevent exceeding context limits
1953
+ warning = `CONTEXT LIMIT REACHED: You are at ${roundedPercent}% context capacity! Automatically clearing cache to prevent context overflow...`;
1954
+ this.messages.push({
1955
+ role: 'system',
1956
+ content: warning
1957
+ });
1958
+ // Perform automatic cache clear
1959
+ await this.clearCache();
1960
+ return;
1961
+ }
1962
+ if (percent >= 95) {
1963
+ // Very stern warning at 95%+ (every time)
1964
+ warning = `CRITICAL CONTEXT WARNING: You are at ${roundedPercent}% context capacity! You MUST immediately save any notes and lessons learned, then run the 'clearCache' tool to reset the conversation context. The conversation will fail if you do not take action now.`;
1965
+ }
1966
+ else if (percent >= 90 && !this.contextWarningAt90) {
1967
+ // Dire warning at 90% (one time only)
1968
+ this.contextWarningAt90 = true;
1969
+ warning = `URGENT CONTEXT WARNING: You are at ${roundedPercent}% context capacity! Perform your final tasks or responses and prepare to be reset.`;
1970
+ }
1971
+ else if (percent >= 80 && !this.contextWarningAt80) {
1972
+ // Initial warning at 80% (one time only)
1973
+ this.contextWarningAt80 = true;
1974
+ warning = `Context Warning: You are at ${roundedPercent}% context capacity! You are approaching the limit. Be concise and avoid lengthy outputs.`;
1975
+ }
1976
+ if (warning) {
1977
+ // Add as a system message
1978
+ this.messages.push({
1979
+ role: 'system',
1980
+ content: warning
1981
+ });
1982
+ }
1983
+ }
1984
+ async executeCommand(command, skipConfirmation = false) {
1985
+ return await this.zsh.execute(command, 30000, skipConfirmation);
1986
+ }
1987
+ getCurrentModel() {
1988
+ return this.grokClient.getCurrentModel();
1989
+ }
1990
+ setModel(model) {
1991
+ this.grokClient.setModel(model);
1992
+ // Update token counter for new model
1993
+ this.tokenCounter.dispose();
1994
+ this.tokenCounter = createTokenCounter(model);
1995
+ }
1996
+ /**
1997
+ * Strip in-progress tool calls from messages for backend/model testing
1998
+ * Removes tool_calls from the last assistant message and any corresponding tool results
1999
+ * @returns Cleaned copy of messages array, or original if no stripping needed
2000
+ */
2001
+ static stripInProgressToolCalls(messages) {
2002
+ // Find the last assistant message
2003
+ let lastAssistantIndex = -1;
2004
+ for (let i = messages.length - 1; i >= 0; i--) {
2005
+ if (messages[i].role === 'assistant') {
2006
+ lastAssistantIndex = i;
2007
+ break;
2008
+ }
2009
+ }
2010
+ // If no assistant message or it has no tool_calls, return original
2011
+ if (lastAssistantIndex === -1 || !messages[lastAssistantIndex].tool_calls) {
2012
+ return messages;
2013
+ }
2014
+ // Create deep copy to avoid modifying original
2015
+ const cleanedMessages = JSON.parse(JSON.stringify(messages));
2016
+ // Collect tool_call_ids from the last assistant message
2017
+ const toolCallIds = new Set((cleanedMessages[lastAssistantIndex].tool_calls || []).map((tc) => tc.id));
2018
+ // Remove tool_calls from the last assistant message
2019
+ delete cleanedMessages[lastAssistantIndex].tool_calls;
2020
+ // Remove any tool result messages that correspond to those tool_call_ids
2021
+ // (in case some finished but not all)
2022
+ return cleanedMessages.filter((msg, idx) => {
2023
+ if (idx <= lastAssistantIndex) {
2024
+ return true; // Keep all messages before and including the assistant message
2025
+ }
2026
+ if (msg.role === 'tool' && toolCallIds.has(msg.tool_call_id)) {
2027
+ return false; // Remove tool results for the in-progress tool calls
2028
+ }
2029
+ return true;
2030
+ });
2031
+ }
2032
+ /**
2033
+ * Test a model change by making a test API call with current conversation context
2034
+ * Rolls back to previous model if test fails
2035
+ * @param newModel Model to test
2036
+ * @returns Promise with success status and optional error message
2037
+ */
2038
+ async testModel(newModel) {
2039
+ const previousModel = this.getCurrentModel();
2040
+ const previousTokenCounter = this.tokenCounter;
2041
+ // Strip in-progress tool calls to avoid sending incomplete assistant messages
2042
+ const testMessages = GrokAgent.stripInProgressToolCalls(this.messages);
2043
+ // Build request payload for logging
2044
+ const supportsTools = this.grokClient.getSupportsTools();
2045
+ const tools = supportsTools ? await getAllGrokTools() : [];
2046
+ const requestPayload = {
2047
+ model: newModel,
2048
+ messages: testMessages,
2049
+ tools: supportsTools && tools.length > 0 ? tools : undefined,
2050
+ temperature: this.temperature,
2051
+ max_tokens: 10
2052
+ };
2053
+ try {
2054
+ // Temporarily set the new model
2055
+ this.grokClient.setModel(newModel);
2056
+ this.tokenCounter = createTokenCounter(newModel);
2057
+ // Test with actual conversation context to verify the model can handle it
2058
+ // This catches issues like ollama models that fail to parse tool calls
2059
+ const response = await this.grokClient.chat(testMessages, tools, newModel, undefined, this.temperature, undefined, 10);
2060
+ // Check if response is valid
2061
+ if (!response || !response.choices || response.choices.length === 0) {
2062
+ throw new Error("Invalid response from API");
2063
+ }
2064
+ // Test succeeded - keep the new model
2065
+ previousTokenCounter.dispose();
2066
+ return { success: true };
2067
+ }
2068
+ catch (error) {
2069
+ // Test failed - roll back to previous model
2070
+ this.grokClient.setModel(previousModel);
2071
+ this.tokenCounter.dispose();
2072
+ this.tokenCounter = previousTokenCounter;
2073
+ // Log test failure with full request/response for debugging
2074
+ const { message: logPaths } = await logApiError(requestPayload, error, { errorType: 'model-switch-test-failure', previousModel, newModel }, 'test-fail');
2075
+ const errorMessage = error.message || "Unknown error during model test";
2076
+ return {
2077
+ success: false,
2078
+ error: `Model test failed: ${errorMessage}\n${logPaths}`
2079
+ };
2080
+ }
2081
+ }
2082
+ /**
2083
+ * Test backend/baseUrl/model changes by making a test API call with current conversation context
2084
+ * Rolls back all changes if test fails
2085
+ * @param backend Backend display name
2086
+ * @param baseUrl Base URL for API calls
2087
+ * @param apiKeyEnvVar Name of environment variable containing API key
2088
+ * @param model Model to use (optional, uses current model if not specified)
2089
+ * @returns Promise with success status and optional error message
2090
+ */
2091
+ async testBackendModelChange(backend, baseUrl, apiKeyEnvVar, model) {
2092
+ const previousClient = this.grokClient;
2093
+ const previousApiKeyEnvVar = this.apiKeyEnvVar;
2094
+ const previousBackend = this.grokClient.getBackendName();
2095
+ const previousModel = this.getCurrentModel();
2096
+ let requestPayload;
2097
+ let newModel;
2098
+ try {
2099
+ // Get API key from environment
2100
+ const apiKey = process.env[apiKeyEnvVar];
2101
+ if (!apiKey) {
2102
+ throw new Error(`API key not found in environment variable: ${apiKeyEnvVar}`);
2103
+ }
2104
+ // Use current model if not specified
2105
+ newModel = model || this.getCurrentModel();
2106
+ // Create new client with new configuration
2107
+ this.grokClient = new GrokClient(apiKey, newModel, baseUrl, backend);
2108
+ // Store the API key env var name for session persistence
2109
+ this.apiKeyEnvVar = apiKeyEnvVar;
2110
+ // Reinitialize MCP servers since we're switching to a new backend/model
2111
+ try {
2112
+ const config = loadMCPConfig();
2113
+ if (config.servers.length > 0) {
2114
+ await initializeMCPServers();
2115
+ }
2116
+ }
2117
+ catch (mcpError) {
2118
+ console.warn("MCP reinitialization failed:", mcpError);
2119
+ }
2120
+ // Strip in-progress tool calls to avoid sending incomplete assistant messages
2121
+ const testMessages = GrokAgent.stripInProgressToolCalls(this.messages);
2122
+ // Build request payload for logging
2123
+ const supportsTools = this.grokClient.getSupportsTools();
2124
+ const tools = supportsTools ? await getAllGrokTools() : [];
2125
+ requestPayload = {
2126
+ backend,
2127
+ baseUrl,
2128
+ model: newModel,
2129
+ messages: testMessages,
2130
+ tools: supportsTools && tools.length > 0 ? tools : undefined,
2131
+ temperature: this.temperature,
2132
+ max_tokens: 10
2133
+ };
2134
+ // Test with actual conversation context to verify the backend/model can handle it
2135
+ // This catches issues like ollama models that fail to parse tool calls
2136
+ const response = await this.grokClient.chat(testMessages, tools, newModel, undefined, this.temperature, undefined, 10);
2137
+ // Check if response is valid
2138
+ if (!response || !response.choices || response.choices.length === 0) {
2139
+ throw new Error("Invalid response from API");
2140
+ }
2141
+ // Test succeeded - new client is now active
2142
+ return { success: true };
2143
+ }
2144
+ catch (error) {
2145
+ // Test failed - restore previous client and API key env var
2146
+ this.grokClient = previousClient;
2147
+ this.apiKeyEnvVar = previousApiKeyEnvVar;
2148
+ // Log test failure with full request/response for debugging (if we got far enough to build the payload)
2149
+ let logPaths = '';
2150
+ if (requestPayload) {
2151
+ const result = await logApiError(requestPayload, error, {
2152
+ errorType: 'backend-switch-test-failure',
2153
+ previousBackend,
2154
+ previousModel,
2155
+ newBackend: backend,
2156
+ newModel,
2157
+ baseUrl,
2158
+ apiKeyEnvVar
2159
+ }, 'test-fail');
2160
+ logPaths = result.message;
2161
+ }
2162
+ const errorMessage = error.message || "Unknown error during backend/model test";
2163
+ return {
2164
+ success: false,
2165
+ error: logPaths ? `${errorMessage}\n${logPaths}` : errorMessage
2166
+ };
2167
+ }
2168
+ }
2169
+ /**
2170
+ * Process hook result including commands and transformations
2171
+ * Handles ENV transformations, model/backend testing, and error messaging
2172
+ * @param hookResult Hook execution result
2173
+ * @param envKey Optional ENV key to check for transformation (e.g., ZDS_AI_AGENT_PERSONA)
2174
+ * @returns Object with success status and transformed value (if any)
2175
+ */
2176
+ async processHookResult(hookResult, envKey) {
2177
+ if (!hookResult.commands) {
2178
+ return { success: true };
2179
+ }
2180
+ const results = applyHookCommands(hookResult.commands);
2181
+ // Check for transformation via ENV if key provided
2182
+ let transformedValue;
2183
+ if (envKey && results.env[envKey]) {
2184
+ transformedValue = results.env[envKey];
2185
+ }
2186
+ // Process commands (test model/backend, apply ENV vars, add SYSTEM messages)
2187
+ const success = await this.processHookCommands(results);
2188
+ return { success, transformedValue };
2189
+ }
2190
+ /**
2191
+ * Process hook commands (MODEL, BACKEND, BASE_URL, SYSTEM, ENV)
2192
+ * Handles model/backend testing and error messaging
2193
+ * @param commands Hook commands from applyHookCommands()
2194
+ */
2195
+ async processHookCommands(commands) {
2196
+ // Import the helper function
2197
+ const { applyEnvVariables } = await import('../utils/hook-executor.js');
2198
+ // Check if backend or model change is requested
2199
+ const hasBackendChange = commands.backend && commands.baseUrl && commands.apiKeyEnvVar;
2200
+ const hasModelChange = commands.model;
2201
+ // Test backend/model changes FIRST before applying anything
2202
+ if (hasBackendChange) {
2203
+ // Backend change - test backend/baseUrl/model together
2204
+ const testResult = await this.testBackendModelChange(commands.backend, commands.baseUrl, commands.apiKeyEnvVar, commands.model);
2205
+ if (!testResult.success) {
2206
+ // Test failed - don't apply ANYTHING
2207
+ const parts = [];
2208
+ if (commands.backend)
2209
+ parts.push(`backend to "${commands.backend}"`);
2210
+ if (commands.model)
2211
+ parts.push(`model to "${commands.model}"`);
2212
+ const errorMsg = `Failed to change ${parts.join(' and ')}: ${testResult.error}`;
2213
+ // Note: Don't add to this.messages during tool execution - only chatHistory
2214
+ this.chatHistory.push({
2215
+ type: "system",
2216
+ content: errorMsg,
2217
+ timestamp: new Date(),
2218
+ });
2219
+ return false; // Signal failure - caller should not apply other changes
2220
+ }
2221
+ // Test succeeded - apply ENV variables and add success message
2222
+ applyEnvVariables(commands.env);
2223
+ const parts = [];
2224
+ if (commands.backend)
2225
+ parts.push(`backend to "${commands.backend}"`);
2226
+ if (commands.model)
2227
+ parts.push(`model to "${commands.model}"`);
2228
+ const successMsg = `Changed ${parts.join(' and ')}`;
2229
+ // Note: Don't add to this.messages during tool execution - only chatHistory
2230
+ this.chatHistory.push({
2231
+ type: "system",
2232
+ content: successMsg,
2233
+ timestamp: new Date(),
2234
+ });
2235
+ // Emit events for UI updates
2236
+ if (commands.backend) {
2237
+ this.emit('backendChange', {
2238
+ backend: commands.backend
2239
+ });
2240
+ }
2241
+ if (commands.model) {
2242
+ this.emit('modelChange', {
2243
+ model: commands.model
2244
+ });
2245
+ }
2246
+ }
2247
+ else if (hasModelChange) {
2248
+ // Model-only change
2249
+ const testResult = await this.testModel(commands.model);
2250
+ if (!testResult.success) {
2251
+ // Test failed - don't apply ANYTHING
2252
+ const errorMsg = `Failed to change model to "${commands.model}": ${testResult.error}`;
2253
+ // Note: Don't add to this.messages during tool execution - only chatHistory
2254
+ this.chatHistory.push({
2255
+ type: "system",
2256
+ content: errorMsg,
2257
+ timestamp: new Date(),
2258
+ });
2259
+ return false; // Signal failure - caller should not apply other changes
2260
+ }
2261
+ // Test succeeded - apply ENV variables and add success message
2262
+ applyEnvVariables(commands.env);
2263
+ const successMsg = `Model changed to "${commands.model}"`;
2264
+ // Note: Don't add to this.messages during tool execution - only chatHistory
2265
+ this.chatHistory.push({
2266
+ type: "system",
2267
+ content: successMsg,
2268
+ timestamp: new Date(),
2269
+ });
2270
+ // Emit modelChange event for UI updates
2271
+ this.emit('modelChange', {
2272
+ model: commands.model
2273
+ });
2274
+ }
2275
+ else {
2276
+ // No model or backend change - just apply ENV variables
2277
+ applyEnvVariables(commands.env);
2278
+ }
2279
+ // Add SYSTEM message if present
2280
+ if (commands.system) {
2281
+ // Note: Don't add to this.messages during tool execution - only chatHistory
2282
+ this.chatHistory.push({
2283
+ type: "system",
2284
+ content: commands.system,
2285
+ timestamp: new Date(),
2286
+ });
2287
+ }
2288
+ return true; // Signal success - caller can apply other changes
2289
+ }
2290
+ getBackend() {
2291
+ // Just return the backend name from the client (no detection)
2292
+ return this.grokClient.getBackendName();
2293
+ }
2294
+ abortCurrentOperation() {
2295
+ if (this.abortController) {
2296
+ this.abortController.abort();
2297
+ }
2298
+ }
2299
+ async clearCache() {
2300
+ const { ChatHistoryManager } = await import("../utils/chat-history-manager.js");
2301
+ const { executeStartupHook } = await import("../utils/startup-hook.js");
2302
+ const { executeOperationHook, applyHookCommands } = await import("../utils/hook-executor.js");
2303
+ const historyManager = ChatHistoryManager.getInstance();
2304
+ // Backup current context to timestamped files
2305
+ historyManager.backupHistory();
2306
+ // Clear the context
2307
+ this.chatHistory = [];
2308
+ this.messages = [];
2309
+ this.contextWarningAt80 = false;
2310
+ this.contextWarningAt90 = false;
2311
+ this.firstMessageProcessed = false;
2312
+ // Add temporary system message (will be replaced by initialize())
2313
+ this.messages.push({
2314
+ role: "system",
2315
+ content: "Initializing...",
2316
+ });
2317
+ this.chatHistory.push({
2318
+ type: "system",
2319
+ content: "Initializing...",
2320
+ timestamp: new Date(),
2321
+ });
2322
+ try {
2323
+ // Re-execute startup hook to get fresh output
2324
+ this.startupHookOutput = await executeStartupHook();
2325
+ // Reinitialize with system message and startup hook
2326
+ // Instance hook runs automatically at end of initialize()
2327
+ await this.initialize();
2328
+ }
2329
+ catch (error) {
2330
+ console.error("Error during initialize() in clearCache():", error);
2331
+ // Continue anyway - we still want to save the cleared state
2332
+ }
2333
+ // Save the cleared state FIRST before emitting (in case emit causes exit)
2334
+ const sessionState = this.getSessionState();
2335
+ historyManager.saveContext(this.systemPrompt, this.chatHistory, sessionState);
2336
+ historyManager.saveMessages(this.messages);
2337
+ // Emit context change WITHOUT calling addContextWarningIfNeeded (to avoid recursive clearCache)
2338
+ const percent = this.getContextUsagePercent();
2339
+ this.emit('contextChange', {
2340
+ current: this.getCurrentTokenCount(),
2341
+ max: this.getMaxContextSize(),
2342
+ percent
2343
+ });
2344
+ // Note: Intentionally NOT calling addContextWarningIfNeeded here to prevent recursion
2345
+ }
2346
+ /**
2347
+ * Get current session state for persistence
2348
+ */
2349
+ getSessionState() {
2350
+ return {
2351
+ session: process.env.ZDS_AI_AGENT_SESSION || "",
2352
+ persona: this.persona,
2353
+ personaColor: this.personaColor,
2354
+ mood: this.mood,
2355
+ moodColor: this.moodColor,
2356
+ activeTask: this.activeTask,
2357
+ activeTaskAction: this.activeTaskAction,
2358
+ activeTaskColor: this.activeTaskColor,
2359
+ cwd: process.cwd(),
2360
+ contextCurrent: this.getCurrentTokenCount(),
2361
+ contextMax: this.getMaxContextSize(),
2362
+ backend: this.grokClient.getBackendName(),
2363
+ baseUrl: this.grokClient.getBaseURL(),
2364
+ apiKeyEnvVar: this.apiKeyEnvVar,
2365
+ model: this.getCurrentModel(),
2366
+ };
2367
+ }
2368
+ /**
2369
+ * Restore session state from persistence
2370
+ */
2371
+ async restoreSessionState(state) {
2372
+ // Restore session ID
2373
+ if (state.session) {
2374
+ process.env.ZDS_AI_AGENT_SESSION = state.session;
2375
+ }
2376
+ // Restore cwd early (hooks may need correct working directory)
2377
+ if (state.cwd) {
2378
+ try {
2379
+ process.chdir(state.cwd);
2380
+ }
2381
+ catch (error) {
2382
+ console.warn(`Failed to restore working directory to ${state.cwd}:`, error);
2383
+ }
2384
+ }
2385
+ // Restore backend/baseUrl/apiKeyEnvVar/model if present (creates initial client)
2386
+ if (state.backend && state.baseUrl && state.apiKeyEnvVar) {
2387
+ try {
2388
+ // Get API key from environment
2389
+ const apiKey = process.env[state.apiKeyEnvVar];
2390
+ if (apiKey) {
2391
+ // Create new client with restored configuration
2392
+ const model = state.model || this.getCurrentModel();
2393
+ this.grokClient = new GrokClient(apiKey, model, state.baseUrl, state.backend);
2394
+ this.apiKeyEnvVar = state.apiKeyEnvVar;
2395
+ // Reinitialize MCP servers when restoring session
2396
+ try {
2397
+ const config = loadMCPConfig();
2398
+ if (config.servers.length > 0) {
2399
+ await initializeMCPServers();
2400
+ }
2401
+ }
2402
+ catch (mcpError) {
2403
+ console.warn("MCP reinitialization failed:", mcpError);
2404
+ }
2405
+ // Dispose old token counter and create new one for the restored model
2406
+ this.tokenCounter.dispose();
2407
+ this.tokenCounter = createTokenCounter(model);
2408
+ // Emit events for UI updates
2409
+ this.emit('backendChange', { backend: state.backend });
2410
+ this.emit('modelChange', { model });
2411
+ }
2412
+ else {
2413
+ console.warn(`Failed to restore backend: API key not found in environment variable ${state.apiKeyEnvVar}`);
2414
+ }
2415
+ }
2416
+ catch (error) {
2417
+ console.warn(`Failed to restore backend configuration:`, error);
2418
+ }
2419
+ }
2420
+ // Restore persona (hook may change backend/model and sets env vars)
2421
+ if (state.persona) {
2422
+ try {
2423
+ await this.setPersona(state.persona, state.personaColor);
2424
+ }
2425
+ catch (error) {
2426
+ console.warn(`Failed to restore persona "${state.persona}":`, error);
2427
+ }
2428
+ }
2429
+ // Restore mood (hook sets env vars)
2430
+ if (state.mood) {
2431
+ try {
2432
+ await this.setMood(state.mood, state.moodColor);
2433
+ }
2434
+ catch (error) {
2435
+ console.warn(`Failed to restore mood "${state.mood}":`, error);
2436
+ }
2437
+ }
2438
+ // Restore active task (hook sets env vars)
2439
+ if (state.activeTask) {
2440
+ try {
2441
+ await this.startActiveTask(state.activeTask, state.activeTaskAction, state.activeTaskColor);
2442
+ }
2443
+ catch (error) {
2444
+ console.warn(`Failed to restore active task "${state.activeTask}":`, error);
2445
+ }
2446
+ }
2447
+ }
2448
+ /**
2449
+ * Get all tool instances and their class names for display purposes
2450
+ */
2451
+ getToolClassInfo() {
2452
+ const toolInstances = this.getToolInstances();
2453
+ return toolInstances.map(({ instance, className }) => ({
2454
+ className,
2455
+ methods: instance.getHandledToolNames ? instance.getHandledToolNames() : []
2456
+ }));
2457
+ }
2458
+ /**
2459
+ * Get all tool instances via reflection
2460
+ */
2461
+ getToolInstances() {
2462
+ const instances = [];
2463
+ // Use reflection to find all tool instance properties
2464
+ const propertyNames = Object.getOwnPropertyNames(this);
2465
+ for (const propName of propertyNames) {
2466
+ const propValue = this[propName];
2467
+ // Check if this property is a tool instance (has getHandledToolNames method)
2468
+ if (propValue &&
2469
+ typeof propValue === 'object' &&
2470
+ typeof propValue.getHandledToolNames === 'function') {
2471
+ instances.push({
2472
+ instance: propValue,
2473
+ className: propValue.constructor.name
2474
+ });
2475
+ }
2476
+ }
2477
+ return instances;
2478
+ }
2479
+ }
2480
+ //# sourceMappingURL=grok-agent.js.map