@tyvm/knowhow 0.0.62 → 0.0.64

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 (42) hide show
  1. package/package.json +1 -1
  2. package/src/chat/modules/AgentModule.ts +10 -4
  3. package/src/clients/anthropic.ts +33 -1
  4. package/src/processors/Base64ImageDetector.ts +193 -40
  5. package/src/processors/JsonCompressor.ts +6 -6
  6. package/src/processors/TokenCompressor.ts +1 -1
  7. package/src/processors/ToolResponseCache.ts +64 -11
  8. package/src/processors/index.ts +1 -1
  9. package/tests/compressor/toolResponseCache.test.ts +303 -0
  10. package/tests/plugins/language/languagePlugin-content-triggers.test.ts +5 -1
  11. package/tests/plugins/language/languagePlugin.test.ts +5 -1
  12. package/tests/processors/Base64ImageDetector.test.ts +263 -70
  13. package/tests/services/Tools.test.ts +6 -4
  14. package/ts_build/package.json +1 -1
  15. package/ts_build/src/chat/modules/AgentModule.js +7 -2
  16. package/ts_build/src/chat/modules/AgentModule.js.map +1 -1
  17. package/ts_build/src/clients/anthropic.js +30 -1
  18. package/ts_build/src/clients/anthropic.js.map +1 -1
  19. package/ts_build/src/processors/Base64ImageDetector.d.ts +7 -3
  20. package/ts_build/src/processors/Base64ImageDetector.js +147 -27
  21. package/ts_build/src/processors/Base64ImageDetector.js.map +1 -1
  22. package/ts_build/src/processors/JsonCompressor.js +5 -5
  23. package/ts_build/src/processors/JsonCompressor.js.map +1 -1
  24. package/ts_build/src/processors/TokenCompressor.js +1 -1
  25. package/ts_build/src/processors/TokenCompressor.js.map +1 -1
  26. package/ts_build/src/processors/ToolResponseCache.d.ts +4 -2
  27. package/ts_build/src/processors/ToolResponseCache.js +50 -10
  28. package/ts_build/src/processors/ToolResponseCache.js.map +1 -1
  29. package/ts_build/src/processors/index.d.ts +1 -1
  30. package/ts_build/src/processors/index.js +2 -2
  31. package/ts_build/src/processors/index.js.map +1 -1
  32. package/ts_build/tests/compressor/toolResponseCache.test.d.ts +1 -0
  33. package/ts_build/tests/compressor/toolResponseCache.test.js +240 -0
  34. package/ts_build/tests/compressor/toolResponseCache.test.js.map +1 -0
  35. package/ts_build/tests/plugins/language/languagePlugin-content-triggers.test.js +5 -1
  36. package/ts_build/tests/plugins/language/languagePlugin-content-triggers.test.js.map +1 -1
  37. package/ts_build/tests/plugins/language/languagePlugin.test.js +5 -1
  38. package/ts_build/tests/plugins/language/languagePlugin.test.js.map +1 -1
  39. package/ts_build/tests/processors/Base64ImageDetector.test.js +221 -59
  40. package/ts_build/tests/processors/Base64ImageDetector.test.js.map +1 -1
  41. package/ts_build/tests/services/Tools.test.js +3 -3
  42. package/ts_build/tests/services/Tools.test.js.map +1 -1
