@tyvm/knowhow 0.0.110 → 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 (54) hide show
  1. package/package.json +1 -1
  2. package/scripts/test-repetition-hint.ts +234 -0
  3. package/src/auth/browserLogin.ts +129 -3
  4. package/src/chat/CliChatService.ts +14 -6
  5. package/src/chat/modules/AgentModule.ts +4 -1
  6. package/src/chat/modules/ClipboardImageModule.ts +136 -0
  7. package/src/chat/modules/InternalChatModule.ts +3 -0
  8. package/src/chat/modules/RendererModule.ts +30 -2
  9. package/src/clients/xai.ts +20 -3
  10. package/src/login.ts +3 -2
  11. package/src/processors/CustomVariables.ts +175 -0
  12. package/src/services/EventService.ts +5 -33
  13. package/src/services/Mcp.ts +14 -1
  14. package/src/utils/http.ts +9 -2
  15. package/src/utils/index.ts +1 -0
  16. package/tests/fixtures/fake-secret.txt +1 -0
  17. package/tests/manual/modalities/xai.modalities.test.ts +1 -1
  18. package/tests/processors/CustomVariables.test.ts +416 -1
  19. package/ts_build/package.json +1 -1
  20. package/ts_build/src/auth/browserLogin.d.ts +2 -0
  21. package/ts_build/src/auth/browserLogin.js +91 -3
  22. package/ts_build/src/auth/browserLogin.js.map +1 -1
  23. package/ts_build/src/chat/CliChatService.js +9 -4
  24. package/ts_build/src/chat/CliChatService.js.map +1 -1
  25. package/ts_build/src/chat/modules/AgentModule.js +3 -1
  26. package/ts_build/src/chat/modules/AgentModule.js.map +1 -1
  27. package/ts_build/src/chat/modules/ClipboardImageModule.d.ts +15 -0
  28. package/ts_build/src/chat/modules/ClipboardImageModule.js +157 -0
  29. package/ts_build/src/chat/modules/ClipboardImageModule.js.map +1 -0
  30. package/ts_build/src/chat/modules/InternalChatModule.d.ts +1 -0
  31. package/ts_build/src/chat/modules/InternalChatModule.js +3 -0
  32. package/ts_build/src/chat/modules/InternalChatModule.js.map +1 -1
  33. package/ts_build/src/chat/modules/RendererModule.js +30 -1
  34. package/ts_build/src/chat/modules/RendererModule.js.map +1 -1
  35. package/ts_build/src/clients/xai.js +14 -2
  36. package/ts_build/src/clients/xai.js.map +1 -1
  37. package/ts_build/src/login.js +2 -2
  38. package/ts_build/src/login.js.map +1 -1
  39. package/ts_build/src/processors/CustomVariables.d.ts +10 -0
  40. package/ts_build/src/processors/CustomVariables.js +127 -0
  41. package/ts_build/src/processors/CustomVariables.js.map +1 -1
  42. package/ts_build/src/services/EventService.d.ts +0 -4
  43. package/ts_build/src/services/EventService.js +4 -15
  44. package/ts_build/src/services/EventService.js.map +1 -1
  45. package/ts_build/src/services/Mcp.js +9 -1
  46. package/ts_build/src/services/Mcp.js.map +1 -1
  47. package/ts_build/src/utils/http.d.ts +2 -1
  48. package/ts_build/src/utils/http.js +11 -2
  49. package/ts_build/src/utils/http.js.map +1 -1
  50. package/ts_build/src/utils/index.js.map +1 -1
  51. package/ts_build/tests/manual/modalities/xai.modalities.test.js +1 -1
  52. package/ts_build/tests/manual/modalities/xai.modalities.test.js.map +1 -1
  53. package/ts_build/tests/processors/CustomVariables.test.js +347 -0
  54. package/ts_build/tests/processors/CustomVariables.test.js.map +1 -1
