@zds-ai/cli 0.1.2 → 0.1.4

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 (49) hide show
  1. package/README.md +94 -42
  2. package/dist/agent/grok-agent.d.ts +19 -5
  3. package/dist/agent/grok-agent.js +256 -207
  4. package/dist/agent/grok-agent.js.map +1 -1
  5. package/dist/bin/generate_image_sd.sh +252 -0
  6. package/dist/bin/read_xlsx.py +308 -0
  7. package/dist/grok/client.d.ts +7 -0
  8. package/dist/grok/client.js +88 -7
  9. package/dist/grok/client.js.map +1 -1
  10. package/dist/grok/tools.js +12 -0
  11. package/dist/grok/tools.js.map +1 -1
  12. package/dist/hooks/use-input-handler.d.ts +2 -0
  13. package/dist/hooks/use-input-handler.js +76 -48
  14. package/dist/hooks/use-input-handler.js.map +1 -1
  15. package/dist/index.js +144 -21
  16. package/dist/index.js.map +1 -1
  17. package/dist/mcp/client.js +3 -4
  18. package/dist/mcp/client.js.map +1 -1
  19. package/dist/tools/file-conversion-tool.js +2 -2
  20. package/dist/tools/file-conversion-tool.js.map +1 -1
  21. package/dist/tools/image-tool.d.ts +8 -2
  22. package/dist/tools/image-tool.js +65 -6
  23. package/dist/tools/image-tool.js.map +1 -1
  24. package/dist/tools/search.d.ts +1 -1
  25. package/dist/tools/search.js +41 -53
  26. package/dist/tools/search.js.map +1 -1
  27. package/dist/ui/components/chat-history.js +25 -12
  28. package/dist/ui/components/chat-history.js.map +1 -1
  29. package/dist/ui/components/chat-interface.js +16 -9
  30. package/dist/ui/components/chat-interface.js.map +1 -1
  31. package/dist/ui/components/rephrase-menu.d.ts +8 -0
  32. package/dist/ui/components/rephrase-menu.js +25 -0
  33. package/dist/ui/components/rephrase-menu.js.map +1 -0
  34. package/dist/utils/chat-history-manager.js +16 -6
  35. package/dist/utils/chat-history-manager.js.map +1 -1
  36. package/dist/utils/content-utils.d.ts +5 -0
  37. package/dist/utils/content-utils.js +15 -0
  38. package/dist/utils/content-utils.js.map +1 -0
  39. package/dist/utils/image-encoder.d.ts +35 -0
  40. package/dist/utils/image-encoder.js +133 -0
  41. package/dist/utils/image-encoder.js.map +1 -0
  42. package/dist/utils/rephrase-handler.d.ts +15 -0
  43. package/dist/utils/rephrase-handler.js +106 -0
  44. package/dist/utils/rephrase-handler.js.map +1 -0
  45. package/dist/utils/slash-commands.js +1 -23
  46. package/dist/utils/slash-commands.js.map +1 -1
  47. package/dist/utils/token-counter.js +17 -2
  48. package/dist/utils/token-counter.js.map +1 -1
  49. package/package.json +9 -6
@@ -3,12 +3,75 @@ import { getAllGrokTools, getMCPManager, initializeMCPServers, } from "../grok/t
3
3
  import { loadMCPConfig } from "../mcp/config.js";
4
4
  import { ChatHistoryManager } from "../utils/chat-history-manager.js";
5
5
  import { logApiError } from "../utils/error-logger.js";
6
+ import { parseImagesFromMessage, hasImageReferences } from "../utils/image-encoder.js";
7
+ import { getTextContent } from "../utils/content-utils.js";
6
8
  import fs from "fs";
7
9
  import { TextEditorTool, MorphEditorTool, ZshTool, ConfirmationTool, SearchTool, EnvTool, IntrospectTool, ClearCacheTool, CharacterTool, TaskTool, InternetTool, ImageTool, FileConversionTool, RestartTool } from "../tools/index.js";
8
10
  import { EventEmitter } from "events";
9
11
  import { createTokenCounter } from "../utils/token-counter.js";
