@tyvm/knowhow 0.0.15 → 0.0.17

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 (129) hide show
  1. package/package.json +2 -1
  2. package/src/agents/base/base.ts +25 -6
  3. package/src/agents/index.ts +2 -2
  4. package/src/agents/tools/agentCall.ts +0 -1
  5. package/src/agents/tools/execCommand.ts +95 -4
  6. package/src/agents/tools/list.ts +23 -19
  7. package/src/agents/tools/writeFile.ts +1 -1
  8. package/src/chat.ts +11 -0
  9. package/src/config.ts +3 -1
  10. package/src/processors/Base64ImageDetector.ts +190 -0
  11. package/src/processors/TokenCompressor.ts +357 -0
  12. package/src/processors/ToolResponseCache.ts +235 -0
  13. package/src/services/Mcp.ts +4 -1
  14. package/src/services/MessageProcessor.ts +107 -0
  15. package/src/services/Tools.ts +100 -1
  16. package/src/services/types.ts +57 -0
  17. package/ts_build/src/agents/base/base.d.ts +3 -1
  18. package/ts_build/src/agents/base/base.js +20 -5
  19. package/ts_build/src/agents/base/base.js.map +1 -1
  20. package/ts_build/src/agents/index.d.ts +2 -2
  21. package/ts_build/src/agents/index.js +5 -3
  22. package/ts_build/src/agents/index.js.map +1 -1
  23. package/ts_build/src/agents/tools/agentCall.js.map +1 -1
  24. package/ts_build/src/agents/tools/execCommand.d.ts +6 -1
  25. package/ts_build/src/agents/tools/execCommand.js +70 -4
  26. package/ts_build/src/agents/tools/execCommand.js.map +1 -1
  27. package/ts_build/src/agents/tools/expandTokens.d.ts +3 -0
  28. package/ts_build/src/agents/tools/expandTokens.js +33 -0
  29. package/ts_build/src/agents/tools/expandTokens.js.map +1 -0
  30. package/ts_build/src/agents/tools/getBigString.d.ts +3 -0
  31. package/ts_build/src/agents/tools/getBigString.js +33 -0
  32. package/ts_build/src/agents/tools/getBigString.js.map +1 -0
  33. package/ts_build/src/agents/tools/list.js +19 -17
  34. package/ts_build/src/agents/tools/list.js.map +1 -1
  35. package/ts_build/src/agents/tools/writeFile.js +1 -1
  36. package/ts_build/src/agents/tools/writeFile.js.map +1 -1
  37. package/ts_build/src/chat.js +6 -0
  38. package/ts_build/src/chat.js.map +1 -1
  39. package/ts_build/src/config.js +1 -1
  40. package/ts_build/src/config.js.map +1 -1
  41. package/ts_build/src/processors/Base64ImageDetector.d.ts +14 -0
  42. package/ts_build/src/processors/Base64ImageDetector.js +153 -0
  43. package/ts_build/src/processors/Base64ImageDetector.js.map +1 -0
  44. package/ts_build/src/processors/TokenCompressor.d.ts +28 -0
  45. package/ts_build/src/processors/TokenCompressor.js +226 -0
  46. package/ts_build/src/processors/TokenCompressor.js.map +1 -0
  47. package/ts_build/src/processors/ToolResponseCache.d.ts +22 -0
  48. package/ts_build/src/processors/ToolResponseCache.js +164 -0
  49. package/ts_build/src/processors/ToolResponseCache.js.map +1 -0
  50. package/ts_build/src/processors/ToolResponseManipulator.d.ts +22 -0
  51. package/ts_build/src/processors/ToolResponseManipulator.js +162 -0
  52. package/ts_build/src/processors/ToolResponseManipulator.js.map +1 -0
  53. package/ts_build/src/services/Mcp.js +3 -1
  54. package/ts_build/src/services/Mcp.js.map +1 -1
  55. package/ts_build/src/services/MessageProcessor.d.ts +17 -0
  56. package/ts_build/src/services/MessageProcessor.js +63 -0
  57. package/ts_build/src/services/MessageProcessor.js.map +1 -0
  58. package/ts_build/src/services/Tools.d.ts +12 -0
  59. package/ts_build/src/services/Tools.js +71 -0
  60. package/ts_build/src/services/Tools.js.map +1 -1
  61. package/ts_build/src/services/types.d.ts +32 -0
  62. package/ts_build/src/services/types.js +38 -0
  63. package/ts_build/src/services/types.js.map +1 -0
  64. package/ts_build/src/agents/configurable/OpenAIAgent.d.ts +0 -0
  65. package/ts_build/src/agents/configurable/OpenAIAgent.js +0 -1
  66. package/ts_build/src/agents/configurable/OpenAIAgent.js.map +0 -1
  67. package/ts_build/src/agents/tools/client.d.ts +0 -5
  68. package/ts_build/src/agents/tools/client.js +0 -21
  69. package/ts_build/src/agents/tools/client.js.map +0 -1
  70. package/ts_build/src/agents/tools/googleSearchTypes.d.ts +0 -74
  71. package/ts_build/src/agents/tools/googleSearchTypes.js +0 -3
  72. package/ts_build/src/agents/tools/googleSearchTypes.js.map +0 -1
  73. package/ts_build/src/commands/chat-ui.d.ts +0 -1
  74. package/ts_build/src/commands/chat-ui.js +0 -14
  75. package/ts_build/src/commands/chat-ui.js.map +0 -1
  76. package/ts_build/src/demo/chat-ui-demo.d.ts +0 -3
  77. package/ts_build/src/demo/chat-ui-demo.js +0 -20
  78. package/ts_build/src/demo/chat-ui-demo.js.map +0 -1
  79. package/ts_build/src/plugins/EmbeddingPluginV2.d.ts +0 -7
  80. package/ts_build/src/plugins/EmbeddingPluginV2.js +0 -41
  81. package/ts_build/src/plugins/EmbeddingPluginV2.js.map +0 -1
  82. package/ts_build/src/plugins/GitHubPluginV2.d.ts +0 -10
  83. package/ts_build/src/plugins/GitHubPluginV2.js +0 -57
  84. package/ts_build/src/plugins/GitHubPluginV2.js.map +0 -1
  85. package/ts_build/src/plugins/downloader/index.d.ts +0 -3
  86. package/ts_build/src/plugins/downloader/index.js +0 -41
  87. package/ts_build/src/plugins/downloader/index.js.map +0 -1
  88. package/ts_build/src/services/MessagePreprocessor.d.ts +0 -26
  89. package/ts_build/src/services/MessagePreprocessor.js +0 -190
  90. package/ts_build/src/services/MessagePreprocessor.js.map +0 -1
  91. package/ts_build/src/services/__tests__/MessagePreprocessor.test.d.ts +0 -1
  92. package/ts_build/src/services/__tests__/MessagePreprocessor.test.js +0 -117
  93. package/ts_build/src/services/__tests__/MessagePreprocessor.test.js.map +0 -1
  94. package/ts_build/src/terminal.d.ts +0 -1
  95. package/ts_build/src/terminal.js +0 -35
  96. package/ts_build/src/terminal.js.map +0 -1
  97. package/ts_build/src/ui/InkChatUI.d.ts +0 -1
  98. package/ts_build/src/ui/InkChatUI.js +0 -792
  99. package/ts_build/src/ui/InkChatUI.js.map +0 -1
  100. package/ts_build/src/ui/components/ChatInterface.d.ts +0 -15
  101. package/ts_build/src/ui/components/ChatInterface.js +0 -39
  102. package/ts_build/src/ui/components/ChatInterface.js.map +0 -1
  103. package/ts_build/src/ui/components/ChatMessage.d.ts +0 -8
  104. package/ts_build/src/ui/components/ChatMessage.js +0 -7
  105. package/ts_build/src/ui/components/ChatMessage.js.map +0 -1
  106. package/ts_build/src/ui/components/CommandPalette.d.ts +0 -8
  107. package/ts_build/src/ui/components/CommandPalette.js +0 -23
  108. package/ts_build/src/ui/components/CommandPalette.js.map +0 -1
  109. package/ts_build/src/ui/components/InputBar.d.ts +0 -8
  110. package/ts_build/src/ui/components/InputBar.js +0 -8
  111. package/ts_build/src/ui/components/InputBar.js.map +0 -1
  112. package/ts_build/src/ui/components/Sidebar.d.ts +0 -9
  113. package/ts_build/src/ui/components/Sidebar.js +0 -7
  114. package/ts_build/src/ui/components/Sidebar.js.map +0 -1
  115. package/ts_build/src/ui/components/StatusBar.d.ts +0 -10
  116. package/ts_build/src/ui/components/StatusBar.js +0 -8
  117. package/ts_build/src/ui/components/StatusBar.js.map +0 -1
  118. package/ts_build/src/ui/demo.d.ts +0 -3
  119. package/ts_build/src/ui/demo.js +0 -26
  120. package/ts_build/src/ui/demo.js.map +0 -1
  121. package/ts_build/src/ui/index.d.ts +0 -13
  122. package/ts_build/src/ui/index.js +0 -16
  123. package/ts_build/src/ui/index.js.map +0 -1
  124. package/ts_build/tests/integration/OpenAI-MessagePreprocessor.test.d.ts +0 -1
  125. package/ts_build/tests/integration/OpenAI-MessagePreprocessor.test.js +0 -148
  126. package/ts_build/tests/integration/OpenAI-MessagePreprocessor.test.js.map +0 -1
  127. package/ts_build/tests/services/MessagePreprocessor.test.d.ts +0 -1
  128. package/ts_build/tests/services/MessagePreprocessor.test.js +0 -117
  129. package/ts_build/tests/services/MessagePreprocessor.test.js.map +0 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tyvm/knowhow",
