@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
@@ -86,7 +86,7 @@ knowhow chat
86
86
  ---
87
87
 
88
88
  ### `knowhow agent`
89
- **Purpose:** Run a one-shot agent task directly from the CLI (default agent: `Patcher`). Can also resume a previously started task.
89
+ **Purpose:** Start a new one-shot agent task directly from the CLI (default agent: `Patcher`).
90
90
 
91
91
  **Usage:**
92
92
  ```bash
@@ -114,8 +114,6 @@ knowhow agent [options]
114
114
  Custom prompt template file with a `{text}` placeholder
115
115
  - `--input <text>`
116
116
  Task input (fallback to stdin if not provided)
117
- - `--resume`
118
- Resume a previously started task using `--task-id` (local FS or remote)
119
117
  - `--renderer <name>`
120
118
  Renderer to use: `basic`, `compact`, `fancy`, or a path/package
121
119
  (default: from config or `basic`)
@@ -136,14 +134,45 @@ Use a custom prompt template file:
136
134
  knowhow agent --prompt-file .knowhow/prompts/BasicCodeDocumenter.mdx --input "src/index.ts"
137
135
  ```
138
136
 
139
- Resume a task:
137
+ Choose provider + renderer:
140
138
  ```bash
141
- knowhow agent --resume --task-id 123 --message-id 456 --input "Continue where you left off"
139
+ knowhow agent --provider openai --model gpt-4.1-mini --renderer fancy --input "Fix the failing tests"
142
140
  ```
143
141
 
144
- Choose provider + renderer:
142
+ ---
143
+
144
+ ### `knowhow agents resume` / `knowhow agents fork`
145
+ **Purpose:** Continue an existing agent task, or branch its history into a new task without modifying the source.
146
+
147
+ **Usage:**
145
148
  ```bash
146
- knowhow agent --provider openai --model gpt-4.1-mini --renderer fancy --input "Fix the failing tests"
149
+ knowhow agents resume [taskId] [options]
150
+ knowhow agents fork [sourceTaskId] [options]
151
+ ```
152
+
153
+ Both commands accept a full or partial task ID. Use `-i <number>` instead to select the corresponding row from `knowhow agents list`.
154
+
155
+ #### Options
156
+ - `-i, --index <number>` — Select a task by row number from `knowhow agents list`
157
+ - `--rollback <count>` — Discard the newest N agent interactions before continuing (default: `0`)
158
+ - `--input <text>` — Message to send after restoring history
159
+ - `--message-id <messageId>` — Knowhow message ID for remote task lookup
160
+ - `--sync-fs` — Enable filesystem-based synchronization
161
+ - `--provider <provider>` — Override the saved AI provider
162
+ - `--model <model>` — Override the saved model
163
+ - `--agent-name <name>` — Override the saved agent
164
+ - `--reasoning-effort <effort>` — Override the saved reasoning effort
165
+ - `--renderer <name>` — Renderer to use (default: from config or `basic`)
166
+ - `--task-id <taskId>` — Explicit destination task ID (`fork` only)
167
+
168
+ Resume the first listed task after removing its newest assistant interaction:
169
+ ```bash
170
+ knowhow agents resume -i 1 --rollback 1
171
+ ```
172
+
173
+ Fork the third listed task into an explicit destination task ID:
174
+ ```bash
175
+ knowhow agents fork -i 3 --rollback 1 --task-id my-new-task
147
176
  ```
148
177
 
149
178
  ---
package/bin/knowhow.js CHANGED
@@ -1,12 +1,14 @@
1
- #!/usr/bin/env node --no-node-snapshot
1
+ #!/usr/bin/env node
2
2
 
3
3
  // knowhow CLI entry point.
4
4
  //
