@tyvm/knowhow 0.0.105 → 0.0.107

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 (219) hide show
  1. package/CONFIG.md +8 -5
  2. package/package.json +3 -2
  3. package/scripts/check-model-pricing.ts +509 -0
  4. package/scripts/compare-openrouter-coverage.ts +576 -0
  5. package/src/agents/base/base.ts +169 -5
  6. package/src/agents/tools/execCommand.ts +4 -0
  7. package/src/agents/tools/executeScript/definition.ts +1 -1
  8. package/src/agents/tools/index.ts +0 -1
  9. package/src/agents/tools/list.ts +3 -43
  10. package/src/agents/tools/writeFile.ts +1 -1
  11. package/src/auth/browserLogin.ts +9 -4
  12. package/src/chat/modules/RemoteSyncModule.ts +3 -0
  13. package/src/cli.ts +31 -1
  14. package/src/clients/anthropic.ts +8 -2
  15. package/src/clients/cerebras.ts +10 -0
  16. package/src/clients/contextLimits.ts +7 -2
  17. package/src/clients/copilot.ts +23 -0
  18. package/src/clients/deepseek.ts +16 -0
  19. package/src/clients/fireworks.ts +15 -0
  20. package/src/clients/gemini.ts +59 -4
  21. package/src/clients/github.ts +16 -0
  22. package/src/clients/groq.ts +15 -0
  23. package/src/clients/http.ts +194 -6
  24. package/src/clients/index.ts +116 -4
  25. package/src/clients/llama.ts +16 -0
  26. package/src/clients/mistral.ts +16 -0
  27. package/src/clients/nvidia.ts +16 -0
  28. package/src/clients/openai.ts +53 -12
  29. package/src/clients/openrouter.ts +17 -0
  30. package/src/clients/pricing/anthropic.ts +105 -78
  31. package/src/clients/pricing/cerebras.ts +11 -0
  32. package/src/clients/pricing/copilot.ts +60 -0
  33. package/src/clients/pricing/deepseek.ts +15 -0
  34. package/src/clients/pricing/fireworks.ts +32 -0
  35. package/src/clients/pricing/github.ts +69 -0
  36. package/src/clients/pricing/google.ts +245 -206
  37. package/src/clients/pricing/groq.ts +56 -0
  38. package/src/clients/pricing/index.ts +42 -5
  39. package/src/clients/pricing/llama.ts +18 -0
  40. package/src/clients/pricing/mistral.ts +34 -0
  41. package/src/clients/pricing/models.ts +7 -236
  42. package/src/clients/pricing/nvidia.ts +102 -0
  43. package/src/clients/pricing/openai.ts +348 -171
  44. package/src/clients/pricing/openrouter.ts +36 -0
  45. package/src/clients/pricing/types.ts +83 -2
  46. package/src/clients/pricing/xai.ts +121 -65
  47. package/src/clients/types.ts +28 -1
  48. package/src/clients/xai.ts +161 -1
  49. package/src/fileSync.ts +8 -2
  50. package/src/login.ts +11 -3
  51. package/src/services/AgentSyncFs.ts +36 -12
  52. package/src/services/KnowhowClient.ts +11 -0
  53. package/src/services/LazyToolsService.ts +6 -0
  54. package/src/services/S3.ts +0 -7
  55. package/src/services/modules/index.ts +11 -2
  56. package/src/types.ts +56 -279
  57. package/src/worker.ts +174 -0
  58. package/tests/clients/AIClient.test.ts +1 -1
  59. package/tests/clients/anthropic.test.ts +202 -0
  60. package/tests/clients/pricing.test.ts +37 -0
  61. package/tests/manual/clients/completions.json +838 -226
  62. package/tests/manual/clients/completions.test.ts +46 -31
  63. package/ts_build/package.json +3 -2
  64. package/ts_build/src/agents/base/base.d.ts +18 -1
  65. package/ts_build/src/agents/base/base.js +111 -4
  66. package/ts_build/src/agents/base/base.js.map +1 -1
  67. package/ts_build/src/agents/tools/execCommand.js +3 -0
  68. package/ts_build/src/agents/tools/execCommand.js.map +1 -1
  69. package/ts_build/src/agents/tools/executeScript/definition.js +1 -1
  70. package/ts_build/src/agents/tools/executeScript/definition.js.map +1 -1
  71. package/ts_build/src/agents/tools/index.d.ts +0 -1
  72. package/ts_build/src/agents/tools/index.js +0 -1
  73. package/ts_build/src/agents/tools/index.js.map +1 -1
  74. package/ts_build/src/agents/tools/list.js +3 -38
  75. package/ts_build/src/agents/tools/list.js.map +1 -1
  76. package/ts_build/src/agents/tools/visionTool.d.ts +1 -1
  77. package/ts_build/src/agents/tools/writeFile.js +1 -1
  78. package/ts_build/src/agents/tools/writeFile.js.map +1 -1
  79. package/ts_build/src/ai.d.ts +1 -1
  80. package/ts_build/src/auth/browserLogin.d.ts +2 -1
  81. package/ts_build/src/auth/browserLogin.js +10 -3
  82. package/ts_build/src/auth/browserLogin.js.map +1 -1
  83. package/ts_build/src/chat/modules/RemoteSyncModule.js +1 -0
  84. package/ts_build/src/chat/modules/RemoteSyncModule.js.map +1 -1
  85. package/ts_build/src/cli.js +19 -0
  86. package/ts_build/src/cli.js.map +1 -1
  87. package/ts_build/src/clients/anthropic.d.ts +1 -82
  88. package/ts_build/src/clients/anthropic.js +8 -2
  89. package/ts_build/src/clients/anthropic.js.map +1 -1
  90. package/ts_build/src/clients/cerebras.d.ts +4 -0
  91. package/ts_build/src/clients/cerebras.js +14 -0
  92. package/ts_build/src/clients/cerebras.js.map +1 -0
  93. package/ts_build/src/clients/contextLimits.js +7 -2
  94. package/ts_build/src/clients/contextLimits.js.map +1 -1
  95. package/ts_build/src/clients/copilot.d.ts +4 -0
  96. package/ts_build/src/clients/copilot.js +15 -0
  97. package/ts_build/src/clients/copilot.js.map +1 -0
  98. package/ts_build/src/clients/deepseek.d.ts +4 -0
  99. package/ts_build/src/clients/deepseek.js +15 -0
  100. package/ts_build/src/clients/deepseek.js.map +1 -0
  101. package/ts_build/src/clients/fireworks.d.ts +4 -0
  102. package/ts_build/src/clients/fireworks.js +15 -0
  103. package/ts_build/src/clients/fireworks.js.map +1 -0
  104. package/ts_build/src/clients/gemini.d.ts +1 -0
  105. package/ts_build/src/clients/gemini.js +38 -2
  106. package/ts_build/src/clients/gemini.js.map +1 -1
  107. package/ts_build/src/clients/github.d.ts +4 -0
  108. package/ts_build/src/clients/github.js +15 -0
  109. package/ts_build/src/clients/github.js.map +1 -0
  110. package/ts_build/src/clients/groq.d.ts +4 -0
  111. package/ts_build/src/clients/groq.js +15 -0
  112. package/ts_build/src/clients/groq.js.map +1 -0
  113. package/ts_build/src/clients/http.d.ts +22 -1
  114. package/ts_build/src/clients/http.js +135 -7
  115. package/ts_build/src/clients/http.js.map +1 -1
  116. package/ts_build/src/clients/index.d.ts +14 -0
  117. package/ts_build/src/clients/index.js +94 -4
  118. package/ts_build/src/clients/index.js.map +1 -1
  119. package/ts_build/src/clients/llama.d.ts +4 -0
  120. package/ts_build/src/clients/llama.js +15 -0
  121. package/ts_build/src/clients/llama.js.map +1 -0
  122. package/ts_build/src/clients/mistral.d.ts +4 -0
  123. package/ts_build/src/clients/mistral.js +15 -0
  124. package/ts_build/src/clients/mistral.js.map +1 -0
  125. package/ts_build/src/clients/nvidia.d.ts +4 -0
  126. package/ts_build/src/clients/nvidia.js +15 -0
  127. package/ts_build/src/clients/nvidia.js.map +1 -0
  128. package/ts_build/src/clients/openai.d.ts +4 -206
  129. package/ts_build/src/clients/openai.js +38 -10
  130. package/ts_build/src/clients/openai.js.map +1 -1
  131. package/ts_build/src/clients/openrouter.d.ts +4 -0
  132. package/ts_build/src/clients/openrouter.js +15 -0
  133. package/ts_build/src/clients/openrouter.js.map +1 -0
  134. package/ts_build/src/clients/pricing/anthropic.d.ts +26 -78
  135. package/ts_build/src/clients/pricing/anthropic.js +75 -78
  136. package/ts_build/src/clients/pricing/anthropic.js.map +1 -1
  137. package/ts_build/src/clients/pricing/cerebras.d.ts +4 -0
  138. package/ts_build/src/clients/pricing/cerebras.js +11 -0
  139. package/ts_build/src/clients/pricing/cerebras.js.map +1 -0
  140. package/ts_build/src/clients/pricing/copilot.d.ts +5 -0
  141. package/ts_build/src/clients/pricing/copilot.js +35 -0
  142. package/ts_build/src/clients/pricing/copilot.js.map +1 -0
  143. package/ts_build/src/clients/pricing/deepseek.d.ts +5 -0
  144. package/ts_build/src/clients/pricing/deepseek.js +10 -0
  145. package/ts_build/src/clients/pricing/deepseek.js.map +1 -0
  146. package/ts_build/src/clients/pricing/fireworks.d.ts +5 -0
  147. package/ts_build/src/clients/pricing/fireworks.js +21 -0
  148. package/ts_build/src/clients/pricing/fireworks.js.map +1 -0
  149. package/ts_build/src/clients/pricing/github.d.ts +4 -0
  150. package/ts_build/src/clients/pricing/github.js +58 -0
  151. package/ts_build/src/clients/pricing/github.js.map +1 -0
  152. package/ts_build/src/clients/pricing/google.d.ts +59 -6
  153. package/ts_build/src/clients/pricing/google.js +214 -167
  154. package/ts_build/src/clients/pricing/google.js.map +1 -1
  155. package/ts_build/src/clients/pricing/groq.d.ts +5 -0
  156. package/ts_build/src/clients/pricing/groq.js +41 -0
  157. package/ts_build/src/clients/pricing/groq.js.map +1 -0
  158. package/ts_build/src/clients/pricing/index.d.ts +16 -5
  159. package/ts_build/src/clients/pricing/index.js +62 -7
  160. package/ts_build/src/clients/pricing/index.js.map +1 -1
  161. package/ts_build/src/clients/pricing/llama.d.ts +4 -0
  162. package/ts_build/src/clients/pricing/llama.js +14 -0
  163. package/ts_build/src/clients/pricing/llama.js.map +1 -0
  164. package/ts_build/src/clients/pricing/mistral.d.ts +5 -0
  165. package/ts_build/src/clients/pricing/mistral.js +23 -0
  166. package/ts_build/src/clients/pricing/mistral.js.map +1 -0
  167. package/ts_build/src/clients/pricing/models.d.ts +5 -4
  168. package/ts_build/src/clients/pricing/models.js +8 -162
  169. package/ts_build/src/clients/pricing/models.js.map +1 -1
  170. package/ts_build/src/clients/pricing/nvidia.d.ts +8 -0
  171. package/ts_build/src/clients/pricing/nvidia.js +96 -0
  172. package/ts_build/src/clients/pricing/nvidia.js.map +1 -0
  173. package/ts_build/src/clients/pricing/openai.d.ts +86 -197
  174. package/ts_build/src/clients/pricing/openai.js +295 -168
  175. package/ts_build/src/clients/pricing/openai.js.map +1 -1
  176. package/ts_build/src/clients/pricing/openrouter.d.ts +4 -0
  177. package/ts_build/src/clients/pricing/openrouter.js +29 -0
  178. package/ts_build/src/clients/pricing/openrouter.js.map +1 -0
  179. package/ts_build/src/clients/pricing/types.d.ts +27 -2
  180. package/ts_build/src/clients/pricing/types.js +46 -0
  181. package/ts_build/src/clients/pricing/types.js.map +1 -1
  182. package/ts_build/src/clients/pricing/xai.d.ts +37 -57
  183. package/ts_build/src/clients/pricing/xai.js +92 -59
  184. package/ts_build/src/clients/pricing/xai.js.map +1 -1
  185. package/ts_build/src/clients/types.d.ts +12 -1
  186. package/ts_build/src/clients/xai.d.ts +2 -62
  187. package/ts_build/src/clients/xai.js +132 -1
  188. package/ts_build/src/clients/xai.js.map +1 -1
  189. package/ts_build/src/fileSync.js +7 -2
  190. package/ts_build/src/fileSync.js.map +1 -1
  191. package/ts_build/src/login.js +8 -2
  192. package/ts_build/src/login.js.map +1 -1
  193. package/ts_build/src/services/AgentSyncFs.js +1 -0
  194. package/ts_build/src/services/AgentSyncFs.js.map +1 -1
  195. package/ts_build/src/services/KnowhowClient.d.ts +1 -0
  196. package/ts_build/src/services/KnowhowClient.js +7 -0
  197. package/ts_build/src/services/KnowhowClient.js.map +1 -1
  198. package/ts_build/src/services/LazyToolsService.d.ts +1 -0
  199. package/ts_build/src/services/LazyToolsService.js +3 -0
  200. package/ts_build/src/services/LazyToolsService.js.map +1 -1
  201. package/ts_build/src/services/S3.js +0 -7
  202. package/ts_build/src/services/S3.js.map +1 -1
  203. package/ts_build/src/services/modules/index.js +41 -1
  204. package/ts_build/src/services/modules/index.js.map +1 -1
  205. package/ts_build/src/types.d.ts +163 -124
  206. package/ts_build/src/types.js +33 -213
  207. package/ts_build/src/types.js.map +1 -1
  208. package/ts_build/src/worker.d.ts +4 -0
  209. package/ts_build/src/worker.js +140 -0
  210. package/ts_build/src/worker.js.map +1 -1
  211. package/ts_build/tests/clients/AIClient.test.js +1 -1
  212. package/ts_build/tests/clients/AIClient.test.js.map +1 -1
  213. package/ts_build/tests/clients/anthropic.test.d.ts +1 -0
  214. package/ts_build/tests/clients/anthropic.test.js +159 -0
  215. package/ts_build/tests/clients/anthropic.test.js.map +1 -0
  216. package/ts_build/tests/clients/pricing.test.js +21 -0
  217. package/ts_build/tests/clients/pricing.test.js.map +1 -1
  218. package/ts_build/tests/manual/clients/completions.test.js +27 -24
  219. package/ts_build/tests/manual/clients/completions.test.js.map +1 -1