3
- "version": "0.0.15",
3
+ "version": "0.0.17",
4
4
  "description": "ai cli with plugins and agents",
5
5
  "main": "ts_build/src/index.js",
6
6
  "bin": {
@@ -60,6 +60,7 @@
60
60
  "marked-terminal": "^6.2.0",
61
61
  "morgan": "^1.10.0",
62
62
  "node-fetch": "^3.2.3",
63
+ "node-jq": "^6.0.1",
63
64
  "node-pty": "^1.0.0",
64
65
  "node-record-lpcm16": "^1.0.1",
65
66
  "openai": "4.89.1",
@@ -13,10 +13,10 @@ import {
13
13
  replaceEscapedNewLines,
14
14
  restoreEscapedNewLines,
15
15
  } from "../../utils";
16
- import { Agents, AgentService } from "../../services/AgentService";
17
16
  import { Events, EventService } from "../../services/EventService";
18
17
  import { AIClient, Clients } from "../../clients";
19
18
  import { Models } from "../../ai";
19
+ import { MessageProcessor } from "../../services/MessageProcessor";
20
20
 
21
21
  export { Message, Tool, ToolCall };
22
22
  export interface ModelPreference {
@@ -60,7 +60,8 @@ export abstract class BaseAgent implements IAgent {
60
60
 
61
61
  constructor(
62
62
  public tools: ToolsService = Tools,
63
- public events: EventService = Events
63
+ public events: EventService = Events,
64
+ public messageProcessor: MessageProcessor = new MessageProcessor()
64
65
  ) {}
65
66
 
66
67
  newTask() {
@@ -418,6 +419,11 @@ export abstract class BaseAgent implements IAgent {
418
419
  const model = this.getModel();
419
420
  let messages = _messages || (await this.getInitialMessages(userInput));
420
421
 
422
+ // Process initial messages if this is the first call
423
+ if (!_messages) {
424
+ messages = await this.messageProcessor.processMessages(messages, "initial_call");
425
+ }
426
+
421
427
  if (this.pendingUserMessages.length) {
422
428
  messages.push(...this.pendingUserMessages);
423
429
  this.pendingUserMessages = [];
@@ -429,6 +435,9 @@ export abstract class BaseAgent implements IAgent {
429
435
 
430
436
  const startIndex = 0;
431
437
  const endIndex = messages.length;
438
+
439
+ // Process messages before each AI call
440
+ messages = await this.messageProcessor.processMessages(messages, "per_call");
432
441
  const compressThreshold = 10000;
433
442
 
434
443
  const response = await this.getClient().createChatCompletion({
@@ -479,6 +488,11 @@ export abstract class BaseAgent implements IAgent {
479
488
  }
480
489
  }
481
490
 
491
+ // Process messages after tool execution
492
+ if (newToolCalls && newToolCalls.length > 0) {
493
+ messages = await this.messageProcessor.processMessages(messages, "post_call");
494
+ }
495
+
482
496
  // Early exit: not required to call tool
483
497
  if (
484
498
  response.choices.length === 1 &&
@@ -565,7 +579,9 @@ export abstract class BaseAgent implements IAgent {
565
579
  1. Task List
566
580
  2. Completion Criteria - when the agent should stop
567
581
 
568
- : \n\n${JSON.stringify(messages)}`;
582
+ This output will be used to guide the work of the agent, and determine when we've accomplished the goal
583
+
584
+ \n\n<ToAnalyze>${JSON.stringify(messages)}</ToAnalyze>`;
569
585
 
570
586
  const model = this.getModel();
571
587
 
@@ -577,6 +593,7 @@ export abstract class BaseAgent implements IAgent {
577
593
  content: taskPrompt,
578
594
  },
579
595
  ],
596
+ max_tokens: 2000,
580
597
  });
581
598
 
582
599
  this.adjustTotalCostUsd(response.usd_cost);
@@ -602,10 +619,12 @@ export abstract class BaseAgent implements IAgent {
602
619
  3. Next Steps - what we're about to do next to continue the user's original request.
603
620
  4. Tasks remaining - what tasks are left from the initial task breakdown.
604
621
 
605
- This summary will become the agent's only memory of the past, all other messages will be dropped:
606
- ${JSON.stringify(toCompress)}
622
+ Our initial task breakdown: ${this.taskBreakdown}
623
+
624
+ This summary will become the agent's only memory of the past, all other messages will be dropped:
625
+ <ToSummarize>${JSON.stringify(toCompress)}</ToSummarize>
607
626
 
608
- Our initial task breakdown: ${this.taskBreakdown}`;
627
+ `;
609
628
 
610
629
  const model = this.getModel();
611
630
 
@@ -1,5 +1,5 @@
1
- export * from "./base/base";
2
- export * from "./configurable/ConfigAgent";
1
+ export { BaseAgent } from "./base/base";
2
+ export { ConfigAgent } from "./configurable/ConfigAgent";
3
3
  export * from "./developer/developer";
4
4
  export * from "./patcher/patcher";
5
5
  export * from "./researcher/researcher";
@@ -1,4 +1,3 @@
1
- import { Agents } from "../../services/AgentService";
2
1
  import { Events } from "../../services/EventService";
3
2
  import { Plugins } from "../../plugins/plugins";
4
3
  import { getConfig } from "../../config";
@@ -1,15 +1,106 @@
1
- import { execAsync } from "../../utils";
1
+ import { exec } from "child_process";
2
+ import { promisify } from "util";
3
+
4
+ const execAsync = promisify(exec);
5
+
6
+ export interface ExecCommandOptions {
7
+ timeout?: number; // Timeout in milliseconds
8
+ killOnTimeout?: boolean; // Whether to kill the command on timeout (default: false)
9
+ waitForCompletion?: boolean; // Whether to wait for full completion (default: true)
10
+ }
11
+
12
+ // Enhanced exec function with timeout support
13
+ const execWithTimeout = async (
14
+ command: string,
15
+ options: ExecCommandOptions = {}
16
+ ): Promise<{ stdout: string; stderr: string; timedOut: boolean; killed: boolean }> => {
17
+ const { timeout, killOnTimeout = false, waitForCompletion = true } = options;
18
+
19
+ if (!timeout || waitForCompletion) {
20
+ // Default behavior - wait for completion
21
+ try {
22
+ const result = await execAsync(command);
23
+ return { ...result, timedOut: false, killed: false };
24
+ } catch (error) {
25
+ return {
26
+ stdout: error.stdout || "",
27
+ stderr: error.stderr || error.message,
28
+ timedOut: false,
29
+ killed: false
30
+ };
31
+ }
32
+ }
33
+
34
+ // Timeout behavior
35
+ return new Promise((resolve) => {
36
+ const childProcess = exec(command, (error, stdout, stderr) => {
37
+ if (error && !error.killed) {
38
+ resolve({ stdout, stderr: stderr || error.message, timedOut: false, killed: false });
39
+ } else {
40
+ resolve({ stdout, stderr, timedOut: false, killed: error?.killed || false });
41
+ }
42
+ });
43
+
44
+ const timeoutId = setTimeout(() => {
45
+ if (killOnTimeout) {
46
+ childProcess.kill('SIGTERM');
47
+ // Force kill after additional 5 seconds if still running
48
+ setTimeout(() => {
49
+ if (!childProcess.killed) {
50
+ childProcess.kill('SIGKILL');
51
+ }
52
+ }, 5000);
53
+ resolve({
54
+ stdout: "",
55
+ stderr: `Command timed out after ${timeout}ms and was killed`,
56
+ timedOut: true,
57
+ killed: true
58
+ });
59
+ } else {
60
+ resolve({
61
+ stdout: "",
62
+ stderr: `Command timed out after ${timeout}ms but is still running in background`,
63
+ timedOut: true,
64
+ killed: false
65
+ });
66
+ }
67
+ }, timeout);
68
+
69
+ // Clear timeout if command completes before timeout
70
+ childProcess.on('exit', () => {
71
+ clearTimeout(timeoutId);
72
+ });
73
+ });
74
+ };
2
75
 
3
76
  // Tool to execute a command in the system's command line interface
4
- export const execCommand = async (command: string): Promise<string> => {
77
+ export const execCommand = async (
78
+ command: string,
79
+ timeout?: number,
80
+ killOnTimeout?: boolean,
81
+ waitForCompletion?: boolean
82
+ ): Promise<string> => {
5
83
  let output = "";
6
84
  console.log("execCommand:", command);
7
- const { stdout, stderr } = await execAsync(command).catch((e) => e);
85
+
86
+ const { stdout, stderr, timedOut, killed } = await execWithTimeout(command, {
87
+ timeout,
88
+ killOnTimeout,
89
+ waitForCompletion,
90
+
91
+ });
92
+
8
93
  if (stderr) {
9
94
  output += stderr + "\n";
10
95
  }
11
96
  output += stdout;
12
- console.log(`$ ${command}:\n${output}`);
97
+
98
+ if (timedOut) {
99
+ const statusMsg = killed ? " (killed due to timeout)" : " (timed out, still running)";
100
+ console.log(`$ ${command}${statusMsg}:\n${output}`);
101
+ } else {
102
+ console.log(`$ ${command}:\n${output}`);
103
+ }
13
104
 
14
105
  const fullOutput = output.split("\n");
15
106
 
@@ -7,7 +7,6 @@ import * as github from "./github/definitions";
7
7
  import * as asana from "./asana/definitions";
8
8
  import * as language from "./language/definitions";
9
9
  import { googleSearchDefinition } from "./googleSearch";
10
- import { Agents } from "../../services/AgentService";
11
10
 
12
11
  export const includedTools = [
13
12
  {
@@ -39,7 +38,7 @@ export const includedTools = [
39
38
  function: {
40
39
  name: "execCommand",
41
40
  description:
42
- "Execute a command in the system's command line interface. Use this to run tests and things in the terminal",
41
+ "Execute a command in the system's command line interface. Use this to run tests and things in the terminal. Supports timeout functionality.",
43
42
  parameters: {
44
43
  type: "object",
45
44
  positional: true,
@@ -48,23 +47,28 @@ export const includedTools = [
48
47
  type: "string",
49
48
  description: "The command to execute",
50
49
  },
50
+ timeout: {
51
+ type: "number",
52
+ description:
53
+ "Timeout in milliseconds (optional). If not provided, waits indefinitely.",
54
+ },
55
+ killOnTimeout: {
56
+ type: "boolean",
57
+ description:
58
+ "Whether to kill the command when timeout is reached (default: false). If false, command continues running in background.",
59
+ },
60
+ waitForCompletion: {
61
+ type: "boolean",
62
+ description:
63
+ "Whether to wait for full completion regardless of timeout (default: true). Overrides timeout behavior.",
64
+ },
51
65
  },
52
66
  required: ["command"],
53
67
  },
54
68
  returns: {
55
- type: "object",
56
- properties: {
57
- stdout: {
58
- type: "string",
59
- description: "The standard output of the executed command",
60
- },
61
- stderr: {
62
- type: "string",
63
- description: "The standard error output of the executed command",
64
- },
65
- },
69
+ type: "string",
66
70
  description:
67
- "The result of the command execution, including any output and errors",
71
+ "The result of the command execution, including any output and errors. May include timeout status information.",
68
72
  },
69
73
  },
70
74
  },
@@ -473,8 +477,6 @@ export const includedTools = [
473
477
  },
474
478
  },
475
479
  },
476
-
477
- googleSearchDefinition,
478
480
  {
479
481
  type: "function",
480
482
  function: {
@@ -515,9 +517,6 @@ export const includedTools = [
515
517
  },
516
518
  },
517
519
  },
518
- ...asana.definitions,
519
- ...github.definitions,
520
- ...language.definitions,
521
520
  {
522
521
  type: "function",
523
522
  function: {
@@ -553,4 +552,9 @@ export const includedTools = [
553
552
  },
554
553
  },
555
554
  },
555
+
556
+ googleSearchDefinition,
557
+ ...asana.definitions,
558
+ ...github.definitions,
559
+ ...language.definitions,
556
560
  ] as Tool[];
@@ -27,7 +27,7 @@ export async function writeFileChunk(
27
27
  }
28
28
 
29
29
  if (isContinuing) {
30
- fs.appendFileSync(filePath, content);
30
+ fs.appendFileSync(filePath, "\n" + content);
31
31
  }
32
32
 
33
33
  let message = "";
package/src/chat.ts CHANGED
@@ -21,6 +21,8 @@ import { recordAudio, voiceToText } from "./microphone";
21
21
  import { Models } from "./ai";
22
22
  import { BaseAgent } from "./agents";
23
23
  import { getConfig } from "./config";
24
+ import { TokenCompressor } from "./processors/TokenCompressor";
25
+ import { ToolResponseCache } from "./processors/ToolResponseCache";
24
26
 
25
27
  enum ChatFlags {
26
28
  agent = "agent",
@@ -346,6 +348,15 @@ export async function startAgent(
346
348
  );
347
349
  activeAgent.call(formattedPrompt);
348
350
 
351
+ // Compress tokens of tool responses
352
+ activeAgent.messageProcessor.setProcessors("per_call", [
353
+ new ToolResponseCache(activeAgent.tools).createProcessor(),
354
+
355
+ new TokenCompressor(activeAgent.tools).createProcessor((msg) =>
356
+ Boolean(msg.role === "tool" && msg.tool_call_id)
357
+ ),
358
+ ]);
359
+
349
360
  activeAgent.agentEvents.once(activeAgent.eventTypes.done, (doneMsg) => {
350
361
  console.log("Agent has finished.");
351
362
  done = true;
package/src/config.ts CHANGED
@@ -187,7 +187,9 @@ export function getConfigSync() {
187
187
 
188
188
  export async function getConfig() {
189
189
  if (!fs.existsSync(".knowhow/knowhow.json")) {
190
- return {} as Config;
190
+ throw new Error(
191
+ "KnowHow config file not found. Please run `knowhow init` to create it."
192
+ );
191
193
  }
192
194
  const config = JSON.parse(await readFile(".knowhow/knowhow.json", "utf8"));
193
195
  return config as Config;
@@ -0,0 +1,190 @@
1
+ import { Message } from "../clients/types";
2
+ import { MessageProcessorFunction } from "../services/MessageProcessor";
3
+
4
+ interface ImageContent {
5
+ type: "image_url";
6
+ image_url: {
7
+ url: string;
8
+ detail?: "auto" | "low" | "high";
9
+ };
10
+ }
11
+
12
+ interface TextContent {
13
+ type: "text";
14
+ text: string;
15
+ }
16
+
17
+ export class Base64ImageDetector {
18
+ private imageDetail: "auto" | "low" | "high";
19
+ private supportedFormats: string[];
20
+
21
+ constructor(
22
+ imageDetail: "auto" | "low" | "high" = "auto",
23
+ supportedFormats: string[] = ["png", "jpeg", "jpg", "gif", "webp"]
24
+ ) {
25
+ this.imageDetail = imageDetail;
26
+ this.supportedFormats = supportedFormats;
27
+ }
28
+
29
+ private isBase64Image(text: string): { isImage: boolean; mimeType?: string; data?: string } {
30
+ // Check for data URL format: data:image/type;base64,actualdata
31
+ const dataUrlPattern = /^data:image\/([a-zA-Z]+);base64,(.+)$/;
32
+ const match = text.match(dataUrlPattern);
33
+
34
+ if (match) {
35
+ const [, mimeType, data] = match;
36
+ if (this.supportedFormats.includes(mimeType.toLowerCase())) {
37
+ return { isImage: true, mimeType, data };
38
+ }
39
+ }
40
+
41
+ // Check for plain base64 that might be an image
42
+ // This is a heuristic - look for long base64 strings that might be images
43
+ const base64Pattern = /^[A-Za-z0-9+/]+=*$/;
44
+ if (base64Pattern.test(text) && text.length > 100) {
45
+ // Try to detect image type from base64 header
46
+ const header = text.substring(0, 50);
47
+ try {
48
+ const decoded = atob(header);
49
+ // Check for common image file signatures
50
+ if (decoded.startsWith('\x89PNG')) {
51
+ return { isImage: true, mimeType: 'png', data: text };
52
+ } else if (decoded.startsWith('\xFF\xD8\xFF')) {
53
+ return { isImage: true, mimeType: 'jpeg', data: text };
54
+ } else if (decoded.startsWith('GIF87a') || decoded.startsWith('GIF89a')) {
55
+ return { isImage: true, mimeType: 'gif', data: text };
56
+ } else if (decoded.startsWith('RIFF') && decoded.includes('WEBP')) {
57
+ return { isImage: true, mimeType: 'webp', data: text };
58
+ }
59
+ } catch (e) {
60
+ // Not valid base64 or not an image
61
+ }
62
+ }
63
+
64
+ return { isImage: false };
65
+ }
66
+
67
+ private convertBase64ToImageContent(text: string): ImageContent | null {
68
+ const detection = this.isBase64Image(text);
69
+
70
+ if (!detection.isImage) {
71
+ return null;
72
+ }
73
+
74
+ const dataUrl = detection.data!.startsWith('data:')
75
+ ? detection.data
76
+ : `data:image/${detection.mimeType};base64,${detection.data}`;
77
+
78
+ return {
79
+ type: "image_url",
80
+ image_url: {
81
+ url: dataUrl,
82
+ detail: this.imageDetail
83
+ }
84
+ };
85
+ }
86
+
87
+ private processMessageContent(message: Message): void {
88
+ if (typeof message.content === 'string') {
89
+ const imageContent = this.convertBase64ToImageContent(message.content);
90
+ if (imageContent) {
91
+ // Convert string content to multimodal array
92
+ message.content = [imageContent];
93
+ }
94
+ } else if (Array.isArray(message.content)) {
95
+ // Process each content item
96
+ const newContent: (TextContent | ImageContent)[] = [];
97
+
98
+ for (const item of message.content) {
99
+ if (item.type === 'text' && item.text) {
100
+ const imageContent = this.convertBase64ToImageContent(item.text);
101
+ if (imageContent) {
102
+ newContent.push(imageContent);
103
+ } else {
104
+ newContent.push(item as TextContent);
105
+ }
106
+ } else {
107
+ newContent.push(item as TextContent | ImageContent);
108
+ }
109
+ }
110
+
111
+ message.content = newContent;
112
+ }
113
+ }
114
+
115
+ private processToolCallArguments(message: Message): void {
116
+ if (message.tool_calls) {
117
+ for (const toolCall of message.tool_calls) {
118
+ if (toolCall.function.arguments) {
119
+ try {
120
+ const args = JSON.parse(toolCall.function.arguments);
121
+ let modified = false;
122
+
123
+ // Recursively check all string values in arguments
124
+ const processValue = (obj: any): any => {
125
+ if (typeof obj === 'string') {
126
+ const detection = this.isBase64Image(obj);
127
+ if (detection.isImage) {
128
+ modified = true;
129
+ const dataUrl = detection.data!.startsWith('data:')
130
+ ? detection.data
131
+ : `data:image/${detection.mimeType};base64,${detection.data}`;
132
+ return `[CONVERTED TO IMAGE: ${dataUrl.substring(0, 50)}...]`;
133
+ }
134
+ return obj;
135
+ } else if (Array.isArray(obj)) {
136
+ return obj.map(processValue);
137
+ } else if (obj && typeof obj === 'object') {
138
+ const result = {};
139
+ for (const [key, value] of Object.entries(obj)) {
140
+ result[key] = processValue(value);
141
+ }
142
+ return result;
143
+ }
144
+ return obj;
145
+ };
146
+
147
+ const processedArgs = processValue(args);
148
+ if (modified) {
149
+ toolCall.function.arguments = JSON.stringify(processedArgs);
150
+ }
151
+ } catch (e) {
152
+ // Arguments are not valid JSON, treat as string
153
+ const detection = this.isBase64Image(toolCall.function.arguments);
154
+ if (detection.isImage) {
155
+ const dataUrl = detection.data!.startsWith('data:')
156
+ ? detection.data
157
+ : `data:image/${detection.mimeType};base64,${detection.data}`;
158
+ toolCall.function.arguments = `[CONVERTED TO IMAGE: ${dataUrl.substring(0, 50)}...]`;
159
+ }
160
+ }
161
+ }
162
+ }
163
+ }
164
+ }
165
+
166
+ createProcessor(): MessageProcessorFunction {
167
+ return (originalMessages: Message[], modifiedMessages: Message[]) => {
168
+ for (const message of modifiedMessages) {
169
+ // Only process user messages (images typically come from users)
170
+ if (message.role === 'user') {
171
+ this.processMessageContent(message);
172
+ }
173
+
174
+ // Process tool calls in any message
175
+ this.processToolCallArguments(message);
176
+ }
177
+ };
178
+ }
179
+
180
+ setImageDetail(detail: "auto" | "low" | "high"): void {
181
+ this.imageDetail = detail;
182
+ }
183
+
184
+ setSupportedFormats(formats: string[]): void {
185
+ this.supportedFormats = formats;
186
+ }
187
+ }
188
+
189
+ // Global instance
190
+ export const globalBase64ImageDetector = new Base64ImageDetector();