@@ -425,7 +425,7 @@ export class GenericXAIClient implements GenericClient {
425
425
  options: VideoStatusOptions
426
426
  ): Promise<VideoStatusResponse> {
427
427
  const statusResponse = await fetch(
428
- `https://api.x.ai/v1/videos/${options.jobId}`,
428
+ `https://api.x.ai/v1/videos/generations/${options.jobId}`,
429
429
  {
430
430
  method: "GET",
431
431
  headers: {
@@ -485,8 +485,25 @@ export class GenericXAIClient implements GenericClient {
485
485
  async downloadVideo(
486
486
  options: FileDownloadOptions
487
487
  ): Promise<FileDownloadResponse> {
488
- // XAI returns a URL for the video, not raw bytes from their API
489
- const url = options.uri || options.fileId;
488
+ // XAI returns a presigned URL from the status endpoint, not raw bytes.
489
+ // options.fileId is the request_id (jobId) — we need to fetch the status
490
+ // to get the actual video URL, then download from there.
491
+ let url = options.uri;
492
+ if (!url) {
493
+ const statusResponse = await fetch(
494
+ `https://api.x.ai/v1/videos/generations/${options.fileId}`,
495
+ { headers: { Authorization: `Bearer ${this.apiKey}` } }
496
+ );
497
+ if (!statusResponse.ok) {
498
+ const errorText = await statusResponse.text();
499
+ throw new Error(`XAI video status fetch failed: ${statusResponse.status} ${errorText}`);
500
+ }
501
+ const statusData = await statusResponse.json();
502
+ url = statusData.video?.url;
503
+ if (!url) {
504
+ throw new Error(`XAI video not ready yet or no URL available (status: ${statusData.status})`);
505
+ }
506
+ }
490
507
 
491
508
  const response = await fetch(url);
492
509
  if (!response.ok) {
package/src/login.ts CHANGED
@@ -74,9 +74,10 @@ export async function login(jwtFlag?: boolean): Promise<void> {
74
74
  await updateConfig(config);
75
75
  } catch (error) {
76
76
  if (http.isHttpError(error) && error.response) {
77
- const errData = await error.response.json().catch(() => ({ message: "Unknown error" }));
77
+ // error.body is the parsed JSON response body (set by http.ts when response is not ok)
78
+ const errData = error.body ?? { message: "Unknown error" };
78
79
  throw new Error(
79
- `Error: ${error.status} - ${errData.message || "Unknown error"}`
80
+ `Error: ${error.status} - ${errData?.message || "Unknown error"}`
80
81
  );
81
82
  }
82
83
  console.log(
@@ -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
  /**
@@ -528,7 +528,20 @@ export class McpService {
528
528
  // skip adding tools for unconnected clients
529
529
  continue;
530
530
  }
531
- const clientTools = await client.listTools();
531
+
532
+ let clientTools: Awaited<ReturnType<typeof client.listTools>>;
533
+ try {
534
+ clientTools = await client.listTools();
535
+ } catch (error) {
536
+ // The MCP server connected at the transport level but the underlying
537
+ // server returned an error (e.g. -32603 "Not connected" from the proxy).
538
+ // Mark it as disconnected and skip — don't crash the worker.
539
+ console.warn(
540
+ `⚠ MCP server '${config.name}' failed to list tools (marking as disconnected): ${error instanceof Error ? error.message : error}`
541
+ );
542
+ this.connected[i] = false;
543
+ continue;
544
+ }
532
545
 
533
546
  for (const tool of clientTools.tools) {
534
547
  const transformed = this.toOpenAiTool(i, tool as any as McpTool);
package/src/utils/http.ts CHANGED
@@ -13,7 +13,8 @@ export class HttpError extends Error {
13
13
  constructor(
14
14
  public status: number,
15
15
  public response: Response,
16
- message: string
16
+ message: string,
17
+ public body?: any,
17
18
  ) {
18
19
  super(message);
19
20
  this.name = "HttpError";
@@ -102,7 +103,13 @@ async function request<T = any>(
102
103
 
103
104
  if (!response.ok) {
104
105
  const text = await response.text().catch(() => "");
105
- throw new HttpError(response.status, response, `HTTP ${response.status}: ${text}`);
106
+ let parsedBody: any;
107
+ try {
108
+ parsedBody = JSON.parse(text);
109
+ } catch {
110
+ parsedBody = { message: text };
111
+ }
112
+ throw new HttpError(response.status, response, `HTTP ${response.status}: ${text}`, parsedBody);
106
113
  }
107
114
 
108
115
  const data = await parseBody<T>(response, responseType);
@@ -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",