@tyvm/knowhow 0.0.111 → 0.0.112

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 (40) hide show
  1. package/package.json +1 -1
  2. package/scripts/test-repetition-hint.ts +234 -0
  3. package/src/chat/CliChatService.ts +14 -6
  4. package/src/chat/modules/AgentModule.ts +4 -1
  5. package/src/chat/modules/ClipboardImageModule.ts +136 -0
  6. package/src/chat/modules/InternalChatModule.ts +3 -0
  7. package/src/chat/modules/RendererModule.ts +30 -2
  8. package/src/clients/xai.ts +20 -3
  9. package/src/processors/CustomVariables.ts +175 -0
  10. package/src/services/EventService.ts +5 -33
  11. package/src/utils/index.ts +1 -0
  12. package/tests/fixtures/fake-secret.txt +1 -0
  13. package/tests/manual/modalities/xai.modalities.test.ts +1 -1
  14. package/tests/processors/CustomVariables.test.ts +416 -1
  15. package/ts_build/package.json +1 -1
  16. package/ts_build/src/chat/CliChatService.js +9 -4
  17. package/ts_build/src/chat/CliChatService.js.map +1 -1
  18. package/ts_build/src/chat/modules/AgentModule.js +3 -1
  19. package/ts_build/src/chat/modules/AgentModule.js.map +1 -1
  20. package/ts_build/src/chat/modules/ClipboardImageModule.d.ts +15 -0
  21. package/ts_build/src/chat/modules/ClipboardImageModule.js +157 -0
  22. package/ts_build/src/chat/modules/ClipboardImageModule.js.map +1 -0
  23. package/ts_build/src/chat/modules/InternalChatModule.d.ts +1 -0
  24. package/ts_build/src/chat/modules/InternalChatModule.js +3 -0
  25. package/ts_build/src/chat/modules/InternalChatModule.js.map +1 -1
  26. package/ts_build/src/chat/modules/RendererModule.js +30 -1
  27. package/ts_build/src/chat/modules/RendererModule.js.map +1 -1
  28. package/ts_build/src/clients/xai.js +14 -2
  29. package/ts_build/src/clients/xai.js.map +1 -1
  30. package/ts_build/src/processors/CustomVariables.d.ts +10 -0
  31. package/ts_build/src/processors/CustomVariables.js +127 -0
  32. package/ts_build/src/processors/CustomVariables.js.map +1 -1
  33. package/ts_build/src/services/EventService.d.ts +0 -4
  34. package/ts_build/src/services/EventService.js +4 -15
  35. package/ts_build/src/services/EventService.js.map +1 -1
  36. package/ts_build/src/utils/index.js.map +1 -1
  37. package/ts_build/tests/manual/modalities/xai.modalities.test.js +1 -1
  38. package/ts_build/tests/manual/modalities/xai.modalities.test.js.map +1 -1
  39. package/ts_build/tests/processors/CustomVariables.test.js +347 -0
  40. package/ts_build/tests/processors/CustomVariables.test.js.map +1 -1
@@ -2,6 +2,7 @@ import { Message } from "../clients/types";
2
2
  import { MessageProcessorFunction } from "../services/MessageProcessor";
3
3
  import { ToolsService } from "../services";
4
4
  import { Tool } from "../clients";
5
+ import { ToolCall } from "../clients/types";
5
6
 
6
7
  interface VariableStorage {
7
8
  [name: string]: any;
@@ -238,6 +239,180 @@ export class CustomVariables {
238
239
  };
239
240
  }
240
241
 