10
12
  import { getSettingsManager } from "../utils/settings-manager.js";
11
13
  import { executeOperationHook, executeToolApprovalHook, applyHookCommands } from "../utils/hook-executor.js";
14
+ // Interval (ms) between token count updates when streaming
15
+ const TOKEN_UPDATE_INTERVAL_MS = 250;
16
+ // Minimum delay (in ms) applied when stopping a task to ensure smooth UI/UX.
17
+ const MINIMUM_STOP_TASK_DELAY_MS = 3000;
18
+ // Maximum number of attempts to parse nested JSON strings in executeTool
19
+ const MAX_JSON_PARSE_ATTEMPTS = 5;
20
+ /**
21
+ * Threshold used to determine whether an AI response is "substantial" (in characters).
22
+ */
23
+ const SUBSTANTIAL_RESPONSE_THRESHOLD = 50;
24
+ /**
25
+ * Extracts the first complete JSON object from a string.
26
+ * Handles duplicate/concatenated JSON objects (LLM bug) like: {"key":"val"}{"key":"val"}
27
+ * @param jsonString The string potentially containing concatenated JSON objects
28
+ * @returns The first complete JSON object, or the original string if no duplicates found
29
+ */
30
+ function extractFirstJsonObject(jsonString) {
31
+ if (!jsonString.includes('}{'))
32
+ return jsonString;
33
+ try {
34
+ // Find the end of the first complete JSON object
35
+ let depth = 0;
36
+ let firstObjEnd = -1;
37
+ for (let i = 0; i < jsonString.length; i++) {
38
+ if (jsonString[i] === "{")
39
+ depth++;
40
+ if (jsonString[i] === "}") {
41
+ depth--;
42
+ if (depth === 0) {
43
+ firstObjEnd = i + 1;
44
+ break;
45
+ }
46
+ }
47
+ }
48
+ if (firstObjEnd > 0 && firstObjEnd < jsonString.length) {
49
+ // Extract and validate first object
50
+ const firstObj = jsonString.substring(0, firstObjEnd);
51
+ JSON.parse(firstObj); // Validate it's valid JSON
52
+ return firstObj;
53
+ }
54
+ }
55
+ catch {
56
+ // If extraction fails, return the original string
57
+ }
58
+ return jsonString;
59
+ }
60
+ /**
61
+ * Cleans up LLM-generated JSON argument strings for tool calls.
62
+ * Removes duplicate/concatenated JSON objects and trims.
63
+ * @param args The raw arguments string from the tool call
64
+ * @returns Cleaned and sanitized argument string
65
+ */
66
+ function sanitizeToolArguments(args) {
67
+ let argsString = args?.trim() || "{}";
68
+ // Handle duplicate/concatenated JSON objects (LLM bug)
69
+ const extractedArgsString = extractFirstJsonObject(argsString);
70
+ if (extractedArgsString !== argsString) {
71
+ argsString = extractedArgsString;
72
+ }
73
+ return argsString;
74
+ }
12
75
  export class GrokAgent extends EventEmitter {
13
76
  grokClient;
14
77
  textEditor;
@@ -44,7 +107,8 @@ export class GrokAgent extends EventEmitter {
44
107
  activeTaskAction = "";
45
108
  activeTaskColor = "white";
46
109
  apiKeyEnvVar = "GROK_API_KEY";
47
- pendingContextEdit = null;
110
+ pendingContextEditSession = null;
111
+ rephraseState = null;
48
112
  constructor(apiKey, baseURL, model, maxToolRounds, debugLogFile, startupHookOutput, temperature, maxTokens) {
49
113
  super();
50
114
  const manager = getSettingsManager();
@@ -168,21 +232,24 @@ Current working directory: ${process.cwd()}`;
168
232
  switch (entry.type) {
169
233
  case "system":
170
234
  // All system messages from chatHistory go into conversation (persona, mood, etc.)
235
+ // System messages must always be strings
171
236
  historyMessages.push({
172
237
  role: "system",
173
- content: entry.content,
238
+ content: getTextContent(entry.content),
174
239
  });
175
240
  break;
176
241
  case "user":
242
+ // User messages can have images (content arrays)
177
243
  historyMessages.push({
178
244
  role: "user",
179
- content: entry.content,
245
+ content: entry.content || "",
180
246
  });
181
247
  break;
182
248
  case "assistant":
249
+ // Assistant messages are always text (no images in responses)
183
250
  const assistantMessage = {
184
251
  role: "assistant",
185
- content: entry.content || "", // Ensure content is never null/undefined
252
+ content: getTextContent(entry.content) || "", // Ensure content is never null/undefined
186
253
  };
187
254
  if (entry.tool_calls && entry.tool_calls.length > 0) {
188
255
  // For assistant messages with tool calls, collect the tool results that correspond to them
@@ -283,6 +350,39 @@ Current working directory: ${process.cwd()}`;
283
350
  return false;
284
351
  }
285
352
  async processUserMessage(message) {
353
+ // Detect rephrase commands
354
+ let isRephraseCommand = false;
355
+ let isSystemRephrase = false;
356
+ let messageToSend = message;
357
+ let messageType = "user";
358
+ if (message.startsWith("/system rephrase")) {
359
+ isRephraseCommand = true;
360
+ isSystemRephrase = true;
361
+ messageToSend = message.substring(8).trim(); // Strip "/system " (8 chars including space)
362
+ messageType = "system";
363
+ }
364
+ else if (message.startsWith("/rephrase")) {
365
+ isRephraseCommand = true;
366
+ messageToSend = message; // Keep full text including "/rephrase"
367
+ messageType = "user";
368
+ }
369
+ // If this is a rephrase command, find the last assistant message
370
+ if (isRephraseCommand) {
371
+ // Find index of last assistant message in chatHistory
372
+ let lastAssistantIndex = -1;
373
+ for (let i = this.chatHistory.length - 1; i >= 0; i--) {
374
+ if (this.chatHistory[i].type === "assistant") {
375
+ lastAssistantIndex = i;
376
+ break;
377
+ }
378
+ }
379
+ if (lastAssistantIndex === -1) {
380
+ throw new Error("No previous assistant message to rephrase");
381
+ }
382
+ // Store rephrase state (will be updated with newResponseIndex after response)
383
+ // For now, just mark that we're in rephrase mode
384
+ this.setRephraseState(lastAssistantIndex, this.chatHistory.length, -1, messageType);
385
+ }
286
386
  // Before adding the new user message, check if there are incomplete tool calls
287
387
  // from a previous interrupted turn. This prevents malformed message sequences
288
388
  // that cause Ollama 500 errors.
@@ -313,14 +413,40 @@ Current working directory: ${process.cwd()}`;
313
413
  }
314
414
  }
315
415
  }
316
- // Add user message to conversation
416
+ // Add user/system message to conversation
417
+ // Check for image references and parse if present
418
+ // Note: System messages can only have string content, so images are only supported for user messages
419
+ const supportsVision = this.grokClient.getSupportsVision();
420
+ let messageContent = messageToSend;
421
+ if (messageType === "user" && hasImageReferences(messageToSend) && supportsVision) {
422
+ // Parse images from message (only for user messages)
423
+ const parsed = parseImagesFromMessage(messageToSend);
424
+ if (parsed.images.length > 0) {
425
+ // Construct content array with text and images
426
+ messageContent = [
427
+ { type: "text", text: parsed.text },
428
+ ...parsed.images
429
+ ];
430
+ }
431
+ else {
432
+ // No valid images found (errors will be in parsed.text)
433
+ messageContent = parsed.text;
434
+ }
435
+ }
317
436
  const userEntry = {
318
- type: "user",
319
- content: message,
437
+ type: messageType,
438
+ content: messageContent,
320
439
  timestamp: new Date(),
321
440
  };
322
441
  this.chatHistory.push(userEntry);
323
- this.messages.push({ role: "user", content: message });
442
+ // Push to messages array with proper typing based on role
443
+ if (messageType === "user") {
444
+ this.messages.push({ role: "user", content: messageContent });
445
+ }
446
+ else {
447
+ // System messages must have string content only
448
+ this.messages.push({ role: "system", content: typeof messageContent === "string" ? messageContent : messageToSend });
449
+ }
324
450
  await this.emitContextChange();
325
451
  const newEntries = [userEntry];
326
452
  const maxToolRounds = this.maxToolRounds; // Prevent infinite loops
@@ -350,33 +476,7 @@ Current working directory: ${process.cwd()}`;
350
476
  // Clean up tool call arguments before adding to conversation history
351
477
  // This prevents Ollama from rejecting malformed tool calls on subsequent API calls
352
478
  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
- }
479
+ let argsString = sanitizeToolArguments(toolCall.function.arguments);
380
480
  return {
381
481
  ...toolCall,
382
482
  function: {
@@ -494,10 +594,11 @@ Current working directory: ${process.cwd()}`;
494
594
  // Collect system messages that appeared after this assistant message
495
595
  for (let i = assistantIndex + 1; i < this.chatHistory.length; i++) {
496
596
  const entry = this.chatHistory[i];
497
- if (entry.type === 'system' && entry.content && entry.content.trim()) {
597
+ const content = getTextContent(entry.content);
598
+ if (entry.type === 'system' && content && content.trim()) {
498
599
  this.messages.push({
499
600
  role: 'system',
500
- content: entry.content
601
+ content: content
501
602
  });
502
603
  }
503
604
  // Stop if we hit another assistant or user message (next turn)
@@ -538,6 +639,11 @@ Current working directory: ${process.cwd()}`;
538
639
  content: trimmedContent,
539
640
  });
540
641
  newEntries.push(responseEntry);
642
+ // Update rephrase state with the new response index
643
+ if (this.rephraseState && this.rephraseState.newResponseIndex === -1) {
644
+ const newResponseIndex = this.chatHistory.length - 1;
645
+ this.setRephraseState(this.rephraseState.originalAssistantMessageIndex, this.rephraseState.rephraseRequestIndex, newResponseIndex, this.rephraseState.messageType);
646
+ }
541
647
  }
542
648
  // TODO: HACK - This is a temporary fix to prevent duplicate responses.
543
649
  // We need a proper way for the bot to signal task completion, such as:
@@ -546,8 +652,8 @@ Current working directory: ${process.cwd()}`;
546
652
  // - A structured response format that explicitly marks completion
547
653
  // For now, we break immediately after a substantial response to avoid
548
654
  // 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) {
655
+ // If the AI provided a substantial response (>SUBSTANTIAL_RESPONSE_THRESHOLD chars), task is complete
656
+ if (assistantMessage.content && assistantMessage.content.trim().length > SUBSTANTIAL_RESPONSE_THRESHOLD) {
551
657
  break; // Task complete - bot gave a full response
552
658
  }
553
659
  // Short/empty response, give AI another chance
@@ -693,6 +799,39 @@ Current working directory: ${process.cwd()}`;
693
799
  async *processUserMessageStream(message) {
694
800
  // Create new abort controller for this request
695
801
  this.abortController = new AbortController();
802
+ // Detect rephrase commands
803
+ let isRephraseCommand = false;
804
+ let isSystemRephrase = false;
805
+ let messageToSend = message;
806
+ let messageType = "user";
807
+ if (message.startsWith("/system rephrase")) {
808
+ isRephraseCommand = true;
809
+ isSystemRephrase = true;
810
+ messageToSend = message.substring(8).trim(); // Strip "/system " (8 chars including space)
811
+ messageType = "system";
812
+ }
813
+ else if (message.startsWith("/rephrase")) {
814
+ isRephraseCommand = true;
815
+ messageToSend = message; // Keep full text including "/rephrase"
816
+ messageType = "user";
817
+ }
818
+ // If this is a rephrase command, find the last assistant message
819
+ if (isRephraseCommand) {
820
+ // Find index of last assistant message in chatHistory
821
+ let lastAssistantIndex = -1;
822
+ for (let i = this.chatHistory.length - 1; i >= 0; i--) {
823
+ if (this.chatHistory[i].type === "assistant") {
824
+ lastAssistantIndex = i;
825
+ break;
826
+ }
827
+ }
828
+ if (lastAssistantIndex === -1) {
829
+ throw new Error("No previous assistant message to rephrase");
830
+ }
831
+ // Store rephrase state (will be updated with newResponseIndex after response)
832
+ // For now, just mark that we're in rephrase mode
833
+ this.setRephraseState(lastAssistantIndex, this.chatHistory.length, -1, messageType);
834
+ }
696
835
  // Before adding the new user message, check if there are incomplete tool calls
697
836
  // from a previous interrupted turn. This prevents malformed message sequences
698
837
  // that cause Ollama 500 errors.
@@ -723,14 +862,40 @@ Current working directory: ${process.cwd()}`;
723
862
  }
724
863
  }
725
864
  }
726
- // Add user message to both API conversation and chat history
865
+ // Add user/system message to both API conversation and chat history
866
+ // Check for image references and parse if present
867
+ // Note: System messages can only have string content, so images are only supported for user messages
868
+ const supportsVision = this.grokClient.getSupportsVision();
869
+ let messageContent = messageToSend;
870
+ if (messageType === "user" && hasImageReferences(messageToSend) && supportsVision) {
871
+ // Parse images from message (only for user messages)
872
+ const parsed = parseImagesFromMessage(messageToSend);
873
+ if (parsed.images.length > 0) {
874
+ // Construct content array with text and images
875
+ messageContent = [
876
+ { type: "text", text: parsed.text },
877
+ ...parsed.images
878
+ ];
879
+ }
880
+ else {
881
+ // No valid images found (errors will be in parsed.text)
882
+ messageContent = parsed.text;
883
+ }
884
+ }
727
885
  const userEntry = {
728
- type: "user",
729
- content: message,
886
+ type: messageType,
887
+ content: messageContent,
730
888
  timestamp: new Date(),
731
889
  };
732
890
  this.chatHistory.push(userEntry);
733
- this.messages.push({ role: "user", content: message });
891
+ // Push to messages array with proper typing based on role
892
+ if (messageType === "user") {
893
+ this.messages.push({ role: "user", content: messageContent });
894
+ }
895
+ else {
896
+ // System messages must have string content only
897
+ this.messages.push({ role: "system", content: typeof messageContent === "string" ? messageContent : messageToSend });
898
+ }
734
899
  await this.emitContextChange();
735
900
  // Yield user message so UI can display it immediately
736
901
  yield {
@@ -847,7 +1012,7 @@ Current working directory: ${process.cwd()}`;
847
1012
  };
848
1013
  // Emit token count update
849
1014
  const now = Date.now();
850
- if (now - lastTokenUpdate > 250) {
1015
+ if (now - lastTokenUpdate > TOKEN_UPDATE_INTERVAL_MS) {
851
1016
  lastTokenUpdate = now;
852
1017
  yield {
853
1018
  type: "token_count",
@@ -875,33 +1040,7 @@ Current working directory: ${process.cwd()}`;
875
1040
  // Clean up tool call arguments before adding to conversation history
876
1041
  // This prevents Ollama from rejecting malformed tool calls on subsequent API calls
877
1042
  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
- }
1043
+ let argsString = sanitizeToolArguments(toolCall.function.arguments);
905
1044
  return {
906
1045
  ...toolCall,
907
1046
  function: {
@@ -925,6 +1064,11 @@ Current working directory: ${process.cwd()}`;
925
1064
  };
926
1065
  this.chatHistory.push(assistantEntry);
927
1066
  await this.emitContextChange();
1067
+ // Update rephrase state if this is a final response (no tool calls)
1068
+ if (this.rephraseState && this.rephraseState.newResponseIndex === -1 && (!accumulatedMessage.tool_calls || accumulatedMessage.tool_calls.length === 0)) {
1069
+ const newResponseIndex = this.chatHistory.length - 1;
1070
+ this.setRephraseState(this.rephraseState.originalAssistantMessageIndex, this.rephraseState.rephraseRequestIndex, newResponseIndex, this.rephraseState.messageType);
1071
+ }
928
1072
  // Handle tool calls if present
929
1073
  if (accumulatedMessage.tool_calls?.length > 0) {
930
1074
  toolRounds++;
@@ -1039,10 +1183,11 @@ Current working directory: ${process.cwd()}`;
1039
1183
  // Collect system messages that appeared after this assistant message
1040
1184
  for (let i = assistantIndex + 1; i < this.chatHistory.length; i++) {
1041
1185
  const entry = this.chatHistory[i];
1042
- if (entry.type === 'system' && entry.content && entry.content.trim()) {
1186
+ const content = getTextContent(entry.content);
1187
+ if (entry.type === 'system' && content && content.trim()) {
1043
1188
  this.messages.push({
1044
1189
  role: 'system',
1045
- content: entry.content
1190
+ content: content
1046
1191
  });
1047
1192
  }
1048
1193
  // Stop if we hit another assistant or user message (next turn)
@@ -1100,7 +1245,7 @@ Current working directory: ${process.cwd()}`;
1100
1245
  this.chatHistory.push(errorEntry);
1101
1246
  yield {
1102
1247
  type: "content",
1103
- content: errorEntry.content,
1248
+ content: getTextContent(errorEntry.content),
1104
1249
  };
1105
1250
  // Mark first message as processed even on error
1106
1251
  this.firstMessageProcessed = true;
@@ -1178,41 +1323,16 @@ Current working directory: ${process.cwd()}`;
1178
1323
  // Handle duplicate/concatenated JSON objects (LLM bug)
1179
1324
  // Pattern: {"key":"val"}{"key":"val"}
1180
1325
  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
- }
1326
+ const extractedArgsString = extractFirstJsonObject(argsString);
1327
+ if (extractedArgsString !== argsString) {
1328
+ hadDuplicateJson = true;
1329
+ argsString = extractedArgsString;
1210
1330
  }
1211
1331
  let args = JSON.parse(argsString);
1212
1332
  // Handle multiple layers of JSON encoding (API bug)
1213
1333
  // Keep parsing until we get an object, not a string
1214
1334
  let parseCount = 0;
1215
- while (typeof args === 'string' && parseCount < 5) {
1335
+ while (typeof args === 'string' && parseCount < MAX_JSON_PARSE_ATTEMPTS) {
1216
1336
  parseCount++;
1217
1337
  try {
1218
1338
  args = JSON.parse(args);
@@ -1261,18 +1381,9 @@ Current working directory: ${process.cwd()}`;
1261
1381
  // Validate tool arguments against schema
1262
1382
  const validationError = await this.validateToolArguments(toolCall.function.name, args);
1263
1383
  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
- });
1384
+ // Validation failed - return error
1385
+ const errorMsg = `Tool call validation failed: ${validationError}. Please try again with correct parameters.`;
1386
+ console.warn(`[VALIDATION ERROR] ${errorMsg}`);
1276
1387
  return {
1277
1388
  success: false,
1278
1389
  error: validationError
@@ -1388,6 +1499,8 @@ Current working directory: ${process.cwd()}`;
1388
1499
  return await this.imageTool.captionImage(args.filename, args.prompt);
1389
1500
  case "pngInfo":
1390
1501
  return await this.imageTool.pngInfo(args.filename);
1502
+ case "listImageModels":
1503
+ return await this.imageTool.listImageModels();
1391
1504
  case "readXlsx":
1392
1505
  return await this.fileConversionTool.readXlsx(args.filename, args.sheetName, args.outputFormat, args.output);
1393
1506
  case "listXlsxSheets":
@@ -1509,17 +1622,17 @@ Current working directory: ${process.cwd()}`;
1509
1622
  const timestamp = entry.timestamp.toLocaleTimeString();
1510
1623
  if (entry.type === 'user') {
1511
1624
  lines.push(`(${msgNum}) ${userName} (user) - ${timestamp}`);
1512
- lines.push(entry.content || "");
1625
+ lines.push(getTextContent(entry.content) || "");
1513
1626
  lines.push("");
1514
1627
  }
1515
1628
  else if (entry.type === 'assistant') {
1516
1629
  lines.push(`(${msgNum}) ${agentName} (assistant) - ${timestamp}`);
1517
- lines.push(entry.content || "");
1630
+ lines.push(getTextContent(entry.content) || "");
1518
1631
  lines.push("");
1519
1632
  }
1520
1633
  else if (entry.type === 'system') {
1521
1634
  lines.push(`(${msgNum}) System (system) - ${timestamp}`);
1522
- lines.push(entry.content || "");
1635
+ lines.push(getTextContent(entry.content) || "");
1523
1636
  lines.push("");
1524
1637
  }
1525
1638
  else if (entry.type === 'tool_call') {
@@ -1534,7 +1647,7 @@ Current working directory: ${process.cwd()}`;
1534
1647
  const toolCall = entry.toolCall;
1535
1648
  const toolName = toolCall?.function?.name || "unknown";
1536
1649
  lines.push(`(${msgNum}) System (tool_result: ${toolName}) - ${timestamp}`);
1537
- lines.push(entry.content || "");
1650
+ lines.push(getTextContent(entry.content) || "");
1538
1651
  lines.push("");
1539
1652
  }
1540
1653
  });
@@ -1561,14 +1674,23 @@ Current working directory: ${process.cwd()}`;
1561
1674
  getActiveTaskColor() {
1562
1675
  return this.activeTaskColor;
1563
1676
  }
1564
- setPendingContextEdit(tmpJsonPath, contextFilePath) {
1565
- this.pendingContextEdit = { tmpJsonPath, contextFilePath };
1677
+ setPendingContextEditSession(tmpJsonPath, contextFilePath) {
1678
+ this.pendingContextEditSession = { tmpJsonPath, contextFilePath };
1679
+ }
1680
+ getPendingContextEditSession() {
1681
+ return this.pendingContextEditSession;
1682
+ }
1683
+ clearPendingContextEditSession() {
1684
+ this.pendingContextEditSession = null;
1685
+ }
1686
+ setRephraseState(originalAssistantMessageIndex, rephraseRequestIndex, newResponseIndex, messageType) {
1687
+ this.rephraseState = { originalAssistantMessageIndex, rephraseRequestIndex, newResponseIndex, messageType };
1566
1688
  }
1567
- getPendingContextEdit() {
1568
- return this.pendingContextEdit;
1689
+ getRephraseState() {
1690
+ return this.rephraseState;
1569
1691
  }
1570
- clearPendingContextEdit() {
1571
- this.pendingContextEdit = null;
1692
+ clearRephraseState() {
1693
+ this.rephraseState = null;
1572
1694
  }
1573
1695
  async setPersona(persona, color) {
1574
1696
  // Execute hook if configured
@@ -1577,12 +1699,6 @@ Current working directory: ${process.cwd()}`;
1577
1699
  const hookMandatory = settings.isPersonaHookMandatory();
1578
1700
  if (!hookPath && hookMandatory) {
1579
1701
  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
1702
  return {
1587
1703
  success: false,
1588
1704
  error: reason
@@ -1600,24 +1716,13 @@ Current working directory: ${process.cwd()}`;
1600
1716
  // Even in rejection, we process commands (might have MODEL change)
1601
1717
  await this.processHookResult(hookResult);
1602
1718
  // 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
1719
  return {
1610
1720
  success: false,
1611
1721
  error: reason
1612
1722
  };
1613
1723
  }
1614
1724
  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
- });
1725
+ // Hook timed out but was auto-approved
1621
1726
  }
1622
1727
  // Process hook commands (ENV, MODEL, SYSTEM)
1623
1728
  const result = await this.processHookResult(hookResult, 'ZDS_AI_AGENT_PERSONA');
@@ -1638,25 +1743,7 @@ Current working directory: ${process.cwd()}`;
1638
1743
  this.persona = persona;
1639
1744
  this.personaColor = color || "white";
1640
1745
  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
- });
1746
+ // Persona hook generates success message - no need for redundant CLI message
1660
1747
  this.emit('personaChange', {
1661
1748
  persona: this.persona,
1662
1749
  color: this.personaColor
@@ -1670,12 +1757,6 @@ Current working directory: ${process.cwd()}`;
1670
1757
  const hookMandatory = settings.isMoodHookMandatory();
1671
1758
  if (!hookPath && hookMandatory) {
1672
1759
  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
1760
  return {
1680
1761
  success: false,
1681
1762
  error: reason
@@ -1691,24 +1772,13 @@ Current working directory: ${process.cwd()}`;
1691
1772
  const reason = hookResult.reason || "Hook rejected mood change";
1692
1773
  // Process rejection commands (MODEL, SYSTEM)
1693
1774
  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
1775
  return {
1701
1776
  success: false,
1702
1777
  error: reason
1703
1778
  };
1704
1779
  }
1705
1780
  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
- });
1781
+ // Hook timed out but was auto-approved
1712
1782
  }
1713
1783
  // Process hook commands (ENV, MODEL, SYSTEM)
1714
1784
  const result = await this.processHookResult(hookResult, 'ZDS_AI_AGENT_MOOD');
@@ -1776,20 +1846,13 @@ Current working directory: ${process.cwd()}`;
1776
1846
  await this.processHookResult(hookResult);
1777
1847
  if (!hookResult.approved) {
1778
1848
  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
1849
  return {
1784
1850
  success: false,
1785
1851
  error: reason
1786
1852
  };
1787
1853
  }
1788
1854
  if (hookResult.timedOut) {
1789
- this.messages.push({
1790
- role: 'system',
1791
- content: `Task start hook timed out (auto-approved)`
1792
- });
1855
+ // Hook timed out but was auto-approved
1793
1856
  }
1794
1857
  }
1795
1858
  // Set the task
@@ -1831,20 +1894,13 @@ Current working directory: ${process.cwd()}`;
1831
1894
  await this.processHookResult(hookResult);
1832
1895
  if (!hookResult.approved) {
1833
1896
  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
1897
  return {
1839
1898
  success: false,
1840
1899
  error: reason
1841
1900
  };
1842
1901
  }
1843
1902
  if (hookResult.timedOut) {
1844
- this.messages.push({
1845
- role: 'system',
1846
- content: `Task transition hook timed out (auto-approved)`
1847
- });
1903
+ // Hook timed out but was auto-approved
1848
1904
  }
1849
1905
  }
1850
1906
  // Store old action for system message
@@ -1890,25 +1946,18 @@ Current working directory: ${process.cwd()}`;
1890
1946
  await this.processHookResult(hookResult);
1891
1947
  if (!hookResult.approved) {
1892
1948
  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
1949
  return {
1898
1950
  success: false,
1899
1951
  error: hookReason
1900
1952
  };
1901
1953
  }
1902
1954
  if (hookResult.timedOut) {
1903
- this.messages.push({
1904
- role: 'system',
1905
- content: `Task stop hook timed out (auto-approved)`
1906
- });
1955
+ // Hook timed out but was auto-approved
1907
1956
  }
1908
1957
  }
1909
1958
  // Calculate remaining time to meet 3-second minimum
1910
1959
  const elapsed = Date.now() - startTime;
1911
- const minimumDelay = 3000;
1960
+ const minimumDelay = MINIMUM_STOP_TASK_DELAY_MS;
1912
1961
  const remainingDelay = Math.max(0, minimumDelay - elapsed);
1913
1962
  // Wait for remaining time if needed
1914
1963
  if (remainingDelay > 0) {
@@ -2410,7 +2459,7 @@ Current working directory: ${process.cwd()}`;
2410
2459
  this.emit('modelChange', { model });
2411
2460
  }
2412
2461
  else {
2413
- console.warn(`Failed to restore backend: API key not found in environment variable ${state.apiKeyEnvVar}`);
2462
+ console.warn("Failed to restore backend: API key not found in environment.");
2414
2463
  }
2415
2464
  }
2416
2465
  catch (error) {