@@ -0,0 +1,303 @@
1
+ import { describe, it, expect, beforeEach } from "@jest/globals";
2
+ import { ToolResponseCache } from "../../src/processors/ToolResponseCache";
3
+ import { ToolsService } from "../../src/services";
4
+ import * as fs from "fs";
5
+ import * as path from "path";
6
+
7
+ describe("ToolResponseCache - MCP Format Support", () => {
8
+ let cache: ToolResponseCache;
9
+ let toolsService: ToolsService;
10
+ let githubJsonContent: string;
11
+
12
+ beforeEach(() => {
13
+ toolsService = new ToolsService();
14
+ cache = new ToolResponseCache(toolsService);
15
+
16
+ // Load the githubjson.txt test data
17
+ const githubJsonPath = path.join(__dirname, "githubjson.txt");
18
+ githubJsonContent = fs.readFileSync(githubJsonPath, "utf-8");
19
+ });
20
+
21
+ describe("MCP Format Detection and Storage", () => {
22
+ it("should detect and parse MCP format responses", () => {
23
+ // Store the MCP format response
24
+ cache.storeToolResponse(githubJsonContent, "test-mcp-1");
25
+
26
+ // Verify it was stored
27
+ const stored = cache.retrieveRawResponse("test-mcp-1");
28
+ expect(stored).toBeDefined();
29
+ expect(stored).not.toBeNull();
30
+
31
+ // Verify the stored data is normalized JSON
32
+ const parsed = JSON.parse(stored!);
33
+ expect(parsed._mcp_format).toBe(true);
34
+ expect(parsed._data).toBeDefined();
35
+ expect(Array.isArray(parsed._data)).toBe(true);
36
+ });
37
+
38
+ it("should preserve the data array in normalized structure", () => {
39
+ cache.storeToolResponse(githubJsonContent, "test-mcp-2");
40
+ const stored = cache.retrieveRawResponse("test-mcp-2");
41
+ const parsed = JSON.parse(stored!);
42
+
43
+ // Verify the data array is present and has the expected structure
44
+ expect(parsed._data).toBeDefined();
45
+ expect(Array.isArray(parsed._data)).toBe(true);
46
+ expect(parsed._data.length).toBeGreaterThan(0);
47
+
48
+ // Verify first item has expected GitHub repo structure
49
+ const firstRepo = parsed._data[0];
50
+ expect(firstRepo).toHaveProperty("id");
51
+ expect(firstRepo).toHaveProperty("name");
52
+ expect(firstRepo).toHaveProperty("full_name");
53
+ expect(firstRepo).toHaveProperty("owner");
54
+ });
55
+
56
+ it("should store _mcp_format flag and _raw_structure metadata", () => {
57
+ cache.storeToolResponse(githubJsonContent, "test-mcp-3");
58
+ const stored = cache.retrieveRawResponse("test-mcp-3");
59
+ const parsed = JSON.parse(stored!);
60
+
61
+ expect(parsed._mcp_format).toBe(true);
62
+ expect(parsed._raw_structure).toBeDefined();
63
+ expect(parsed._raw_structure).toHaveProperty("content");
64
+ expect(Array.isArray(parsed._raw_structure.content)).toBe(true);
65
+ });
66
+ });
67
+
68
+ describe("JQ Queries Against MCP Data", () => {
69
+ beforeEach(() => {
70
+ cache.storeToolResponse(githubJsonContent, "github-repos");
71
+ });
72
+
73
+ it("should query the data array length with ._data | length", async () => {
74
+ const result = await cache.queryToolResponse("github-repos", "._data | length");
75
+
76
+ // Parse the result to verify it's a number
77
+ const length = JSON.parse(result);
78
+ expect(typeof length).toBe("number");
79
+ expect(length).toBeGreaterThan(0);
80
+ });
81
+
82
+ it("should query first item in data array with ._data[0]", async () => {
83
+ const result = await cache.queryToolResponse("github-repos", "._data[0]");
84
+
85
+ // Parse and verify it's an object with GitHub repo structure
86
+ const firstItem = JSON.parse(result);
87
+ expect(firstItem).toHaveProperty("id");
88
+ expect(firstItem).toHaveProperty("name");
89
+ expect(firstItem).toHaveProperty("full_name");
90
+ });
91
+
92
+ it("should query specific field in first item with ._data[0].name", async () => {
93
+ const result = await cache.queryToolResponse("github-repos", "._data[0].name");
94
+
95
+ // Parse and verify it's a string (repo name)
96
+ const name = JSON.parse(result);
97
+ expect(typeof name).toBe("string");
98
+ expect(name.length).toBeGreaterThan(0);
99
+ });
100
+
101
+ it("should map over data array with ._data | map(.name)", async () => {
102
+ const result = await cache.queryToolResponse("github-repos", "._data | map(.name)");
103
+
104
+ // Parse and verify it's an array of strings
105
+ const names = JSON.parse(result);
106
+ expect(Array.isArray(names)).toBe(true);
107
+ expect(names.length).toBeGreaterThan(0);
108
+ expect(typeof names[0]).toBe("string");
109
+ });
110
+
111
+ it("should filter data array with ._data | map(select(.private == true))", async () => {
112
+ const result = await cache.queryToolResponse(
113
+ "github-repos",
114
+ "._data | map(select(.private == true))"
115
+ );
116
+
117
+ // Parse and verify filtering worked
118
+ const privateRepos = JSON.parse(result);
119
+ expect(Array.isArray(privateRepos)).toBe(true);
120
+
121
+ // Verify all results have private = true
122
+ privateRepos.forEach((repo: any) => {
123
+ expect(repo.private).toBe(true);
124
+ });
125
+ });
126
+
127
+ it("should access nested properties with ._data[0].owner.login", async () => {
128
+ const result = await cache.queryToolResponse("github-repos", "._data[0].owner.login");
129
+
130
+ // Parse and verify it's a string (owner login)
131
+ const login = JSON.parse(result);
132
+ expect(typeof login).toBe("string");
133
+ expect(login.length).toBeGreaterThan(0);
134
+ });
135
+ });
136
+
137
+ describe("Non-MCP Format Compatibility", () => {
138
+ it("should handle plain JSON arrays without MCP wrapper", () => {
139
+ const plainArray = JSON.stringify([
140
+ { id: 1, name: "test1" },
141
+ { id: 2, name: "test2" },
142
+ ]);
143
+
144
+ cache.storeToolResponse(plainArray, "plain-json");
145
+ const stored = cache.retrieveRawResponse("plain-json");
146
+
147
+ expect(stored).toBe(plainArray);
148
+
149
+ // Should not have MCP format markers
150
+ const parsed = JSON.parse(stored!);
151
+ expect(parsed._mcp_format).toBeUndefined();
152
+ expect(Array.isArray(parsed)).toBe(true);
153
+ });
154
+
155
+ it("should handle plain JSON objects without MCP wrapper", () => {
156
+ const plainObject = JSON.stringify({ status: "success", data: { value: 42 } });
157
+
158
+ cache.storeToolResponse(plainObject, "plain-object");
159
+ const stored = cache.retrieveRawResponse("plain-object");
160
+
161
+ expect(stored).toBe(plainObject);
162
+
163
+ // Should not have MCP format markers (note: plain objects may have a "data" field which is fine)
164
+ const parsed = JSON.parse(stored!);
165
+ expect(parsed._mcp_format).toBeUndefined();
166
+ expect(parsed.status).toBe("success");
167
+ });
168
+
169
+ it("should handle plain text without JSON parsing", () => {
170
+ const plainText = "This is just plain text, not JSON";
171
+
172
+ cache.storeToolResponse(plainText, "plain-text");
173
+ const stored = cache.retrieveRawResponse("plain-text");
174
+
175
+ expect(stored).toBe(plainText);
176
+ });
177
+
178
+ it("should query plain JSON arrays with direct JQ queries", async () => {
179
+ const plainArray = JSON.stringify([
180
+ { id: 1, name: "test1" },
181
+ { id: 2, name: "test2" },
182
+ ]);
183
+
184
+ cache.storeToolResponse(plainArray, "plain-array");
185
+
186
+ // Query without .data prefix since there's no MCP wrapper
187
+ const result = await cache.queryToolResponse("plain-array", ".[0].name");
188
+ const name = JSON.parse(result);
189
+
190
+ expect(name).toBe("test1");
191
+ });
192
+ });
193
+
194
+ describe("Edge Cases and Double-Encoded JSON", () => {
195
+ it("should handle double-encoded JSON strings", () => {
196
+ const data = { test: "value" };
197
+ const doubleEncoded = JSON.stringify(JSON.stringify(data));
198
+
199
+ cache.storeToolResponse(doubleEncoded, "double-encoded");
200
+ const stored = cache.retrieveRawResponse("double-encoded");
201
+
202
+ // Should be parsed to single encoding
203
+ const parsed = JSON.parse(stored!);
204
+ expect(parsed.test).toBe("value");
205
+ });
206
+
207
+ it("should handle MCP format with double-encoded text field", () => {
208
+ const innerData = [{ id: 1, value: "test" }];
209
+ const doubleEncodedText = JSON.stringify(JSON.stringify(innerData));
210
+ const mcpWrapper = JSON.stringify({
211
+ content: [{ type: "text", text: doubleEncodedText }],
212
+ });
213
+
214
+ cache.storeToolResponse(mcpWrapper, "mcp-double-encoded");
215
+ const stored = cache.retrieveRawResponse("mcp-double-encoded");
216
+ const parsed = JSON.parse(stored!);
217
+
218
+ expect(parsed._mcp_format).toBe(true);
219
+ expect(parsed._data).toBeDefined();
220
+ expect(Array.isArray(parsed._data)).toBe(true);
221
+ expect(parsed._data[0].value).toBe("test");
222
+ });
223
+
224
+ it("should handle empty MCP content array", () => {
225
+ const emptyMcp = JSON.stringify({
226
+ content: [],
227
+ });
228
+
229
+ cache.storeToolResponse(emptyMcp, "empty-mcp");
230
+ const stored = cache.retrieveRawResponse("empty-mcp");
231
+ const parsed = JSON.parse(stored!);
232
+
233
+ // Should not be treated as MCP format since content is empty
234
+ expect(parsed._mcp_format).toBeUndefined();
235
+ expect(parsed.content).toBeDefined();
236
+ expect(Array.isArray(parsed.content)).toBe(true);
237
+ expect(parsed.content.length).toBe(0);
238
+ });
239
+
240
+ it("should handle MCP format with non-JSON text", () => {
241
+ const mcpWithText = JSON.stringify({
242
+ content: [{ type: "text", text: "This is plain text, not JSON" }],
243
+ });
244
+
245
+ cache.storeToolResponse(mcpWithText, "mcp-plain-text");
246
+ const stored = cache.retrieveRawResponse("mcp-plain-text");
247
+ const parsed = JSON.parse(stored!);
248
+
249
+ // Should not be treated as MCP format since text is not JSON
250
+ expect(parsed._mcp_format).toBeUndefined();
251
+ expect(parsed.content[0].text).toBe("This is plain text, not JSON");
252
+ });
253
+
254
+ it("should not overwrite stored responses on subsequent stores", () => {
255
+ const originalData = JSON.stringify({ original: true });
256
+ const newData = JSON.stringify({ original: false });
257
+
258
+ cache.storeToolResponse(originalData, "no-overwrite");
259
+ cache.storeToolResponse(newData, "no-overwrite");
260
+
261
+ const stored = cache.retrieveRawResponse("no-overwrite");
262
+ const parsed = JSON.parse(stored!);
263
+
264
+ // Should still have the original data
265
+ expect(parsed.original).toBe(true);
266
+ });
267
+ });
268
+
269
+ describe("Metadata and Storage Management", () => {
270
+ it("should track metadata for MCP format responses", () => {
271
+ cache.storeToolResponse(githubJsonContent, "meta-test", "github_list_repos");
272
+
273
+ const keys = cache.getStorageKeys();
274
+ expect(keys).toContain("meta-test");
275
+
276
+ const size = cache.getStorageSize();
277
+ expect(size).toBeGreaterThanOrEqual(1);
278
+ });
279
+
280
+ it("should list stored tool responses including MCP format", async () => {
281
+ cache.storeToolResponse(githubJsonContent, "repo-list", "github_repos");
282
+ cache.storeToolResponse(JSON.stringify({ test: "data" }), "test-data", "test_tool");
283
+
284
+ const list = await cache.listStoredToolResponses();
285
+
286
+ expect(list).toContain("repo-list");
287
+ expect(list).toContain("test-data");
288
+ });
289
+
290
+ it("should clear all stored responses", () => {
291
+ cache.storeToolResponse(githubJsonContent, "clear-test-1");
292
+ cache.storeToolResponse(JSON.stringify({ test: "data" }), "clear-test-2");
293
+
294
+ expect(cache.getStorageSize()).toBe(2);
295
+
296
+ cache.clearStorage();
297
+
298
+ expect(cache.getStorageSize()).toBe(0);
299
+ expect(cache.retrieveRawResponse("clear-test-1")).toBeNull();
300
+ expect(cache.retrieveRawResponse("clear-test-2")).toBeNull();
301
+ });
302
+ });
303
+ });
@@ -7,7 +7,11 @@ import { getConfig, getLanguageConfig } from "../../../src/config";
7
7
  jest.mock("../../../src/utils", () => ({
8
8
  readFile: jest.fn(),
9
9
  fileExists: jest.fn().mockReturnValue(true),
10
- fileStat: jest.fn(),
10
+ fileStat: jest.fn().mockResolvedValue({
11
+ isDirectory: jest.fn().mockReturnValue(false),
12
+ isFile: jest.fn().mockReturnValue(true),
13
+ size: 1024,
14
+ }),
11
15
  }));
12
16
 
13
17
  jest.mock("../../../src/services/EventService", () => ({
@@ -1,7 +1,11 @@
1
1
  jest.mock("../../../src/utils", () => ({
2
2
  readFile: jest.fn().mockReturnValue(Buffer.from("test")),
3
3
  fileExists: jest.fn().mockReturnValue(true),
4
- fileStat: jest.fn(),
4
+ fileStat: jest.fn().mockResolvedValue({
5
+ isDirectory: jest.fn().mockReturnValue(false),
6
+ isFile: jest.fn().mockReturnValue(true),
7
+ size: 1024,
8
+ }),
5
9
  }));
6
10
 
7
11
  jest.mock("../../../src/services/EventService", () => ({