@zds-ai/cli 0.1.3 → 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 (45) hide show
  1. package/README.md +32 -0
  2. package/dist/agent/grok-agent.d.ts +15 -1
  3. package/dist/agent/grok-agent.js +176 -111
  4. package/dist/agent/grok-agent.js.map +1 -1
  5. package/dist/bin/generate_image_sd.sh +252 -0
  6. package/dist/grok/client.d.ts +3 -0
  7. package/dist/grok/client.js +62 -1
  8. package/dist/grok/client.js.map +1 -1
  9. package/dist/grok/tools.js +12 -0
  10. package/dist/grok/tools.js.map +1 -1
  11. package/dist/hooks/use-input-handler.d.ts +2 -0
  12. package/dist/hooks/use-input-handler.js +70 -44
  13. package/dist/hooks/use-input-handler.js.map +1 -1
  14. package/dist/index.js +97 -7
  15. package/dist/index.js.map +1 -1
  16. package/dist/mcp/client.js +3 -4
  17. package/dist/mcp/client.js.map +1 -1
  18. package/dist/tools/file-conversion-tool.js +4 -11
  19. package/dist/tools/file-conversion-tool.js.map +1 -1
  20. package/dist/tools/image-tool.d.ts +6 -1
  21. package/dist/tools/image-tool.js +53 -3
  22. package/dist/tools/image-tool.js.map +1 -1
  23. package/dist/ui/components/chat-history.js +25 -12
  24. package/dist/ui/components/chat-history.js.map +1 -1
  25. package/dist/ui/components/chat-interface.js +16 -9
  26. package/dist/ui/components/chat-interface.js.map +1 -1
  27. package/dist/ui/components/rephrase-menu.d.ts +8 -0
  28. package/dist/ui/components/rephrase-menu.js +25 -0
  29. package/dist/ui/components/rephrase-menu.js.map +1 -0
  30. package/dist/utils/chat-history-manager.js +16 -6
  31. package/dist/utils/chat-history-manager.js.map +1 -1
  32. package/dist/utils/content-utils.d.ts +5 -0
  33. package/dist/utils/content-utils.js +15 -0
  34. package/dist/utils/content-utils.js.map +1 -0
  35. package/dist/utils/image-encoder.d.ts +35 -0
  36. package/dist/utils/image-encoder.js +133 -0
  37. package/dist/utils/image-encoder.js.map +1 -0
  38. package/dist/utils/rephrase-handler.d.ts +15 -0
  39. package/dist/utils/rephrase-handler.js +106 -0
  40. package/dist/utils/rephrase-handler.js.map +1 -0
  41. package/dist/utils/slash-commands.js +1 -23
  42. package/dist/utils/slash-commands.js.map +1 -1
  43. package/dist/utils/token-counter.js +17 -2
  44. package/dist/utils/token-counter.js.map +1 -1
  45. package/package.json +5 -3
package/README.md CHANGED
@@ -339,6 +339,38 @@ Add to `~/.grok/user-settings.json`:
339
339
 
340
340
  **Model Priority**: `--model` flag > `GROK_MODEL` environment variable > user default model > system default (grok-code-fast-1)
341
341
 
342
+ ### Image Support
343
+
344
+ zai-cli supports sending images to vision-capable AI models. Use the `@` prefix to reference image files in your messages:
345
+
346
+ ```sh
347
+ # Absolute path
348
+ zai-cli --prompt "What's in this image? @/Users/joseph/photos/image.jpg"
349
+
350
+ # Relative path
351
+ zai-cli --prompt "Analyze @./screenshot.png"
352
+
353
+ # Tilde expansion
354
+ zai-cli --prompt "Describe @~/Pictures/photo.jpg"
355
+
356
+ # Paths with spaces (quoted)
357
+ zai-cli --prompt 'Compare these images: @"~/My Pictures/photo1.jpg" @"~/My Pictures/photo2.jpg"'
358
+
359
+ # Paths with spaces (escaped)
360
+ zai-cli --prompt "What's here? @/Users/joseph/My\ Documents/image.png"
361
+ ```
362
+
363
+ **Supported Image Formats**: .jpg, .jpeg, .png, .gif, .webp, .bmp
364
+
365
+ **Vision-Capable Models**: Image support works with vision models like:
366
+ - `grok-4-1-fast-reasoning`
367
+ - `grok-vision-beta`
368
+ - Other vision-enabled models (via custom base URLs)
369
+
370
+ **Automatic Fallback**: If you send an image to a model that doesn't support vision, zai-cli will automatically detect the error and retry with text-only content.
371
+
372
+ **Interactive Mode**: The `@` syntax works in both interactive and headless (`--prompt`) modes.
373
+
342
374
  ### Command Line Options