242
+ /**
243
+ * Extracts all string values from a JSON-parsed object (recursively)
244
+ */
245
+ private extractStringValues(obj: any, results: string[] = []): string[] {
246
+ if (typeof obj === "string") {
247
+ results.push(obj);
248
+ } else if (Array.isArray(obj)) {
249
+ for (const item of obj) {
250
+ this.extractStringValues(item, results);
251
+ }
252
+ } else if (obj && typeof obj === "object") {
253
+ for (const val of Object.values(obj)) {
254
+ this.extractStringValues(val, results);
255
+ }
256
+ }
257
+ return results;
258
+ }
259
+
260
+ /**
261
+ * Collects all large string values from tool calls, keyed by tool name.
262
+ * Returns an array of {value, toolName} pairs.
263
+ */
264
+ private collectToolCallStrings(
265
+ messages: Message[],
266
+ minLength: number
267
+ ): Array<{ value: string; toolName: string }> {
268
+ const collected: Array<{ value: string; toolName: string }> = [];
269
+ for (const message of messages) {
270
+ if (!message.tool_calls) continue;
271
+ for (const toolCall of message.tool_calls) {
272
+ const strings = this.getToolCallStrings(toolCall);
273
+ for (const str of strings) {
274
+ if (str.length >= minLength) {
275
+ collected.push({ value: str, toolName: toolCall.function.name });
276
+ }
277
+ }
278
+ }
279
+ }
280
+ return collected;
281
+ }
282
+
283
+ /**
284
+ * Finds the longest common substring between two strings that is >= minLength.
285
+ * Returns the substring or null if none found.
286
+ */
287
+ private longestCommonSubstring(a: string, b: string, minLength: number): string | null {
288
+ let best = "";
289
+ for (let i = 0; i < a.length - minLength + 1; i++) {
290
+ for (let j = a.length; j > i + minLength - 1; j--) {
291
+ const sub = a.slice(i, j);
292
+ if (sub.length <= best.length) break; // already found longer
293
+ if (b.includes(sub)) {
294
+ best = sub;
295
+ break;
296
+ }
297
+ }
298
+ }
299
+ return best.length >= minLength ? best : null;
300
+ }
301
+
302
+ /**
303
+ * Extracts all string values from a tool call's arguments
304
+ */
305
+ private getToolCallStrings(toolCall: ToolCall): string[] {
306
+ try {
307
+ const parsed = JSON.parse(toolCall.function.arguments);
308
+ return this.extractStringValues(parsed);
309
+ } catch {
310
+ // If not JSON, treat the whole arguments string as a single value
311
+ return [toolCall.function.arguments];
312
+ }
313
+ }
314
+
315
+ /**
316
+ * Creates a processor that scans messages for repeated large string values
317
+ * in tool call arguments, and appends a hint suggesting variable storage.
318
+ *
319
+ * This helps the LLM discover that it can avoid re-outputting long strings
320
+ * (e.g. JWTs, file contents) by storing them once with setVariable or
321
+ * storeToolCallToVariable and then referencing them via {{varName}}.
322
+ */
323
+ createRepetitionHintProcessor(options: {
324
+ minLength?: number; // Minimum string length to consider (default: 50)
325
+ minRepetitions?: number; // Minimum occurrences to trigger hint (default: 2)
326
+ minSubstringLength?: number; // Minimum repeated substring length (default: 50)
327
+ recentMessagesWindow?: number; // Only scan the last N messages (default: 10)
328
+ } = {}): MessageProcessorFunction {
329
+ const minLength = options.minLength ?? 50;
330
+ const minRepetitions = options.minRepetitions ?? 2;
331
+ const minSubstringLength = options.minSubstringLength ?? 50;
332
+ const recentMessagesWindow = options.recentMessagesWindow ?? 10;
333
+
334
+ return async (originalMessages: Message[], modifiedMessages: Message[]) => {
335
+ // Count occurrences of each string value across all tool call arguments
336
+ const stringCounts = new Map<string, { count: number; toolNames: Set<string> }>();
337
+
338
+ // Only scan the most recent N messages to keep cost bounded
339
+ const recentMessages = modifiedMessages.slice(-recentMessagesWindow);
340
+
341
+ // Step 1: exact full-string matches
342
+ const toolStrings = this.collectToolCallStrings(recentMessages, minLength);
343
+
344
+ for (const { value, toolName } of toolStrings) {
345
+ const existing = stringCounts.get(value);
346
+ if (existing) {
347
+ existing.count++;
348
+ existing.toolNames.add(toolName);
349
+ } else {
350
+ stringCounts.set(value, { count: 1, toolNames: new Set([toolName]) });
351
+ }
352
+ }
353
+
354
+ // Step 2: detect repeated substrings across different full strings
355
+ // e.g. the same JWT embedded in many different commands
356
+ const substringCounts = new Map<string, { count: number; toolNames: Set<string> }>();
357
+
358
+ for (let i = 0; i < toolStrings.length; i++) {
359
+ for (let j = i + 1; j < toolStrings.length; j++) {
360
+ const a = toolStrings[i];
361
+ const b = toolStrings[j];
362
+ // Skip if the full strings are identical (already counted above)
363
+ if (a.value === b.value) continue;
364
+
365
+ const common = this.longestCommonSubstring(a.value, b.value, minSubstringLength);
366
+ if (common) {
367
+ const existing = substringCounts.get(common);
368
+ if (existing) {
369
+ existing.count++;
370
+ existing.toolNames.add(a.toolName);
371
+ existing.toolNames.add(b.toolName);
372
+ } else {
373
+ substringCounts.set(common, {
374
+ count: 1,
375
+ toolNames: new Set([a.toolName, b.toolName]),
376
+ });
377
+ }
378
+ }
379
+ }
380
+ }
381
+
382
+ // Merge substring counts: count = number of unique pairs, so count+1 = occurrences
383
+ for (const [sub, info] of substringCounts.entries()) {
384
+ if (info.count + 1 >= minRepetitions) {
385
+ const existing = stringCounts.get(sub);
386
+ if (!existing) {
387
+ stringCounts.set(sub, { count: info.count + 1, toolNames: info.toolNames });
388
+ }
389
+ }
390
+ }
391
+
392
+ // Find entries that exceed the repetition threshold
393
+ const repeatedTools: string[] = [];
394
+ for (const [str, info] of stringCounts.entries()) {
395
+ if (info.count >= minRepetitions) {
396
+ for (const toolName of info.toolNames) {
397
+ if (!repeatedTools.includes(toolName)) {
398
+ repeatedTools.push(toolName);
399
+ }
400
+ }
401
+ }
402
+ }
403
+
404
+ if (repeatedTools.length > 0) {
405
+ modifiedMessages.push({
406
+ role: "user",
407
+ content:
408
+ `⚠️ Tool inputs have large repetitions detected in: ${repeatedTools.join(", ")}. ` +
409
+ `Consider storing repeated values with \`setVariable\` or \`storeToolCallToVariable\`, ` +
410
+ `then reference them via {{variableName}} in future tool calls to avoid re-outputting large strings.`,
411
+ });
412
+ }
413
+ };
414
+ }
415
+
241
416
  /**
242
417
  * Registers all custom variable tools with the ToolsService
243
418
  */
