@tyvm/knowhow 0.0.111 → 0.0.113

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 +42 -20
  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 +42 -55
  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 +36 -19
  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 +29 -46
  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",
@@ -199,42 +199,42 @@ describe("XAI (Grok) Modalities", () => {
199
199
  return;
200
200
  }
201
201
 
202
- const apiKey = process.env.XAI_API_KEY!;
203
202
  const prompt =
204
203
  "A cyberpunk cityscape at night with flying cars and neon lights, " +
205
204
  "cinematic camera slowly panning upward to reveal the skyline";
206
205
 
207
- // Helper: poll a known request ID until done, then save results
208
- async function pollAndSave(requestId: string) {
206
+ // Helper: poll a known job ID via client until done, then save results
207
+ async function pollAndSave(jobId: string) {
209
208
  const maxPollingTime = 20 * 60 * 1000; // 20 minutes
210
209
  const pollingInterval = 5000; // 5 seconds
211
210
  const startTime = Date.now();
212
211
 
213
- console.log(`⏳ Polling request ID: ${requestId}`);
212
+ console.log(`⏳ Polling job ID: ${jobId}`);
214
213
 
215
214
  while (Date.now() - startTime < maxPollingTime) {
216
215
  await new Promise((resolve) => setTimeout(resolve, pollingInterval));
217
216
 
218
- const pollResponse = await fetch(
219
- `https://api.x.ai/v1/videos/${requestId}`,
220
- { headers: { Authorization: `Bearer ${apiKey}` } }
217
+ const status = await client.getVideoStatus("xai", { jobId });
218
+ console.log(
219
+ ` Status: ${status.status}, has data: ${!!status.data?.length}`
221
220
  );
222
221
 
223
- if (!pollResponse.ok) {
224
- const errorText = await pollResponse.text();
225
- throw new Error(`XAI video polling failed: ${pollResponse.status} ${errorText}`);
222
+ if (status.status === "failed") {
223
+ throw new Error(
224
+ `XAI video generation failed: ${status.error || "unknown error"}`
225
+ );
226
226
  }
227
227
 
228
- const pollData = await pollResponse.json();
229
- console.log(` Status: ${pollData.status || "unknown"}, has video: ${!!pollData.video}`);
228
+ if (status.status === "expired") {
229
+ throw new Error("XAI video generation request expired");
230
+ }
230
231
 
231
- // XAI returns video data directly (no status:"done") when complete
232
- if (pollData.video?.url) {
233
- const videoUrl = pollData.video.url;
232
+ if (status.status === "completed" && status.data?.[0]?.url) {
233
+ const videoUrl = status.data[0].url;
234
234
  const outputContent = [
235
235
  `Generated at: ${new Date().toISOString()}`,
236
236
  `Prompt: ${prompt}`,
237
- `Request ID: ${requestId}`,
237
+ `Job ID: ${jobId}`,
238
238
  `Video URL: ${videoUrl}`,
239
239
  ].join("\n");
240
240
  fs.writeFileSync(outputPath, outputContent);
@@ -243,61 +243,48 @@ describe("XAI (Grok) Modalities", () => {
243
243
 
244
244
  // Clean up job ID file
245
245
  if (fs.existsSync(jobIdPath)) fs.unlinkSync(jobIdPath);
246
- return pollData;
247
- } else if (pollData.status === "expired") {
248
- throw new Error("XAI video generation request expired");
249
- } else if (pollData.status === "failed") {
250
- throw new Error(`XAI video generation failed: ${JSON.stringify(pollData)}`);
246
+ return;
251
247
  }
252
- // pending – keep polling
248
+ // queued / in_progress – keep polling
253
249
  }
254
250
 
255
- throw new Error("XAI video generation timed out after 20 minutes of polling");
251
+ throw new Error(
252
+ "XAI video generation timed out after 20 minutes of polling"
253
+ );
256
254
  }
257
255
 
258
- // If we already have a request ID from a previous (timed-out) run, resume
256
+ // If we already have a job ID from a previous (timed-out) run, resume
259
257
  if (fs.existsSync(jobIdPath)) {
260
- const requestId = fs.readFileSync(jobIdPath, "utf8").trim();
261
- console.log(`🔄 Resuming poll for existing request ID: ${requestId}`);
262
- await pollAndSave(requestId);
258
+ const jobId = fs.readFileSync(jobIdPath, "utf8").trim();
259
+ console.log(`🔄 Resuming poll for existing job ID: ${jobId}`);
260
+ await pollAndSave(jobId);
263
261
  expect(fs.existsSync(outputPath)).toBe(true);
264
262
  return;
265
263
  }
266
264
 
267
- // Otherwise start a new job
265
+ // Otherwise start a new job via the client
268
266
  console.log("⏳ Submitting XAI video generation job...");
269
267
 
270
- const startResponse = await fetch("https://api.x.ai/v1/videos/generations", {
271
- method: "POST",
272
- headers: {
273
- "Content-Type": "application/json",
274
- Authorization: `Bearer ${apiKey}`,
275
- },
276
- body: JSON.stringify({
277
- model: "grok-imagine-video",
278
- prompt,
279
- duration: 5,
280
- aspect_ratio: "16:9",
281
- }),
268
+ const response = await client.createVideoGeneration("xai", {
269
+ model: "grok-imagine-video",
270
+ prompt,
271
+ duration: 5,
272
+ aspect_ratio: "16:9",
282
273
  });
283
274
 
284
- if (!startResponse.ok) {
285
- const errorText = await startResponse.text();
286
- throw new Error(`XAI video generation start failed: ${startResponse.status} ${errorText}`);
287
- }
288
-
289
- const startData = await startResponse.json();
290
- const requestId = startData.request_id;
291
-
292
- if (!requestId) {
293
- throw new Error(`No request_id in response: ${JSON.stringify(startData)}`);
275
+ const jobId = response.jobId;
276
+ if (!jobId) {
277
+ throw new Error(
278
+ `No jobId returned from video generation: ${JSON.stringify(response)}`
279
+ );
294
280
  }
295
281
 
296
- // Persist the request ID so subsequent runs can resume if this run times out
297
- fs.writeFileSync(jobIdPath, requestId);
298
- console.log(`📝 Request ID saved to: ${jobIdPath} (ID: ${requestId})`);
282
+ // Persist the job ID so subsequent runs can resume if this run times out
283
+ fs.writeFileSync(jobIdPath, jobId);
284
+ console.log(`📝 Job ID saved to: ${jobIdPath} (ID: ${jobId})`);
285
+ console.log(` Estimated cost: $${response.usd_cost?.toFixed(6)}`);
299
286
 
300
- await pollAndSave(requestId);
287
+ await pollAndSave(jobId);
301
288
 
302
289
  expect(fs.existsSync(outputPath)).toBe(true);
303
290
  },