343
375
 
344
376
  ```sh
@@ -1,9 +1,10 @@
1
1
  import { GrokMessage, GrokToolCall } from "../grok/client.js";
2
+ import type { ChatCompletionContentPart } from "openai/resources/chat/completions.js";
2
3
  import { ToolResult } from "../types/index.js";
3
4
  import { EventEmitter } from "events";
4
5
  export interface ChatEntry {
5
6
  type: "user" | "assistant" | "tool_result" | "tool_call" | "system";
6
- content?: string;
7
+ content?: string | ChatCompletionContentPart[];
7
8
  timestamp: Date;
8
9
  tool_calls?: GrokToolCall[];
9
10
  toolCall?: GrokToolCall;
@@ -15,6 +16,10 @@ export interface ChatEntry {
15
16
  };
16
17
  isStreaming?: boolean;
17
18
  preserveFormatting?: boolean;
19
+ metadata?: {
20
+ rephrased_note?: string;
21
+ [key: string]: any;
22
+ };
18
23
  }
19
24
  export interface StreamingChunk {
20
25
  type: "content" | "tool_calls" | "tool_result" | "done" | "token_count" | "user_message";
@@ -62,6 +67,7 @@ export declare class GrokAgent extends EventEmitter {
62
67
  private activeTaskColor;
63
68
  private apiKeyEnvVar;
64
69
  private pendingContextEditSession;
70
+ private rephraseState;
65
71
  constructor(apiKey: string, baseURL?: string, model?: string, maxToolRounds?: number, debugLogFile?: string, startupHookOutput?: string, temperature?: number, maxTokens?: number);
66
72
  private startupHookOutput?;
67
73
  private systemPrompt;
@@ -125,6 +131,14 @@ export declare class GrokAgent extends EventEmitter {
125
131
  contextFilePath: string;
126
132
  } | null;
127
133
  clearPendingContextEditSession(): void;
134
+ setRephraseState(originalAssistantMessageIndex: number, rephraseRequestIndex: number, newResponseIndex: number, messageType: "user" | "system"): void;
135
+ getRephraseState(): {
136
+ originalAssistantMessageIndex: number;
137
+ rephraseRequestIndex: number;
138
+ newResponseIndex: number;
139
+ messageType: "user" | "system";
140
+ } | null;
141
+ clearRephraseState(): void;
128
142
  setPersona(persona: string, color?: string): Promise<{
129
143
  success: boolean;
130
144
  error?: string;
@@ -3,6 +3,8 @@ 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";
@@ -106,6 +108,7 @@ export class GrokAgent extends EventEmitter {
106
108
  activeTaskColor = "white";
107
109
  apiKeyEnvVar = "GROK_API_KEY";
108
110
  pendingContextEditSession = null;
111
+ rephraseState = null;
109
112
  constructor(apiKey, baseURL, model, maxToolRounds, debugLogFile, startupHookOutput, temperature, maxTokens) {
110
113
  super();
111
114
  const manager = getSettingsManager();
@@ -229,21 +232,24 @@ Current working directory: ${process.cwd()}`;
229
232
  switch (entry.type) {
230
233
  case "system":
231
234
  // All system messages from chatHistory go into conversation (persona, mood, etc.)
235
+ // System messages must always be strings
232
236
  historyMessages.push({
233
237
  role: "system",
234
- content: entry.content,
238
+ content: getTextContent(entry.content),
235
239
  });
236
240
  break;
237
241
  case "user":
242
+ // User messages can have images (content arrays)
238
243
  historyMessages.push({
239
244
  role: "user",
240
- content: entry.content,
245
+ content: entry.content || "",
241
246
  });
242
247
  break;
243
248
  case "assistant":
249
+ // Assistant messages are always text (no images in responses)
244
250
  const assistantMessage = {
245
251
  role: "assistant",
246
- content: entry.content || "", // Ensure content is never null/undefined
252
+ content: getTextContent(entry.content) || "", // Ensure content is never null/undefined
247
253
  };
248
254
  if (entry.tool_calls && entry.tool_calls.length > 0) {
249
255
  // For assistant messages with tool calls, collect the tool results that correspond to them
@@ -344,6 +350,39 @@ Current working directory: ${process.cwd()}`;
344
350
  return false;
345
351
  }
346
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
+ }
347
386
  // Before adding the new user message, check if there are incomplete tool calls
348
387
  // from a previous interrupted turn. This prevents malformed message sequences
349
388
  // that cause Ollama 500 errors.
@@ -374,14 +413,40 @@ Current working directory: ${process.cwd()}`;
374
413
  }
375
414
  }
