@tyvm/knowhow 0.0.117 → 0.0.119
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 -3
- package/src/agents/base/base.ts +72 -9
- package/src/agents/researcher/researcher.ts +9 -2
- package/src/agents/tools/list.ts +13 -2
- package/src/agents/tools/patch.ts +318 -32
- package/src/agents/tools/readFile.ts +48 -5
- package/src/chat/modules/AgentModule.ts +12 -0
- package/src/cli.ts +4 -0
- package/src/clients/anthropic.ts +12 -2
- package/src/clients/contextLimits.ts +77 -0
- package/src/commands/convert.ts +291 -0
- package/src/commands/mcp.ts +742 -0
- package/src/conversion.ts +15 -61
- package/src/index.ts +3 -0
- package/src/processors/TokenCompressor.ts +95 -9
- package/src/services/AgentSyncFs.ts +26 -4
- package/src/services/AgentSyncKnowhowWeb.ts +26 -4
- package/src/services/Mcp.ts +3 -1
- package/src/services/SyncedAgentWatcher.ts +8 -0
- package/src/services/conversion/ConversionService.ts +763 -0
- package/src/services/conversion/index.ts +2 -0
- package/src/services/conversion/types.ts +79 -0
- package/src/services/index.ts +8 -1
- package/src/services/modules/types.ts +2 -0
- package/src/services/watchers/FsSyncer.ts +6 -0
- package/src/services/watchers/RemoteSyncer.ts +5 -0
- package/src/types.ts +1 -0
- package/tests/agents/tools/readFile.test.ts +88 -0
- package/tests/clients/AIClient.test.ts +5 -0
- package/tests/clients/contextLimits.test.ts +71 -0
- package/tests/patching/patchFileOutput.test.ts +217 -0
- package/tests/patching/regression-2026.test.ts +278 -0
- package/tests/processors/CustomVariables.test.ts +4 -4
- package/tests/processors/TokenCompressor.test.ts +59 -1
- package/tests/processors/tools/grepToolResponse.test.ts +72 -0
- package/tests/services/ConversionService.test.ts +154 -0
- package/tests/test.spec.ts +1 -1
- package/tests/unit/clients/AIClient.test.ts +8 -0
- package/ts_build/package.json +1 -3
- package/ts_build/src/agents/base/base.d.ts +3 -0
- package/ts_build/src/agents/base/base.js +46 -3
- package/ts_build/src/agents/base/base.js.map +1 -1
- package/ts_build/src/agents/researcher/researcher.js +5 -2
- package/ts_build/src/agents/researcher/researcher.js.map +1 -1
- package/ts_build/src/agents/tools/list.js +10 -2
- package/ts_build/src/agents/tools/list.js.map +1 -1
- package/ts_build/src/agents/tools/patch.js +202 -24
- package/ts_build/src/agents/tools/patch.js.map +1 -1
- package/ts_build/src/agents/tools/readFile.d.ts +1 -1
- package/ts_build/src/agents/tools/readFile.js +17 -4
- package/ts_build/src/agents/tools/readFile.js.map +1 -1
- package/ts_build/src/chat/modules/AgentModule.js +12 -0
- package/ts_build/src/chat/modules/AgentModule.js.map +1 -1
- package/ts_build/src/cli.js +4 -0
- package/ts_build/src/cli.js.map +1 -1
- package/ts_build/src/clients/anthropic.js +7 -2
- package/ts_build/src/clients/anthropic.js.map +1 -1
- package/ts_build/src/clients/contextLimits.js +70 -0
- package/ts_build/src/clients/contextLimits.js.map +1 -1
- package/ts_build/src/commands/convert.d.ts +2 -0
- package/ts_build/src/commands/convert.js +275 -0
- package/ts_build/src/commands/convert.js.map +1 -0
- package/ts_build/src/commands/mcp.d.ts +2 -0
- package/ts_build/src/commands/mcp.js +664 -0
- package/ts_build/src/commands/mcp.js.map +1 -0
- package/ts_build/src/conversion.js +6 -38
- package/ts_build/src/conversion.js.map +1 -1
- package/ts_build/src/index.d.ts +2 -0
- package/ts_build/src/index.js +4 -1
- package/ts_build/src/index.js.map +1 -1
- package/ts_build/src/processors/TokenCompressor.d.ts +2 -0
- package/ts_build/src/processors/TokenCompressor.js +57 -7
- package/ts_build/src/processors/TokenCompressor.js.map +1 -1
- package/ts_build/src/services/AgentSyncFs.d.ts +1 -0
- package/ts_build/src/services/AgentSyncFs.js +21 -4
- package/ts_build/src/services/AgentSyncFs.js.map +1 -1
- package/ts_build/src/services/AgentSyncKnowhowWeb.d.ts +1 -0
- package/ts_build/src/services/AgentSyncKnowhowWeb.js +21 -4
- package/ts_build/src/services/AgentSyncKnowhowWeb.js.map +1 -1
- package/ts_build/src/services/Mcp.js +2 -1
- package/ts_build/src/services/Mcp.js.map +1 -1
- package/ts_build/src/services/SyncedAgentWatcher.d.ts +3 -0
- package/ts_build/src/services/SyncedAgentWatcher.js +4 -0
- package/ts_build/src/services/SyncedAgentWatcher.js.map +1 -1
- package/ts_build/src/services/conversion/ConversionService.d.ts +18 -0
- package/ts_build/src/services/conversion/ConversionService.js +585 -0
- package/ts_build/src/services/conversion/ConversionService.js.map +1 -0
- package/ts_build/src/services/conversion/index.d.ts +2 -0
- package/ts_build/src/services/conversion/index.js +19 -0
- package/ts_build/src/services/conversion/index.js.map +1 -0
- package/ts_build/src/services/conversion/types.d.ts +49 -0
- package/ts_build/src/services/conversion/types.js +3 -0
- package/ts_build/src/services/conversion/types.js.map +1 -0
- package/ts_build/src/services/index.d.ts +3 -0
- package/ts_build/src/services/index.js +6 -1
- package/ts_build/src/services/index.js.map +1 -1
- package/ts_build/src/services/modules/index.d.ts +2 -0
- package/ts_build/src/services/modules/types.d.ts +2 -0
- package/ts_build/src/services/watchers/FsSyncer.d.ts +1 -0
- package/ts_build/src/services/watchers/FsSyncer.js +5 -0
- package/ts_build/src/services/watchers/FsSyncer.js.map +1 -1
- package/ts_build/src/services/watchers/RemoteSyncer.d.ts +1 -0
- package/ts_build/src/services/watchers/RemoteSyncer.js +4 -0
- package/ts_build/src/services/watchers/RemoteSyncer.js.map +1 -1
- package/ts_build/src/types.d.ts +1 -0
- package/ts_build/src/types.js.map +1 -1
- package/ts_build/tests/agents/tools/readFile.test.d.ts +1 -0
- package/ts_build/tests/agents/tools/readFile.test.js +90 -0
- package/ts_build/tests/agents/tools/readFile.test.js.map +1 -0
- package/ts_build/tests/clients/AIClient.test.js +1 -0
- package/ts_build/tests/clients/AIClient.test.js.map +1 -1
- package/ts_build/tests/clients/contextLimits.test.d.ts +1 -0
- package/ts_build/tests/clients/contextLimits.test.js +57 -0
- package/ts_build/tests/clients/contextLimits.test.js.map +1 -0
- package/ts_build/tests/patching/patchFileOutput.test.d.ts +1 -0
- package/ts_build/tests/patching/patchFileOutput.test.js +187 -0
- package/ts_build/tests/patching/patchFileOutput.test.js.map +1 -0
- package/ts_build/tests/patching/regression-2026.test.js +214 -0
- package/ts_build/tests/patching/regression-2026.test.js.map +1 -1
- package/ts_build/tests/processors/CustomVariables.test.js +4 -4
- package/ts_build/tests/processors/CustomVariables.test.js.map +1 -1
- package/ts_build/tests/processors/TokenCompressor.test.js +37 -1
- package/ts_build/tests/processors/TokenCompressor.test.js.map +1 -1
- package/ts_build/tests/processors/tools/grepToolResponse.test.d.ts +1 -0
- package/ts_build/tests/processors/tools/grepToolResponse.test.js +40 -0
- package/ts_build/tests/processors/tools/grepToolResponse.test.js.map +1 -0
- package/ts_build/tests/services/ConversionService.test.d.ts +1 -0
- package/ts_build/tests/services/ConversionService.test.js +154 -0
- package/ts_build/tests/services/ConversionService.test.js.map +1 -0
- package/ts_build/tests/test.spec.js +1 -1
- package/ts_build/tests/test.spec.js.map +1 -1
- package/ts_build/tests/unit/clients/AIClient.test.js +3 -0
- package/ts_build/tests/unit/clients/AIClient.test.js.map +1 -1
|
@@ -280,4 +280,282 @@ describe("Patch Engine - Edge Case Regression Suite", () => {
|
|
|
280
280
|
expect(finalApplication).toContain("const c = 4;");
|
|
281
281
|
});
|
|
282
282
|
});
|
|
283
|
+
|
|
284
|
+
// --- Test Case 14: Insertion-Before-Anchor Reordering (real session bug) ---
|
|
285
|
+
// Reproduces a bug observed while editing SnapshotManager.ts: adding new lines
|
|
286
|
+
// ABOVE an existing `return snapshot;` (using that return as trailing context)
|
|
287
|
+
// anchored on the PRECEDING context line and inserted the new lines AFTER the
|
|
288
|
+
// return instead of before it. The added log line became unreachable dead code
|
|
289
|
+
// and, in a follow-up edit, the `return` was dropped entirely — a silent
|
|
290
|
+
// control-flow / correctness change. Hunk: insert N lines before a kept line.
|
|
291
|
+
describe("Error Type 14: Insertion-Before-Anchor Reordering", () => {
|
|
292
|
+
it("should insert new lines BEFORE the trailing-context line, not after it", () => {
|
|
293
|
+
const originalFileContent = ` if (rootfsStillExists) {
|
|
294
|
+
return snapshot;
|
|
295
|
+
}
|
|
296
|
+
this.logger.info("missing");
|
|
297
|
+
`;
|
|
298
|
+
// Add a comment + log + KEEP the existing `return snapshot;` as trailing context.
|
|
299
|
+
const insertBeforeReturnPatch = `@@ -1,3 +1,6 @@
|
|
300
|
+
if (rootfsStillExists) {
|
|
301
|
+
+ // reusing local overlay
|
|
302
|
+
+ this.logger.info("reusing");
|
|
303
|
+
return snapshot;
|
|
304
|
+
}
|
|
305
|
+
`;
|
|
306
|
+
|
|
307
|
+
const fixedPatchOutput = fixPatch(
|
|
308
|
+
originalFileContent,
|
|
309
|
+
insertBeforeReturnPatch
|
|
310
|
+
);
|
|
311
|
+
const finalApplication = applyPatch(
|
|
312
|
+
originalFileContent,
|
|
313
|
+
fixedPatchOutput
|
|
314
|
+
);
|
|
315
|
+
|
|
316
|
+
expect(finalApplication).not.toBe(false);
|
|
317
|
+
const out = finalApplication as string;
|
|
318
|
+
// The kept `return snapshot;` must still be present exactly once.
|
|
319
|
+
expect(out).toContain("return snapshot;");
|
|
320
|
+
// The new log must appear BEFORE the return (otherwise it is unreachable).
|
|
321
|
+
const idxLog = out.indexOf('this.logger.info("reusing")');
|
|
322
|
+
const idxReturn = out.indexOf("return snapshot;");
|
|
323
|
+
expect(idxLog).toBeGreaterThanOrEqual(0);
|
|
324
|
+
expect(idxReturn).toBeGreaterThanOrEqual(0);
|
|
325
|
+
expect(idxLog).toBeLessThan(idxReturn);
|
|
326
|
+
});
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
// --- Test Case 15: Append-After-Function Scope Bleed (real session bug) ---
|
|
330
|
+
// Reproduces a bug observed while editing test-cross-machine-restore.ts: appending
|
|
331
|
+
// a brand-new top-level function right after an existing function's closing brace
|
|
332
|
+
// (using the function body + closing `}` as leading context) caused the new
|
|
333
|
+
// function to be injected INSIDE the existing function's body, between its
|
|
334
|
+
// signature and its `return`, corrupting both functions. The anchor latched onto
|
|
335
|
+
// the wrong `}` (a shared/duplicate brace earlier in the file).
|
|
336
|
+
describe("Error Type 15: Append-After-Function Scope Bleed", () => {
|
|
337
|
+
it("should append a new function AFTER the anchor function's closing brace, leaving it intact", () => {
|
|
338
|
+
const originalFileContent = `function getArg(flag) {
|
|
339
|
+
return idx;
|
|
340
|
+
}
|
|
341
|
+
function hasFlag(flag) {
|
|
342
|
+
return args.includes(flag);
|
|
343
|
+
}
|
|
344
|
+
const x = 1;
|
|
345
|
+
`;
|
|
346
|
+
// Anchor on hasFlag's full body + closing brace, then append a new function.
|
|
347
|
+
const appendFunctionPatch = `@@ -4,3 +4,8 @@
|
|
348
|
+
function hasFlag(flag) {
|
|
349
|
+
return args.includes(flag);
|
|
350
|
+
}
|
|
351
|
+
+
|
|
352
|
+
+function helper(lines) {
|
|
353
|
+
+ return lines.some((l) => true);
|
|
354
|
+
+}
|
|
355
|
+
`;
|
|
356
|
+
|
|
357
|
+
const fixedPatchOutput = fixPatch(
|
|
358
|
+
originalFileContent,
|
|
359
|
+
appendFunctionPatch
|
|
360
|
+
);
|
|
361
|
+
const finalApplication = applyPatch(
|
|
362
|
+
originalFileContent,
|
|
363
|
+
fixedPatchOutput
|
|
364
|
+
);
|
|
365
|
+
|
|
366
|
+
expect(finalApplication).not.toBe(false);
|
|
367
|
+
const out = finalApplication as string;
|
|
368
|
+
// The new function must exist.
|
|
369
|
+
expect(out).toContain("function helper(lines)");
|
|
370
|
+
// CRITICAL: hasFlag must remain a contiguous, intact function — the new
|
|
371
|
+
// function must NOT be injected between its signature and its return.
|
|
372
|
+
expect(out).toMatch(
|
|
373
|
+
/function hasFlag\(flag\) \{\s*\n\s*return args\.includes\(flag\);\s*\n\}/
|
|
374
|
+
);
|
|
375
|
+
// helper must come AFTER hasFlag, not before / inside it.
|
|
376
|
+
const idxHasFlag = out.indexOf("function hasFlag");
|
|
377
|
+
const idxHelper = out.indexOf("function helper");
|
|
378
|
+
expect(idxHelper).toBeGreaterThan(idxHasFlag);
|
|
379
|
+
});
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
// --- Test Case 16: Block-Replace-And-Append (try-block placement + new methods) ---
|
|
383
|
+
// Reproduces a bug where a patch that:
|
|
384
|
+
// (a) removes a large inline body from inside a try{} block and replaces it with
|
|
385
|
+
// a single call, AND
|
|
386
|
+
// (b) appends brand-new methods after the method's closing brace
|
|
387
|
+
// was auto-corrected incorrectly:
|
|
388
|
+
// - The replacement call landed inside the `catch (error) {` block instead of
|
|
389
|
+
// inside the `try {` block where the removed code lived.
|
|
390
|
+
// - The appended new methods were silently dropped entirely.
|
|
391
|
+
describe("Error Type 16: Block-Replace-And-Append (try-block placement + append)", () => {
|
|
392
|
+
it("should place the replacement call inside the try block (before catch) and preserve appended new methods", () => {
|
|
393
|
+
// Source mirrors the original onSessionStop method before refactoring.
|
|
394
|
+
const originalFileContent = [
|
|
395
|
+
" /**",
|
|
396
|
+
" * Finalize billing when a session stops",
|
|
397
|
+
" */",
|
|
398
|
+
" async onSessionStop(sessionKey: string): Promise<void> {",
|
|
399
|
+
" try {",
|
|
400
|
+
" const event = await prisma.cloudBillingEvent.findUnique({",
|
|
401
|
+
" where: { sessionKey },",
|
|
402
|
+
" });",
|
|
403
|
+
"",
|
|
404
|
+
" if (!event) {",
|
|
405
|
+
" this.logger.warn(",
|
|
406
|
+
" `[CloudBilling] No billing event found for session: ${sessionKey}`",
|
|
407
|
+
" );",
|
|
408
|
+
" return;",
|
|
409
|
+
" }",
|
|
410
|
+
"",
|
|
411
|
+
" if (event.status !== \"running\") {",
|
|
412
|
+
" this.logger.info(",
|
|
413
|
+
" `[CloudBilling] Session ${sessionKey} already billed (status: ${event.status})`",
|
|
414
|
+
" );",
|
|
415
|
+
" return;",
|
|
416
|
+
" }",
|
|
417
|
+
"",
|
|
418
|
+
" const stoppedAt = new Date();",
|
|
419
|
+
" const uptimeMs = stoppedAt.getTime() - event.startedAt.getTime();",
|
|
420
|
+
" const uptimeMinutes = uptimeMs / 1000 / 60;",
|
|
421
|
+
" const ratePerMinute = parseSandboxSpecRate(event.serverSpec) ?? RATE;",
|
|
422
|
+
" const costUsd = uptimeMinutes * ratePerMinute;",
|
|
423
|
+
"",
|
|
424
|
+
" await prisma.cloudBillingEvent.update({",
|
|
425
|
+
" where: { sessionKey },",
|
|
426
|
+
" data: {",
|
|
427
|
+
" stoppedAt,",
|
|
428
|
+
" uptimeMinutes,",
|
|
429
|
+
" costUsd,",
|
|
430
|
+
" status: \"billed\",",
|
|
431
|
+
" billedAt: new Date(),",
|
|
432
|
+
" },",
|
|
433
|
+
" });",
|
|
434
|
+
"",
|
|
435
|
+
" const result = await this.usageService.deductCredits(",
|
|
436
|
+
" event.orgId,",
|
|
437
|
+
" costUsd,",
|
|
438
|
+
" \"cloud\",",
|
|
439
|
+
" undefined,",
|
|
440
|
+
" event.orgUserId ?? undefined",
|
|
441
|
+
" );",
|
|
442
|
+
"",
|
|
443
|
+
" await this.usageService.recordUsage(",
|
|
444
|
+
" event.orgId,",
|
|
445
|
+
" \"cloud\",",
|
|
446
|
+
" event.serverSpec,",
|
|
447
|
+
" costUsd,",
|
|
448
|
+
" event.orgUserId ?? undefined,",
|
|
449
|
+
" \"cloud\",",
|
|
450
|
+
" result.fundedFrom",
|
|
451
|
+
" );",
|
|
452
|
+
"",
|
|
453
|
+
" this.logger.info(",
|
|
454
|
+
" `[CloudBilling] Session ${sessionKey} billed`",
|
|
455
|
+
" );",
|
|
456
|
+
" } catch (error) {",
|
|
457
|
+
" this.logger.error(",
|
|
458
|
+
" `[CloudBilling] Failed to bill session ${sessionKey}:`,",
|
|
459
|
+
" error",
|
|
460
|
+
" );",
|
|
461
|
+
" }",
|
|
462
|
+
" }",
|
|
463
|
+
].join("\n");
|
|
464
|
+
|
|
465
|
+
// Patch: remove the inline finalize body, replace with single call,
|
|
466
|
+
// AND append two new methods after the closing brace.
|
|
467
|
+
const refactorPatch = [
|
|
468
|
+
"@@ -19,43 +19,7 @@",
|
|
469
|
+
"",
|
|
470
|
+
" if (event.status !== \"running\") {",
|
|
471
|
+
" this.logger.info(",
|
|
472
|
+
" `[CloudBilling] Session ${sessionKey} already billed (status: ${event.status})`",
|
|
473
|
+
" );",
|
|
474
|
+
" return;",
|
|
475
|
+
" }",
|
|
476
|
+
"",
|
|
477
|
+
"- const stoppedAt = new Date();",
|
|
478
|
+
"- const uptimeMs = stoppedAt.getTime() - event.startedAt.getTime();",
|
|
479
|
+
"- const uptimeMinutes = uptimeMs / 1000 / 60;",
|
|
480
|
+
"- const ratePerMinute = parseSandboxSpecRate(event.serverSpec) ?? RATE;",
|
|
481
|
+
"- const costUsd = uptimeMinutes * ratePerMinute;",
|
|
482
|
+
"-",
|
|
483
|
+
"- await prisma.cloudBillingEvent.update({",
|
|
484
|
+
"- where: { sessionKey },",
|
|
485
|
+
"- data: {",
|
|
486
|
+
"- stoppedAt,",
|
|
487
|
+
"- uptimeMinutes,",
|
|
488
|
+
"- costUsd,",
|
|
489
|
+
"- status: \"billed\",",
|
|
490
|
+
"- billedAt: new Date(),",
|
|
491
|
+
"- },",
|
|
492
|
+
"- });",
|
|
493
|
+
"-",
|
|
494
|
+
"- const result = await this.usageService.deductCredits(",
|
|
495
|
+
"- event.orgId,",
|
|
496
|
+
"- costUsd,",
|
|
497
|
+
"- \"cloud\",",
|
|
498
|
+
"- undefined,",
|
|
499
|
+
"- event.orgUserId ?? undefined",
|
|
500
|
+
"- );",
|
|
501
|
+
"-",
|
|
502
|
+
"- await this.usageService.recordUsage(",
|
|
503
|
+
"- event.orgId,",
|
|
504
|
+
"- \"cloud\",",
|
|
505
|
+
"- event.serverSpec,",
|
|
506
|
+
"- costUsd,",
|
|
507
|
+
"- event.orgUserId ?? undefined,",
|
|
508
|
+
"- \"cloud\",",
|
|
509
|
+
"- result.fundedFrom",
|
|
510
|
+
"- );",
|
|
511
|
+
"-",
|
|
512
|
+
"- this.logger.info(",
|
|
513
|
+
"- `[CloudBilling] Session ${sessionKey} billed`",
|
|
514
|
+
"- );",
|
|
515
|
+
"+ await this._finalizeEvent(event);",
|
|
516
|
+
" } catch (error) {",
|
|
517
|
+
" this.logger.error(",
|
|
518
|
+
" `[CloudBilling] Failed to bill session ${sessionKey}:`,",
|
|
519
|
+
"@@ -62,3 +26,19 @@",
|
|
520
|
+
" }",
|
|
521
|
+
" }",
|
|
522
|
+
"+",
|
|
523
|
+
"+ async onSessionStopByResource(",
|
|
524
|
+
"+ resourceType: string,",
|
|
525
|
+
"+ resourceId: string",
|
|
526
|
+
"+ ): Promise<void> {",
|
|
527
|
+
"+ try {",
|
|
528
|
+
"+ this.logger.info(`[CloudBilling] stop by resource ${resourceType}/${resourceId}`);",
|
|
529
|
+
"+ } catch (error) {",
|
|
530
|
+
"+ this.logger.error(`[CloudBilling] Failed:`, error);",
|
|
531
|
+
"+ }",
|
|
532
|
+
"+ }",
|
|
533
|
+
"+",
|
|
534
|
+
"+ private async _finalizeEvent(event: { id: string; sessionKey: string; status: string; startedAt: Date; serverSpec: string; orgId: string; orgUserId: string | null; }): Promise<void> {",
|
|
535
|
+
"+ this.logger.info(`[CloudBilling] finalizing ${event.sessionKey}`);",
|
|
536
|
+
"+ }",
|
|
537
|
+
].join("\n");
|
|
538
|
+
|
|
539
|
+
const fixedPatchOutput = fixPatch(originalFileContent, refactorPatch);
|
|
540
|
+
const finalApplication = applyPatch(originalFileContent, fixedPatchOutput);
|
|
541
|
+
|
|
542
|
+
expect(finalApplication).not.toBe(false);
|
|
543
|
+
const out = finalApplication as string;
|
|
544
|
+
|
|
545
|
+
// (1) The replacement call must appear BEFORE the catch block (inside the try block).
|
|
546
|
+
const idxFinalizeCall = out.indexOf("await this._finalizeEvent(event);");
|
|
547
|
+
const idxCatch = out.indexOf("} catch (error) {");
|
|
548
|
+
expect(idxFinalizeCall).toBeGreaterThanOrEqual(0);
|
|
549
|
+
expect(idxCatch).toBeGreaterThanOrEqual(0);
|
|
550
|
+
expect(idxFinalizeCall).toBeLessThan(idxCatch);
|
|
551
|
+
|
|
552
|
+
// (2) The catch block should only contain the logger.error, NOT _finalizeEvent.
|
|
553
|
+
const catchBlock = out.slice(idxCatch);
|
|
554
|
+
expect(catchBlock).not.toContain("await this._finalizeEvent(event);");
|
|
555
|
+
|
|
556
|
+
// (3) The appended new methods must be present in the output.
|
|
557
|
+
expect(out).toContain("async onSessionStopByResource(");
|
|
558
|
+
expect(out).toContain("private async _finalizeEvent(");
|
|
559
|
+
});
|
|
560
|
+
});
|
|
283
561
|
});
|
|
@@ -826,7 +826,7 @@ describe("CustomVariables", () => {
|
|
|
826
826
|
},
|
|
827
827
|
];
|
|
828
828
|
|
|
829
|
-
const processor = customVariables.createRepetitionHintProcessor({ minLength: 50, minRepetitions: 2 });
|
|
829
|
+
const processor = customVariables.createRepetitionHintProcessor({ minLength: 50, minRepetitions: 2, hintMessageTokens: 0 });
|
|
830
830
|
const modified = JSON.parse(JSON.stringify(messages));
|
|
831
831
|
await processor(messages, modified);
|
|
832
832
|
|
|
@@ -901,7 +901,7 @@ describe("CustomVariables", () => {
|
|
|
901
901
|
tool_calls: [{ id: "c3", type: "function", function: { name: "toolZ", arguments: JSON.stringify({ auth: longString }) } }],
|
|
902
902
|
},
|
|
903
903
|
];
|
|
904
|
-
const processor = customVariables.createRepetitionHintProcessor({ minLength: 50, minRepetitions: 2 });
|
|
904
|
+
const processor = customVariables.createRepetitionHintProcessor({ minLength: 50, minRepetitions: 2, hintMessageTokens: 0 });
|
|
905
905
|
const modified = JSON.parse(JSON.stringify(messages));
|
|
906
906
|
await processor(messages, modified);
|
|
907
907
|
const hint = modified[modified.length - 1];
|
|
@@ -911,7 +911,7 @@ describe("CustomVariables", () => {
|
|
|
911
911
|
});
|
|
912
912
|
|
|
913
913
|
it("should use default options when none provided", async () => {
|
|
914
|
-
const longString = "a".repeat(
|
|
914
|
+
const longString = "a".repeat(800); // long enough to produce net positive token savings with default hintMessageTokens
|
|
915
915
|
const messages: Message[] = [
|
|
916
916
|
{
|
|
917
917
|
role: "assistant",
|
|
@@ -955,7 +955,7 @@ describe("CustomVariables", () => {
|
|
|
955
955
|
},
|
|
956
956
|
];
|
|
957
957
|
|
|
958
|
-
const processor = customVariables.createRepetitionHintProcessor({ minLength: 50, minRepetitions: 2, minSubstringLength: 50 });
|
|
958
|
+
const processor = customVariables.createRepetitionHintProcessor({ minLength: 50, minRepetitions: 2, minSubstringLength: 50, hintMessageTokens: 0 });
|
|
959
959
|
const modified = JSON.parse(JSON.stringify(messages));
|
|
960
960
|
await processor(messages, modified);
|
|
961
961
|
|
|
@@ -105,7 +105,9 @@ describe("TokenCompressor", () => {
|
|
|
105
105
|
});
|
|
106
106
|
|
|
107
107
|
it("should create chain of NEXT_CHUNK_KEY references", () => {
|
|
108
|
-
|
|
108
|
+
// Large enough to require multiple full-size chunks even after the
|
|
109
|
+
// minimum-chunk-size merge (characterLimit is 16000, minChunk is 8000).
|
|
110
|
+
const content = "a".repeat(40000);
|
|
109
111
|
tokenCompressor.compressStringInChunks(content);
|
|
110
112
|
|
|
111
113
|
const keys = tokenCompressor.getStorageKeys();
|
|
@@ -115,6 +117,22 @@ describe("TokenCompressor", () => {
|
|
|
115
117
|
const firstChunk = tokenCompressor.retrieveString(keys[0]);
|
|
116
118
|
expect(firstChunk).toContain("NEXT_CHUNK_KEY");
|
|
117
119
|
});
|
|
120
|
+
|
|
121
|
+
it("should merge a small leftover chunk so no tiny chunks are emitted", () => {
|
|
122
|
+
// characterLimit is 16000; a 20000-char input would naively split into a
|
|
123
|
+
// 16000-char chunk and a tiny 4000-char leading chunk. The leftover is
|
|
124
|
+
// below the minimum chunk size, so it should be merged into a single chunk.
|
|
125
|
+
const content = "a".repeat(20000);
|
|
126
|
+
tokenCompressor.compressStringInChunks(content);
|
|
127
|
+
|
|
128
|
+
const keys = tokenCompressor.getStorageKeys();
|
|
129
|
+
expect(keys.length).toBe(1);
|
|
130
|
+
|
|
131
|
+
const onlyChunk = tokenCompressor.retrieveString(keys[0]);
|
|
132
|
+
expect(onlyChunk).not.toContain("NEXT_CHUNK_KEY");
|
|
133
|
+
// The full content should be retrievable from the single chunk.
|
|
134
|
+
expect(tokenCompressor.retrieveFullString(keys[0])).toBe(content);
|
|
135
|
+
});
|
|
118
136
|
});
|
|
119
137
|
|
|
120
138
|
describe("compressJsonProperties", () => {
|
|
@@ -316,6 +334,46 @@ describe("TokenCompressor", () => {
|
|
|
316
334
|
expect(result).toContain("Error: No data found for key");
|
|
317
335
|
expect(result).toContain("Available keys:");
|
|
318
336
|
});
|
|
337
|
+
|
|
338
|
+
it("should auto-stitch a multi-chunk chain into the full content", () => {
|
|
339
|
+
// Force a multi-chunk compression then expand the first key.
|
|
340
|
+
const content = "a".repeat(40000);
|
|
341
|
+
const placeholder = tokenCompressor.compressStringInChunks(content);
|
|
342
|
+
const firstKey = placeholder.match(/Key:\s*(\S+)/)[1];
|
|
343
|
+
|
|
344
|
+
const toolsServiceCalls = mockToolsService.addFunctions.mock.calls;
|
|
345
|
+
const functions = toolsServiceCalls[0][0];
|
|
346
|
+
const result = functions.expandTokens(firstKey);
|
|
347
|
+
|
|
348
|
+
// Full content is returned in one call with no chunk-linking markers.
|
|
349
|
+
expect(result).toBe(content);
|
|
350
|
+
expect(result).not.toContain("NEXT_CHUNK_KEY");
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
it("should support a ranged read with real line numbers", () => {
|
|
354
|
+
const key = "ranged_key";
|
|
355
|
+
const value = ["line one", "line two", "line three", "line four"].join(
|
|
356
|
+
"\n"
|
|
357
|
+
);
|
|
358
|
+
tokenCompressor.storeString(key, value);
|
|
359
|
+
|
|
360
|
+
const toolsServiceCalls = mockToolsService.addFunctions.mock.calls;
|
|
361
|
+
const functions = toolsServiceCalls[0][0];
|
|
362
|
+
const result = functions.expandTokens(key, 2, 3);
|
|
363
|
+
|
|
364
|
+
expect(result).toBe("2: line two\n3: line three");
|
|
365
|
+
});
|
|
366
|
+
|
|
367
|
+
it("should error on an invalid line range", () => {
|
|
368
|
+
const key = "bad_range_key";
|
|
369
|
+
tokenCompressor.storeString(key, "a\nb\nc");
|
|
370
|
+
|
|
371
|
+
const toolsServiceCalls = mockToolsService.addFunctions.mock.calls;
|
|
372
|
+
const functions = toolsServiceCalls[0][0];
|
|
373
|
+
const result = functions.expandTokens(key, 5, 2);
|
|
374
|
+
|
|
375
|
+
expect(result).toContain("Error: Invalid line range");
|
|
376
|
+
});
|
|
319
377
|
});
|
|
320
378
|
|
|
321
379
|
describe("edge cases", () => {
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { executeGrep } from "../../../src/processors/tools/grepToolResponse";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Verifies grepToolResponse operates on the decompressed/plain stored content
|
|
5
|
+
* and returns REAL source line numbers. This is the core feedback win: now that
|
|
6
|
+
* readFile returns plain text (no unified-diff wrapper), a grep hit maps straight
|
|
7
|
+
* back to an editable source location.
|
|
8
|
+
*/
|
|
9
|
+
describe("executeGrep", () => {
|
|
10
|
+
const toolCallId = "call_grep_test";
|
|
11
|
+
|
|
12
|
+
// Plain source content as readFile now returns it (no Index:/@@/+ wrapper).
|
|
13
|
+
const fileContent = [
|
|
14
|
+
"import { foo } from './foo';", // line 1
|
|
15
|
+
"", // line 2
|
|
16
|
+
"export class CloudBillingService {", // line 3
|
|
17
|
+
" async chargeCredits(amount: number) {", // line 4
|
|
18
|
+
" return amount;", // line 5
|
|
19
|
+
" }", // line 6
|
|
20
|
+
"}", // line 7
|
|
21
|
+
].join("\n");
|
|
22
|
+
|
|
23
|
+
it("returns matches with real 1-based source line numbers", async () => {
|
|
24
|
+
const result = await executeGrep(
|
|
25
|
+
fileContent,
|
|
26
|
+
toolCallId,
|
|
27
|
+
"chargeCredits",
|
|
28
|
+
[toolCallId]
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
// The match is on source line 4 and must be reported as such.
|
|
32
|
+
expect(result).toContain("> 4: ");
|
|
33
|
+
expect(result).toContain("async chargeCredits(amount: number) {");
|
|
34
|
+
// No diff-prefix noise should be present.
|
|
35
|
+
expect(result).not.toContain("+import");
|
|
36
|
+
expect(result).not.toContain("Index:");
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it("includes surrounding context with correct line numbers", async () => {
|
|
40
|
+
const result = await executeGrep(
|
|
41
|
+
fileContent,
|
|
42
|
+
toolCallId,
|
|
43
|
+
"chargeCredits",
|
|
44
|
+
[toolCallId],
|
|
45
|
+
{ contextBefore: 1, contextAfter: 1 }
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
expect(result).toContain(" 3: export class CloudBillingService {");
|
|
49
|
+
expect(result).toContain("> 4: ");
|
|
50
|
+
expect(result).toContain(" 5: return amount;");
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it("returns a helpful error when no response is stored", async () => {
|
|
54
|
+
const result = await executeGrep("", toolCallId, "anything", [
|
|
55
|
+
"other_call",
|
|
56
|
+
]);
|
|
57
|
+
|
|
58
|
+
expect(result).toContain("No tool response found");
|
|
59
|
+
expect(result).toContain("other_call");
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it("reports when there are no matches", async () => {
|
|
63
|
+
const result = await executeGrep(
|
|
64
|
+
fileContent,
|
|
65
|
+
toolCallId,
|
|
66
|
+
"doesNotExistAnywhere",
|
|
67
|
+
[toolCallId]
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
expect(result).toContain("No matches found");
|
|
71
|
+
});
|
|
72
|
+
});
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import * as fs from "fs";
|
|
2
|
+
import * as os from "os";
|
|
3
|
+
import * as path from "path";
|
|
4
|
+
import { ConversionService } from "../../src/services/conversion/ConversionService";
|
|
5
|
+
import { Converter, ConvertInput, ConverterContext, ConvertResult } from "../../src/services/conversion/types";
|
|
6
|
+
|
|
7
|
+
// Minimal stubs
|
|
8
|
+
const stubClients = {} as any;
|
|
9
|
+
const stubMediaProcessor = {
|
|
10
|
+
processAudio: async () => ["chunk1", "chunk2"],
|
|
11
|
+
} as any;
|
|
12
|
+
|
|
13
|
+
function makeService() {
|
|
14
|
+
return new ConversionService(stubClients, stubMediaProcessor);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
describe("ConversionService", () => {
|
|
18
|
+
describe("register / list", () => {
|
|
19
|
+
it("should register and list converters", () => {
|
|
20
|
+
const svc = makeService();
|
|
21
|
+
const initial = svc.list().length;
|
|
22
|
+
const conv: Converter = {
|
|
23
|
+
name: "fake-pdf-to-text",
|
|
24
|
+
inputExts: ["pdf"],
|
|
25
|
+
outputType: "text",
|
|
26
|
+
convert: async () => ({ outputType: "text", text: "hello" }),
|
|
27
|
+
};
|
|
28
|
+
svc.register(conv);
|
|
29
|
+
expect(svc.list().length).toBe(initial + 1);
|
|
30
|
+
expect(svc.list().find((c) => c.name === "fake-pdf-to-text")).toBeDefined();
|
|
31
|
+
});
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
describe("convert - path composition", () => {
|
|
35
|
+
it("should chain pdf->image + image->text converters to produce text from a pdf", async () => {
|
|
36
|
+
const svc = makeService();
|
|
37
|
+
|
|
38
|
+
// Create a temp pdf-like file
|
|
39
|
+
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "conv-test-"));
|
|
40
|
+
const pdfPath = path.join(tmpDir, "test.pdf");
|
|
41
|
+
fs.writeFileSync(pdfPath, "fake pdf content");
|
|
42
|
+
|
|
43
|
+
// Register pdf -> image converter
|
|
44
|
+
const pdfToImage: Converter = {
|
|
45
|
+
name: "fake-pdf-to-image",
|
|
46
|
+
inputExts: ["pdf"],
|
|
47
|
+
outputType: "image",
|
|
48
|
+
convert: async (input: ConvertInput, _ctx: ConverterContext): Promise<ConvertResult> => {
|
|
49
|
+
const imgPath = path.join(tmpDir, "page.png");
|
|
50
|
+
fs.writeFileSync(imgPath, "fake image bytes");
|
|
51
|
+
return { outputType: "image", files: [imgPath] };
|
|
52
|
+
},
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
// Register image -> text converter
|
|
56
|
+
const imageToText: Converter = {
|
|
57
|
+
name: "fake-image-to-text",
|
|
58
|
+
inputModality: "image",
|
|
59
|
+
outputType: "text",
|
|
60
|
+
convert: async (_input: ConvertInput, _ctx: ConverterContext): Promise<ConvertResult> => {
|
|
61
|
+
return { outputType: "text", text: "extracted text from image" };
|
|
62
|
+
},
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
svc.register(pdfToImage);
|
|
66
|
+
svc.register(imageToText);
|
|
67
|
+
|
|
68
|
+
const result = await svc.convert(pdfPath, "text", { force: true });
|
|
69
|
+
expect(result.outputType).toBe("text");
|
|
70
|
+
expect(result.text).toBe("extracted text from image");
|
|
71
|
+
|
|
72
|
+
// cleanup
|
|
73
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
describe("quality gate fallthrough", () => {
|
|
78
|
+
it("should fall through to a second converter when isGoodEnough returns false for the first", async () => {
|
|
79
|
+
const svc = makeService();
|
|
80
|
+
|
|
81
|
+
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "conv-qg-"));
|
|
82
|
+
const pdfPath = path.join(tmpDir, "test.pdf");
|
|
83
|
+
// Write a file > 500KB so the default quality gate fires
|
|
84
|
+
fs.writeFileSync(pdfPath, Buffer.alloc(600 * 1024, "x"));
|
|
85
|
+
|
|
86
|
+
let firstCalled = false;
|
|
87
|
+
let secondCalled = false;
|
|
88
|
+
|
|
89
|
+
// First converter returns bad text (short)
|
|
90
|
+
const badConverter: Converter = {
|
|
91
|
+
name: "bad-pdf-converter",
|
|
92
|
+
inputExts: ["pdf"],
|
|
93
|
+
outputType: "text",
|
|
94
|
+
convert: async (): Promise<ConvertResult> => {
|
|
95
|
+
firstCalled = true;
|
|
96
|
+
return { outputType: "text", text: "short" }; // < 50 chars, file > 500KB -> fails quality gate
|
|
97
|
+
},
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
// Second converter returns good text
|
|
101
|
+
const goodConverter: Converter = {
|
|
102
|
+
name: "good-pdf-converter",
|
|
103
|
+
inputExts: ["pdf"],
|
|
104
|
+
outputType: "text",
|
|
105
|
+
convert: async (): Promise<ConvertResult> => {
|
|
106
|
+
secondCalled = true;
|
|
107
|
+
return { outputType: "text", text: "a".repeat(100) };
|
|
108
|
+
},
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
svc.register(badConverter);
|
|
112
|
+
svc.register(goodConverter);
|
|
113
|
+
|
|
114
|
+
const result = await svc.convert(pdfPath, "text", { force: true });
|
|
115
|
+
|
|
116
|
+
expect(firstCalled).toBe(true);
|
|
117
|
+
expect(secondCalled).toBe(true);
|
|
118
|
+
expect(result.text).toBe("a".repeat(100));
|
|
119
|
+
|
|
120
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
121
|
+
});
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
describe("convertToText", () => {
|
|
125
|
+
it("should return text string from text passthrough for a plain text file", async () => {
|
|
126
|
+
const svc = makeService();
|
|
127
|
+
|
|
128
|
+
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "conv-txt-"));
|
|
129
|
+
const txtPath = path.join(tmpDir, "hello.txt");
|
|
130
|
+
fs.writeFileSync(txtPath, "hello world");
|
|
131
|
+
|
|
132
|
+
const text = await svc.convertToText(txtPath, { force: true });
|
|
133
|
+
expect(text).toBe("hello world");
|
|
134
|
+
|
|
135
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
136
|
+
});
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
describe("startLine/endLine slicing", () => {
|
|
140
|
+
it("should slice text output by line range", async () => {
|
|
141
|
+
const svc = makeService();
|
|
142
|
+
|
|
143
|
+
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "conv-slice-"));
|
|
144
|
+
const txtPath = path.join(tmpDir, "lines.txt");
|
|
145
|
+
fs.writeFileSync(txtPath, "line1\nline2\nline3\nline4\nline5");
|
|
146
|
+
|
|
147
|
+
const text = await svc.convertToText(txtPath, { force: true, startLine: 2, endLine: 4 });
|
|
148
|
+
const lines = text.split("\n");
|
|
149
|
+
expect(lines).toEqual(["line2", "line3", "line4"]);
|
|
150
|
+
|
|
151
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
152
|
+
});
|
|
153
|
+
});
|
|
154
|
+
});
|
package/tests/test.spec.ts
CHANGED
|
@@ -136,7 +136,7 @@ describe("Agent Tools Tests", () => {
|
|
|
136
136
|
const result = await patchFile(filePath, patch);
|
|
137
137
|
|
|
138
138
|
// Verify the function returns a success message
|
|
139
|
-
expect(result).toContain("Patch applied
|
|
139
|
+
expect(result).toContain("Patch applied to");
|
|
140
140
|
}, 60000); // Increase timeout to 60 seconds
|
|
141
141
|
|
|
142
142
|
test("execCommand should execute a system command and return its output", async () => {
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
11
|
// Prevent real _initDefaultProviders from firing (it reads env vars / files)
|
|
12
|
+
// resolveClient returning null causes all DEFAULT_PROVIDERS to be skipped,
|
|
12
13
|
jest.mock("../../../src/config", () => ({
|
|
13
14
|
getConfig: jest.fn().mockResolvedValue({ modules: [] }),
|
|
14
15
|
getGlobalConfig: jest.fn().mockResolvedValue({ modules: [] }),
|
|
@@ -20,6 +21,13 @@ jest.mock("../../../src/services/KnowhowClient", () => ({
|
|
|
20
21
|
}));
|
|
21
22
|
|
|
22
23
|
import { AIClient } from "../../../src/clients/index";
|
|
24
|
+
|
|
25
|
+
// Mock resolveClient AFTER import so the prototype exists.
|
|
26
|
+
// Returning null causes registerModelProviders to skip all DEFAULT_PROVIDERS,
|
|
27
|
+
// preventing any real HTTP calls to provider model endpoints.
|
|
28
|
+
beforeAll(() => {
|
|
29
|
+
jest.spyOn(AIClient.prototype as any, "resolveClient").mockReturnValue(null);
|
|
30
|
+
});
|
|
23
31
|
import type { GenericClient } from "../../../src/clients/types";
|
|
24
32
|
|
|
25
33
|
// ─── Helpers ────────────────────────────────────────────────────────────────
|
package/ts_build/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tyvm/knowhow",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.119",
|
|
4
4
|
"description": "ai cli with plugins and agents",
|
|
5
5
|
"main": "ts_build/src/index.js",
|
|
6
6
|
"bin": {
|
|
@@ -40,7 +40,6 @@
|
|
|
40
40
|
"@types/jest": "^29.5.13",
|
|
41
41
|
"@types/mocha": "^10.0.8",
|
|
42
42
|
"@types/node": "^20.6.3",
|
|
43
|
-
"@types/pdf-parse": "^1.1.4",
|
|
44
43
|
"@types/ws": "^8.18.1",
|
|
45
44
|
"jest": "^29.1.1",
|
|
46
45
|
"prettier": "2.6.2",
|
|
@@ -67,7 +66,6 @@
|
|
|
67
66
|
"minimatch": "^10.1.2",
|
|
68
67
|
"node-jq": "^6.0.1",
|
|
69
68
|
"openai": "4.89.1",
|
|
70
|
-
"pdf-parse": "^1.1.1",
|
|
71
69
|
"ws": "^8.18.1",
|
|
72
70
|
"zod": "^3.25.0"
|
|
73
71
|
},
|