5
- // --no-node-snapshot is required for isolated-vm (used by executeScript).
6
- // It is declared in the shebang above so that Node starts with it directly —
7
- // no re-exec needed. This means the process stays as a direct child of the
8
- // terminal (e.g. Ghostty) and inherits its macOS TCC (Screen Recording /
9
- // Accessibility) permissions without needing to grant them to node itself.
5
+ // --no-node-snapshot is NOT needed here. The script module (knowhow-module-script)
6
+ // forks a child process with --no-node-snapshot only when executeScript is actually
7
+ // called. This means:
8
+ // - The shebang works on Linux (GNU env) and macOS without any argument-splitting tricks.
9
+ // - Ghostty (or whichever terminal launched knowhow) remains the TCC subject for
10
+ // macOS Screen Recording / Accessibility permissions — computer-use features work
11
+ // without having to grant those permissions to the node binary itself.
10
12
 
11
13
  const path = require("node:path");
12
14
  const cliJs = path.join(__dirname, "../ts_build/src/cli.js");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tyvm/knowhow",
3
- "version": "0.0.145",
3
+ "version": "0.0.147",
4
4
  "description": "ai cli with plugins and agents",
5
5
  "main": "ts_build/src/index.js",
6
6
  "bin": {
@@ -22,6 +22,7 @@ import { Models } from "../../ai";
22
22
  import { MessageProcessor } from "../../services/MessageProcessor";
23
23
  import { DEFAULT_CONTEXT_LIMIT } from "../../clients/contextLimits";
24
24
  import { Marked } from "../../utils";
25
+ import { TraceAll } from "../../util/Trace";
25
26
 
26
27
  export { Message, Tool, ToolCall };
27
28
  export interface ModelPreference {
@@ -83,6 +84,7 @@ interface ActiveObservation {
83
84
  expiry?: NodeJS.Timeout;
84
85
  }
85
86
 
87
+ @TraceAll()
86
88
  export abstract class BaseAgent implements IAgent {
87
89
  abstract name: string;
88
90
  abstract description: string;
@@ -471,6 +473,28 @@ export abstract class BaseAgent implements IAgent {
471
473
  return this.getEnabledTools().map((t) => t.function.name);
472
474
  }
473
475
 
476
+ /** Restore the exact request-visible tool list persisted for a prior run. */
477
+ restoreEnabledTools(names: string[]) {
478
+ const uniqueNames = [...new Set(names)];
479
+ const lazyTools = this.tools as ToolsService & {
480
+ enableTools?: (patterns: string[]) => unknown;
481
+ restoreEnabledTools?: (names: string[]) => void;
482
+ };
483
+
484
+ // LazyToolsService only exposes explicitly enabled tools. Materialize the
485
+ // saved concrete names before applying the agent-level exact-set filter.
486
+ if (lazyTools.restoreEnabledTools) {
487
+ lazyTools.restoreEnabledTools(uniqueNames);
488
+ } else {
489
+ lazyTools.enableTools?.(uniqueNames);
490
+ }
491
+ const savedNames = new Set(uniqueNames);
492
+ this.disabledTools = this.tools
493
+ .getTools()
494
+ .map((tool) => tool.function.name)
495
+ .filter((name) => !savedNames.has(name));
496
+ }
497
+
474
498
  disableTool(toolName: string) {
475
499
  this.disabledTools.push(toolName);
476
500
  }
@@ -0,0 +1,36 @@
1
+ import { Message } from "../clients/types";
2
+
3
+ /**
4
+ * Remove the newest agent interactions from the final persisted thread.
5
+ *
6
+ * An interaction starts with an assistant message and includes every following
7
+ * tool result up to (but not including) the next assistant message. Cutting at
8
+ * the assistant boundary keeps tool_call/tool-result pairs valid and makes
9
+ * `rollback: 1` useful for recovering from a failed final tool-use turn.
10
+ */
11
+ export function rollbackAgentInteractions(
12
+ threads: Message[][],
13
+ rollback: number = 0
14
+ ): Message[][] {
15
+ if (!Number.isInteger(rollback) || rollback < 0) {
16
+ throw new Error("rollback must be a non-negative integer");
17
+ }
18
+
19
+ const cloned = threads.map((thread) => thread.map((message) => ({ ...message })));
20
+ if (rollback === 0 || cloned.length === 0) return cloned;
21
+
22
+ const lastThread = cloned[cloned.length - 1];
23
+ const assistantIndexes = lastThread
24
+ .map((message, index) => (message.role === "assistant" ? index : -1))
25
+ .filter((index) => index >= 0);
26
+
27
+ if (rollback > assistantIndexes.length) {
28
+ throw new Error(
29
+ `Cannot rollback ${rollback} interaction(s); the latest thread only has ${assistantIndexes.length} assistant interaction(s).`
30
+ );
31
+ }
32
+
33
+ const cutIndex = assistantIndexes[assistantIndexes.length - rollback];
34
+ cloned[cloned.length - 1] = lastThread.slice(0, cutIndex);
35
+ return cloned;
36
+ }
@@ -8,6 +8,8 @@ export interface StartAgentTaskParams {
8
8
  syncFs?: boolean;
9
9
  taskId?: string;
10
10
  resume?: boolean;
11
+ forkTaskId?: string;
12
+ rollback?: number;
11
13
  prompt: string;
12
14
  /**
13
15
  * Push this agent's work to a remote Knowhow task identified by taskId.
@@ -104,6 +106,8 @@ export async function startAgentTask(params: StartAgentTaskParams): Promise<stri
104
106
  prompt,
105
107
  taskId: providedTaskId,
106
108
  resume,
109
+ forkTaskId,
110
+ rollback = 0,
107
111
  syncFs,
108
112
  syncRemote,
109
113
  provider,
@@ -118,6 +122,18 @@ export async function startAgentTask(params: StartAgentTaskParams): Promise<stri
118
122
  if (!prompt) {
119
123
  throw new Error("prompt is required to create a chat task");
120
124
  }
125
+ if (resume && forkTaskId) {
126
+ throw new Error("resume and forkTaskId are mutually exclusive");
127
+ }
128
+ if (resume && !providedTaskId) {
129
+ throw new Error("taskId is required when resuming");
130
+ }
131
+ if (!Number.isInteger(rollback) || rollback < 0) {
132
+ throw new Error("rollback must be a non-negative integer");
133
+ }
134
+ if (rollback > 0 && !resume && !forkTaskId) {
135
+ throw new Error("rollback requires resume or forkTaskId");
136
+ }
121
137
 
122
138
  // Default filesystem synchronization ON unless the caller explicitly opts out
123
139
  // (syncFs: false) or is using a messageId-based web sync. This ensures spawned
@@ -134,7 +150,14 @@ export async function startAgentTask(params: StartAgentTaskParams): Promise<stri
134
150
  explicitParentTaskId ?? _ctx?.taskId ?? (_ctx?.caller as any)?.currentTaskId;
135
151
 
136
152
  // Build args array (no shell escaping needed - args are passed directly)
137
- const args: string[] = ["agent"];
153
+ const isHistoryRun = !!resume || !!forkTaskId;
154
+ const args: string[] = resume
155
+ ? ["agents", "resume", providedTaskId!]
156
+ : forkTaskId
157
+ ? ["agents", "fork", forkTaskId]
158
+ : ["agent"];
159
+
160
+ if (forkTaskId) args.push("--task-id", taskId);
138
161
 
139
162
  if (messageId) {
140
163
  args.push("--message-id", messageId);
@@ -144,11 +167,11 @@ export async function startAgentTask(params: StartAgentTaskParams): Promise<stri
144
167
  // When syncRemote is requested, pass it through so the spawned agent pushes
145
168
  // its work to the remote task identified by --task-id rather than staying
146
169
  // local-only.
147
- if (syncRemote) {
170
+ if (syncRemote && !isHistoryRun) {
148
171
  args.push("--sync-remote");
149
172
  }
150
173
 
151
- if (useSyncFs || providedTaskId) {
174
+ if (!isHistoryRun && (useSyncFs || providedTaskId)) {
152
175
  // Pass --task-id whenever we have a known taskId (syncFs or explicit taskId)
153
176
  args.push("--task-id", taskId);
154
177
  }
@@ -165,20 +188,19 @@ export async function startAgentTask(params: StartAgentTaskParams): Promise<stri
165
188
  args.push("--agent-name", agentName);
166
189
  }
167
190
 
168
- if (maxTimeLimit !== undefined) {
191
+ if (maxTimeLimit !== undefined && !isHistoryRun) {
169
192
  args.push("--max-time-limit", String(maxTimeLimit));
170
193
  }
171
194
 
172
- if (maxSpendLimit !== undefined) {
195
+ if (maxSpendLimit !== undefined && !isHistoryRun) {
173
196
  args.push("--max-spend-limit", String(maxSpendLimit));
174
197
  }
175
- if (parentTaskId) {
198
+ if (parentTaskId && !isHistoryRun) {
176
199
  // Tell the child who spawned it so it can report back to the parent.
177
200
  args.push("--parent-task-id", parentTaskId);
178
201
  }
179
- if (resume) {
180
- // --resume is a boolean flag; task ID is already passed via --task-id above
181
- args.push("--resume");
202
+ if (rollback > 0) {
203
+ args.push("--rollback", String(rollback));
182
204
  }
183
205
 
184
206
  const timeoutMs = maxTimeLimit ? maxTimeLimit * 60 * 1000 : 60 * 60 * 1000;
@@ -320,6 +342,16 @@ export const startAgentTaskDefinition: Tool = {
320
342
  description:
321
343
  "Resume a previously started task from where it left off. Must be used together with taskId which identifies the task to resume.",
322
344
  },
345
+ forkTaskId: {
346
+ type: "string",
347
+ description:
348
+ "Fork this existing task into a new task. The source remains unchanged; taskId optionally selects the new task ID.",
349
+ },
350
+ rollback: {
351
+ type: "number",
352
+ description:
353
+ "Discard this many newest agent interactions before resuming or forking. Must be a non-negative integer.",
354
+ },
323
355
  parentTaskId: {
324
356
  type: "string",
325
357
  description:
@@ -1,6 +1,7 @@
1
1
  import http from "../utils/http";
2
2
  import { exec } from "child_process";
3
3
  import { promisify } from "util";
4
+ import crypto from "crypto";
4
5
  import * as os from "os";
5
6
  import * as fs from "fs";
6
7
  import * as path from "path";
@@ -28,6 +29,8 @@ interface RetrieveTokenResponse {
28
29
 
29
30
  export class BrowserLoginService {
30
31
  private baseUrl: string;
32
+ /** PKCE verifier — generated once per login attempt, never sent in a URL */
33
+ private verifier: string = crypto.randomBytes(32).toString("hex");
31
34
 
32
35
  constructor(baseUrl: string = KNOWHOW_API_URL, private orgId?: string) {
33
36
  if (!baseUrl) {
@@ -100,9 +103,10 @@ export class BrowserLoginService {
100
103
  spinner.stop();
101
104
  spinner.start("Authentication successful! Retrieving token");
102
105
 
103
- // Step 4: Retrieve JWT token
106
+ // Step 4: Redeem session using PKCE verifier (never sent in URL)
104
107
  const tokenResponse = await http.post(
105
- `${this.baseUrl}/api/cli-login/session/${sessionData.sessionId}/token`
108
+ `${this.baseUrl}/api/cli-login/session/${sessionData.sessionId}/redeem`,
109
+ { verifier: this.verifier }
106
110
  );
107
111
 
108
112
  const tokenData = tokenResponse.data as RetrieveTokenResponse;
@@ -170,9 +174,11 @@ export class BrowserLoginService {
170
174
  */
171
175
  private async createSession(): Promise<CreateSessionResponse> {
172
176
  try {
177
+ // Compute the PKCE challenge (sha256 of verifier) to store server-side
178
+ const challenge = crypto.createHash("sha256").update(this.verifier, "utf8").digest("hex");
173
179
  const response = await http.post<CreateSessionResponse>(
174
180
  `${this.baseUrl}/api/cli-login/session`,
175
- {},
181
+ { challenge },
176
182
  { headers: { "User-Agent": getCliUserAgent() } }
177
183
  );
178
184
  return response.data;
@@ -0,0 +1,115 @@
1
+ import fs from "fs";
2
+ import os from "os";
3
+ import path from "path";
4
+ import http from "../utils/http";
5
+ import { getCliUserAgent } from "./browserLogin";
6
+ import { keyPairExists, loadKeyPair, signMessage } from "./keyManager";
7
+
8
+ const JWT_FILE_PATH = path.join(process.cwd(), ".knowhow", ".jwt");
9
+ const DEFAULT_API_URL = process.env.KNOWHOW_API_URL || "https://api.knowhow.tyvm.ai";
10
+
11
+ /**
12
+ * This is intentionally based only on the selected identity. Registration is
13
+ * server-side and scoped to its environment/user/org; a local `.registered`
14
+ * marker cannot safely represent that state. An unknown or revoked key is
15
+ * cheaply rejected by the selected API and login then falls back to PKCE.
16
+ */
17
+ export function hasKeyPair(privateKeyPath?: string): boolean {
18
+ return keyPairExists(privateKeyPath);
19
+ }
20
+
21
+ /** Exchange a registered Ed25519 identity for a new short-lived JWT. */
22
+ export async function exchangePublicKeyJwt(
23
+ orgId: string,
24
+ apiUrl: string = DEFAULT_API_URL,
25
+ privateKeyPath?: string
26
+ ): Promise<string> {
27
+ if (!keyPairExists(privateKeyPath)) {
28
+ throw new Error("CLI identity is unavailable");
29
+ }
30
+
31
+ const keyPair = loadKeyPair(privateKeyPath);
32
+ const headers = { "User-Agent": getCliUserAgent() };
33
+ const challengeResponse = await http.post<{
34
+ challengeId: string;
35
+ message: string;
36
+ expiresAt: string;
37
+ }>(
38
+ `${apiUrl}/api/public-keys/challenge`,
39
+ { publicKey: keyPair.publicKeyBase64, orgId },
40
+ { headers }
41
+ );
42
+ const { challengeId, message } = challengeResponse.data;
43
+ if (
44
+ typeof challengeId !== "string" ||
45
+ challengeId.length === 0 ||
46
+ challengeId.length > 128 ||
47
+ !/^[A-Za-z0-9_-]+$/.test(challengeId) ||
48
+ typeof message !== "string" ||
49
+ message.length === 0
50
+ ) {
51
+ throw new Error("Public-key challenge response is invalid");
52
+ }
53
+
54
+ const response = await http.post<{ jwt: string }>(
55
+ `${apiUrl}/api/public-keys/authenticate`,
56
+ { challengeId, signature: signMessage(message, privateKeyPath) },
57
+ { headers }
58
+ );
59
+ if (!response.data.jwt) {
60
+ throw new Error("Public-key authentication returned no JWT");
61
+ }
62
+ return response.data.jwt;
63
+ }
64
+
65
+ export async function authenticateWithKey(
66
+ orgId: string,
67
+ apiUrl: string = DEFAULT_API_URL,
68
+ privateKeyPath?: string
69
+ ): Promise<boolean> {
70
+ if (!keyPairExists(privateKeyPath)) return false;
71
+
72
+ try {
73
+ storeJwt(await exchangePublicKeyJwt(orgId, apiUrl, privateKeyPath));
74
+ return true;
75
+ } catch (error: unknown) {
76
+ // The identity is not registered for this environment/org (or was revoked).
77
+ if (http.isHttpError(error) && (error.status === 401 || error.status === 404)) {
78
+ return false;
79
+ }
80
+ throw error;
81
+ }
82
+ }
83
+
84
+ /** Register an Ed25519 CLI-auth key using the JWT obtained by browser PKCE. */
85
+ export async function registerPublicKey(
86
+ jwt: string,
87
+ publicKeyBase64: string,
88
+ apiUrl: string = DEFAULT_API_URL
89
+ ): Promise<void> {
90
+ const rawPublicKey = Buffer.from(publicKeyBase64, "base64");
91
+ if (rawPublicKey.length !== 32 || rawPublicKey.toString("base64") !== publicKeyBase64) {
92
+ throw new Error("CLI authentication requires a 32-byte Ed25519 public key");
93
+ }
94
+
95
+ await http.post(
96
+ `${apiUrl}/api/public-keys/register`,
97
+ {
98
+ publicKey: publicKeyBase64,
99
+ label: os.hostname(),
100
+ },
101
+ {
102
+ headers: {
103
+ Authorization: `Bearer ${jwt}`,
104
+ "User-Agent": getCliUserAgent(),
105
+ },
106
+ }
107
+ );
108
+ }
109
+
110
+ export function storeJwt(jwt: string): void {
111
+ const directory = path.dirname(JWT_FILE_PATH);
112
+ fs.mkdirSync(directory, { recursive: true });
113
+ fs.writeFileSync(JWT_FILE_PATH, jwt, { mode: 0o600 });
114
+ fs.chmodSync(JWT_FILE_PATH, 0o600);
115
+ }
@@ -0,0 +1,166 @@
1
+ import crypto from "crypto";
2
+ import fs from "fs";
3
+ import os from "os";
4
+ import path from "path";
5
+
6
+ const ED25519_KEY_TYPE = "ssh-ed25519";
7
+ const ED25519_RAW_KEY_LENGTH = 32;
8
+ const ED25519_SPKI_PREFIX = Buffer.from("302a300506032b6570032100", "hex");
9
+
10
+ export interface KeyPair {
11
+ privateKeyPath: string;
12
+ publicKeyPath: string;
13
+ /** Raw 32-byte Ed25519 key, base64 encoded for the authentication API. */
14
+ publicKeyBase64: string;
15
+ /** OpenSSH SHA-256 fingerprint (hashes the SSH wire-format public key). */
16
+ fingerprint: string;
17
+ }
18
+
19
+ export function getKeyDir(): string {
20
+ return path.join(os.homedir(), ".knowhow", "keys");
21
+ }
22
+
23
+ export function getDefaultPrivateKeyPath(): string {
24
+ return path.join(getKeyDir(), "id_ed25519");
25
+ }
26
+
27
+ export function getDefaultPublicKeyPath(): string {
28
+ return `${getDefaultPrivateKeyPath()}.pub`;
29
+ }
30
+
31
+ export function keyPairExists(privateKeyPath?: string): boolean {
32
+ const privatePath = privateKeyPath ?? getDefaultPrivateKeyPath();
33
+ return fs.existsSync(privatePath) && fs.existsSync(`${privatePath}.pub`);
34
+ }
35
+
36
+ /** Generate a CLI-only Ed25519 key and an OpenSSH-compatible public-key file. */
37
+ export function generateKeyPair(privateKeyPath?: string): KeyPair {
38
+ const privatePath = privateKeyPath ?? getDefaultPrivateKeyPath();
39
+ const publicPath = `${privatePath}.pub`;
40
+
41
+ fs.mkdirSync(path.dirname(privatePath), { recursive: true, mode: 0o700 });
42
+ const { privateKey, publicKey } = crypto.generateKeyPairSync("ed25519", {
43
+ privateKeyEncoding: { type: "pkcs8", format: "pem" },
44
+ publicKeyEncoding: { type: "spki", format: "der" },
45
+ });
46
+ const rawPublicKey = extractRawPublicKey(publicKey);
47
+
48
+ // Refuse to overwrite either half of an identity. Partial identities should be
49
+ // repaired explicitly rather than silently replacing a user's key.
50
+ if (fs.existsSync(privatePath) || fs.existsSync(publicPath)) {
51
+ throw new Error(`Cannot generate identity: a key file already exists at ${privatePath} or ${publicPath}`);
52
+ }
53
+
54
+ fs.writeFileSync(privatePath, privateKey, { mode: 0o600, flag: "wx" });
55
+ try {
56
+ fs.writeFileSync(publicPath, formatOpenSshPublicKey(rawPublicKey), {
57
+ mode: 0o644,
58
+ flag: "wx",
59
+ });
60
+ } catch (error) {
61
+ fs.rmSync(privatePath, { force: true });
62
+ throw error;
63
+ }
64
+
65
+ return keyPairMetadata(privatePath, rawPublicKey);
66
+ }
67
+
68
+ /** Load and validate an Ed25519 identity. Legacy raw-base64 .pub files remain supported. */
69
+ export function loadKeyPair(privateKeyPath?: string): KeyPair {
70
+ const privatePath = privateKeyPath ?? getDefaultPrivateKeyPath();
71
+ const publicPath = `${privatePath}.pub`;
72
+ if (!fs.existsSync(privatePath)) throw new Error(`Private key not found at: ${privatePath}`);
73
+ if (!fs.existsSync(publicPath)) throw new Error(`Public key not found at: ${publicPath}`);
74
+
75
+ const privateKey = readEd25519PrivateKey(privatePath);
76
+ const publicKey = parsePublicKey(fs.readFileSync(publicPath, "utf8"));
77
+ const derivedPublicKey = extractRawPublicKey(
78
+ crypto.createPublicKey(privateKey).export({ type: "spki", format: "der" })
79
+ );
80
+ if (!crypto.timingSafeEqual(publicKey, derivedPublicKey)) {
81
+ throw new Error(`Public key does not match private key: ${publicPath}`);
82
+ }
83
+
84
+ return keyPairMetadata(privatePath, publicKey);
85
+ }
86
+
87
+ export function getOrCreatePublicKey(privateKeyPath?: string): KeyPair {
88
+ return keyPairExists(privateKeyPath)
89
+ ? loadKeyPair(privateKeyPath)
90
+ : generateKeyPair(privateKeyPath);
91
+ }
92
+
93
+ export function signMessage(message: string, privateKeyPath?: string): string {
94
+ const privatePath = privateKeyPath ?? getDefaultPrivateKeyPath();
95
+ if (!fs.existsSync(privatePath)) throw new Error(`Private key not found at: ${privatePath}`);
96
+ return crypto.sign(null, Buffer.from(message, "utf8"), readEd25519PrivateKey(privatePath)).toString("base64");
97
+ }
98
+
99
+ function readEd25519PrivateKey(privatePath: string): crypto.KeyObject {
100
+ let key: crypto.KeyObject;
101
+ try {
102
+ key = crypto.createPrivateKey(fs.readFileSync(privatePath));
103
+ } catch {
104
+ throw new Error(`Identity at ${privatePath} is not a supported PEM Ed25519 private key`);
105
+ }
106
+ if (key.asymmetricKeyType !== "ed25519") {
107
+ throw new Error(`Identity at ${privatePath} must be an Ed25519 private key`);
108
+ }
109
+ return key;
110
+ }
111
+
112
+ function extractRawPublicKey(spkiDer: Buffer): Buffer {
113
+ if (
114
+ spkiDer.length !== ED25519_SPKI_PREFIX.length + ED25519_RAW_KEY_LENGTH ||
115
+ !spkiDer.subarray(0, ED25519_SPKI_PREFIX.length).equals(ED25519_SPKI_PREFIX)
116
+ ) {
117
+ throw new Error("Invalid Ed25519 public key");
118
+ }
119
+ return spkiDer.subarray(ED25519_SPKI_PREFIX.length);
120
+ }
121
+
122
+ function sshPublicKeyBlob(rawPublicKey: Buffer): Buffer {
123
+ const type = Buffer.from(ED25519_KEY_TYPE, "ascii");
124
+ const typeLength = Buffer.alloc(4);
125
+ const keyLength = Buffer.alloc(4);
126
+ typeLength.writeUInt32BE(type.length);
127
+ keyLength.writeUInt32BE(rawPublicKey.length);
128
+ return Buffer.concat([typeLength, type, keyLength, rawPublicKey]);
129
+ }
130
+
131
+ function formatOpenSshPublicKey(rawPublicKey: Buffer): string {
132
+ return `${ED25519_KEY_TYPE} ${sshPublicKeyBlob(rawPublicKey).toString("base64")} knowhow-cli\n`;
133
+ }
134
+
135
+ function parsePublicKey(value: string): Buffer {
136
+ const trimmed = value.trim();
137
+ if (trimmed.startsWith(`${ED25519_KEY_TYPE} `)) {
138
+ const parts = trimmed.split(/\s+/);
139
+ const blob = Buffer.from(parts[1] ?? "", "base64");
140
+ const expected = sshPublicKeyBlob(blob.subarray(blob.length - ED25519_RAW_KEY_LENGTH));
141
+ if (blob.length !== expected.length || !blob.equals(expected)) {
142
+ throw new Error("Invalid OpenSSH Ed25519 public key");
143
+ }
144
+ return blob.subarray(blob.length - ED25519_RAW_KEY_LENGTH);
145
+ }
146
+
147
+ // Compatibility with key files written by early CLI builds.
148
+ if (!/^[A-Za-z0-9+/]+={0,2}$/.test(trimmed)) {
149
+ throw new Error("Public key must be an OpenSSH Ed25519 key");
150
+ }
151
+ const raw = Buffer.from(trimmed, "base64");
152
+ if (raw.length !== ED25519_RAW_KEY_LENGTH || raw.toString("base64") !== trimmed) {
153
+ throw new Error("Ed25519 public key must contain exactly 32 bytes");
154
+ }
155
+ return raw;
156
+ }
157
+
158
+ function keyPairMetadata(privatePath: string, rawPublicKey: Buffer): KeyPair {
159
+ const hash = crypto.createHash("sha256").update(sshPublicKeyBlob(rawPublicKey)).digest("base64").replace(/=+$/, "");
160
+ return {
161
+ privateKeyPath: privatePath,
162
+ publicKeyPath: `${privatePath}.pub`,
163
+ publicKeyBase64: rawPublicKey.toString("base64"),
164
+ fingerprint: `SHA256:${hash}`,
165
+ };
166
+ }
@@ -43,6 +43,22 @@ export class CliChatService implements ChatService {
43
43
  chatHistory: this.chatHistory,
44
44
  plugins,
45
45
  };
46
+ this.registerCommand({
47
+ name: "reload",
48
+ description: "Reload configuration, tools, MCP connections, and modules",
49
+ handler: async () => {
50
+ try {
51
+ console.log("Reloading configuration...");
52
+ const result = await services().RuntimeReload.reload();
53
+ console.log(
54
+ `✓ Reloaded ${result.tools} tools, ${result.mcps} MCP servers, and ${result.modules} modules`
55
+ );
56
+ } catch (error) {
57
+ const message = error instanceof Error ? error.message : String(error);
58
+ console.error(`⚠ Reload failed: ${message}`);
59
+ }
60
+ },
61
+ });
46
62
  this.loadInputHistory();
47
63
 
48
64
  // Set up callback to add entries to inputHistory immediately when user presses Enter