376
415
  }
377
- // 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
+ }
378
436
  const userEntry = {
379
- type: "user",
380
- content: message,
437
+ type: messageType,
438
+ content: messageContent,
381
439
  timestamp: new Date(),
382
440
  };
383
441
  this.chatHistory.push(userEntry);
384
- 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
+ }
385
450
  await this.emitContextChange();
386
451
  const newEntries = [userEntry];
387
452
  const maxToolRounds = this.maxToolRounds; // Prevent infinite loops
@@ -529,10 +594,11 @@ Current working directory: ${process.cwd()}`;
529
594
  // Collect system messages that appeared after this assistant message
530
595
  for (let i = assistantIndex + 1; i < this.chatHistory.length; i++) {
531
596
  const entry = this.chatHistory[i];
532
- if (entry.type === 'system' && entry.content && entry.content.trim()) {
597
+ const content = getTextContent(entry.content);
598
+ if (entry.type === 'system' && content && content.trim()) {
533
599
  this.messages.push({
534
600
  role: 'system',
535
- content: entry.content
601
+ content: content
536
602
  });
537
603
  }
538
604
  // Stop if we hit another assistant or user message (next turn)
@@ -573,6 +639,11 @@ Current working directory: ${process.cwd()}`;
573
639
  content: trimmedContent,
574
640
  });
575
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
+ }
576
647
  }
577
648
  // TODO: HACK - This is a temporary fix to prevent duplicate responses.
578
649
  // We need a proper way for the bot to signal task completion, such as:
@@ -728,6 +799,39 @@ Current working directory: ${process.cwd()}`;
728
799
  async *processUserMessageStream(message) {
729
800
  // Create new abort controller for this request
730
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
+ }
731
835
  // Before adding the new user message, check if there are incomplete tool calls
732
836
  // from a previous interrupted turn. This prevents malformed message sequences
733
837
  // that cause Ollama 500 errors.
@@ -758,14 +862,40 @@ Current working directory: ${process.cwd()}`;
758
862
  }
759
863
  }
760
864
  }
761
- // 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
+ }
762
885
  const userEntry = {
763
- type: "user",
764
- content: message,
886
+ type: messageType,
887
+ content: messageContent,
765
888
  timestamp: new Date(),
766
889
  };
767
890
  this.chatHistory.push(userEntry);
768
- 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
+ }
769
899
  await this.emitContextChange();
770
900
  // Yield user message so UI can display it immediately
