@tyvm/knowhow 0.0.145 → 0.0.147

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 (320) hide show
  1. package/autodoc/cli-reference.md +36 -7
  2. package/bin/knowhow.js +8 -6
  3. package/package.json +1 -1
  4. package/src/agents/base/base.ts +24 -0
  5. package/src/agents/historyRollback.ts +36 -0
  6. package/src/agents/tools/startAgentTask.ts +41 -9
  7. package/src/auth/browserLogin.ts +9 -3
  8. package/src/auth/keyAuth.ts +115 -0
  9. package/src/auth/keyManager.ts +166 -0
  10. package/src/chat/CliChatService.ts +16 -0
  11. package/src/chat/modules/AgentModule.ts +144 -56
  12. package/src/chat/modules/AskModule.ts +2 -0
  13. package/src/chat/modules/BaseChatModule.ts +0 -1
  14. package/src/chat/modules/CustomCommandsModule.ts +2 -0
  15. package/src/chat/modules/InternalChatModule.ts +2 -0
  16. package/src/chat/modules/RemoteSyncModule.ts +4 -0
  17. package/src/chat/modules/RendererModule.ts +2 -0
  18. package/src/chat/modules/SearchModule.ts +2 -0
  19. package/src/chat/modules/SessionsModule.ts +110 -39
  20. package/src/chat/modules/SetupModule.ts +2 -0
  21. package/src/chat/modules/ShellCommandModule.ts +2 -0
  22. package/src/chat/modules/SystemModule.ts +2 -0
  23. package/src/chat/types.ts +5 -2
  24. package/src/cli.ts +1 -1
  25. package/src/clients/anthropic.ts +24 -0
  26. package/src/clients/fireworks.ts +3 -0
  27. package/src/clients/gemini.ts +8 -0
  28. package/src/clients/index.ts +2 -0
  29. package/src/clients/openai.ts +7 -4
  30. package/src/clients/types.ts +30 -7
  31. package/src/clients/xai.ts +23 -4
  32. package/src/commands/agent.ts +11 -39
  33. package/src/commands/agents.ts +403 -53
  34. package/src/commands/misc.ts +3 -2
  35. package/src/commands/services.ts +49 -3
  36. package/src/login.ts +89 -28
  37. package/src/plugins/plugins.ts +16 -0
  38. package/src/processors/TokenCompressor.ts +40 -8
  39. package/src/processors/ToolResponseCache.ts +17 -8
  40. package/src/processors/tools/boundedToolResponse.ts +49 -0
  41. package/src/processors/tools/grepToolResponse.ts +164 -38
  42. package/src/processors/tools/index.ts +2 -0
  43. package/src/processors/tools/jqToolResponse.ts +40 -10
  44. package/src/processors/tools/listStoredToolResponses.ts +51 -10
  45. package/src/processors/tools/tailToolResponse.ts +85 -17
  46. package/src/services/AgentService.ts +2 -0
  47. package/src/services/AgentSyncFs.ts +65 -0
  48. package/src/services/AgentSyncKnowhowWeb.ts +22 -4
  49. package/src/services/BehaviorsService.ts +2 -0
  50. package/src/services/DockerService.ts +2 -0
  51. package/src/services/EmbeddingService.ts +2 -0
  52. package/src/services/EmbeddingsService.ts +3 -0
  53. package/src/services/KnowhowClient.ts +68 -7
  54. package/src/services/LazyToolsService.ts +16 -0
  55. package/src/services/Mcp.ts +11 -2
  56. package/src/services/McpServer.ts +2 -0
  57. package/src/services/MessageProcessor.ts +2 -0
  58. package/src/services/MinimalToolsService.ts +2 -0
  59. package/src/services/RuntimeReloadService.ts +31 -0
  60. package/src/services/SessionManager.ts +5 -0
  61. package/src/services/SkillsService.ts +2 -0
  62. package/src/services/SyncedAgentWatcher.ts +2 -0
  63. package/src/services/SyncerService.ts +23 -8
  64. package/src/services/TaskRegistry.ts +2 -0
  65. package/src/services/Tools.ts +13 -0
  66. package/src/services/TracingService.ts +116 -0
  67. package/src/services/index.ts +10 -0
  68. package/src/services/modules/index.ts +49 -3
  69. package/src/services/modules/types.ts +8 -0
  70. package/src/types.ts +2 -0
  71. package/src/util/Trace.ts +236 -0
  72. package/src/utils/http.ts +18 -2
  73. package/tests/clients/fireworks.test.ts +65 -0
  74. package/tests/clients/reasoningEffort.test.ts +79 -0
  75. package/tests/compressor/toolResponseCache.test.ts +2 -1
  76. package/tests/manual/clients/completions.test.ts +27 -10
  77. package/tests/processors/TokenCompressor.test.ts +17 -3
  78. package/tests/processors/tools/boundedRetrievalTools.test.ts +68 -0
  79. package/tests/processors/tools/grepToolResponse.test.ts +88 -1
  80. package/tests/unit/agents/historyRollback.test.ts +53 -0
  81. package/tests/unit/auth/keyAuth.test.ts +102 -0
  82. package/tests/unit/auth/keyManager.test.ts +67 -0
  83. package/tests/unit/chat/historyCommandArgs.test.ts +37 -0
  84. package/tests/unit/commands/agents.test.ts +68 -0
  85. package/tests/unit/login.test.ts +119 -0
  86. package/tests/unit/plugins/pluginLoading.test.ts +31 -0
  87. package/tests/unit/services/AgentSyncKnowhowWeb.test.ts +38 -0
  88. package/tests/unit/services/LazyToolsService.test.ts +29 -0
  89. package/tests/unit/services/RuntimeReloadService.test.ts +29 -0
  90. package/tests/unit/services/SessionManager.test.ts +43 -0
  91. package/tests/unit/services/SyncerService.test.ts +45 -0
  92. package/tests/unit/util/Trace.test.ts +119 -0
  93. package/tests/unit/utils/http.test.ts +43 -0
  94. package/ts_build/package.json +1 -1
  95. package/ts_build/src/agents/base/base.d.ts +1 -0
  96. package/ts_build/src/agents/base/base.js +31 -2
  97. package/ts_build/src/agents/base/base.js.map +1 -1
  98. package/ts_build/src/agents/historyRollback.d.ts +2 -0
  99. package/ts_build/src/agents/historyRollback.js +22 -0
  100. package/ts_build/src/agents/historyRollback.js.map +1 -0
  101. package/ts_build/src/agents/tools/startAgentTask.d.ts +2 -0
  102. package/ts_build/src/agents/tools/startAgentTask.js +36 -9
  103. package/ts_build/src/agents/tools/startAgentTask.js.map +1 -1
  104. package/ts_build/src/auth/browserLogin.d.ts +1 -0
  105. package/ts_build/src/auth/browserLogin.js +5 -2
  106. package/ts_build/src/auth/browserLogin.js.map +1 -1
  107. package/ts_build/src/auth/keyAuth.d.ts +5 -0
  108. package/ts_build/src/auth/keyAuth.js +79 -0
  109. package/ts_build/src/auth/keyAuth.js.map +1 -0
  110. package/ts_build/src/auth/keyManager.d.ts +14 -0
  111. package/ts_build/src/auth/keyManager.js +145 -0
  112. package/ts_build/src/auth/keyManager.js.map +1 -0
  113. package/ts_build/src/chat/CliChatService.js +15 -0
  114. package/ts_build/src/chat/CliChatService.js.map +1 -1
  115. package/ts_build/src/chat/modules/AgentModule.d.ts +13 -1
  116. package/ts_build/src/chat/modules/AgentModule.js +96 -43
  117. package/ts_build/src/chat/modules/AgentModule.js.map +1 -1
  118. package/ts_build/src/chat/modules/AskModule.js +12 -2
  119. package/ts_build/src/chat/modules/AskModule.js.map +1 -1
  120. package/ts_build/src/chat/modules/BaseChatModule.js.map +1 -1
  121. package/ts_build/src/chat/modules/CustomCommandsModule.js +12 -2
  122. package/ts_build/src/chat/modules/CustomCommandsModule.js.map +1 -1
  123. package/ts_build/src/chat/modules/InternalChatModule.js +16 -2
  124. package/ts_build/src/chat/modules/InternalChatModule.js.map +1 -1
  125. package/ts_build/src/chat/modules/RemoteSyncModule.js +19 -2
  126. package/ts_build/src/chat/modules/RemoteSyncModule.js.map +1 -1
  127. package/ts_build/src/chat/modules/RendererModule.js +17 -2
  128. package/ts_build/src/chat/modules/RendererModule.js.map +1 -1
  129. package/ts_build/src/chat/modules/SearchModule.js +12 -2
  130. package/ts_build/src/chat/modules/SearchModule.js.map +1 -1
  131. package/ts_build/src/chat/modules/SessionsModule.d.ts +6 -0
  132. package/ts_build/src/chat/modules/SessionsModule.js +114 -24
  133. package/ts_build/src/chat/modules/SessionsModule.js.map +1 -1
  134. package/ts_build/src/chat/modules/SetupModule.js +17 -2
  135. package/ts_build/src/chat/modules/SetupModule.js.map +1 -1
  136. package/ts_build/src/chat/modules/ShellCommandModule.js +12 -2
  137. package/ts_build/src/chat/modules/ShellCommandModule.js.map +1 -1
  138. package/ts_build/src/chat/modules/SystemModule.js +12 -2
  139. package/ts_build/src/chat/modules/SystemModule.js.map +1 -1
  140. package/ts_build/src/chat/types.d.ts +5 -2
  141. package/ts_build/src/cli.js +1 -1
  142. package/ts_build/src/cli.js.map +1 -1
  143. package/ts_build/src/clients/anthropic.d.ts +1 -0
  144. package/ts_build/src/clients/anthropic.js +12 -0
  145. package/ts_build/src/clients/anthropic.js.map +1 -1
  146. package/ts_build/src/clients/fireworks.js +1 -0
  147. package/ts_build/src/clients/fireworks.js.map +1 -1
  148. package/ts_build/src/clients/gemini.js +8 -0
  149. package/ts_build/src/clients/gemini.js.map +1 -1
  150. package/ts_build/src/clients/index.js +19 -4
  151. package/ts_build/src/clients/index.js.map +1 -1
  152. package/ts_build/src/clients/openai.js +4 -2
  153. package/ts_build/src/clients/openai.js.map +1 -1
  154. package/ts_build/src/clients/types.d.ts +4 -1
  155. package/ts_build/src/clients/types.js +14 -0
  156. package/ts_build/src/clients/types.js.map +1 -1
  157. package/ts_build/src/clients/xai.d.ts +1 -0
  158. package/ts_build/src/clients/xai.js +23 -4
  159. package/ts_build/src/clients/xai.js.map +1 -1
  160. package/ts_build/src/commands/agent.js +7 -22
  161. package/ts_build/src/commands/agent.js.map +1 -1
  162. package/ts_build/src/commands/agents.d.ts +1 -1
  163. package/ts_build/src/commands/agents.js +174 -16
  164. package/ts_build/src/commands/agents.js.map +1 -1
  165. package/ts_build/src/commands/misc.js +3 -2
  166. package/ts_build/src/commands/misc.js.map +1 -1
  167. package/ts_build/src/commands/services.d.ts +2 -0
  168. package/ts_build/src/commands/services.js +45 -3
  169. package/ts_build/src/commands/services.js.map +1 -1
  170. package/ts_build/src/login.d.ts +1 -1
  171. package/ts_build/src/login.js +53 -15
  172. package/ts_build/src/login.js.map +1 -1
  173. package/ts_build/src/plugins/plugins.d.ts +1 -0
  174. package/ts_build/src/plugins/plugins.js +14 -0
  175. package/ts_build/src/plugins/plugins.js.map +1 -1
  176. package/ts_build/src/processors/TokenCompressor.js +15 -6
  177. package/ts_build/src/processors/TokenCompressor.js.map +1 -1
  178. package/ts_build/src/processors/ToolResponseCache.d.ts +3 -3
  179. package/ts_build/src/processors/ToolResponseCache.js +8 -8
  180. package/ts_build/src/processors/ToolResponseCache.js.map +1 -1
  181. package/ts_build/src/processors/tools/boundedToolResponse.d.ts +7 -0
  182. package/ts_build/src/processors/tools/boundedToolResponse.js +27 -0
  183. package/ts_build/src/processors/tools/boundedToolResponse.js.map +1 -0
  184. package/ts_build/src/processors/tools/grepToolResponse.d.ts +4 -0
  185. package/ts_build/src/processors/tools/grepToolResponse.js +101 -25
  186. package/ts_build/src/processors/tools/grepToolResponse.js.map +1 -1
  187. package/ts_build/src/processors/tools/index.d.ts +2 -2
  188. package/ts_build/src/processors/tools/index.js.map +1 -1
  189. package/ts_build/src/processors/tools/jqToolResponse.d.ts +5 -1
  190. package/ts_build/src/processors/tools/jqToolResponse.js +26 -9
  191. package/ts_build/src/processors/tools/jqToolResponse.js.map +1 -1
  192. package/ts_build/src/processors/tools/listStoredToolResponses.d.ts +7 -1
  193. package/ts_build/src/processors/tools/listStoredToolResponses.js +39 -9
  194. package/ts_build/src/processors/tools/listStoredToolResponses.js.map +1 -1
  195. package/ts_build/src/processors/tools/tailToolResponse.d.ts +5 -0
  196. package/ts_build/src/processors/tools/tailToolResponse.js +56 -13
  197. package/ts_build/src/processors/tools/tailToolResponse.js.map +1 -1
  198. package/ts_build/src/services/AgentService.js +18 -2
  199. package/ts_build/src/services/AgentService.js.map +1 -1
  200. package/ts_build/src/services/AgentSyncFs.d.ts +3 -0
  201. package/ts_build/src/services/AgentSyncFs.js +62 -15
  202. package/ts_build/src/services/AgentSyncFs.js.map +1 -1
  203. package/ts_build/src/services/AgentSyncKnowhowWeb.d.ts +2 -0
  204. package/ts_build/src/services/AgentSyncKnowhowWeb.js +28 -6
  205. package/ts_build/src/services/AgentSyncKnowhowWeb.js.map +1 -1
  206. package/ts_build/src/services/BehaviorsService.js +12 -2
  207. package/ts_build/src/services/BehaviorsService.js.map +1 -1
  208. package/ts_build/src/services/DockerService.js +20 -8
  209. package/ts_build/src/services/DockerService.js.map +1 -1
  210. package/ts_build/src/services/EmbeddingService.js +16 -2
  211. package/ts_build/src/services/EmbeddingService.js.map +1 -1
  212. package/ts_build/src/services/EmbeddingsService.js +12 -2
  213. package/ts_build/src/services/EmbeddingsService.js.map +1 -1
  214. package/ts_build/src/services/KnowhowClient.d.ts +7 -2
  215. package/ts_build/src/services/KnowhowClient.js +108 -10
  216. package/ts_build/src/services/KnowhowClient.js.map +1 -1
  217. package/ts_build/src/services/LazyToolsService.d.ts +2 -0
  218. package/ts_build/src/services/LazyToolsService.js +26 -2
  219. package/ts_build/src/services/LazyToolsService.js.map +1 -1
  220. package/ts_build/src/services/Mcp.js +20 -4
  221. package/ts_build/src/services/Mcp.js.map +1 -1
  222. package/ts_build/src/services/McpServer.js +17 -2
  223. package/ts_build/src/services/McpServer.js.map +1 -1
  224. package/ts_build/src/services/MessageProcessor.js +16 -2
  225. package/ts_build/src/services/MessageProcessor.js.map +1 -1
  226. package/ts_build/src/services/MinimalToolsService.js +16 -2
  227. package/ts_build/src/services/MinimalToolsService.js.map +1 -1
  228. package/ts_build/src/services/RuntimeReloadService.d.ts +12 -0
  229. package/ts_build/src/services/RuntimeReloadService.js +26 -0
  230. package/ts_build/src/services/RuntimeReloadService.js.map +1 -0
  231. package/ts_build/src/services/SessionManager.js +19 -2
  232. package/ts_build/src/services/SessionManager.js.map +1 -1
  233. package/ts_build/src/services/SkillsService.js +16 -2
  234. package/ts_build/src/services/SkillsService.js.map +1 -1
  235. package/ts_build/src/services/SyncedAgentWatcher.js +16 -2
  236. package/ts_build/src/services/SyncedAgentWatcher.js.map +1 -1
  237. package/ts_build/src/services/SyncerService.d.ts +1 -0
  238. package/ts_build/src/services/SyncerService.js +27 -5
  239. package/ts_build/src/services/SyncerService.js.map +1 -1
  240. package/ts_build/src/services/TaskRegistry.js +12 -2
  241. package/ts_build/src/services/TaskRegistry.js.map +1 -1
  242. package/ts_build/src/services/Tools.d.ts +1 -0
  243. package/ts_build/src/services/Tools.js +21 -2
  244. package/ts_build/src/services/Tools.js.map +1 -1
  245. package/ts_build/src/services/TracingService.d.ts +16 -0
  246. package/ts_build/src/services/TracingService.js +35 -0
  247. package/ts_build/src/services/TracingService.js.map +1 -0
  248. package/ts_build/src/services/index.d.ts +8 -0
  249. package/ts_build/src/services/index.js +9 -1
  250. package/ts_build/src/services/index.js.map +1 -1
  251. package/ts_build/src/services/modules/index.d.ts +6 -0
  252. package/ts_build/src/services/modules/index.js +29 -3
  253. package/ts_build/src/services/modules/index.js.map +1 -1
  254. package/ts_build/src/services/modules/types.d.ts +3 -0
  255. package/ts_build/src/types.d.ts +1 -0
  256. package/ts_build/src/types.js.map +1 -1
  257. package/ts_build/src/util/Trace.d.ts +11 -0
  258. package/ts_build/src/util/Trace.js +107 -0
  259. package/ts_build/src/util/Trace.js.map +1 -0
  260. package/ts_build/src/utils/http.d.ts +6 -2
  261. package/ts_build/src/utils/http.js +8 -2
  262. package/ts_build/src/utils/http.js.map +1 -1
  263. package/ts_build/tests/clients/fireworks.test.d.ts +1 -0
  264. package/ts_build/tests/clients/fireworks.test.js +65 -0
  265. package/ts_build/tests/clients/fireworks.test.js.map +1 -0
  266. package/ts_build/tests/clients/reasoningEffort.test.d.ts +1 -0
  267. package/ts_build/tests/clients/reasoningEffort.test.js +68 -0
  268. package/ts_build/tests/clients/reasoningEffort.test.js.map +1 -0
  269. package/ts_build/tests/compressor/toolResponseCache.test.js +1 -1
  270. package/ts_build/tests/compressor/toolResponseCache.test.js.map +1 -1
  271. package/ts_build/tests/manual/clients/completions.test.js +19 -6
  272. package/ts_build/tests/manual/clients/completions.test.js.map +1 -1
  273. package/ts_build/tests/processors/TokenCompressor.test.js +11 -2
  274. package/ts_build/tests/processors/TokenCompressor.test.js.map +1 -1
  275. package/ts_build/tests/processors/tools/boundedRetrievalTools.test.d.ts +1 -0
  276. package/ts_build/tests/processors/tools/boundedRetrievalTools.test.js +57 -0
  277. package/ts_build/tests/processors/tools/boundedRetrievalTools.test.js.map +1 -0
  278. package/ts_build/tests/processors/tools/grepToolResponse.test.js +37 -0
  279. package/ts_build/tests/processors/tools/grepToolResponse.test.js.map +1 -1
  280. package/ts_build/tests/unit/agents/historyRollback.test.d.ts +1 -0
  281. package/ts_build/tests/unit/agents/historyRollback.test.js +42 -0
  282. package/ts_build/tests/unit/agents/historyRollback.test.js.map +1 -0
  283. package/ts_build/tests/unit/auth/keyAuth.test.d.ts +1 -0
  284. package/ts_build/tests/unit/auth/keyAuth.test.js +77 -0
  285. package/ts_build/tests/unit/auth/keyAuth.test.js.map +1 -0
  286. package/ts_build/tests/unit/auth/keyManager.test.d.ts +1 -0
  287. package/ts_build/tests/unit/auth/keyManager.test.js +52 -0
  288. package/ts_build/tests/unit/auth/keyManager.test.js.map +1 -0
  289. package/ts_build/tests/unit/chat/historyCommandArgs.test.d.ts +1 -0
  290. package/ts_build/tests/unit/chat/historyCommandArgs.test.js +29 -0
  291. package/ts_build/tests/unit/chat/historyCommandArgs.test.js.map +1 -0
  292. package/ts_build/tests/unit/commands/agents.test.d.ts +1 -0
  293. package/ts_build/tests/unit/commands/agents.test.js +91 -0
  294. package/ts_build/tests/unit/commands/agents.test.js.map +1 -0
  295. package/ts_build/tests/unit/login.test.d.ts +1 -0
  296. package/ts_build/tests/unit/login.test.js +97 -0
  297. package/ts_build/tests/unit/login.test.js.map +1 -0
  298. package/ts_build/tests/unit/plugins/pluginLoading.test.js +59 -0
  299. package/ts_build/tests/unit/plugins/pluginLoading.test.js.map +1 -1
  300. package/ts_build/tests/unit/services/AgentSyncKnowhowWeb.test.d.ts +1 -0
  301. package/ts_build/tests/unit/services/AgentSyncKnowhowWeb.test.js +32 -0
  302. package/ts_build/tests/unit/services/AgentSyncKnowhowWeb.test.js.map +1 -0
  303. package/ts_build/tests/unit/services/LazyToolsService.test.d.ts +1 -0
  304. package/ts_build/tests/unit/services/LazyToolsService.test.js +27 -0
  305. package/ts_build/tests/unit/services/LazyToolsService.test.js.map +1 -0
  306. package/ts_build/tests/unit/services/RuntimeReloadService.test.d.ts +1 -0
  307. package/ts_build/tests/unit/services/RuntimeReloadService.test.js +26 -0
  308. package/ts_build/tests/unit/services/RuntimeReloadService.test.js.map +1 -0
  309. package/ts_build/tests/unit/services/SessionManager.test.d.ts +1 -0
  310. package/ts_build/tests/unit/services/SessionManager.test.js +72 -0
  311. package/ts_build/tests/unit/services/SessionManager.test.js.map +1 -0
  312. package/ts_build/tests/unit/services/SyncerService.test.d.ts +1 -0
  313. package/ts_build/tests/unit/services/SyncerService.test.js +40 -0
  314. package/ts_build/tests/unit/services/SyncerService.test.js.map +1 -0
  315. package/ts_build/tests/unit/util/Trace.test.d.ts +1 -0
  316. package/ts_build/tests/unit/util/Trace.test.js +107 -0
  317. package/ts_build/tests/unit/util/Trace.test.js.map +1 -0
  318. package/ts_build/tests/unit/utils/http.test.d.ts +1 -0
  319. package/ts_build/tests/unit/utils/http.test.js +69 -0
  320. package/ts_build/tests/unit/utils/http.test.js.map +1 -0
