@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.
- package/package.json +1 -1
- package/scripts/test-repetition-hint.ts +234 -0
- package/src/auth/browserLogin.ts +129 -3
- package/src/chat/CliChatService.ts +14 -6
- package/src/chat/modules/AgentModule.ts +4 -1
- package/src/chat/modules/ClipboardImageModule.ts +136 -0
- package/src/chat/modules/InternalChatModule.ts +3 -0
- package/src/chat/modules/RendererModule.ts +30 -2
- package/src/clients/xai.ts +20 -3
- package/src/login.ts +3 -2
- package/src/processors/CustomVariables.ts +175 -0
- package/src/services/EventService.ts +5 -33
- package/src/services/Mcp.ts +14 -1
- package/src/utils/http.ts +9 -2
- package/src/utils/index.ts +1 -0
- package/tests/fixtures/fake-secret.txt +1 -0
- package/tests/manual/modalities/xai.modalities.test.ts +1 -1
- package/tests/processors/CustomVariables.test.ts +416 -1
- package/ts_build/package.json +1 -1
- package/ts_build/src/auth/browserLogin.d.ts +2 -0
- package/ts_build/src/auth/browserLogin.js +91 -3
- package/ts_build/src/auth/browserLogin.js.map +1 -1
- package/ts_build/src/chat/CliChatService.js +9 -4
- package/ts_build/src/chat/CliChatService.js.map +1 -1
- package/ts_build/src/chat/modules/AgentModule.js +3 -1
- package/ts_build/src/chat/modules/AgentModule.js.map +1 -1
- package/ts_build/src/chat/modules/ClipboardImageModule.d.ts +15 -0
- package/ts_build/src/chat/modules/ClipboardImageModule.js +157 -0
- package/ts_build/src/chat/modules/ClipboardImageModule.js.map +1 -0
- package/ts_build/src/chat/modules/InternalChatModule.d.ts +1 -0
- package/ts_build/src/chat/modules/InternalChatModule.js +3 -0
- package/ts_build/src/chat/modules/InternalChatModule.js.map +1 -1
- package/ts_build/src/chat/modules/RendererModule.js +30 -1
- package/ts_build/src/chat/modules/RendererModule.js.map +1 -1
- package/ts_build/src/clients/xai.js +14 -2
- package/ts_build/src/clients/xai.js.map +1 -1
- package/ts_build/src/login.js +2 -2
- package/ts_build/src/login.js.map +1 -1
- package/ts_build/src/processors/CustomVariables.d.ts +10 -0
- package/ts_build/src/processors/CustomVariables.js +127 -0
- package/ts_build/src/processors/CustomVariables.js.map +1 -1
- package/ts_build/src/services/EventService.d.ts +0 -4
- package/ts_build/src/services/EventService.js +4 -15
- package/ts_build/src/services/EventService.js.map +1 -1
- package/ts_build/src/services/Mcp.js +9 -1
- package/ts_build/src/services/Mcp.js.map +1 -1
- package/ts_build/src/utils/http.d.ts +2 -1
- package/ts_build/src/utils/http.js +11 -2
- package/ts_build/src/utils/http.js.map +1 -1
- package/ts_build/src/utils/index.js.map +1 -1
- package/ts_build/tests/manual/modalities/xai.modalities.test.js +1 -1
- package/ts_build/tests/manual/modalities/xai.modalities.test.js.map +1 -1
- package/ts_build/tests/processors/CustomVariables.test.js +347 -0
- package/ts_build/tests/processors/CustomVariables.test.js.map +1 -1
package/src/clients/xai.ts
CHANGED
|
@@ -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
|
|
489
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
*
|
|
40
|
+
* Respects logger.silence() — if the logger is silenced, this handler is a no-op.
|
|
40
41
|
*
|
|
41
|
-
*
|
|
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
|
-
|
|
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
|
/**
|
package/src/services/Mcp.ts
CHANGED
|
@@ -528,7 +528,20 @@ export class McpService {
|
|
|
528
528
|
// skip adding tools for unconnected clients
|
|
529
529
|
continue;
|
|
530
530
|
}
|
|
531
|
-
|
|
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
|
-
|
|
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);
|
package/src/utils/index.ts
CHANGED
|
@@ -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.
|
|
101
|
+
model: Models.xai.Grok_4_20_NonReasoning,
|
|
102
102
|
messages: [
|
|
103
103
|
{
|
|
104
104
|
role: "user",
|