@tyvm/knowhow 0.0.63 → 0.0.65
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.
- package/package.json +2 -1
- package/src/chat/modules/AgentModule.ts +18 -2
- package/src/cli.ts +5 -4
- package/src/clients/anthropic.ts +68 -5
- package/src/config.ts +7 -0
- package/src/processors/Base64ImageDetector.ts +193 -40
- package/src/processors/index.ts +1 -1
- package/src/services/AgentSynchronization.ts +27 -10
- package/src/types.ts +11 -0
- package/src/worker.ts +145 -8
- package/src/workers/tools/index.ts +10 -0
- package/src/workers/tools/listAllowedPorts.ts +30 -0
- package/tests/plugins/language/languagePlugin-content-triggers.test.ts +5 -1
- package/tests/plugins/language/languagePlugin.test.ts +5 -1
- package/tests/processors/Base64ImageDetector.test.ts +263 -70
- package/tests/services/Tools.test.ts +6 -4
- package/ts_build/package.json +2 -1
- package/ts_build/src/chat/modules/AgentModule.js +12 -2
- package/ts_build/src/chat/modules/AgentModule.js.map +1 -1
- package/ts_build/src/cli.js +5 -4
- package/ts_build/src/cli.js.map +1 -1
- package/ts_build/src/clients/anthropic.d.ts +9 -0
- package/ts_build/src/clients/anthropic.js +61 -5
- package/ts_build/src/clients/anthropic.js.map +1 -1
- package/ts_build/src/config.js +6 -0
- package/ts_build/src/config.js.map +1 -1
- package/ts_build/src/processors/Base64ImageDetector.d.ts +7 -3
- package/ts_build/src/processors/Base64ImageDetector.js +147 -27
- package/ts_build/src/processors/Base64ImageDetector.js.map +1 -1
- package/ts_build/src/processors/index.d.ts +1 -1
- package/ts_build/src/processors/index.js +2 -2
- package/ts_build/src/processors/index.js.map +1 -1
- package/ts_build/src/services/AgentSynchronization.d.ts +2 -0
- package/ts_build/src/services/AgentSynchronization.js +20 -10
- package/ts_build/src/services/AgentSynchronization.js.map +1 -1
- package/ts_build/src/types.d.ts +11 -0
- package/ts_build/src/types.js +1 -0
- package/ts_build/src/types.js.map +1 -1
- package/ts_build/src/worker/handlers/proxyHandler.d.ts +2 -0
- package/ts_build/src/worker/handlers/proxyHandler.js +41 -0
- package/ts_build/src/worker/handlers/proxyHandler.js.map +1 -0
- package/ts_build/src/worker/tools/index.d.ts +1 -0
- package/ts_build/src/worker/tools/index.js +18 -0
- package/ts_build/src/worker/tools/index.js.map +1 -0
- package/ts_build/src/worker/tools/portForwarding.d.ts +49 -0
- package/ts_build/src/worker/tools/portForwarding.js +173 -0
- package/ts_build/src/worker/tools/portForwarding.js.map +1 -0
- package/ts_build/src/worker/types/proxy.d.ts +18 -0
- package/ts_build/src/worker/types/proxy.js +3 -0
- package/ts_build/src/worker/types/proxy.js.map +1 -0
- package/ts_build/src/worker.js +119 -3
- package/ts_build/src/worker.js.map +1 -1
- package/ts_build/src/workers/tools/index.d.ts +9 -0
- package/ts_build/src/workers/tools/index.js +23 -0
- package/ts_build/src/workers/tools/index.js.map +1 -0
- package/ts_build/src/workers/tools/listAllowedPorts.d.ts +3 -0
- package/ts_build/src/workers/tools/listAllowedPorts.js +25 -0
- package/ts_build/src/workers/tools/listAllowedPorts.js.map +1 -0
- package/ts_build/src/workers/tools/listForwardedPorts.d.ts +10 -0
- package/ts_build/src/workers/tools/listForwardedPorts.js +22 -0
- package/ts_build/src/workers/tools/listForwardedPorts.js.map +1 -0
- package/ts_build/tests/plugins/language/languagePlugin-content-triggers.test.js +5 -1
- package/ts_build/tests/plugins/language/languagePlugin-content-triggers.test.js.map +1 -1
- package/ts_build/tests/plugins/language/languagePlugin.test.js +5 -1
- package/ts_build/tests/plugins/language/languagePlugin.test.js.map +1 -1
- package/ts_build/tests/processors/Base64ImageDetector.test.js +221 -59
- package/ts_build/tests/processors/Base64ImageDetector.test.js.map +1 -1
- package/ts_build/tests/services/Tools.test.js +3 -3
- package/ts_build/tests/services/Tools.test.js.map +1 -1
- package/ts_build/tests/worker/handlers/proxyHandler.test.d.ts +1 -0
- package/ts_build/tests/worker/handlers/proxyHandler.test.js +170 -0
- package/ts_build/tests/worker/handlers/proxyHandler.test.js.map +1 -0
- package/tsconfig.json +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tyvm/knowhow",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.65",
|
|
4
4
|
"description": "ai cli with plugins and agents",
|
|
5
5
|
"main": "ts_build/src/index.js",
|
|
6
6
|
"bin": {
|
|
@@ -40,6 +40,7 @@
|
|
|
40
40
|
"dependencies": {
|
|
41
41
|
"@anthropic-ai/sdk": "^0.39.0",
|
|
42
42
|
"@aws-sdk/client-s3": "^3.588.0",
|
|
43
|
+
"@tyvm/knowhow-tunnel": "^0.0.1",
|
|
43
44
|
"@google/genai": "^0.14.1",
|
|
44
45
|
"@inquirer/editor": "^4.2.18",
|
|
45
46
|
"@linear/sdk": "^12.0.0",
|
|
@@ -21,6 +21,7 @@ import {
|
|
|
21
21
|
CustomVariables,
|
|
22
22
|
XmlToolCallProcessor,
|
|
23
23
|
HarmonyToolProcessor,
|
|
24
|
+
Base64ImageProcessor,
|
|
24
25
|
} from "../../processors/index";
|
|
25
26
|
import { TaskInfo, ChatSession } from "../types";
|
|
26
27
|
import { agents } from "../../agents";
|
|
@@ -589,7 +590,9 @@ Please continue from where you left off and complete the original request.
|
|
|
589
590
|
Boolean(msg.role === "tool" && msg.tool_call_id)
|
|
590
591
|
),
|
|
591
592
|
];
|
|
593
|
+
|
|
592
594
|
agent.messageProcessor.setProcessors("pre_call", [
|
|
595
|
+
new Base64ImageProcessor(agent.tools).createProcessor(),
|
|
593
596
|
...caching,
|
|
594
597
|
new CustomVariables(agent.tools).createProcessor(),
|
|
595
598
|
]);
|
|
@@ -599,7 +602,10 @@ Please continue from where you left off and complete the original request.
|
|
|
599
602
|
new HarmonyToolProcessor().createProcessor(),
|
|
600
603
|
]);
|
|
601
604
|
|
|
602
|
-
agent.messageProcessor.setProcessors("post_tools",
|
|
605
|
+
agent.messageProcessor.setProcessors("post_tools", [
|
|
606
|
+
new Base64ImageProcessor(agent.tools).createProcessor(),
|
|
607
|
+
...caching,
|
|
608
|
+
]);
|
|
603
609
|
|
|
604
610
|
// Set up event listeners
|
|
605
611
|
if (!agent.agentEvents.listenerCount(agent.eventTypes.toolCall)) {
|
|
@@ -628,11 +634,19 @@ Please continue from where you left off and complete the original request.
|
|
|
628
634
|
|
|
629
635
|
const taskCompleted = new Promise<string>((resolve) => {
|
|
630
636
|
agent.agentEvents.once(agent.eventTypes.done, async (doneMsg) => {
|
|
631
|
-
console.log("
|
|
637
|
+
console.log("🎯 [AgentModule] Task Completed");
|
|
632
638
|
done = true;
|
|
633
639
|
output = doneMsg || "No response from the AI";
|
|
634
640
|
// Update task info
|
|
635
641
|
taskInfo = this.taskRegistry.get(taskId);
|
|
642
|
+
|
|
643
|
+
// Wait for AgentSync to finish before resolving
|
|
644
|
+
if (knowhowTaskId) {
|
|
645
|
+
console.log("🎯 [AgentModule] Waiting for sync finalization...");
|
|
646
|
+
await this.agentSync.waitForFinalization();
|
|
647
|
+
console.log("🎯 [AgentModule] Sync finalization complete");
|
|
648
|
+
}
|
|
649
|
+
|
|
636
650
|
if (taskInfo) {
|
|
637
651
|
taskInfo.status = "completed";
|
|
638
652
|
// Update final cost from agent
|
|
@@ -641,7 +655,9 @@ Please continue from where you left off and complete the original request.
|
|
|
641
655
|
this.updateSession(taskId, agent.getThreads());
|
|
642
656
|
taskInfo.endTime = Date.now();
|
|
643
657
|
}
|
|
658
|
+
|
|
644
659
|
console.log(Marked.parse(output));
|
|
660
|
+
console.log("🎯 [AgentModule] Task Complete");
|
|
645
661
|
resolve(doneMsg);
|
|
646
662
|
});
|
|
647
663
|
});
|
package/src/cli.ts
CHANGED
|
@@ -54,10 +54,11 @@ async function setupServices() {
|
|
|
54
54
|
// Add Mcp service to tool context directly so MCP management tools can access it
|
|
55
55
|
Tools.addContext("Mcp", Mcp);
|
|
56
56
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
57
|
+
console.log("🔌 Connecting to MCP...");
|
|
58
|
+
await Mcp.connectToConfigured(Tools);
|
|
59
|
+
console.log("Connecting to clients...");
|
|
60
|
+
await Clients.registerConfiguredModels();
|
|
61
|
+
console.log("✓ Services are set up and ready to go!");
|
|
61
62
|
}
|
|
62
63
|
|
|
63
64
|
// Utility function to read from stdin
|
package/src/clients/anthropic.ts
CHANGED
|
@@ -184,12 +184,44 @@ export class GenericAnthropicClient implements GenericClient {
|
|
|
184
184
|
});
|
|
185
185
|
}
|
|
186
186
|
|
|
187
|
+
// Convert tool message content to appropriate format
|
|
188
|
+
let toolResultContent: string | (Anthropic.TextBlockParam | Anthropic.ImageBlockParam)[];
|
|
189
|
+
|
|
190
|
+
if (typeof msg.content === "string") {
|
|
191
|
+
toolResultContent = msg.content;
|
|
192
|
+
} else if (Array.isArray(msg.content)) {
|
|
193
|
+
// Transform image_url format to Anthropic's image format
|
|
194
|
+
toolResultContent = msg.content.map((item): Anthropic.TextBlockParam | Anthropic.ImageBlockParam => {
|
|
195
|
+
if (item.type === "image_url") {
|
|
196
|
+
const url = item.image_url.url;
|
|
197
|
+
const isDataUrl = url.startsWith("data:");
|
|
198
|
+
const base64Data = isDataUrl ? url.split(",")[1] : url;
|
|
199
|
+
const mediaType = isDataUrl ? url.match(/data:([^;]+);/)?.[1] || "image/jpeg" : "image/jpeg";
|
|
200
|
+
|
|
201
|
+
return {
|
|
202
|
+
type: "image" as const,
|
|
203
|
+
source: {
|
|
204
|
+
type: "base64" as const,
|
|
205
|
+
media_type: mediaType as any,
|
|
206
|
+
data: base64Data,
|
|
207
|
+
},
|
|
208
|
+
};
|
|
209
|
+
} else if (item.type === "text") {
|
|
210
|
+
return { type: "text" as const, text: item.text };
|
|
211
|
+
}
|
|
212
|
+
// Fallback for unknown types
|
|
213
|
+
return { type: "text" as const, text: String(item) };
|
|
214
|
+
}) as (Anthropic.TextBlockParam | Anthropic.ImageBlockParam)[];
|
|
215
|
+
} else {
|
|
216
|
+
toolResultContent = String(msg.content);
|
|
217
|
+
}
|
|
218
|
+
|
|
187
219
|
toolMessages.push({
|
|
188
220
|
role: "user",
|
|
189
221
|
content: [
|
|
190
222
|
{
|
|
191
223
|
type: "tool_result",
|
|
192
|
-
content:
|
|
224
|
+
content: toolResultContent,
|
|
193
225
|
tool_use_id: msg.tool_call_id,
|
|
194
226
|
},
|
|
195
227
|
],
|
|
@@ -327,9 +359,19 @@ export class GenericAnthropicClient implements GenericClient {
|
|
|
327
359
|
return {
|
|
328
360
|
[Models.anthropic.Opus4_6]: {
|
|
329
361
|
input: 5.0,
|
|
362
|
+
input_gt_200k: 10.0,
|
|
330
363
|
cache_write: 6.25,
|
|
331
364
|
cache_hit: 0.5,
|
|
332
365
|
output: 25.0,
|
|
366
|
+
output_gt_200k: 37.5,
|
|
367
|
+
},
|
|
368
|
+
[Models.anthropic.Sonnet4_6]: {
|
|
369
|
+
input: 3.0,
|
|
370
|
+
input_gt_200k: 6.0,
|
|
371
|
+
cache_write: 3.75,
|
|
372
|
+
cache_hit: 0.3,
|
|
373
|
+
output: 15.0,
|
|
374
|
+
output_gt_200k: 22.5,
|
|
333
375
|
},
|
|
334
376
|
[Models.anthropic.Opus4_5]: {
|
|
335
377
|
input: 5.0,
|
|
@@ -351,15 +393,19 @@ export class GenericAnthropicClient implements GenericClient {
|
|
|
351
393
|
},
|
|
352
394
|
[Models.anthropic.Sonnet4]: {
|
|
353
395
|
input: 3.0,
|
|
396
|
+
input_gt_200k: 6.0,
|
|
354
397
|
cache_write: 3.75,
|
|
355
398
|
cache_hit: 0.3,
|
|
356
399
|
output: 15.0,
|
|
400
|
+
output_gt_200k: 22.5,
|
|
357
401
|
},
|
|
358
402
|
[Models.anthropic.Sonnet4_5]: {
|
|
359
403
|
input: 3.0,
|
|
404
|
+
input_gt_200k: 6.0,
|
|
360
405
|
cache_write: 3.75,
|
|
361
406
|
cache_hit: 0.3,
|
|
362
407
|
output: 15.0,
|
|
408
|
+
output_gt_200k: 22.5,
|
|
363
409
|
},
|
|
364
410
|
[Models.anthropic.Haiku4_5]: {
|
|
365
411
|
input: 1,
|
|
@@ -407,19 +453,36 @@ export class GenericAnthropicClient implements GenericClient {
|
|
|
407
453
|
return undefined;
|
|
408
454
|
}
|
|
409
455
|
|
|
456
|
+
// Calculate total input tokens (excluding cached tokens)
|
|
457
|
+
const inputTokens = usage.input_tokens;
|
|
458
|
+
|
|
410
459
|
const cachedInputTokens = usage.cache_creation_input_tokens;
|
|
411
460
|
const cachedInputCost = (cachedInputTokens * pricing.cache_write) / 1e6;
|
|
412
461
|
|
|
413
462
|
const cachedReadTokens = usage.cache_read_input_tokens;
|
|
414
463
|
const cachedReadCost = (cachedReadTokens * pricing.cache_hit) / 1e6;
|
|
415
464
|
|
|
416
|
-
|
|
417
|
-
|
|
465
|
+
// Calculate input cost with >200K input token pricing if applicable
|
|
466
|
+
let inputCost = 0;
|
|
467
|
+
const totalInputTokens = inputTokens + cachedInputTokens + cachedReadTokens;
|
|
468
|
+
|
|
469
|
+
if (totalInputTokens > 200000 && pricing.input_gt_200k) {
|
|
470
|
+
inputCost = (totalInputTokens * pricing.input_gt_200k) / 1e6;
|
|
471
|
+
} else {
|
|
472
|
+
inputCost = (totalInputTokens * pricing.input) / 1e6;
|
|
473
|
+
}
|
|
418
474
|
|
|
419
475
|
const outputTokens = usage.output_tokens;
|
|
420
|
-
const outputCost = (outputTokens * pricing.output) / 1e6;
|
|
421
476
|
|
|
422
|
-
|
|
477
|
+
// Calculate output cost with >200K input token pricing if applicable
|
|
478
|
+
let outputCost = 0;
|
|
479
|
+
if (totalInputTokens > 200000 && pricing.output_gt_200k) {
|
|
480
|
+
outputCost = (outputTokens * pricing.output_gt_200k) / 1e6;
|
|
481
|
+
} else {
|
|
482
|
+
outputCost = (outputTokens * pricing.output) / 1e6;
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
const total = cachedInputCost + cachedReadCost + inputCost + outputCost;
|
|
423
486
|
return total;
|
|
424
487
|
}
|
|
425
488
|
|
package/src/config.ts
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
|
+
import * as fs from "fs";
|
|
2
|
+
import * as path from "path";
|
|
1
3
|
import { Message } from "../clients/types";
|
|
2
4
|
import { MessageProcessorFunction } from "../services/MessageProcessor";
|
|
5
|
+
import { ToolsService } from "../services";
|
|
3
6
|
|
|
4
7
|
interface ImageContent {
|
|
5
8
|
type: "image_url";
|
|
@@ -14,23 +17,23 @@ interface TextContent {
|
|
|
14
17
|
text: string;
|
|
15
18
|
}
|
|
16
19
|
|
|
17
|
-
export class
|
|
18
|
-
private imageDetail: "auto" | "low" | "high";
|
|
19
|
-
private supportedFormats
|
|
20
|
+
export class Base64ImageProcessor {
|
|
21
|
+
private imageDetail: "auto" | "low" | "high" = "auto";
|
|
22
|
+
private supportedFormats = ["png", "jpeg", "jpg", "gif", "webp"];
|
|
20
23
|
|
|
21
|
-
constructor(
|
|
22
|
-
|
|
23
|
-
supportedFormats: string[] = ["png", "jpeg", "jpg", "gif", "webp"]
|
|
24
|
-
) {
|
|
25
|
-
this.imageDetail = imageDetail;
|
|
26
|
-
this.supportedFormats = supportedFormats;
|
|
24
|
+
constructor(toolsService?: ToolsService) {
|
|
25
|
+
this.registerTool(toolsService);
|
|
27
26
|
}
|
|
28
27
|
|
|
29
|
-
private isBase64Image(text: string): {
|
|
28
|
+
private isBase64Image(text: string): {
|
|
29
|
+
isImage: boolean;
|
|
30
|
+
mimeType?: string;
|
|
31
|
+
data?: string;
|
|
32
|
+
} {
|
|
30
33
|
// Check for data URL format: data:image/type;base64,actualdata
|
|
31
34
|
const dataUrlPattern = /^data:image\/([a-zA-Z]+);base64,(.+)$/;
|
|
32
35
|
const match = text.match(dataUrlPattern);
|
|
33
|
-
|
|
36
|
+
|
|
34
37
|
if (match) {
|
|
35
38
|
const [, mimeType, data] = match;
|
|
36
39
|
if (this.supportedFormats.includes(mimeType.toLowerCase())) {
|
|
@@ -47,14 +50,17 @@ export class Base64ImageDetector {
|
|
|
47
50
|
try {
|
|
48
51
|
const decoded = atob(header);
|
|
49
52
|
// Check for common image file signatures
|
|
50
|
-
if (decoded.startsWith(
|
|
51
|
-
return { isImage: true, mimeType:
|
|
52
|
-
} else if (decoded.startsWith(
|
|
53
|
-
return { isImage: true, mimeType:
|
|
54
|
-
} else if (
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
53
|
+
if (decoded.startsWith("\x89PNG")) {
|
|
54
|
+
return { isImage: true, mimeType: "png", data: text };
|
|
55
|
+
} else if (decoded.startsWith("\xFF\xD8\xFF")) {
|
|
56
|
+
return { isImage: true, mimeType: "jpeg", data: text };
|
|
57
|
+
} else if (
|
|
58
|
+
decoded.startsWith("GIF87a") ||
|
|
59
|
+
decoded.startsWith("GIF89a")
|
|
60
|
+
) {
|
|
61
|
+
return { isImage: true, mimeType: "gif", data: text };
|
|
62
|
+
} else if (decoded.startsWith("RIFF") && decoded.includes("WEBP")) {
|
|
63
|
+
return { isImage: true, mimeType: "webp", data: text };
|
|
58
64
|
}
|
|
59
65
|
} catch (e) {
|
|
60
66
|
// Not valid base64 or not an image
|
|
@@ -66,26 +72,26 @@ export class Base64ImageDetector {
|
|
|
66
72
|
|
|
67
73
|
private convertBase64ToImageContent(text: string): ImageContent | null {
|
|
68
74
|
const detection = this.isBase64Image(text);
|
|
69
|
-
|
|
75
|
+
|
|
70
76
|
if (!detection.isImage) {
|
|
71
77
|
return null;
|
|
72
78
|
}
|
|
73
79
|
|
|
74
|
-
const dataUrl = detection.data!.startsWith(
|
|
75
|
-
? detection.data
|
|
80
|
+
const dataUrl = detection.data!.startsWith("data:")
|
|
81
|
+
? detection.data
|
|
76
82
|
: `data:image/${detection.mimeType};base64,${detection.data}`;
|
|
77
83
|
|
|
78
84
|
return {
|
|
79
85
|
type: "image_url",
|
|
80
86
|
image_url: {
|
|
81
87
|
url: dataUrl,
|
|
82
|
-
detail: this.imageDetail
|
|
83
|
-
}
|
|
88
|
+
detail: this.imageDetail,
|
|
89
|
+
},
|
|
84
90
|
};
|
|
85
91
|
}
|
|
86
92
|
|
|
87
93
|
private processMessageContent(message: Message): void {
|
|
88
|
-
if (typeof message.content ===
|
|
94
|
+
if (typeof message.content === "string") {
|
|
89
95
|
const imageContent = this.convertBase64ToImageContent(message.content);
|
|
90
96
|
if (imageContent) {
|
|
91
97
|
// Convert string content to multimodal array
|
|
@@ -94,9 +100,9 @@ export class Base64ImageDetector {
|
|
|
94
100
|
} else if (Array.isArray(message.content)) {
|
|
95
101
|
// Process each content item
|
|
96
102
|
const newContent: (TextContent | ImageContent)[] = [];
|
|
97
|
-
|
|
103
|
+
|
|
98
104
|
for (const item of message.content) {
|
|
99
|
-
if (item.type ===
|
|
105
|
+
if (item.type === "text" && item.text) {
|
|
100
106
|
const imageContent = this.convertBase64ToImageContent(item.text);
|
|
101
107
|
if (imageContent) {
|
|
102
108
|
newContent.push(imageContent);
|
|
@@ -107,7 +113,7 @@ export class Base64ImageDetector {
|
|
|
107
113
|
newContent.push(item as TextContent | ImageContent);
|
|
108
114
|
}
|
|
109
115
|
}
|
|
110
|
-
|
|
116
|
+
|
|
111
117
|
message.content = newContent;
|
|
112
118
|
}
|
|
113
119
|
}
|
|
@@ -119,22 +125,22 @@ export class Base64ImageDetector {
|
|
|
119
125
|
try {
|
|
120
126
|
const args = JSON.parse(toolCall.function.arguments);
|
|
121
127
|
let modified = false;
|
|
122
|
-
|
|
128
|
+
|
|
123
129
|
// Recursively check all string values in arguments
|
|
124
130
|
const processValue = (obj: any): any => {
|
|
125
|
-
if (typeof obj ===
|
|
131
|
+
if (typeof obj === "string") {
|
|
126
132
|
const detection = this.isBase64Image(obj);
|
|
127
133
|
if (detection.isImage) {
|
|
128
134
|
modified = true;
|
|
129
|
-
const dataUrl = detection.data!.startsWith(
|
|
130
|
-
? detection.data
|
|
135
|
+
const dataUrl = detection.data!.startsWith("data:")
|
|
136
|
+
? detection.data
|
|
131
137
|
: `data:image/${detection.mimeType};base64,${detection.data}`;
|
|
132
138
|
return `[CONVERTED TO IMAGE: ${dataUrl.substring(0, 50)}...]`;
|
|
133
139
|
}
|
|
134
140
|
return obj;
|
|
135
141
|
} else if (Array.isArray(obj)) {
|
|
136
142
|
return obj.map(processValue);
|
|
137
|
-
} else if (obj && typeof obj ===
|
|
143
|
+
} else if (obj && typeof obj === "object") {
|
|
138
144
|
const result = {};
|
|
139
145
|
for (const [key, value] of Object.entries(obj)) {
|
|
140
146
|
result[key] = processValue(value);
|
|
@@ -143,7 +149,7 @@ export class Base64ImageDetector {
|
|
|
143
149
|
}
|
|
144
150
|
return obj;
|
|
145
151
|
};
|
|
146
|
-
|
|
152
|
+
|
|
147
153
|
const processedArgs = processValue(args);
|
|
148
154
|
if (modified) {
|
|
149
155
|
toolCall.function.arguments = JSON.stringify(processedArgs);
|
|
@@ -152,10 +158,13 @@ export class Base64ImageDetector {
|
|
|
152
158
|
// Arguments are not valid JSON, treat as string
|
|
153
159
|
const detection = this.isBase64Image(toolCall.function.arguments);
|
|
154
160
|
if (detection.isImage) {
|
|
155
|
-
const dataUrl = detection.data!.startsWith(
|
|
156
|
-
? detection.data
|
|
161
|
+
const dataUrl = detection.data!.startsWith("data:")
|
|
162
|
+
? detection.data
|
|
157
163
|
: `data:image/${detection.mimeType};base64,${detection.data}`;
|
|
158
|
-
toolCall.function.arguments = `[CONVERTED TO IMAGE: ${dataUrl.substring(
|
|
164
|
+
toolCall.function.arguments = `[CONVERTED TO IMAGE: ${dataUrl.substring(
|
|
165
|
+
0,
|
|
166
|
+
50
|
|
167
|
+
)}...]`;
|
|
159
168
|
}
|
|
160
169
|
}
|
|
161
170
|
}
|
|
@@ -163,14 +172,45 @@ export class Base64ImageDetector {
|
|
|
163
172
|
}
|
|
164
173
|
}
|
|
165
174
|
|
|
175
|
+
private processToolMessageContent(message: Message): void {
|
|
176
|
+
// Tool messages have string content that might be a JSON string containing image data
|
|
177
|
+
if (typeof message.content === "string" && message.content.trim()) {
|
|
178
|
+
try {
|
|
179
|
+
// Try to parse as JSON
|
|
180
|
+
const parsed = JSON.parse(message.content);
|
|
181
|
+
|
|
182
|
+
// Check if it's an image_url object
|
|
183
|
+
if (parsed.type === "image_url" && parsed.image_url?.url) {
|
|
184
|
+
// Convert the tool message content from JSON string to an array with the image
|
|
185
|
+
message.content = [parsed];
|
|
186
|
+
}
|
|
187
|
+
} catch (e) {
|
|
188
|
+
// Not JSON, check if it's a plain base64 string (only if still a string)
|
|
189
|
+
if (typeof message.content === "string") {
|
|
190
|
+
const imageContent = this.convertBase64ToImageContent(message.content);
|
|
191
|
+
if (imageContent) {
|
|
192
|
+
message.content = [imageContent];
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
166
199
|
createProcessor(): MessageProcessorFunction {
|
|
167
200
|
return (originalMessages: Message[], modifiedMessages: Message[]) => {
|
|
168
201
|
for (const message of modifiedMessages) {
|
|
169
|
-
//
|
|
170
|
-
if (message.role ===
|
|
202
|
+
// Process user messages (images from user input)
|
|
203
|
+
if (message.role === "user") {
|
|
171
204
|
this.processMessageContent(message);
|
|
172
205
|
}
|
|
173
206
|
|
|
207
|
+
// Process tool messages (images from loadImageAsBase64 tool)
|
|
208
|
+
// Tool responses come back as JSON strings that need to be parsed
|
|
209
|
+
// and converted to proper image content before the agent sees them
|
|
210
|
+
if (message.role === "tool") {
|
|
211
|
+
this.processToolMessageContent(message);
|
|
212
|
+
}
|
|
213
|
+
|
|
174
214
|
// Process tool calls in any message
|
|
175
215
|
this.processToolCallArguments(message);
|
|
176
216
|
}
|
|
@@ -184,7 +224,120 @@ export class Base64ImageDetector {
|
|
|
184
224
|
setSupportedFormats(formats: string[]): void {
|
|
185
225
|
this.supportedFormats = formats;
|
|
186
226
|
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Registers the loadImageAsBase64 tool with the ToolsService
|
|
230
|
+
*/
|
|
231
|
+
registerTool(toolsService?: ToolsService): void {
|
|
232
|
+
if (toolsService) {
|
|
233
|
+
const toolDefinition = {
|
|
234
|
+
type: "function" as const,
|
|
235
|
+
function: {
|
|
236
|
+
name: "loadImageAsBase64",
|
|
237
|
+
description:
|
|
238
|
+
"Load an image file from a file path and return it as a base64 data URL. This enables you to view and analyze images from the filesystem. Use this when the user provides a screenshot path or asks you to look at an image file.",
|
|
239
|
+
parameters: {
|
|
240
|
+
type: "object",
|
|
241
|
+
positional: true,
|
|
242
|
+
properties: {
|
|
243
|
+
filePath: {
|
|
244
|
+
type: "string",
|
|
245
|
+
description: "The absolute or relative path to the image file",
|
|
246
|
+
},
|
|
247
|
+
detail: {
|
|
248
|
+
type: "string",
|
|
249
|
+
description:
|
|
250
|
+
"The level of detail for image analysis. Options: 'auto' (default), 'low' (faster, less detail), 'high' (slower, more detail)",
|
|
251
|
+
},
|
|
252
|
+
},
|
|
253
|
+
required: ["filePath"],
|
|
254
|
+
},
|
|
255
|
+
},
|
|
256
|
+
};
|
|
257
|
+
|
|
258
|
+
toolsService.addTools([toolDefinition]);
|
|
259
|
+
toolsService.addFunctions({
|
|
260
|
+
loadImageAsBase64: async (
|
|
261
|
+
filePath: string,
|
|
262
|
+
detail?: "auto" | "low" | "high"
|
|
263
|
+
) => {
|
|
264
|
+
return await this.loadImageAsBase64(filePath, detail);
|
|
265
|
+
},
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* Loads an image from a file path and returns it as a base64 data URL
|
|
272
|
+
*/
|
|
273
|
+
private async loadImageAsBase64(
|
|
274
|
+
filePath: string,
|
|
275
|
+
detail: "auto" | "low" | "high" = "auto"
|
|
276
|
+
): Promise<string> {
|
|
277
|
+
try {
|
|
278
|
+
// Check if file exists
|
|
279
|
+
if (!fs.existsSync(filePath)) {
|
|
280
|
+
throw new Error(`File not found: ${filePath}`);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// Get file stats to verify it's a file
|
|
284
|
+
const stats = fs.statSync(filePath);
|
|
285
|
+
if (!stats.isFile()) {
|
|
286
|
+
throw new Error(`Path is not a file: ${filePath}`);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// Detect MIME type from file extension
|
|
290
|
+
const ext = path.extname(filePath).toLowerCase().replace(".", "");
|
|
291
|
+
const mimeTypeMap: { [key: string]: string } = {
|
|
292
|
+
png: "image/png",
|
|
293
|
+
jpg: "image/jpeg",
|
|
294
|
+
jpeg: "image/jpeg",
|
|
295
|
+
gif: "image/gif",
|
|
296
|
+
webp: "image/webp",
|
|
297
|
+
bmp: "image/bmp",
|
|
298
|
+
svg: "image/svg+xml",
|
|
299
|
+
};
|
|
300
|
+
|
|
301
|
+
const mimeType = mimeTypeMap[ext];
|
|
302
|
+
if (!mimeType) {
|
|
303
|
+
throw new Error(
|
|
304
|
+
`Unsupported image format: ${ext}. Supported formats: ${Object.keys(
|
|
305
|
+
mimeTypeMap
|
|
306
|
+
).join(", ")}`
|
|
307
|
+
);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// Check if format is supported
|
|
311
|
+
const simpleType = ext === "jpg" ? "jpeg" : ext;
|
|
312
|
+
if (!this.supportedFormats.includes(simpleType)) {
|
|
313
|
+
throw new Error(
|
|
314
|
+
`Image format ${ext} is not in supported formats: ${this.supportedFormats.join(
|
|
315
|
+
", "
|
|
316
|
+
)}`
|
|
317
|
+
);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// Read the file as base64
|
|
321
|
+
const imageBuffer = fs.readFileSync(filePath);
|
|
322
|
+
const base64Data = imageBuffer.toString("base64");
|
|
323
|
+
|
|
324
|
+
// Create data URL
|
|
325
|
+
const dataUrl = `data:${mimeType};base64,${base64Data}`;
|
|
326
|
+
|
|
327
|
+
// Return in a format that indicates this is an image
|
|
328
|
+
// The Base64ImageDetector will convert this to proper image content
|
|
329
|
+
return JSON.stringify({
|
|
330
|
+
type: "image_url",
|
|
331
|
+
image_url: {
|
|
332
|
+
url: dataUrl,
|
|
333
|
+
detail: detail || this.imageDetail,
|
|
334
|
+
},
|
|
335
|
+
});
|
|
336
|
+
} catch (error) {
|
|
337
|
+
throw new Error(`Failed to load image: ${error.message}`);
|
|
338
|
+
}
|
|
339
|
+
}
|
|
187
340
|
}
|
|
188
341
|
|
|
189
342
|
// Global instance
|
|
190
|
-
export const globalBase64ImageDetector = new
|
|
343
|
+
export const globalBase64ImageDetector = new Base64ImageProcessor();
|
package/src/processors/index.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export {
|
|
1
|
+
export { Base64ImageProcessor } from "./Base64ImageDetector";
|
|
2
2
|
export { CustomVariables } from "./CustomVariables";
|
|
3
3
|
export { TokenCompressor } from "./TokenCompressor";
|
|
4
4
|
export { JsonCompressor, JsonSchema, CompressionMetadata, JsonCompressorStorage } from "./JsonCompressor";
|
|
@@ -32,6 +32,7 @@ export class AgentSynchronization {
|
|
|
32
32
|
private baseUrl: string;
|
|
33
33
|
private knowhowTaskId: string | undefined;
|
|
34
34
|
private eventHandlersSetup: boolean = false;
|
|
35
|
+
private finalizationPromise: Promise<void> | null = null;
|
|
35
36
|
|
|
36
37
|
constructor(baseUrl: string = KNOWHOW_API_URL) {
|
|
37
38
|
this.baseUrl = baseUrl;
|
|
@@ -248,28 +249,44 @@ export class AgentSynchronization {
|
|
|
248
249
|
// Listen to completion event to finalize task
|
|
249
250
|
agent.agentEvents.on(agent.eventTypes.done, async (result: string) => {
|
|
250
251
|
if (!this.knowhowTaskId || !this.baseUrl) {
|
|
252
|
+
console.warn(`⚠️ [AgentSync] Cannot finalize: knowhowTaskId=${this.knowhowTaskId}, baseUrl=${this.baseUrl}`);
|
|
251
253
|
return;
|
|
252
254
|
}
|
|
253
255
|
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
256
|
+
console.log(`🎯 [AgentSync] Done event received for task: ${this.knowhowTaskId}`);
|
|
257
|
+
|
|
258
|
+
// Create a promise that tracks finalization
|
|
259
|
+
this.finalizationPromise = (async () => {
|
|
260
|
+
try {
|
|
261
|
+
console.log(
|
|
262
|
+
`Updating Knowhow chat task on completion..., ${this.knowhowTaskId}`
|
|
263
|
+
);
|
|
264
|
+
await this.updateChatTask(this.knowhowTaskId!, agent, false, result);
|
|
265
|
+
console.log(`✅ Completed Knowhow chat task: ${this.knowhowTaskId}`);
|
|
266
|
+
} catch (error) {
|
|
267
|
+
console.error(`❌ Error finalizing task:`, error);
|
|
268
|
+
throw error; // Re-throw so CLI can handle it
|
|
269
|
+
}
|
|
270
|
+
})();
|
|
264
271
|
});
|
|
265
272
|
}
|
|
266
273
|
|
|
274
|
+
/**
|
|
275
|
+
* Wait for finalization to complete (for CLI usage)
|
|
276
|
+
*/
|
|
277
|
+
async waitForFinalization(): Promise<void> {
|
|
278
|
+
if (this.finalizationPromise) {
|
|
279
|
+
await this.finalizationPromise;
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
267
283
|
/**
|
|
268
284
|
* Reset synchronization state (useful for reusing the service)
|
|
269
285
|
*/
|
|
270
286
|
reset(): void {
|
|
271
287
|
this.knowhowTaskId = undefined;
|
|
272
288
|
this.eventHandlersSetup = false;
|
|
289
|
+
this.finalizationPromise = null;
|
|
273
290
|
}
|
|
274
291
|
|
|
275
292
|
/**
|
package/src/types.ts
CHANGED
|
@@ -66,6 +66,16 @@ export type Config = {
|
|
|
66
66
|
sandbox?: boolean;
|
|
67
67
|
volumes?: string[];
|
|
68
68
|
envFile?: string;
|
|
69
|
+
tunnel?: {
|
|
70
|
+
enabled?: boolean;
|
|
71
|
+
allowedPorts?: number[];
|
|
72
|
+
maxConcurrentStreams?: number;
|
|
73
|
+
portMapping?: {
|
|
74
|
+
[containerPort: number]: number; // containerPort -> hostPort
|
|
75
|
+
};
|
|
76
|
+
localHost?: string; // Default: "127.0.0.1", can be "host.docker.internal" for Docker
|
|
77
|
+
enableUrlRewriting?: boolean; // Enable URL rewriting for localhost URLs (default: true)
|
|
78
|
+
};
|
|
69
79
|
};
|
|
70
80
|
};
|
|
71
81
|
|
|
@@ -143,6 +153,7 @@ export type ChatInteraction = {
|
|
|
143
153
|
export const Models = {
|
|
144
154
|
anthropic: {
|
|
145
155
|
Opus4_6: "claude-opus-4-6",
|
|
156
|
+
Sonnet4_6: "claude-sonnet-4-6",
|
|
146
157
|
Opus4_5: "claude-opus-4-5-20251101",
|
|
147
158
|
Opus4: "claude-opus-4-20250514",
|
|
148
159
|
Opus4_1: "claude-opus-4-1-20250805",
|