771
901
  yield {
@@ -934,6 +1064,11 @@ Current working directory: ${process.cwd()}`;
934
1064
  };
935
1065
  this.chatHistory.push(assistantEntry);
936
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
+ }
937
1072
  // Handle tool calls if present
938
1073
  if (accumulatedMessage.tool_calls?.length > 0) {
939
1074
  toolRounds++;
@@ -1048,10 +1183,11 @@ Current working directory: ${process.cwd()}`;
1048
1183
  // Collect system messages that appeared after this assistant message
1049
1184
  for (let i = assistantIndex + 1; i < this.chatHistory.length; i++) {
1050
1185
  const entry = this.chatHistory[i];
1051
- if (entry.type === 'system' && entry.content && entry.content.trim()) {
1186
+ const content = getTextContent(entry.content);
1187
+ if (entry.type === 'system' && content && content.trim()) {
1052
1188
  this.messages.push({
1053
1189
  role: 'system',
1054
- content: entry.content
1190
+ content: content
1055
1191
  });
1056
1192
  }
1057
1193
  // Stop if we hit another assistant or user message (next turn)
@@ -1109,7 +1245,7 @@ Current working directory: ${process.cwd()}`;
1109
1245
  this.chatHistory.push(errorEntry);
1110
1246
  yield {
1111
1247
  type: "content",
1112
- content: errorEntry.content,
1248
+ content: getTextContent(errorEntry.content),
1113
1249
  };
1114
1250
  // Mark first message as processed even on error
1115
1251
  this.firstMessageProcessed = true;
@@ -1245,18 +1381,9 @@ Current working directory: ${process.cwd()}`;
1245
1381
  // Validate tool arguments against schema
1246
1382
  const validationError = await this.validateToolArguments(toolCall.function.name, args);
1247
1383
  if (validationError) {
1248
- // Add system message explaining the validation error
1249
- const systemMsg = `Tool call validation failed: ${validationError}. Please try again with correct parameters.`;
1250
- console.warn(`[VALIDATION ERROR] ${systemMsg}`);
1251
- this.messages.push({
1252
- role: 'system',
1253
- content: systemMsg
1254
- });
1255
- this.chatHistory.push({
1256
- type: 'system',
1257
- content: systemMsg,
1258
- timestamp: new Date()
1259
- });
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}`);
1260
1387
  return {
1261
1388
  success: false,
1262
1389
  error: validationError
@@ -1372,6 +1499,8 @@ Current working directory: ${process.cwd()}`;
1372
1499
  return await this.imageTool.captionImage(args.filename, args.prompt);
1373
1500
  case "pngInfo":
1374
1501
  return await this.imageTool.pngInfo(args.filename);
1502
+ case "listImageModels":
1503
+ return await this.imageTool.listImageModels();
1375
1504
  case "readXlsx":
1376
1505
  return await this.fileConversionTool.readXlsx(args.filename, args.sheetName, args.outputFormat, args.output);
1377
1506
  case "listXlsxSheets":
@@ -1493,17 +1622,17 @@ Current working directory: ${process.cwd()}`;
1493
1622
  const timestamp = entry.timestamp.toLocaleTimeString();
1494
1623
  if (entry.type === 'user') {
1495
1624
  lines.push(`(${msgNum}) ${userName} (user) - ${timestamp}`);
1496
- lines.push(entry.content || "");
1625
+ lines.push(getTextContent(entry.content) || "");
1497
1626
  lines.push("");
1498
1627
  }
1499
1628
  else if (entry.type === 'assistant') {
1500
1629
  lines.push(`(${msgNum}) ${agentName} (assistant) - ${timestamp}`);
1501
- lines.push(entry.content || "");
1630
+ lines.push(getTextContent(entry.content) || "");
1502
1631
  lines.push("");
1503
1632
  }
1504
1633
  else if (entry.type === 'system') {
1505
1634
  lines.push(`(${msgNum}) System (system) - ${timestamp}`);
1506
- lines.push(entry.content || "");
1635
+ lines.push(getTextContent(entry.content) || "");
1507
1636
  lines.push("");
1508
1637
  }
1509
1638
  else if (entry.type === 'tool_call') {
@@ -1518,7 +1647,7 @@ Current working directory: ${process.cwd()}`;
1518
1647
  const toolCall = entry.toolCall;
1519
1648
  const toolName = toolCall?.function?.name || "unknown";
1520
1649
  lines.push(`(${msgNum}) System (tool_result: ${toolName}) - ${timestamp}`);
1521
- lines.push(entry.content || "");
1650
+ lines.push(getTextContent(entry.content) || "");
1522
1651
  lines.push("");
1523
1652
  }
1524
1653
  });