@@ -1,5 +1,6 @@
1
1
  import { EventEmitter } from "events"; // kept for reference; agentEvents now uses EventService
2
2
  import {
3
+ CompletionResponse,
3
4
  GenericClient,
4
5
  Message,
5
6
  MessageContent,
@@ -59,6 +60,10 @@ export abstract class BaseAgent implements IAgent {
59
60
  protected turnCount = 0;
60
61
  protected totalCostUsd = 0;
61
62
  protected currentThread = 0;
63
+ protected totalInputTokens = 0;
64
+ protected totalOutputTokens = 0;
65
+ protected totalCacheReadTokens = 0;
66
+ protected totalCacheWriteTokens = 0;
62
67
 
63
68
  protected compressThreshold = 30000;
64
69
  protected compressMinMessages = 30;
@@ -95,6 +100,7 @@ export abstract class BaseAgent implements IAgent {
95
100
  agentSay: "agent:say",
96
101
  agentNewTask: "agent:newTask",
97
102
  agentTaskComplete: "agent:taskComplete",
103
+ tokenUsage: "agent:tokenUsage",
98
104
  };
99
105
 
100
106
  public tools: ToolsService;
@@ -194,6 +200,10 @@ export abstract class BaseAgent implements IAgent {
194
200
  this.taskBreakdown = "";
195
201
  this.summaries = [];
196
202
  this.totalCostUsd = 0;
203
+ this.totalInputTokens = 0;
204
+ this.totalOutputTokens = 0;
205
+ this.totalCacheReadTokens = 0;
206
+ this.totalCacheWriteTokens = 0;
197
207
  this.status = this.eventTypes.inProgress;
198
208
  this.turnCount = 0;
199
209
  this.startTimeMs = Date.now();
@@ -283,6 +293,33 @@ export abstract class BaseAgent implements IAgent {
283
293
  this.easyFinalAnswer = value;
284
294
  }
285
295
 
296
+ /**
297
+ * Detect if the model's response is a termination signal (e.g. "Done", "Complete", "Finished", "finalAnswer")
298
+ * This handles the case where an agent refuses to call finalAnswer and just says a short termination word.
299
+ */
300
+ protected isTerminationResponse(content: string): boolean {
301
+ const trimmed = content.trim();
302
+ // Short response (≤ 3 words) that matches a termination word/phrase exactly
303
+ const wordCount = trimmed.split(/\s+/).filter(Boolean).length;
304
+ if (wordCount <= 3) {
305
+ const terminationPattern = /^(done|complete|completed|finished|final\s*answer|task\s*complete|all\s*done|that'?s\s*(all|it)|ok(ay)?|yes)[.!]*$/i;
306
+ if (terminationPattern.test(trimmed)) return true;
307
+ }
308
+
309
+ // Check if the first 1-3 words indicate task completion (for longer responses)
310
+ // e.g. "Task complete: ...", "All done.", "No further changes needed.", "Confirmed complete."
311
+ const firstWords = trimmed.split(/\s+/).slice(0, 3).join(" ");
312
+ const firstWordPattern = /^(task\s*(complete|completed|done|finished)|all\s*done|no\s*(further|more|additional|changes|action)|confirmed?\s*(complete|done|finished|one\s*last)|nothing\s*(more|further|else)|standing\s*by|everything\s*is|still\s*confirmed|acknowledged|done\s*and|complete\s*(and|\.)|completed\s*successfully|no\s*additional|verified\s*and)/i;
313
+ if (firstWordPattern.test(firstWords)) return true;
314
+
315
+ // If easyFinalAnswer mode is on, also match response starting with "✅" or numbered confirmation lists
316
+ if (this.easyFinalAnswer) {
317
+ if (trimmed.startsWith("✅") || /^[\d\.\-\*]/.test(trimmed)) return true;
318
+ }
319
+
320
+ return false;
321
+ }
322
+
286
323
  getEnabledTools() {
287
324
  return this.tools
288
325
  .getTools()
@@ -369,6 +406,44 @@ export abstract class BaseAgent implements IAgent {
369
406
  return this.totalCostUsd;
370
407
  }
371
408
 
409
+ adjustTokenUsage(usage: any) {
410
+ if (!usage) return;
411
+ // Support both OpenAI-style (prompt_tokens/completion_tokens) and Anthropic-style (input_tokens/output_tokens)
412
+ const inputTokens = usage.input_tokens ?? usage.prompt_tokens ?? 0;
413
+ const outputTokens = usage.output_tokens ?? usage.completion_tokens ?? 0;
414
+
415
+ const cacheReadTokens =
416
+ usage.cache_read_input_tokens ?? usage.cache_read_tokens ??
417
+ usage.prompt_tokens_details?.cached_tokens ?? 0;
418
+ const cacheWriteTokens =
419
+ usage.cache_creation_input_tokens ?? usage.cache_write_tokens ?? 0;
420
+
421
+ this.totalInputTokens += inputTokens;
422
+ this.totalOutputTokens += outputTokens;
423
+ this.totalCacheReadTokens += cacheReadTokens;
424
+ this.totalCacheWriteTokens += cacheWriteTokens;
425
+
426
+ this.agentEvents.emit(this.eventTypes.tokenUsage, {
427
+ inputTokens,
428
+ outputTokens,
429
+ cacheReadTokens,
430
+ cacheWriteTokens,
431
+ totalInputTokens: this.totalInputTokens,
432
+ totalOutputTokens: this.totalOutputTokens,
433
+ totalCacheReadTokens: this.totalCacheReadTokens,
434
+ totalCacheWriteTokens: this.totalCacheWriteTokens,
435
+ });
436
+ }
437
+
438
+ getTokenUsage() {
439
+ return {
440
+ totalInputTokens: this.totalInputTokens,
441
+ totalOutputTokens: this.totalOutputTokens,
442
+ totalCacheReadTokens: this.totalCacheReadTokens,
443
+ totalCacheWriteTokens: this.totalCacheWriteTokens,
444
+ };
445
+ }
446
+
372
447
  startNewThread(messages: Message[]) {
373
448
  this.currentThread++;
374
449
  this.agentEvents.emit(this.eventTypes.newThread, messages);
@@ -544,8 +619,14 @@ export abstract class BaseAgent implements IAgent {
544
619
 
545
620
  async kill() {
546
621
  this.log("Killing agent");
547
- if (this.status === this.eventTypes.kill || this.status === this.eventTypes.done) {
548
- this.log("Agent is already being killed or done, ignoring duplicate kill()", "warn");
622
+ if (
623
+ this.status === this.eventTypes.kill ||
624
+ this.status === this.eventTypes.done
625
+ ) {
626
+ this.log(
627
+ "Agent is already being killed or done, ignoring duplicate kill()",
628
+ "warn"
629
+ );
549
630
  return;
550
631
  }
551
632
  this.agentEvents.emit(this.eventTypes.kill, this);
@@ -651,15 +732,22 @@ export abstract class BaseAgent implements IAgent {
651
732
  "warn"
652
733
  );
653
734
  const error = response as any;
654
- if ("response" in error && "data" in error.response) {
735
+ if (error != null && "response" in error && "data" in error.response) {
655
736
  this.log(
656
737
  `Response data: ${JSON.stringify(error.response.data, null, 2)}`,
657
738
  "warn"
658
739
  );
659
740
  }
741
+ if (!response?.choices) {
742
+ const errMsg =
743
+ (error?.error?.message ?? error?.message) ||
744
+ JSON.stringify(response);
745
+ throw new Error(`AI response error: ${errMsg}`);
746
+ }
660
747
  }
661
748
 
662
749
  this.adjustTotalCostUsd(response?.usd_cost);
750
+ this.adjustTokenUsage(response?.usage);
663
751
  this.log("agent response cost: " + response?.usd_cost);
664
752
 
665
753
  // Typically, there's only one choice in the array, but you could have many
@@ -687,6 +775,16 @@ export abstract class BaseAgent implements IAgent {
687
775
 
688
776
  this.updateCurrentThread(messages);
689
777
 
778
+ const truncationWarning = this.detectTruncatedToolCalls(
779
+ toolCalls,
780
+ response
781
+ );
782
+ if (truncationWarning) {
783
+ messages.push(truncationWarning as Message);
784
+ this.updateCurrentThread(messages);
785
+ return this.call(userInput, messages);
786
+ }
787
+
690
788
  for (const toolCall of toolCalls) {
691
789
  if (this.status === this.eventTypes.pause) {
692
790
  this.log(
@@ -755,6 +853,18 @@ export abstract class BaseAgent implements IAgent {
755
853
 
756
854
  // Early exit: not required to call tool
757
855
  const firstMessage = response.choices[0].message;
856
+ // Auto-detect termination words: if the model is just saying "Done", "Complete", etc.
857
+ if (
858
+ response.choices.length === 1 &&
859
+ firstMessage.content &&
860
+ this.isTerminationResponse(firstMessage.content)
861
+ ) {
862
+ this.log(`Termination word detected: "${firstMessage.content.trim()}", treating as finalAnswer`);
863
+ this.status = this.eventTypes.done;
864
+ this.agentEvents.emit(this.eventTypes.done, firstMessage.content);
865
+ return firstMessage.content;
866
+ }
867
+
758
868
  if (
759
869
  response.choices.length === 1 &&
760
870
  firstMessage.content &&
@@ -808,7 +918,7 @@ export abstract class BaseAgent implements IAgent {
808
918
  this.logStatus();
809
919
 
810
920
  const continuation = `<Workflow>
811
- workflow continues until you call one of ${this.requiredToolNames}.\n
921
+ workflow continues until you call one of ${JSON.stringify(this.requiredToolNames)}.\n
812
922
  ${statusMessage}
813
923
  </Workflow>`;
814
924
 
@@ -854,7 +964,7 @@ export abstract class BaseAgent implements IAgent {
854
964
 
855
965
  this.log(`Agent failed: ${e}`, "error");
856
966
 
857
- if ("response" in e && "data" in e.response) {
967
+ if (e != null && typeof e === "object" && "response" in e && "data" in (e as any).response) {
858
968
  this.log(
859
969
  `Error response data: ${JSON.stringify(e.response.data, null, 2)}`,
860
970
  "error"
@@ -958,6 +1068,60 @@ export abstract class BaseAgent implements IAgent {
958
1068
  return JSON.stringify(messages).split(" ").length;
959
1069
  }
960
1070
 
1071
+ /**
1072
+ * Detects whether tool call arguments appear truncated due to hitting the output token limit.
1073
+ * Two signals are checked:
1074
+ * 1. Any tool call argument is empty or invalid JSON (hard truncation).
1075
+ * 2. The model reported many output tokens but the total argument content received is tiny
1076
+ * relative to what those tokens should represent (soft/silent truncation).
1077
+ *
1078
+ * Returns a warning system message if truncation is detected, or null otherwise.
1079
+ */
1080
+ detectTruncatedToolCalls(
1081
+ toolCalls: ToolCall[],
1082
+ response: CompletionResponse
1083
+ ): { role: string; content: string } | null {
1084
+ const outputTokens: number = response?.usage?.completion_tokens || 0;
1085
+ const totalArgLength = toolCalls.reduce(
1086
+ (sum, tc) => sum + (tc.function?.arguments?.length || 0),
1087
+ 0
1088
+ );
1089
+
1090
+ // Percentage-based heuristic: if actual arg chars are less than ~10% of the
1091
+ // expected chars (outputTokens * 4 chars/token), the output was likely truncated.
1092
+ // Only apply when outputTokens > 1000 to avoid false positives on small responses.
1093
+ const expectedArgChars = outputTokens * 4;
1094
+ const suspiciouslySmallArgs =
1095
+ outputTokens > 1000 && totalArgLength < expectedArgChars * 0.1;
1096
+
1097
+ for (const toolCall of toolCalls) {
1098
+ const args = toolCall.function?.arguments || "";
1099
+ let isInvalidJson = false;
1100
+ try {
1101
+ JSON.parse(args);
1102
+ } catch {
1103
+ isInvalidJson = true;
1104
+ }
1105
+ if (isInvalidJson || args.trim() === "" || suspiciouslySmallArgs) {
1106
+ this.log(
1107
+ `Tool call '${toolCall.function?.name}' has malformed/truncated arguments — likely hit output token limit (outputTokens=${outputTokens}, argLength=${args.length})`,
1108
+ "warn"
1109
+ );
1110
+ return {
1111
+ role: "user",
1112
+ content:
1113
+ "⚠️ Output limit warning: Your last tool call had incomplete or missing arguments, which usually means you exceeded the output token limit mid-response. The model reported " +
1114
+ outputTokens +
1115
+ " output tokens but only " +
1116
+ totalArgLength +
1117
+ " characters of tool call arguments were received. Please write smaller, more concise content in your tool calls. Aim for no more than 4000 tokens of output per response. Break large responses into smaller pieces if needed.",
1118
+ };
1119
+ }
1120
+ }
1121
+
1122
+ return null;
1123
+ }
1124
+
961
1125
  async getTaskBreakdown(messages: Message[]) {
962
1126
  if (this.taskBreakdown) {
963
1127
  return this.taskBreakdown;
@@ -262,6 +262,10 @@ export const execCommand = async (
262
262
  continueInBackground?: boolean,
263
263
  logFileName?: string
264
264
  ): Promise<string> => {
265
+ if(!command || typeof command !== "string") {
266
+ throw new Error("Invalid command. We received a non-string value. Please ensure you are sending strings of 4k tokens or less.");
267
+ }
268
+
265
269
  const { stdout, stderr, timedOut, killed, pid, logPath } =
266
270
  await execWithTimeout(command, {
267
271
  timeout,
@@ -53,7 +53,7 @@ export const executeScriptDefinition: Tool = {
53
53
  properties: {
54
54
  script: {
55
55
  type: "string",
56
- description: "The TypeScript code to execute",
56
+ description: "The TypeScript code to execute. 4000 tokens or less",
57
57
  },
58
58
  maxToolCalls: {
59
59
  type: "number",
@@ -19,7 +19,6 @@ export * from "./language";
19
19
  export * from "./askHuman";
20
20
  export * from "./aiClient";
21
21
  export * from "./googleSearch";
22
- export * from "./loadWebpage";
23
22
  export * from "./stringReplace";
24
23
  export * from "./executeScript";
25
24
  export * from "./startAgentTask";
@@ -55,7 +55,7 @@ export const includedTools = [
55
55
  properties: {
56
56
  command: {
57
57
  type: "string",
58
- description: "The command to execute",
58
+ description: "The command to execute. 4000 tokens or less",
59
59
  },
60
60
  timeout: {
61
61
  type: "number",
@@ -346,7 +346,7 @@ export const includedTools = [
346
346
  function: {
347
347
  name: "writeFileChunk",
348
348
  description:
349
- "Update or create files by writing in small chunks of text. Suitable for larger files, this tool allows incremental writing by calling it multiple times.",
349
+ "Update or create files by writing in small chunks of text. Suitable for larger files, this tool allows incremental writing by calling it multiple times. Write chunks of around 4000 tokens",
350
350
  parameters: {
351
351
  type: "object",
352
352
  positional: true,
@@ -358,7 +358,7 @@ export const includedTools = [
358
358
  },
359
359
  content: {
360
360
  type: "string",
361
- description: "The chunk of content to write to the file",
361
+ description: "The chunk of content to write to the file. 4000 tokens or less",
362
362
  },
363
363
  isContinuing: {
364
364
  type: "boolean",
@@ -631,46 +631,6 @@ export const includedTools = [
631
631
  },
632
632
  },
633
633
  },
634
- {
635
- type: "function",
636
- function: {
637
- name: "loadWebpage",
638
- description:
639
- "Load a webpage using a stealth browser to avoid bot detection. Can return either text content with console logs or a screenshot.",
640
- parameters: {
641
- type: "object",
642
- positional: true,
643
- properties: {
644
- url: {
645
- type: "string",
646
- description: "The URL of the webpage to load",
647
- },
648
- mode: {
649
- type: "string",
650
- description:
651
- "The mode for content extraction: 'text' for text content with console logs, 'screenshot' for a base64 encoded screenshot",
652
- enum: ["text", "screenshot"],
653
- },
654
- waitForSelector: {
655
- type: "string",
656
- description:
657
- "Optional CSS selector to wait for before extracting content",
658
- },
659
- timeout: {
660
- type: "number",
661
- description:
662
- "Timeout in milliseconds for page loading (default: 30000)",
663
- },
664
- },
665
- required: ["url"],
666
- },
667
- returns: {
668
- type: "string",
669
- description:
670
- "The webpage content as text with console logs, or a base64 encoded screenshot",
671
- },
672
- },
673
- },
674
634
  {
675
635
  type: "function",
676
636
  function: {
@@ -28,7 +28,7 @@ export async function writeFileChunk(
28
28
  `File path and content are both required. We received: ${JSON.stringify({
29
29
  filePath,
30
30
  content,
31
- })}. Make sure you write small chunks of content, otherwise you will hit output limits, resulting in content being empty.`
31
+ })}. Make sure you write small chunks of content (4k tokens), otherwise you will hit output limits, resulting in content being empty.`
32
32
  );
33
33
  }
34
34
 
@@ -26,7 +26,7 @@ interface RetrieveTokenResponse {
26
26
  export class BrowserLoginService {
27
27
  private baseUrl: string;
28
28
 
29
- constructor(baseUrl: string = KNOWHOW_API_URL) {
29
+ constructor(baseUrl: string = KNOWHOW_API_URL, private orgId?: string) {
30
30
  if (!baseUrl) {
31
31
  throw new BrowserLoginError(
32
32
  "KNOWHOW_API_URL environment variable not set"
@@ -52,11 +52,16 @@ export class BrowserLoginService {
52
52
  spinner.start("Opening browser for authentication");
53
53
 
54
54
  // Step 2: Open browser
55
- await openBrowser(sessionData.browserUrl);
55
+ let browserUrl = sessionData.browserUrl;
56
+ // Append orgId as query string so the frontend can pre-select the correct organization
57
+ if (this.orgId) {
58
+ const separator = browserUrl.includes("?") ? "&" : "?";
59
+ browserUrl = `${browserUrl}${separator}orgId=${encodeURIComponent(this.orgId)}`;
60
+ }
61
+ await openBrowser(browserUrl);
56
62
  console.log(
57
- `\nIf the browser didn't open automatically, please visit: ${sessionData.browserUrl}\n`
63
+ `\nIf the browser didn't open automatically, please visit: ${browserUrl}\n`
58
64
  );
59
-
60
65
  spinner.stop();
61
66
  spinner.start("Waiting for browser authentication");
62
67
 
@@ -247,6 +247,9 @@ export class RemoteSyncModule extends BaseChatModule {
247
247
  const registry = this.agentModule.getTaskRegistry();
248
248
  const taskInfo = registry.get(taskId);
249
249
 
250
+ // Refresh JWT in case it was updated since client was instantiated (e.g. after knowhow login)
251
+ this.client.refreshJwt();
252
+
250
253
  if (!taskInfo) {
251
254
  console.log(
252
255
  `⚠️ Task "${taskId}" not found in registry.`
package/src/cli.ts CHANGED
@@ -14,9 +14,10 @@ import { includedTools } from "./agents/tools/list";
14
14
  import * as allTools from "./agents/tools";
15
15
  import { LazyToolsService, services } from "./services";
16
16
  import { login } from "./login";
17
- import { worker } from "./worker";
17
+ import { worker, tunnel } from "./worker";
18
18
  import { fileSync } from "./fileSync";
19
19
  import { KnowhowSimpleClient } from "./services/KnowhowClient";
20
+ import { ModulesService } from "./services/modules";
20
21
  import {
21
22
  startAllWorkers,
22
23
  listWorkerPaths,
@@ -56,6 +57,7 @@ async function setupServices() {
56
57
  const { Agents, Mcp, Clients, Tools: OldTools } = services();
57
58
  const Tools = new LazyToolsService(); // eslint-disable-line no-shadow
58
59
 
60
+ // Load modules from config first so module-provided tools/agents/plugins are available
59
61
  // We need to wireup the LazyTools to be connected to the same singletons that are in services()
60
62
  Tools.setContext({
61
63
  ...OldTools.getContext(),
@@ -100,6 +102,19 @@ async function setupServices() {
100
102
  await Clients.registerConfiguredModels();
101
103
  console.log("✓ Services are set up and ready to go!");
102
104
 
105
+ // Load modules (tools, plugins, agents) from knowhow.json config
106
+ console.log("📦 Loading modules from config...");
107
+ const modulesService = new ModulesService();
108
+ await modulesService.loadModulesFromConfig({
109
+ Agents,
110
+ Embeddings: services().Embeddings,
111
+ Plugins: services().Plugins,
112
+ Clients,
113
+ // Use LazyToolsService so module-provided tools are visible to agents and scripts
114
+ Tools: Tools as any,
115
+ MediaProcessor: services().MediaProcessor,
116
+ });
117
+
103
118
  // Return both LazyToolsService (for agents) and OldTools (plain ToolsService with all tools for scripts)
104
119
  return { Tools, Clients, PlainTools: OldTools };
105
120
  }
@@ -527,6 +542,21 @@ async function main() {
527
542
  }
528
543
  });
529
544
 
545
+ program
546
+ .command("tunnel")
547
+ .description(
548
+ "Start tunnel-only mode: expose local ports to the cloud without registering any tools"
549
+ )
550
+ .option(
551
+ "--share",
552
+ "Share this tunnel with your organization (allows other users to use it)"
553
+ )
554
+ .option("--unshare", "Make this tunnel private (only you can use it)")
555
+ .action(async (options) => {
556
+ await tunnel(options);
557
+ });
558
+
559
+
530
560
  program
531
561
  .command("script")
532
562
  .description("Run a local tool script file using the executeScript sandbox")
@@ -207,7 +207,7 @@ export class GenericAnthropicClient implements GenericClient {
207
207
  const toolCalls = messages.flatMap((msg) => msg.tool_calls || []);
208
208
  const claudeMessages: MessageParam[] = messages
209
209
  .filter((msg) => msg.role !== "system")
210
- .filter((msg) => msg.content)
210
+ .filter((msg) => msg.content || msg.role === "tool")
211
211
  .map((msg) => {
212
212
  if (msg.role === "tool") {
213
213
  const toolCall = toolCalls.find((tc) => tc.id === msg.tool_call_id);
@@ -412,7 +412,13 @@ export class GenericAnthropicClient implements GenericClient {
412
412
  }),
413
413
 
414
414
  model: options.model,
415
- usage: response.usage,
415
+ usage: response.usage ? {
416
+ prompt_tokens: response.usage.input_tokens ?? 0,
417
+ completion_tokens: response.usage.output_tokens ?? 0,
418
+ total_tokens: (response.usage.input_tokens ?? 0) + (response.usage.output_tokens ?? 0),
419
+ cache_creation_input_tokens: response.usage.cache_creation_input_tokens ?? 0,
420
+ cache_read_input_tokens: response.usage.cache_read_input_tokens ?? 0,
421
+ } : undefined,
416
422
  usd_cost: this.calculateCost(options.model, response.usage),
417
423
  };
418
424
  } catch (err) {
@@ -0,0 +1,10 @@
1
+ import { HttpClient } from "./http";
2
+ import { CerebrasTextPricing } from "./pricing/cerebras";
3
+
4
+ export class GenericCerebrasClient extends HttpClient {
5
+ constructor(apiKey: string) {
6
+ super("https://api.cerebras.ai");
7
+ this.setJwt(apiKey);
8
+ this.setPrices(CerebrasTextPricing);
9
+ }
10
+ }
@@ -19,8 +19,8 @@ export const ContextLimits: Record<string, number> = {
19
19
  [Models.openai.GPT_5]: 1_000_000,
20
20
  [Models.openai.GPT_5_Mini]: 1_000_000,
21
21
  [Models.openai.GPT_5_Nano]: 1_000_000,
22
- [Models.openai.GPT_5_1]: 1_000_000,
23
- [Models.openai.GPT_5_2]: 1_000_000,
22
+ [Models.openai.GPT_51]: 1_000_000,
23
+ [Models.openai.GPT_52]: 1_000_000,
24
24
  [Models.openai.GPT_41]: 1_047_576,
25
25
  [Models.openai.GPT_41_Mini]: 1_047_576,
26
26
  [Models.openai.GPT_41_Nano]: 1_047_576,
@@ -43,6 +43,7 @@ export const ContextLimits: Record<string, number> = {
43
43
 
44
44
  // ─── Anthropic ────────────────────────────────────────────────────────────
45
45
  [Models.anthropic.Opus4_6]: 1_000_000,
46
+ [Models.anthropic.Opus4_6Fast]: 1_000_000,
46
47
  [Models.anthropic.Sonnet4_6]: 1_000_000,
47
48
  [Models.anthropic.Opus4_5]: 1_000_000,
48
49
  [Models.anthropic.Opus4]: 200_000,
@@ -54,6 +55,7 @@ export const ContextLimits: Record<string, number> = {
54
55
  [Models.anthropic.Sonnet3_5]: 200_000,
55
56
  [Models.anthropic.Opus3]: 200_000,
56
57
  [Models.anthropic.Haiku3]: 200_000,
58
+ [Models.anthropic.Haiku3_5]: 200_000,
57
59
 
58
60
  // ─── Google ───────────────────────────────────────────────────────────────
59
61
  [Models.google.Gemini_31_Pro_Preview]: 1_000_000,
@@ -82,6 +84,9 @@ export const ContextLimits: Record<string, number> = {
82
84
  // ─── xAI ──────────────────────────────────────────────────────────────────
83
85
  [Models.xai.Grok4_1_Fast_Reasoning]: 2_000_000,
84
86
  [Models.xai.Grok4_1_Fast_NonReasoning]: 2_000_000,
87
+ [Models.xai.Grok_4_20_Reasoning]: 131_072,
88
+ [Models.xai.Grok_4_20_NonReasoning]: 131_072,
89
+ [Models.xai.Grok_4_20_MultiAgent]: 2_000_000,
85
90
  [Models.xai.GrokCodeFast]: 2_000_000,
86
91
  [Models.xai.Grok4]: 131_072,
87
92
  [Models.xai.Grok3Beta]: 131_072,
@@ -0,0 +1,23 @@
1
+ import { HttpClient } from "./http";
2
+ import { CopilotTextPricing } from "./pricing/copilot";
3
+
4
+ /**
5
+ * GitHub Copilot client — OpenAI-compatible API
6
+ * https://docs.github.com/en/copilot/reference/ai-models/supported-models
7
+ *
8
+ * GitHub Copilot exposes an OpenAI-compatible endpoint at https://api.githubcopilot.com
9
+ * that allows subscribers to use premium models (Claude Opus, GPT-5.x, Gemini, Grok etc.)
10
+ * via their Copilot subscription's premium request allowance — no per-token charges.
11
+ *
12
+ * Authentication: uses a GitHub token (same as GITHUB_TOKEN / a personal access token
13
+ * or OAuth token with copilot scope).
14
+ *
15
+ * Set env var GITHUB_COPILOT_TOKEN (preferred) or GITHUB_TOKEN to enable.
16
+ */
17
+ export class GitHubCopilotClient extends HttpClient {
18
+ constructor(apiKey = process.env.GITHUB_COPILOT_TOKEN ?? process.env.GITHUB_TOKEN) {
19
+ super("https://api.githubcopilot.com");
20
+ if (apiKey) this.setJwt(apiKey);
21
+ this.setPrices(CopilotTextPricing);
22
+ }
23
+ }
@@ -0,0 +1,16 @@
1
+ import { HttpClient } from "./http";
2
+ import { DeepSeekTextPricing } from "./pricing/deepseek";
3
+
4
+ /**
5
+ * DeepSeek client — OpenAI-compatible API
6
+ * https://platform.deepseek.com/api-docs/
7
+ * Industry-leading reasoning (R1) and coding (V3) models at very low cost.
8
+ * Set env var DEEPSEEK_API_KEY to enable.
9
+ */
10
+ export class GenericDeepSeekClient extends HttpClient {
11
+ constructor(apiKey = process.env.DEEPSEEK_API_KEY) {
12
+ super("https://api.deepseek.com");
13
+ if (apiKey) this.setJwt(apiKey);
14
+ this.setPrices(DeepSeekTextPricing);
15
+ }
16
+ }
@@ -0,0 +1,15 @@
1
+ import { HttpClient } from "./http";
2
+ import { FireworksTextPricing } from "./pricing/fireworks";
3
+
4
+ /**
5
+ * Fireworks AI client — OpenAI-compatible API (fast serverless inference)
6
+ * https://docs.fireworks.ai/api-reference/introduction
7
+ * Set env var FIREWORKS_API_KEY to enable.
8
+ */
9
+ export class GenericFireworksClient extends HttpClient {
10
+ constructor(apiKey = process.env.FIREWORKS_API_KEY) {
11
+ super("https://api.fireworks.ai/inference");
12
+ if (apiKey) this.setJwt(apiKey);
13
+ this.setPrices(FireworksTextPricing);
14
+ }
15
+ }