@@ -1,5 +1,6 @@
1
1
  import { EventEmitter } from "events";
2
2
  import { IAgent } from "../agents/interface";
3
+ import { logger } from "../logger";
3
4
 
4
5
  export type LogLevel = "info" | "warn" | "error";
5
6
 
@@ -36,9 +37,9 @@ type ManagedListenerRecord = {
36
37
  /**
37
38
  * Default console handler for plugin:log events.
38
39
  * Active when no renderer has taken over (e.g. worker mode, CLI before chat starts).
39
- * Can be suppressed by calling suppressDefaultLogger() when a renderer is active.
40
+ * Respects logger.silence() if the logger is silenced, this handler is a no-op.
40
41
  *
41
- * IMPORTANT: Uses process.stdout/stderr directly to avoid infinite recursion
42
+ * Uses process.stdout/stderr directly to avoid infinite recursion
42
43
  * with logger.installConsoleOverload() which overrides console.log/warn.
43
44
  */
44
45
  function defaultConsoleLogHandler(event: {
@@ -46,6 +47,7 @@ function defaultConsoleLogHandler(event: {
46
47
  message: string;
47
48
  level: LogLevel;
48
49
  }): void {
50
+ if (logger.isSilenced()) return;
49
51
  const prefix = event.source ? `[${event.source}] ` : "";
50
52
  const line = `${prefix}${event.message}\n`;
51
53
  switch (event.level) {
@@ -63,8 +65,6 @@ function defaultConsoleLogHandler(event: {
63
65
  export class EventService extends EventEmitter {
64
66
  private blockingHandlers: Map<string, EventHandler[]> = new Map();
65
67
  private managedListeners: Map<string, ManagedListenerRecord> = new Map();
66
- private defaultLoggerActive = true;
67
- private boundDefaultLogHandler = defaultConsoleLogHandler;
68
68
 
69
69
  eventTypes = {
70
70
  agentMsg: "agent:msg",
@@ -76,35 +76,7 @@ export class EventService extends EventEmitter {
76
76
  constructor() {
77
77
  super();
78
78
  this.setMaxListeners(100);
79
- // Register the default console logger so Events.log() always produces output
80
- // even before a renderer is attached (worker mode, module loading, etc.)
81
- this.on(this.eventTypes.pluginLog, this.boundDefaultLogHandler);
82
- }
83
-
84
- /**
85
- * Suppress the default console logger.
86
- * Call this when a renderer has taken over and will handle plugin:log events.
87
- * This prevents double-printing when both the renderer and the default handler fire.
88
- */
89
- suppressDefaultLogger(): void {
90
- if (this.defaultLoggerActive) {
91
- this.removeListener(
92
- this.eventTypes.pluginLog,
93
- this.boundDefaultLogHandler
94
- );
95
- this.defaultLoggerActive = false;
96
- }
97
- }
98
-
99
- /**
100
- * Restore the default console logger.
101
- * Call this when the renderer is torn down.
102
- */
103
- restoreDefaultLogger(): void {
104
- if (!this.defaultLoggerActive) {
105
- this.on(this.eventTypes.pluginLog, this.boundDefaultLogHandler);
106
- this.defaultLoggerActive = true;
107
- }
79
+ this.on(this.eventTypes.pluginLog, defaultConsoleLogHandler);
108
80
  }
109
81
 
110
82
  /**
@@ -40,6 +40,7 @@ export const setOnNewHistoryEntry = (
40
40
  inputQueue.setOnNewEntry(callback);
41
41
  };
42
42
 
43
+
43
44
  export const Marked = marked;
44
45
 
45
46
  export function dotp(x, y) {
@@ -0,0 +1 @@
1
+ eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJmYWtlLXVzZXItaWQiLCJuYW1lIjoiRmFrZSBVc2VyIiwiaWF0IjoxNTE2MjM5MDIyfQ.FAKE_SECRET_DO_NOT_USE
@@ -98,7 +98,7 @@ describe("XAI (Grok) Modalities", () => {
98
98
  const dataUrl = `data:image/png;base64,${base64Image}`;
99
99
 
100
100
  const response = await client.createCompletion("xai", {
101
- model: Models.xai.Grok2Vision1212,
101
+ model: Models.xai.Grok_4_20_NonReasoning,
102
102
  messages: [
103
103
  {
104
104
  role: "user",