@@ -1554,6 +1683,15 @@ Current working directory: ${process.cwd()}`;
1554
1683
  clearPendingContextEditSession() {
1555
1684
  this.pendingContextEditSession = null;
1556
1685
  }
1686
+ setRephraseState(originalAssistantMessageIndex, rephraseRequestIndex, newResponseIndex, messageType) {
1687
+ this.rephraseState = { originalAssistantMessageIndex, rephraseRequestIndex, newResponseIndex, messageType };
1688
+ }
1689
+ getRephraseState() {
1690
+ return this.rephraseState;
1691
+ }
1692
+ clearRephraseState() {
1693
+ this.rephraseState = null;
1694
+ }
1557
1695
  async setPersona(persona, color) {
1558
1696
  // Execute hook if configured
1559
1697
  const settings = getSettingsManager();
@@ -1561,12 +1699,6 @@ Current working directory: ${process.cwd()}`;
1561
1699
  const hookMandatory = settings.isPersonaHookMandatory();
1562
1700
  if (!hookPath && hookMandatory) {
1563
1701
  const reason = "Persona hook is mandatory but not configured";
1564
- // Note: Don't add to this.messages during tool execution - only chatHistory
1565
- this.chatHistory.push({
1566
- type: 'system',
1567
- content: `Failed to change persona to "${persona}": ${reason}`,
1568
- timestamp: new Date()
1569
- });
1570
1702
  return {
1571
1703
  success: false,
1572
1704
  error: reason
@@ -1584,24 +1716,13 @@ Current working directory: ${process.cwd()}`;
1584
1716
  // Even in rejection, we process commands (might have MODEL change)
1585
1717
  await this.processHookResult(hookResult);
1586
1718
  // Note: We ignore the return value here since we're already rejecting the persona
1587
- // Note: Don't add to this.messages during tool execution - only chatHistory
1588
- this.chatHistory.push({
1589
- type: 'system',
1590
- content: `Failed to change persona to "${persona}": ${reason}`,
1591
- timestamp: new Date()
1592
- });
1593
1719
  return {
1594
1720
  success: false,
1595
1721
  error: reason
1596
1722
  };
1597
1723
  }
1598
1724
  if (hookResult.timedOut) {
1599
- // Note: Don't add to this.messages during tool execution - only chatHistory
1600
- this.chatHistory.push({
1601
- type: 'system',
1602
- content: `Persona hook timed out (auto-approved)`,
1603
- timestamp: new Date()
1604
- });
1725
+ // Hook timed out but was auto-approved
1605
1726
  }
1606
1727
  // Process hook commands (ENV, MODEL, SYSTEM)
1607
1728
  const result = await this.processHookResult(hookResult, 'ZDS_AI_AGENT_PERSONA');
@@ -1622,25 +1743,7 @@ Current working directory: ${process.cwd()}`;
1622
1743
  this.persona = persona;
1623
1744
  this.personaColor = color || "white";
1624
1745
  process.env.ZDS_AI_AGENT_PERSONA = persona;
1625
- // Add system message for recordkeeping
1626
- let systemContent;
1627
- if (oldPersona) {
1628
- const oldColorStr = oldColor && oldColor !== "white" ? ` (${oldColor})` : "";
1629
- const newColorStr = this.personaColor && this.personaColor !== "white" ? ` (${this.personaColor})` : "";
1630
- systemContent = `Assistant changed the persona from "${oldPersona}"${oldColorStr} to "${this.persona}"${newColorStr}`;
1631
- }
1632
- else {
1633
- const colorStr = this.personaColor && this.personaColor !== "white" ? ` (${this.personaColor})` : "";
1634
- systemContent = `Assistant set the persona to "${this.persona}"${colorStr}`;
1635
- }
1636
- // Note: Don't add to this.messages during tool execution - only chatHistory
1637
- // System messages added during tool execution create invalid message sequences
1638
- // because they get inserted between tool_calls and tool_results
1639
- this.chatHistory.push({
1640
- type: 'system',
1641
- content: systemContent,
1642
- timestamp: new Date()
1643
- });
1746
+ // Persona hook generates success message - no need for redundant CLI message
1644
1747
  this.emit('personaChange', {
1645
1748
  persona: this.persona,
1646
1749
  color: this.personaColor
@@ -1654,12 +1757,6 @@ Current working directory: ${process.cwd()}`;
1654
1757
  const hookMandatory = settings.isMoodHookMandatory();
1655
1758
  if (!hookPath && hookMandatory) {
1656
1759
  const reason = "Mood hook is mandatory but not configured";
1657
- // Note: Don't add to this.messages during tool execution - only chatHistory
1658
- this.chatHistory.push({
1659
- type: 'system',
1660
- content: `Failed to change mood to "${mood}": ${reason}`,
1661
- timestamp: new Date()
1662
- });
1663
1760
  return {
1664
1761
  success: false,
1665
1762
  error: reason
@@ -1675,24 +1772,13 @@ Current working directory: ${process.cwd()}`;
1675
1772
  const reason = hookResult.reason || "Hook rejected mood change";
1676
1773
  // Process rejection commands (MODEL, SYSTEM)
1677
1774
  await this.processHookResult(hookResult);
1678
- // Note: Don't add to this.messages during tool execution - only chatHistory
1679
- this.chatHistory.push({
1680
- type: 'system',
1681
- content: `Failed to change mood to "${mood}": ${reason}`,
1682
- timestamp: new Date()
1683
- });
1684
1775
  return {
1685
1776
  success: false,
1686
1777
  error: reason
1687
1778
  };
1688
1779
  }
1689
1780
  if (hookResult.timedOut) {
1690
- // Note: Don't add to this.messages during tool execution - only chatHistory
1691
- this.chatHistory.push({
1692
- type: 'system',
1693
- content: `Mood hook timed out (auto-approved)`,
1694
- timestamp: new Date()
1695
- });
1781
+ // Hook timed out but was auto-approved
1696
1782
  }
1697
1783
  // Process hook commands (ENV, MODEL, SYSTEM)
1698
1784
  const result = await this.processHookResult(hookResult, 'ZDS_AI_AGENT_MOOD');
@@ -1760,20 +1846,13 @@ Current working directory: ${process.cwd()}`;
1760
1846
  await this.processHookResult(hookResult);
1761
1847
  if (!hookResult.approved) {
1762
1848
  const reason = hookResult.reason || "Hook rejected task start";
1763
- this.messages.push({
1764
- role: 'system',
1765
- content: `Failed to start task "${activeTask}": ${reason}`
1766
- });
1767
1849
  return {
1768
1850
  success: false,
1769
1851
  error: reason
1770
1852
  };
1771
1853
  }
1772
1854
  if (hookResult.timedOut) {
1773
- this.messages.push({
1774
- role: 'system',
1775
- content: `Task start hook timed out (auto-approved)`
1776
- });
1855
+ // Hook timed out but was auto-approved
1777
1856
  }
1778
1857
  }
1779
1858
  // Set the task
@@ -1815,20 +1894,13 @@ Current working directory: ${process.cwd()}`;
1815
1894
  await this.processHookResult(hookResult);
1816
1895
  if (!hookResult.approved) {
1817
1896
  const reason = hookResult.reason || "Hook rejected task status transition";
1818
- this.messages.push({
1819
- role: 'system',
1820
- content: `Failed to transition task "${this.activeTask}" from ${this.activeTaskAction} to ${action}: ${reason}`
1821
- });
1822
1897
  return {
1823
1898
  success: false,
1824
1899
  error: reason
1825
1900
  };
1826
1901
  }
1827
1902
  if (hookResult.timedOut) {
1828
- this.messages.push({
1829
- role: 'system',
1830
- content: `Task transition hook timed out (auto-approved)`
1831
- });
1903
+ // Hook timed out but was auto-approved
1832
1904
  }
1833
1905
  }
1834
1906
  // Store old action for system message
@@ -1874,20 +1946,13 @@ Current working directory: ${process.cwd()}`;
1874
1946
  await this.processHookResult(hookResult);
1875
1947
  if (!hookResult.approved) {
1876
1948
  const hookReason = hookResult.reason || "Hook rejected task stop";
1877
- this.messages.push({
1878
- role: 'system',
1879
- content: `Failed to stop task "${this.activeTask}": ${hookReason}`
1880
- });
1881
1949
  return {
1882
1950
  success: false,
1883
1951
  error: hookReason
1884
1952
  };
1885
1953
  }
1886
1954
  if (hookResult.timedOut) {
1887
- this.messages.push({
1888
- role: 'system',
1889
- content: `Task stop hook timed out (auto-approved)`
1890
- });
1955
+ // Hook timed out but was auto-approved
1891
1956
  }
1892
1957
  }
1893
1958
  // Calculate remaining time to meet 3-second minimum