@@ -0,0 +1,79 @@
1
+ import { GenericAnthropicClient } from "../../src/clients/anthropic";
2
+ import { GenericGeminiClient } from "../../src/clients/gemini";
3
+ import { GenericOpenAiClient } from "../../src/clients/openai";
4
+ import { AnthropicModels } from "../../src/clients/pricing";
5
+ import {
6
+ REASONING_EFFORTS,
7
+ ReasoningEffort,
8
+ isReasoningEffort,
9
+ } from "../../src/clients/types";
10
+ import {
11
+ GoogleThinkingBudgetModels,
12
+ GoogleThinkingLevelModels,
13
+ } from "../../src/types";
14
+ import { GenericXAIClient } from "../../src/clients/xai";
15
+
16
+ const options = (model: string, reasoning_effort: ReasoningEffort) => ({
17
+ model,
18
+ reasoning_effort,
19
+ messages: [],
20
+ });
21
+
22
+ describe("reasoning effort values", () => {
23
+ it("accepts the complete provider-neutral set and rejects other values", () => {
24
+ expect(REASONING_EFFORTS).toEqual([
25
+ "none",
26
+ "minimal",
27
+ "low",
28
+ "medium",
29
+ "high",
30
+ "xhigh",
31
+ "max",
32
+ ]);
33
+ REASONING_EFFORTS.forEach((effort) => expect(isReasoningEffort(effort)).toBe(true));
34
+ expect(isReasoningEffort("extra-high")).toBe(false);
35
+ expect(isReasoningEffort(undefined)).toBe(false);
36
+ });
37
+
38
+ it("maps provider-neutral values to OpenAI legacy and Responses spellings", () => {
39
+ const client = new GenericOpenAiClient("fake-key");
40
+ expect(client.resolveReasoningEffort(options("unknown", "none"))).toBe("low");
41
+ expect(client.resolveReasoningEffort(options("unknown", "minimal"))).toBe("low");
42
+ expect(client.resolveReasoningEffort(options("unknown", "xhigh"))).toBe("high");
43
+ expect(client.resolveReasoningEffort(options("unknown", "max"))).toBe("high");
44
+ expect(client.resolveReasoningEffortForModel(options("unknown", "max"))).toBe("xhigh");
45
+ });
46
+
47
+ it("maps all values to valid Gemini thinking levels and budgets", () => {
48
+ const client = new GenericGeminiClient("fake-key");
49
+ const levelModel = GoogleThinkingLevelModels[0];
50
+ const budgetModel = GoogleThinkingBudgetModels[0];
51
+
52
+ expect(client.buildThinkingConfig(options(levelModel, "none"))).toEqual({ thinkingLevel: "minimal" });
53
+ expect(client.buildThinkingConfig(options(levelModel, "minimal"))).toEqual({ thinkingLevel: "minimal" });
54
+ expect(client.buildThinkingConfig(options(levelModel, "xhigh"))).toEqual({ thinkingLevel: "high" });
55
+ expect(client.buildThinkingConfig(options(levelModel, "max"))).toEqual({ thinkingLevel: "high" });
56
+ expect(client.buildThinkingConfig(options(budgetModel, "none"))).toEqual({ thinkingBudget: 0 });
57
+ expect(client.buildThinkingConfig(options(budgetModel, "minimal"))).toEqual({ thinkingBudget: 512 });
58
+ expect(client.buildThinkingConfig(options(budgetModel, "medium"))).toEqual({ thinkingBudget: 8192 });
59
+ expect(client.buildThinkingConfig(options(budgetModel, "max"))).toEqual({ thinkingBudget: -1 });
60
+ });
61
+
62
+ it("maps provider-neutral values to Anthropic output effort", () => {
63
+ const client = new GenericAnthropicClient("fake-key");
64
+ expect(client.getEffortOutputConfig(AnthropicModels.Sonnet5, "none")).toBeUndefined();
65
+ expect(client.getEffortOutputConfig(AnthropicModels.Sonnet5, "minimal")).toEqual({ effort: "low" });
66
+ expect(client.getEffortOutputConfig(AnthropicModels.Sonnet5, "xhigh")).toEqual({ effort: "xhigh" });
67
+ expect(client.getEffortOutputConfig(AnthropicModels.Sonnet5, "max")).toEqual({ effort: "max" });
68
+ expect(client.getEffortOutputConfig("unsupported", "high")).toBeUndefined();
69
+ });
70
+
71
+ it("maps provider-neutral values to xAI legacy effort", () => {
72
+ const client = new GenericXAIClient("fake-key");
73
+ expect(client.resolveLegacyReasoningEffort("none")).toBeUndefined();
74
+ expect(client.resolveLegacyReasoningEffort("minimal")).toBe("low");
75
+ expect(client.resolveLegacyReasoningEffort("medium")).toBe("medium");
76
+ expect(client.resolveLegacyReasoningEffort("xhigh")).toBe("high");
77
+ expect(client.resolveLegacyReasoningEffort("max")).toBe("high");
78
+ });
79
+ });
@@ -111,7 +111,8 @@ describe("ToolResponseCache - MCP Format Support", () => {
111
111
  it("should filter data array with ._data | map(select(.private == true))", async () => {
112
112
  const result = await cache.queryToolResponse(
113
113
  "github-repos",
114
- "._data | map(select(.private == true))"
114
+ "._data | map(select(.private == true))",
115
+ { maxCharacters: 50_000 }
115
116
  );
116
117
 
117
118
  // Parse and verify filtering worked
@@ -17,6 +17,7 @@ import { OpenAiChatModels, OpenAiReasoningModels, OpenAiResponsesOnlyModels } fr
17
17
  import { AnthropicTextModels, AnthropicLimitedAvailabilityModels } from "../../../src/clients/pricing/anthropic";
18
18
  import { GoogleTextModels } from "../../../src/clients/pricing/google";
19
19
  import { XaiTextModels, XaiDeprecatedTextModels } from "../../../src/clients/pricing/xai";
20
+ import { FireworksTextPricing } from "../../../src/clients/pricing/fireworks";
20
21
 
21
22
  const OUTPUT_FILE = path.join(__dirname, "completions.json");
22
23
  const PROMPT = "Say hello in 5 words.";
@@ -29,6 +30,7 @@ interface ModelEntry {
29
30
  provider: string;
30
31
  model: string;
31
32
  envKey: string;
33
+ reasoningEffort?: "low";
32
34
  }
33
35
 
34
36
  function openaiEntries(models: string[]): ModelEntry[] {
@@ -47,19 +49,35 @@ function xaiEntries(models: string[]): ModelEntry[] {
47
49
  return models.map((model) => ({ provider: "xai", model, envKey: "XAI_API_KEY" }));
48
50
  }
49
51
 
52
+ function fireworksEntries(): ModelEntry[] {
53
+ return Object.entries(FireworksTextPricing)
54
+ .filter(([, pricing]) => !pricing.deprecated)
55
+ .map(([model]) => ({
56
+ provider: "fireworks",
57
+ model,
58
+ envKey: "FIREWORKS_API_KEY",
59
+ // Fireworks model support for reasoning_effort is model-specific. This
60
+ // suite exercises the request shape used by Patcher without that flag.
61
+ }));
62
+ }
63
+
50
64
  const TEST_MODELS: ModelEntry[] = [
51
65
  // OpenAI — all active chat + reasoning models + responses-API models
52
66
  // Deduplicate since some models appear in multiple lists
53
- ...openaiEntries([...new Set([...OpenAiChatModels, ...OpenAiReasoningModels, ...OpenAiResponsesOnlyModels])]),
67
+ ...openaiEntries([...new Set([...OpenAiChatModels, ...OpenAiReasoningModels, ...OpenAiResponsesOnlyModels])])
68
+ .map((entry) => ({ ...entry, reasoningEffort: "low" as const })),
54
69
 
55
70
  // Anthropic — active text models (limited availability excluded)
56
- ...anthropicEntries(AnthropicTextModels),
71
+ ...anthropicEntries(AnthropicTextModels).map((entry) => ({ ...entry, reasoningEffort: "low" as const })),
57
72
 
58
73
  // Google — all active text models
59
- ...googleEntries(GoogleTextModels),
74
+ ...googleEntries(GoogleTextModels).map((entry) => ({ ...entry, reasoningEffort: "low" as const })),
60
75
 
61
76
  // XAI — active text models (deprecated grok-2 models excluded)
62
- ...xaiEntries(XaiTextModels),
77
+ ...xaiEntries(XaiTextModels).map((entry) => ({ ...entry, reasoningEffort: "low" as const })),
78
+
79
+ // Fireworks — all active catalog models
80
+ ...fireworksEntries(),
63
81
  ];
64
82
 
65
83
  // ─── Persistence helpers ──────────────────────────────────────────────────────
@@ -118,7 +136,7 @@ describe("Text Completions – multi-provider model verification", () => {
118
136
  console.log(` Total records: ${Object.keys(results).length}`);
119
137
  });
120
138
 
121
- for (const { provider, model, envKey } of TEST_MODELS) {
139
+ for (const { provider, model, envKey, reasoningEffort } of TEST_MODELS) {
122
140
  const key = recordKey(provider, model);
123
141
 
124
142
  test(`${provider} / ${model}`, async () => {
@@ -145,11 +163,10 @@ describe("Text Completions – multi-provider model verification", () => {
145
163
  model,
146
164
  messages: [{ role: "user", content: PROMPT }],
147
165
  max_tokens: 2000,
148
- // Use low reasoning effort for all models that support it — keeps latency
149
- // and cost down while still verifying the model responds successfully.
150
- // For OpenAI this maps to reasoning_effort="low", for Gemini to
151
- // thinkingLevel="low" or thinkingBudget=1024, etc.
152
- reasoning_effort: "low",
166
+ // Exercise the summary option enabled by Patcher. Provider clients must
167
+ // translate or remove this internal option rather than forwarding it.
168
+ reasoning_summary: true,
169
+ reasoning_effort: reasoningEffort,
153
170
  });
154
171
  const durationMs = Date.now() - startMs;
155
172
 
@@ -446,7 +446,7 @@ describe("TokenCompressor", () => {
446
446
  expect(result).toContain("Available keys:");
447
447
  });
448
448
 
449
- it("should auto-stitch a multi-chunk chain into the full content", () => {
449
+ it("should auto-stitch and paginate a multi-chunk chain", () => {
450
450
  // Force a multi-chunk compression then expand the first key.
451
451
  const content = "a".repeat(40000);
452
452
  const placeholder = tokenCompressor.compressStringInChunks(content);
@@ -456,9 +456,23 @@ describe("TokenCompressor", () => {
456
456
  const functions = toolsServiceCalls[0][0];
457
457
  const result = functions.expandTokens(firstKey);
458
458
 
459
- // Full content is returned in one call with no chunk-linking markers.
460
- expect(result).toBe(content);
459
+ expect(result.length).toBeLessThanOrEqual(20_000);
460
+ expect(result).toContain("Repeat with characterOffset=");
461
461
  expect(result).not.toContain("NEXT_CHUNK_KEY");
462
+
463
+ const pages = [result];
464
+ let offsetMatch = result.match(/characterOffset=(\d+)/);
465
+ while (offsetMatch) {
466
+ const page = functions.expandTokens(
467
+ firstKey,
468
+ undefined,
469
+ undefined,
470
+ Number(offsetMatch[1])
471
+ );
472
+ pages.push(page);
473
+ offsetMatch = page.match(/Repeat with characterOffset=(\d+)/);
474
+ }
475
+ expect(pages.reduce((sum, page) => sum + (page.match(/^a+/)?.[0].length || 0), 0)).toBe(content.length);
462
476
  });
463
477
 
464
478
  it("should support a ranged read with real line numbers", () => {
@@ -0,0 +1,68 @@
1
+ import {
2
+ executeJqQuery,
3
+ jqToolResponseDefinition,
4
+ } from "../../../src/processors/tools/jqToolResponse";
5
+ import {
6
+ executeTail,
7
+ tailToolResponseDefinition,
8
+ } from "../../../src/processors/tools/tailToolResponse";
9
+ import {
10
+ executeListStoredToolResponses,
11
+ listStoredToolResponsesDefinition,
12
+ } from "../../../src/processors/tools/listStoredToolResponses";
13
+
14
+ describe("bounded retrieval tools", () => {
15
+ it("bounds and paginates JQ output", async () => {
16
+ const data = JSON.stringify(Array.from({ length: 100 }, (_, i) => `value-${i}-${"x".repeat(30)}`));
17
+ const first = await executeJqQuery(data, "jq-id", ".", ["jq-id"], {}, { maxCharacters: 1000 });
18
+ expect(first.length).toBeLessThanOrEqual(1000);
19
+ expect(first).toContain("Repeat with characterOffset=");
20
+
21
+ const offset = Number(first.match(/characterOffset=(\d+)/)?.[1]);
22
+ const next = await executeJqQuery(data, "jq-id", ".", ["jq-id"], {}, {
23
+ characterOffset: offset,
24
+ maxCharacters: 1000,
25
+ });
26
+ expect(next).not.toBe(first);
27
+ });
28
+
29
+ it("bounds tail line lengths and supports backward pagination", async () => {
30
+ const data = ["first", `second ${"a".repeat(5000)}`, "third"].join("\n");
31
+ const result = await executeTail(data, "tail-id", ["tail-id"], {
32
+ lines: 1,
33
+ endLine: 2,
34
+ maxLineCharacters: 1000,
35
+ maxCharacters: 2000,
36
+ });
37
+ expect(result.length).toBeLessThanOrEqual(2000);
38
+ expect(result).toContain("Earlier lines available");
39
+ expect(result).toContain("lineCharacterOffset=1000");
40
+ });
41
+
42
+ it("paginates stored response results", async () => {
43
+ const storage = { a: "one", b: "two", c: "three" };
44
+ const metadata = {
45
+ a: { toolCallId: "a", originalLength: 3, storedAt: 1 },
46
+ b: { toolCallId: "b", originalLength: 3, storedAt: 2 },
47
+ c: { toolCallId: "c", originalLength: 5, storedAt: 3 },
48
+ };
49
+ const result = await executeListStoredToolResponses(storage, metadata, {}, {
50
+ maxResults: 1,
51
+ resultOffset: 1,
52
+ });
53
+ expect(result).toContain("Tool Call ID: b");
54
+ expect(result).not.toContain("Tool Call ID: c");
55
+ expect(result).toContain("resultOffset=2");
56
+ });
57
+
58
+ it("exposes pagination controls in each tool schema", () => {
59
+ const jqOptions = (jqToolResponseDefinition.function.parameters.properties as any).options;
60
+ const tailOptions = (tailToolResponseDefinition.function.parameters.properties as any).options;
61
+ const listOptions = (listStoredToolResponsesDefinition.function.parameters.properties as any).options;
62
+ expect(jqOptions.properties.characterOffset).toBeDefined();
63
+ expect(tailOptions.properties.endLine).toBeDefined();
64
+ expect(tailOptions.properties.maxCharacters).toBeDefined();
65
+ expect(listOptions.properties.resultOffset).toBeDefined();
66
+ expect(listOptions.properties.maxCharacters).toBeDefined();
67
+ });
68
+ });
@@ -1,4 +1,7 @@
1
- import { executeGrep } from "../../../src/processors/tools/grepToolResponse";
1
+ import {
2
+ executeGrep,
3
+ grepToolResponseDefinition,
4
+ } from "../../../src/processors/tools/grepToolResponse";
2
5
 
3
6
  /**
4
7
  * Verifies grepToolResponse operates on the decompressed/plain stored content
@@ -69,4 +72,88 @@ describe("executeGrep", () => {
69
72
 
70
73
  expect(result).toContain("No matches found");
71
74
  });
75
+
76
+ it("bounds the total response and advertises the next result page", async () => {
77
+ const manyMatches = Array.from(
78
+ { length: 20 },
79
+ (_, index) => `match ${index} ${"x".repeat(200)}`
80
+ ).join("\n");
81
+
82
+ const result = await executeGrep(
83
+ manyMatches,
84
+ toolCallId,
85
+ "match",
86
+ [toolCallId],
87
+ { maxCharacters: 1000, maxResults: 20 }
88
+ );
89
+
90
+ expect(result.length).toBeLessThanOrEqual(1000);
91
+ expect(result).toContain("More matches available");
92
+ expect(result).toMatch(/resultOffset=\d+/);
93
+ });
94
+
95
+ it("keeps a large log grep under the default response limit", async () => {
96
+ const logMatches = Array.from(
97
+ { length: 500 },
98
+ (_, index) => `${index}: ${JSON.stringify({ level: 30, message: "sandbox runner", detail: "x".repeat(500) })}`
99
+ ).join("\n");
100
+
101
+ const result = await executeGrep(
102
+ logMatches,
103
+ toolCallId,
104
+ "runner|sandbox",
105
+ [toolCallId]
106
+ );
107
+
108
+ expect(result.length).toBeLessThanOrEqual(20_000);
109
+ expect(result).toContain("More matches available");
110
+ });
111
+
112
+ it("paginates matches using resultOffset", async () => {
113
+ const result = await executeGrep(
114
+ ["match zero", "match one", "match two"].join("\n"),
115
+ toolCallId,
116
+ "match",
117
+ [toolCallId],
118
+ { maxResults: 1, resultOffset: 1 }
119
+ );
120
+
121
+ expect(result).not.toContain("match zero");
122
+ expect(result).toContain("match one");
123
+ expect(result).not.toContain("match two");
124
+ expect(result).toContain("resultOffset=2");
125
+ });
126
+
127
+ it("slices very long matching lines and lets callers page within the line", async () => {
128
+ const longLine = `prefix-${"a".repeat(5000)}-suffix`;
129
+ const firstPage = await executeGrep(
130
+ longLine,
131
+ toolCallId,
132
+ "prefix",
133
+ [toolCallId]
134
+ );
135
+
136
+ expect(firstPage.length).toBeLessThan(10_000);
137
+ expect(firstPage).toContain("line 1 truncated");
138
+ expect(firstPage).toContain("lineCharacterOffset=4000");
139
+ expect(firstPage).not.toContain("-suffix");
140
+
141
+ const secondPage = await executeGrep(
142
+ longLine,
143
+ toolCallId,
144
+ "prefix",
145
+ [toolCallId],
146
+ { lineCharacterOffset: 4000 }
147
+ );
148
+
149
+ expect(secondPage).toContain("-suffix");
150
+ expect(secondPage).toContain("End of matches");
151
+ });
152
+
153
+ it("exposes pagination controls in the tool schema", () => {
154
+ const options = (grepToolResponseDefinition.function.parameters.properties as any).options;
155
+ expect(options.properties.resultOffset).toBeDefined();
156
+ expect(options.properties.lineCharacterOffset).toBeDefined();
157
+ expect(options.properties.maxCharacters).toBeDefined();
158
+ });
72
159
  });
@@ -0,0 +1,53 @@
1
+ import { rollbackAgentInteractions } from "../../../src/agents/historyRollback";
2
+ import { Message } from "../../../src/clients/types";
3
+
4
+ const message = (role: Message["role"], content: string): Message => ({ role, content });
5
+
6
+ describe("rollbackAgentInteractions", () => {
7
+ const threads: Message[][] = [[
8
+ message("user", "request"),
9
+ message("assistant", "first"),
10
+ message("tool", "first result"),
11
+ message("assistant", "second"),
12
+ message("tool", "second result"),
13
+ ]];
14
+
15
+ it("clones history without changing it when rollback is zero", () => {
16
+ const result = rollbackAgentInteractions(threads, 0);
17
+ expect(result).toEqual(threads);
18
+ expect(result).not.toBe(threads);
19
+ expect(result[0]).not.toBe(threads[0]);
20
+ expect(result[0][0]).not.toBe(threads[0][0]);
21
+ });
22
+
23
+ it("removes the newest assistant and its following tool results", () => {
24
+ expect(rollbackAgentInteractions(threads, 1)).toEqual([threads[0].slice(0, 3)]);
25
+ });
26
+
27
+ it("can remove multiple interactions", () => {
28
+ expect(rollbackAgentInteractions(threads, 2)).toEqual([[threads[0][0]]]);
29
+ });
30
+
31
+ it("removes a trailing assistant without tools", () => {
32
+ const input = [[message("user", "request"), message("assistant", "tail")]];
33
+ expect(rollbackAgentInteractions(input, 1)).toEqual([[input[0][0]]]);
34
+ });
35
+
36
+ it.each([-1, 1.5])("rejects invalid rollback %p", (rollback) => {
37
+ expect(() => rollbackAgentInteractions(threads, rollback)).toThrow(
38
+ "rollback must be a non-negative integer"
39
+ );
40
+ });
41
+
42
+ it("rejects rollback beyond available interactions", () => {
43
+ expect(() => rollbackAgentInteractions(threads, 3)).toThrow(
44
+ "only has 2 assistant interaction(s)"
45
+ );
46
+ });
47
+
48
+ it("does not mutate input history", () => {
49
+ const before = JSON.stringify(threads);
50
+ rollbackAgentInteractions(threads, 1);
51
+ expect(JSON.stringify(threads)).toBe(before);
52
+ });
53
+ });
@@ -0,0 +1,102 @@
1
+ const post = jest.fn();
2
+ const publicKeyBase64 = Buffer.alloc(32, 7).toString("base64");
3
+ const keyPairExists = jest.fn(() => true);
4
+ const loadKeyPair = jest.fn(() => ({ publicKeyBase64 }));
5
+ const signMessage = jest.fn(() => "server-message-signature");
6
+
7
+ jest.mock("../../../src/utils/http", () => ({
8
+ __esModule: true,
9
+ default: {
10
+ post,
11
+ isHttpError: (error: unknown) =>
12
+ typeof error === "object" && error !== null && "status" in error,
13
+ },
14
+ }));
15
+
16
+ jest.mock("../../../src/auth/keyManager", () => ({
17
+ keyPairExists,
18
+ loadKeyPair,
19
+ signMessage,
20
+ }));
21
+
22
+ jest.mock("../../../src/auth/browserLogin", () => ({
23
+ getCliUserAgent: () => "test-agent",
24
+ }));
25
+
26
+ import {
27
+ authenticateWithKey,
28
+ exchangePublicKeyJwt,
29
+ hasKeyPair,
30
+ registerPublicKey,
31
+ } from "../../../src/auth/keyAuth";
32
+
33
+ describe("keyAuth", () => {
34
+ beforeEach(() => {
35
+ jest.clearAllMocks();
36
+ keyPairExists.mockReturnValue(true);
37
+ });
38
+
39
+ it("signs the exact server challenge for the selected key, org, and environment", async () => {
40
+ post
41
+ .mockResolvedValueOnce({
42
+ data: {
43
+ challengeId: "challenge_1",
44
+ message: "exact message from server",
45
+ expiresAt: "2099-01-01T00:00:00.000Z",
46
+ },
47
+ status: 201,
48
+ })
49
+ .mockResolvedValueOnce({ data: { jwt: "renewed-jwt" }, status: 200 });
50
+
51
+ expect(hasKeyPair("/keys/work")).toBe(true);
52
+ await expect(
53
+ exchangePublicKeyJwt("org-1", "https://work.example", "/keys/work")
54
+ ).resolves.toBe("renewed-jwt");
55
+
56
+ expect(loadKeyPair).toHaveBeenCalledWith("/keys/work");
57
+ expect(signMessage).toHaveBeenCalledWith("exact message from server", "/keys/work");
58
+ expect(post.mock.calls).toEqual([
59
+ [
60
+ "https://work.example/api/public-keys/challenge",
61
+ { publicKey: publicKeyBase64, orgId: "org-1" },
62
+ { headers: { "User-Agent": "test-agent" } },
63
+ ],
64
+ [
65
+ "https://work.example/api/public-keys/authenticate",
66
+ { challengeId: "challenge_1", signature: "server-message-signature" },
67
+ { headers: { "User-Agent": "test-agent" } },
68
+ ],
69
+ ]);
70
+ });
71
+
72
+ it("falls back when the key is unavailable without signing a challenge", async () => {
73
+ post.mockRejectedValueOnce({ status: 401 });
74
+
75
+ await expect(
76
+ authenticateWithKey("org-1", "https://work.example", "/keys/work")
77
+ ).resolves.toBe(false);
78
+
79
+ expect(post).toHaveBeenCalledTimes(1);
80
+ expect(signMessage).not.toHaveBeenCalled();
81
+ });
82
+
83
+ it("registers only server-owned Ed25519 key fields in the selected environment", async () => {
84
+ post.mockResolvedValueOnce({ data: {}, status: 201 });
85
+
86
+ await registerPublicKey("browser-jwt", publicKeyBase64, "https://dev.example");
87
+
88
+ expect(post).toHaveBeenCalledWith(
89
+ "https://dev.example/api/public-keys/register",
90
+ {
91
+ publicKey: publicKeyBase64,
92
+ label: expect.any(String),
93
+ },
94
+ {
95
+ headers: {
96
+ Authorization: "Bearer browser-jwt",
97
+ "User-Agent": "test-agent",
98
+ },
99
+ }
100
+ );
101
+ });
102
+ });
@@ -0,0 +1,67 @@
1
+ import crypto from "crypto";
2
+ import fs from "fs";
3
+ import os from "os";
4
+ import path from "path";
5
+ import {
6
+ generateKeyPair,
7
+ loadKeyPair,
8
+ signMessage,
9
+ } from "../../../src/auth/keyManager";
10
+
11
+ describe("keyManager", () => {
12
+ let directory: string;
13
+ let identity: string;
14
+
15
+ beforeEach(() => {
16
+ directory = fs.mkdtempSync(path.join(os.tmpdir(), "knowhow-key-manager-"));
17
+ identity = path.join(directory, "identity");
18
+ });
19
+
20
+ afterEach(() => {
21
+ fs.rmSync(directory, { recursive: true, force: true });
22
+ });
23
+
24
+ it("creates an Ed25519 identity with an OpenSSH-compatible public key", () => {
25
+ const generated = generateKeyPair(identity);
26
+ const publicKeyLine = fs.readFileSync(`${identity}.pub`, "utf8").trim();
27
+
28
+ expect(publicKeyLine).toMatch(/^ssh-ed25519 [A-Za-z0-9+/]+=* knowhow-cli$/);
29
+ expect(fs.statSync(identity).mode % 0o1000).toBe(0o600);
30
+ expect(loadKeyPair(identity)).toEqual(generated);
31
+
32
+ const challengeMessage = "exact server-provided challenge";
33
+ const signature = signMessage(challengeMessage, identity);
34
+ const publicKey = crypto.createPublicKey(fs.readFileSync(identity));
35
+ expect(
36
+ crypto.verify(
37
+ null,
38
+ Buffer.from(challengeMessage),
39
+ publicKey,
40
+ Buffer.from(signature, "base64")
41
+ )
42
+ ).toBe(true);
43
+ });
44
+
45
+ it("loads legacy raw-base64 public keys but rejects non-Ed25519 identities", () => {
46
+ const generated = generateKeyPair(identity);
47
+ fs.writeFileSync(`${identity}.pub`, generated.publicKeyBase64);
48
+ expect(loadKeyPair(identity).publicKeyBase64).toBe(generated.publicKeyBase64);
49
+
50
+ const rsaIdentity = path.join(directory, "rsa");
51
+ const rsa = crypto.generateKeyPairSync("rsa", {
52
+ modulusLength: 2048,
53
+ privateKeyEncoding: { type: "pkcs8", format: "pem" },
54
+ publicKeyEncoding: { type: "spki", format: "pem" },
55
+ });
56
+ fs.writeFileSync(rsaIdentity, rsa.privateKey);
57
+ fs.writeFileSync(`${rsaIdentity}.pub`, generated.publicKeyBase64);
58
+
59
+ expect(() => loadKeyPair(rsaIdentity)).toThrow("must be an Ed25519 private key");
60
+ });
61
+
62
+ it("does not overwrite a partial identity", () => {
63
+ fs.writeFileSync(identity, "existing key");
64
+ expect(() => generateKeyPair(identity)).toThrow("a key file already exists");
65
+ expect(fs.readFileSync(identity, "utf8")).toBe("existing key");
66
+ });
67
+ });
@@ -0,0 +1,37 @@
1
+ import { parseHistoryCommandArgs } from "../../../src/chat/modules/SessionsModule";
2
+
3
+ describe("parseHistoryCommandArgs", () => {
4
+ it("parses an id and rollback in either order", () => {
5
+ expect(parseHistoryCommandArgs(["task-1", "--rollback", "2"])).toEqual({
6
+ id: "task-1", rollback: 2, showAll: false,
7
+ });
8
+ expect(parseHistoryCommandArgs(["--rollback", "1", "task-1"])).toEqual({
9
+ id: "task-1", rollback: 1, showAll: false,
10
+ });
11
+ });
12
+
13
+ it("supports interactive --all", () => {
14
+ expect(parseHistoryCommandArgs(["--all"])).toEqual({
15
+ id: undefined, rollback: 0, showAll: true,
16
+ });
17
+ });
18
+
19
+ it.each([
20
+ [["--rollback"]],
21
+ [["--rollback", "-1"]],
22
+ [["--rollback", "1.5"]],
23
+ ])(
24
+ "rejects invalid rollback arguments: %p",
25
+ (args) => {
26
+ expect(() => parseHistoryCommandArgs(args)).toThrow(
27
+ "--rollback must be a non-negative integer"
28
+ );
29
+ }
30
+ );
31
+
32
+ it("rejects unexpected arguments", () => {
33
+ expect(() => parseHistoryCommandArgs(["one", "two"])).toThrow(
34
+ "Unexpected argument: two"
35
+ );
36
+ });
37
+ });