@tyvm/knowhow 0.0.146 → 0.0.148

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 (271) 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 +51 -23
  5. package/src/agents/historyRollback.ts +36 -0
  6. package/src/agents/tools/execCommand.ts +102 -320
  7. package/src/agents/tools/list.ts +2 -2
  8. package/src/agents/tools/startAgentTask.ts +91 -34
  9. package/src/ai.ts +4 -1
  10. package/src/auth/browserLogin.ts +9 -3
  11. package/src/auth/keyAuth.ts +115 -0
  12. package/src/auth/keyManager.ts +166 -0
  13. package/src/chat/CliChatService.ts +23 -2
  14. package/src/chat/modules/AgentModule.ts +114 -33
  15. package/src/chat/modules/AskModule.ts +2 -0
  16. package/src/chat/modules/BaseChatModule.ts +0 -1
  17. package/src/chat/modules/CustomCommandsModule.ts +2 -0
  18. package/src/chat/modules/InternalChatModule.ts +7 -0
  19. package/src/chat/modules/RemoteSyncModule.ts +4 -0
  20. package/src/chat/modules/RendererModule.ts +2 -0
  21. package/src/chat/modules/SearchModule.ts +2 -0
  22. package/src/chat/modules/SessionsModule.ts +103 -34
  23. package/src/chat/modules/SetupModule.ts +2 -0
  24. package/src/chat/modules/ShellCommandModule.ts +2 -0
  25. package/src/chat/modules/SystemModule.ts +2 -0
  26. package/src/chat/modules/ToolCommandModule.ts +102 -0
  27. package/src/chat/modules/index.ts +1 -0
  28. package/src/chat/types.ts +3 -1
  29. package/src/cli.ts +10 -3
  30. package/src/clients/http.ts +67 -7
  31. package/src/clients/index.ts +4 -0
  32. package/src/commands/agent.ts +1 -38
  33. package/src/commands/agents.ts +455 -60
  34. package/src/commands/misc.ts +3 -2
  35. package/src/commands/processes.ts +138 -0
  36. package/src/commands/services.ts +23 -1
  37. package/src/index.ts +1 -0
  38. package/src/login.ts +89 -28
  39. package/src/processes/ProcessManager.ts +95 -0
  40. package/src/processes/managedProcessShim.ts +115 -0
  41. package/src/services/AgentService.ts +2 -0
  42. package/src/services/AgentSyncFs.ts +77 -1
  43. package/src/services/AgentSyncKnowhowWeb.ts +22 -4
  44. package/src/services/BehaviorsService.ts +2 -0
  45. package/src/services/DockerService.ts +2 -0
  46. package/src/services/EmbeddingService.ts +2 -0
  47. package/src/services/EmbeddingsService.ts +3 -0
  48. package/src/services/ExtensionsService.ts +25 -0
  49. package/src/services/KnowhowClient.ts +68 -7
  50. package/src/services/LazyToolsService.ts +24 -20
  51. package/src/services/Mcp.ts +2 -0
  52. package/src/services/McpServer.ts +2 -0
  53. package/src/services/MessageProcessor.ts +2 -0
  54. package/src/services/MinimalToolsService.ts +2 -0
  55. package/src/services/SessionManager.ts +5 -0
  56. package/src/services/SkillsService.ts +2 -0
  57. package/src/services/SyncedAgentWatcher.ts +2 -0
  58. package/src/services/SyncerService.ts +23 -8
  59. package/src/services/TaskRegistry.ts +2 -0
  60. package/src/services/Tools.ts +12 -0
  61. package/src/services/index.ts +6 -0
  62. package/src/services/modules/index.ts +10 -1
  63. package/src/services/modules/types.ts +4 -0
  64. package/src/types.ts +2 -0
  65. package/src/util/Trace.ts +1 -1
  66. package/src/utils/http.ts +18 -2
  67. package/tests/unit/agents/historyRollback.test.ts +53 -0
  68. package/tests/unit/auth/keyAuth.test.ts +102 -0
  69. package/tests/unit/auth/keyManager.test.ts +67 -0
  70. package/tests/unit/chat/ToolCommandModule.test.ts +71 -0
  71. package/tests/unit/chat/historyCommandArgs.test.ts +37 -0
  72. package/tests/unit/commands/agents.test.ts +111 -0
  73. package/tests/unit/login.test.ts +119 -0
  74. package/tests/unit/services/AgentSyncKnowhowWeb.test.ts +38 -0
  75. package/tests/unit/services/LazyToolsService.test.ts +53 -0
  76. package/tests/unit/services/SessionManager.test.ts +43 -0
  77. package/tests/unit/services/SyncerService.test.ts +45 -0
  78. package/tests/unit/util/Trace.test.ts +119 -0
  79. package/tests/unit/utils/http.test.ts +43 -0
  80. package/ts_build/package.json +1 -1
  81. package/ts_build/src/agents/base/base.d.ts +2 -1
  82. package/ts_build/src/agents/base/base.js +47 -14
  83. package/ts_build/src/agents/base/base.js.map +1 -1
  84. package/ts_build/src/agents/historyRollback.d.ts +2 -0
  85. package/ts_build/src/agents/historyRollback.js +22 -0
  86. package/ts_build/src/agents/historyRollback.js.map +1 -0
  87. package/ts_build/src/agents/tools/execCommand.d.ts +4 -1
  88. package/ts_build/src/agents/tools/execCommand.js +100 -223
  89. package/ts_build/src/agents/tools/execCommand.js.map +1 -1
  90. package/ts_build/src/agents/tools/list.js +2 -2
  91. package/ts_build/src/agents/tools/list.js.map +1 -1
  92. package/ts_build/src/agents/tools/startAgentTask.d.ts +16 -1
  93. package/ts_build/src/agents/tools/startAgentTask.js +71 -28
  94. package/ts_build/src/agents/tools/startAgentTask.js.map +1 -1
  95. package/ts_build/src/ai.js +2 -1
  96. package/ts_build/src/ai.js.map +1 -1
  97. package/ts_build/src/auth/browserLogin.d.ts +1 -0
  98. package/ts_build/src/auth/browserLogin.js +5 -2
  99. package/ts_build/src/auth/browserLogin.js.map +1 -1
  100. package/ts_build/src/auth/keyAuth.d.ts +5 -0
  101. package/ts_build/src/auth/keyAuth.js +79 -0
  102. package/ts_build/src/auth/keyAuth.js.map +1 -0
  103. package/ts_build/src/auth/keyManager.d.ts +14 -0
  104. package/ts_build/src/auth/keyManager.js +145 -0
  105. package/ts_build/src/auth/keyManager.js.map +1 -0
  106. package/ts_build/src/chat/CliChatService.d.ts +6 -0
  107. package/ts_build/src/chat/CliChatService.js +17 -2
  108. package/ts_build/src/chat/CliChatService.js.map +1 -1
  109. package/ts_build/src/chat/modules/AgentModule.d.ts +13 -1
  110. package/ts_build/src/chat/modules/AgentModule.js +83 -24
  111. package/ts_build/src/chat/modules/AgentModule.js.map +1 -1
  112. package/ts_build/src/chat/modules/AskModule.js +12 -2
  113. package/ts_build/src/chat/modules/AskModule.js.map +1 -1
  114. package/ts_build/src/chat/modules/BaseChatModule.js.map +1 -1
  115. package/ts_build/src/chat/modules/CustomCommandsModule.js +12 -2
  116. package/ts_build/src/chat/modules/CustomCommandsModule.js.map +1 -1
  117. package/ts_build/src/chat/modules/InternalChatModule.d.ts +1 -0
  118. package/ts_build/src/chat/modules/InternalChatModule.js +21 -2
  119. package/ts_build/src/chat/modules/InternalChatModule.js.map +1 -1
  120. package/ts_build/src/chat/modules/RemoteSyncModule.js +19 -2
  121. package/ts_build/src/chat/modules/RemoteSyncModule.js.map +1 -1
  122. package/ts_build/src/chat/modules/RendererModule.js +17 -2
  123. package/ts_build/src/chat/modules/RendererModule.js.map +1 -1
  124. package/ts_build/src/chat/modules/SearchModule.js +12 -2
  125. package/ts_build/src/chat/modules/SearchModule.js.map +1 -1
  126. package/ts_build/src/chat/modules/SessionsModule.d.ts +6 -0
  127. package/ts_build/src/chat/modules/SessionsModule.js +113 -18
  128. package/ts_build/src/chat/modules/SessionsModule.js.map +1 -1
  129. package/ts_build/src/chat/modules/SetupModule.js +17 -2
  130. package/ts_build/src/chat/modules/SetupModule.js.map +1 -1
  131. package/ts_build/src/chat/modules/ShellCommandModule.js +12 -2
  132. package/ts_build/src/chat/modules/ShellCommandModule.js.map +1 -1
  133. package/ts_build/src/chat/modules/SystemModule.js +12 -2
  134. package/ts_build/src/chat/modules/SystemModule.js.map +1 -1
  135. package/ts_build/src/chat/modules/ToolCommandModule.d.ts +10 -0
  136. package/ts_build/src/chat/modules/ToolCommandModule.js +86 -0
  137. package/ts_build/src/chat/modules/ToolCommandModule.js.map +1 -0
  138. package/ts_build/src/chat/modules/index.d.ts +1 -0
  139. package/ts_build/src/chat/modules/index.js +3 -1
  140. package/ts_build/src/chat/modules/index.js.map +1 -1
  141. package/ts_build/src/chat/types.d.ts +3 -1
  142. package/ts_build/src/cli.js +7 -3
  143. package/ts_build/src/cli.js.map +1 -1
  144. package/ts_build/src/clients/http.d.ts +4 -0
  145. package/ts_build/src/clients/http.js +68 -6
  146. package/ts_build/src/clients/http.js.map +1 -1
  147. package/ts_build/src/clients/index.js +23 -4
  148. package/ts_build/src/clients/index.js.map +1 -1
  149. package/ts_build/src/commands/agent.js +1 -21
  150. package/ts_build/src/commands/agent.js.map +1 -1
  151. package/ts_build/src/commands/agents.d.ts +2 -1
  152. package/ts_build/src/commands/agents.js +216 -23
  153. package/ts_build/src/commands/agents.js.map +1 -1
  154. package/ts_build/src/commands/misc.js +3 -2
  155. package/ts_build/src/commands/misc.js.map +1 -1
  156. package/ts_build/src/commands/processes.d.ts +2 -0
  157. package/ts_build/src/commands/processes.js +213 -0
  158. package/ts_build/src/commands/processes.js.map +1 -0
  159. package/ts_build/src/commands/services.js +11 -1
  160. package/ts_build/src/commands/services.js.map +1 -1
  161. package/ts_build/src/index.d.ts +1 -0
  162. package/ts_build/src/index.js +1 -0
  163. package/ts_build/src/index.js.map +1 -1
  164. package/ts_build/src/login.d.ts +1 -1
  165. package/ts_build/src/login.js +53 -15
  166. package/ts_build/src/login.js.map +1 -1
  167. package/ts_build/src/processes/ProcessManager.d.ts +51 -0
  168. package/ts_build/src/processes/ProcessManager.js +160 -0
  169. package/ts_build/src/processes/ProcessManager.js.map +1 -0
  170. package/ts_build/src/processes/managedProcessShim.d.ts +1 -0
  171. package/ts_build/src/processes/managedProcessShim.js +161 -0
  172. package/ts_build/src/processes/managedProcessShim.js.map +1 -0
  173. package/ts_build/src/services/AgentService.js +18 -2
  174. package/ts_build/src/services/AgentService.js.map +1 -1
  175. package/ts_build/src/services/AgentSyncFs.d.ts +3 -0
  176. package/ts_build/src/services/AgentSyncFs.js +68 -16
  177. package/ts_build/src/services/AgentSyncFs.js.map +1 -1
  178. package/ts_build/src/services/AgentSyncKnowhowWeb.d.ts +2 -0
  179. package/ts_build/src/services/AgentSyncKnowhowWeb.js +28 -6
  180. package/ts_build/src/services/AgentSyncKnowhowWeb.js.map +1 -1
  181. package/ts_build/src/services/BehaviorsService.js +12 -2
  182. package/ts_build/src/services/BehaviorsService.js.map +1 -1
  183. package/ts_build/src/services/DockerService.js +20 -8
  184. package/ts_build/src/services/DockerService.js.map +1 -1
  185. package/ts_build/src/services/EmbeddingService.js +16 -2
  186. package/ts_build/src/services/EmbeddingService.js.map +1 -1
  187. package/ts_build/src/services/EmbeddingsService.js +12 -2
  188. package/ts_build/src/services/EmbeddingsService.js.map +1 -1
  189. package/ts_build/src/services/ExtensionsService.d.ts +10 -0
  190. package/ts_build/src/services/ExtensionsService.js +17 -0
  191. package/ts_build/src/services/ExtensionsService.js.map +1 -0
  192. package/ts_build/src/services/KnowhowClient.d.ts +7 -2
  193. package/ts_build/src/services/KnowhowClient.js +108 -10
  194. package/ts_build/src/services/KnowhowClient.js.map +1 -1
  195. package/ts_build/src/services/LazyToolsService.d.ts +2 -0
  196. package/ts_build/src/services/LazyToolsService.js +26 -9
  197. package/ts_build/src/services/LazyToolsService.js.map +1 -1
  198. package/ts_build/src/services/Mcp.js +12 -2
  199. package/ts_build/src/services/Mcp.js.map +1 -1
  200. package/ts_build/src/services/McpServer.js +17 -2
  201. package/ts_build/src/services/McpServer.js.map +1 -1
  202. package/ts_build/src/services/MessageProcessor.js +16 -2
  203. package/ts_build/src/services/MessageProcessor.js.map +1 -1
  204. package/ts_build/src/services/MinimalToolsService.js +16 -2
  205. package/ts_build/src/services/MinimalToolsService.js.map +1 -1
  206. package/ts_build/src/services/SessionManager.js +19 -2
  207. package/ts_build/src/services/SessionManager.js.map +1 -1
  208. package/ts_build/src/services/SkillsService.js +16 -2
  209. package/ts_build/src/services/SkillsService.js.map +1 -1
  210. package/ts_build/src/services/SyncedAgentWatcher.js +16 -2
  211. package/ts_build/src/services/SyncedAgentWatcher.js.map +1 -1
  212. package/ts_build/src/services/SyncerService.d.ts +1 -0
  213. package/ts_build/src/services/SyncerService.js +27 -5
  214. package/ts_build/src/services/SyncerService.js.map +1 -1
  215. package/ts_build/src/services/TaskRegistry.js +12 -2
  216. package/ts_build/src/services/TaskRegistry.js.map +1 -1
  217. package/ts_build/src/services/Tools.d.ts +1 -0
  218. package/ts_build/src/services/Tools.js +19 -2
  219. package/ts_build/src/services/Tools.js.map +1 -1
  220. package/ts_build/src/services/index.d.ts +5 -0
  221. package/ts_build/src/services/index.js +7 -2
  222. package/ts_build/src/services/index.js.map +1 -1
  223. package/ts_build/src/services/modules/index.d.ts +4 -1
  224. package/ts_build/src/services/modules/index.js +6 -1
  225. package/ts_build/src/services/modules/index.js.map +1 -1
  226. package/ts_build/src/services/modules/types.d.ts +3 -0
  227. package/ts_build/src/types.d.ts +1 -0
  228. package/ts_build/src/types.js.map +1 -1
  229. package/ts_build/src/util/Trace.d.ts +1 -3
  230. package/ts_build/src/utils/http.d.ts +6 -2
  231. package/ts_build/src/utils/http.js +8 -2
  232. package/ts_build/src/utils/http.js.map +1 -1
  233. package/ts_build/tests/unit/agents/historyRollback.test.d.ts +1 -0
  234. package/ts_build/tests/unit/agents/historyRollback.test.js +42 -0
  235. package/ts_build/tests/unit/agents/historyRollback.test.js.map +1 -0
  236. package/ts_build/tests/unit/auth/keyAuth.test.d.ts +1 -0
  237. package/ts_build/tests/unit/auth/keyAuth.test.js +77 -0
  238. package/ts_build/tests/unit/auth/keyAuth.test.js.map +1 -0
  239. package/ts_build/tests/unit/auth/keyManager.test.d.ts +1 -0
  240. package/ts_build/tests/unit/auth/keyManager.test.js +52 -0
  241. package/ts_build/tests/unit/auth/keyManager.test.js.map +1 -0
  242. package/ts_build/tests/unit/chat/ToolCommandModule.test.d.ts +1 -0
  243. package/ts_build/tests/unit/chat/ToolCommandModule.test.js +61 -0
  244. package/ts_build/tests/unit/chat/ToolCommandModule.test.js.map +1 -0
  245. package/ts_build/tests/unit/chat/historyCommandArgs.test.d.ts +1 -0
  246. package/ts_build/tests/unit/chat/historyCommandArgs.test.js +29 -0
  247. package/ts_build/tests/unit/chat/historyCommandArgs.test.js.map +1 -0
  248. package/ts_build/tests/unit/commands/agents.test.d.ts +1 -0
  249. package/ts_build/tests/unit/commands/agents.test.js +119 -0
  250. package/ts_build/tests/unit/commands/agents.test.js.map +1 -0
  251. package/ts_build/tests/unit/login.test.d.ts +1 -0
  252. package/ts_build/tests/unit/login.test.js +97 -0
  253. package/ts_build/tests/unit/login.test.js.map +1 -0
  254. package/ts_build/tests/unit/services/AgentSyncKnowhowWeb.test.d.ts +1 -0
  255. package/ts_build/tests/unit/services/AgentSyncKnowhowWeb.test.js +32 -0
  256. package/ts_build/tests/unit/services/AgentSyncKnowhowWeb.test.js.map +1 -0
  257. package/ts_build/tests/unit/services/LazyToolsService.test.d.ts +1 -0
  258. package/ts_build/tests/unit/services/LazyToolsService.test.js +47 -0
  259. package/ts_build/tests/unit/services/LazyToolsService.test.js.map +1 -0
  260. package/ts_build/tests/unit/services/SessionManager.test.d.ts +1 -0
  261. package/ts_build/tests/unit/services/SessionManager.test.js +72 -0
  262. package/ts_build/tests/unit/services/SessionManager.test.js.map +1 -0
  263. package/ts_build/tests/unit/services/SyncerService.test.d.ts +1 -0
  264. package/ts_build/tests/unit/services/SyncerService.test.js +40 -0
  265. package/ts_build/tests/unit/services/SyncerService.test.js.map +1 -0
  266. package/ts_build/tests/unit/util/Trace.test.d.ts +1 -0
  267. package/ts_build/tests/unit/util/Trace.test.js +107 -0
  268. package/ts_build/tests/unit/util/Trace.test.js.map +1 -0
  269. package/ts_build/tests/unit/utils/http.test.d.ts +1 -0
  270. package/ts_build/tests/unit/utils/http.test.js +69 -0
  271. package/ts_build/tests/unit/utils/http.test.js.map +1 -0
@@ -0,0 +1,111 @@
1
+ import * as fs from "fs";
2
+ import * as os from "os";
3
+ import * as path from "path";
4
+ import { Command } from "commander";
5
+
6
+ const startChat = jest.fn().mockResolvedValue(undefined);
7
+ const setupServices = jest.fn().mockResolvedValue(undefined);
8
+
9
+ jest.mock("../../../src/chat", () => ({ startChat }));
10
+ jest.mock("../../../src/commands/services", () => ({ setupServices }));
11
+
12
+ import { addAgentsCommand, getStatus } from "../../../src/commands/agents";
13
+
14
+ describe("agents resume terminal UX", () => {
15
+ let previousCwd: string;
16
+ let tempDir: string;
17
+ let isTTYDescriptor: PropertyDescriptor | undefined;
18
+
19
+ beforeEach(() => {
20
+ jest.clearAllMocks();
21
+ previousCwd = process.cwd();
22
+ tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "knowhow-agents-command-"));
23
+ process.chdir(tempDir);
24
+ const taskId = "1700000000-resumable-task";
25
+ fs.mkdirSync(path.join(".knowhow", "processes", "agents", taskId), {
26
+ recursive: true,
27
+ });
28
+ fs.writeFileSync(
29
+ path.join(".knowhow", "processes", "agents", taskId, "metadata.json"),
30
+ JSON.stringify({ taskId, status: "completed", threads: [[]] })
31
+ );
32
+ isTTYDescriptor = Object.getOwnPropertyDescriptor(process.stdin, "isTTY");
33
+ Object.defineProperty(process.stdin, "isTTY", {
34
+ configurable: true,
35
+ value: true,
36
+ });
37
+ });
38
+
39
+ afterEach(() => {
40
+ process.chdir(previousCwd);
41
+ fs.rmSync(tempDir, { recursive: true, force: true });
42
+ if (isTTYDescriptor) {
43
+ Object.defineProperty(process.stdin, "isTTY", isTTYDescriptor);
44
+ } else {
45
+ delete (process.stdin as NodeJS.ReadStream & { isTTY?: boolean }).isTTY;
46
+ }
47
+ });
48
+
49
+ test("opens chat with the interactive resume command by default", async () => {
50
+ const program = new Command();
51
+ addAgentsCommand(program, jest.fn());
52
+
53
+ await program.parseAsync([
54
+ "node",
55
+ "knowhow",
56
+ "agents",
57
+ "resume",
58
+ "1700000000-resumable-task",
59
+ "--rollback",
60
+ "2",
61
+ ]);
62
+
63
+ expect(setupServices).toHaveBeenCalledTimes(1);
64
+ expect(startChat).toHaveBeenCalledWith(
65
+ "/resume 1700000000-resumable-task --rollback 2"
66
+ );
67
+ });
68
+ });
69
+
70
+ describe("agents effective status", () => {
71
+ let previousCwd: string;
72
+ let tempDir: string;
73
+
74
+ beforeEach(() => {
75
+ previousCwd = process.cwd();
76
+ tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "knowhow-agent-status-"));
77
+ process.chdir(tempDir);
78
+ });
79
+
80
+ afterEach(() => {
81
+ process.chdir(previousCwd);
82
+ fs.rmSync(tempDir, { recursive: true, force: true });
83
+ });
84
+
85
+ test("keeps a running status when the recorded process is alive", () => {
86
+ expect(getStatus("task", { status: "running", pid: process.pid })).toBe(
87
+ "running"
88
+ );
89
+ });
90
+
91
+ test("reports a running task as failed when its process exited", () => {
92
+ expect(getStatus("task", { status: "running", pid: 2_147_483_647 })).toBe(
93
+ "failed"
94
+ );
95
+ });
96
+
97
+ test("reports old legacy running records without a pid as stale", () => {
98
+ expect(
99
+ getStatus("task", {
100
+ status: "running",
101
+ lastUpdate: new Date(Date.now() - 10 * 60 * 1000).toISOString(),
102
+ })
103
+ ).toBe("stale");
104
+ });
105
+
106
+ test("never overrides a persisted terminal status", () => {
107
+ expect(getStatus("task", { status: "completed", pid: process.pid })).toBe(
108
+ "completed"
109
+ );
110
+ });
111
+ });
@@ -0,0 +1,119 @@
1
+ const httpGet = jest.fn();
2
+ const browserLogin = jest.fn();
3
+ const hasKeyPair = jest.fn();
4
+ const authenticateWithKey = jest.fn();
5
+ const registerPublicKey = jest.fn();
6
+ const getOrCreatePublicKey = jest.fn();
7
+ const getConfig = jest.fn();
8
+ const updateConfig = jest.fn();
9
+
10
+ jest.mock("../../src/utils/http", () => ({
11
+ __esModule: true,
12
+ default: { get: httpGet, isHttpError: () => false },
13
+ }));
14
+
15
+ jest.mock("../../src/services/KnowhowClient", () => ({
16
+ KNOWHOW_API_URL: "https://api.example",
17
+ }));
18
+
19
+ jest.mock("../../src/auth/browserLogin", () => ({
20
+ BrowserLoginService: jest.fn().mockImplementation(() => ({ login: browserLogin })),
21
+ }));
22
+
23
+ jest.mock("../../src/auth/keyAuth", () => ({
24
+ hasKeyPair,
25
+ authenticateWithKey,
26
+ registerPublicKey,
27
+ }));
28
+
29
+ jest.mock("../../src/auth/keyManager", () => ({ getOrCreatePublicKey }));
30
+ jest.mock("../../src/config", () => ({ getConfig, updateConfig }));
31
+ jest.mock("../../src/utils", () => ({ ask: jest.fn() }));
32
+
33
+ import fs from "fs";
34
+ import os from "os";
35
+ import path from "path";
36
+ import { login } from "../../src/login";
37
+
38
+ describe("login public-key bootstrap", () => {
39
+ const originalCwd = process.cwd();
40
+ let workingDirectory: string;
41
+
42
+ beforeEach(() => {
43
+ jest.clearAllMocks();
44
+ jest.spyOn(console, "log").mockImplementation(() => undefined);
45
+ jest.spyOn(console, "warn").mockImplementation(() => undefined);
46
+ workingDirectory = fs.mkdtempSync(path.join(os.tmpdir(), "knowhow-login-"));
47
+ fs.mkdirSync(path.join(workingDirectory, ".knowhow"));
48
+ fs.writeFileSync(path.join(workingDirectory, ".knowhow", ".jwt"), "raw-jwt");
49
+ process.chdir(workingDirectory);
50
+ hasKeyPair.mockReturnValue(false);
51
+ browserLogin.mockResolvedValue(undefined);
52
+ getConfig.mockResolvedValue({ modelProviders: [] });
53
+ getOrCreatePublicKey.mockReturnValue({
54
+ publicKeyBase64: "raw-key",
55
+ fingerprint: "SHA256:key",
56
+ });
57
+ httpGet.mockResolvedValue({
58
+ data: {
59
+ orgId: "org-1",
60
+ user: {
61
+ email: "user@example.com",
62
+ orgs: [{ organizationId: "org-1", organization: { id: "org-1", name: "Org" } }],
63
+ },
64
+ },
65
+ });
66
+ });
67
+
68
+ afterEach(() => {
69
+ process.chdir(originalCwd);
70
+ fs.rmSync(workingDirectory, { recursive: true, force: true });
71
+ jest.restoreAllMocks();
72
+ });
73
+
74
+ it("bootstraps the selected identity after PKCE without invalidating browser login on registration failure", async () => {
75
+ registerPublicKey.mockRejectedValueOnce(new Error("endpoint unavailable"));
76
+
77
+ await expect(login(false, "/keys/custom")).resolves.toBeUndefined();
78
+
79
+ expect(browserLogin).toHaveBeenCalledTimes(1);
80
+ expect(getOrCreatePublicKey).toHaveBeenCalledWith("/keys/custom");
81
+ expect(registerPublicKey).toHaveBeenCalledWith(
82
+ "raw-jwt",
83
+ "raw-key",
84
+ "https://api.example"
85
+ );
86
+ expect(updateConfig).toHaveBeenCalledWith(
87
+ expect.objectContaining({ orgId: "org-1", modelProviders: [{ provider: "knowhow" }] })
88
+ );
89
+ });
90
+
91
+ it("safely falls back to browser PKCE when the selected key cannot authenticate", async () => {
92
+ hasKeyPair.mockReturnValue(true);
93
+ getConfig.mockResolvedValue({ orgId: "org-1", modelProviders: [] });
94
+ authenticateWithKey.mockRejectedValueOnce(new Error("old backend"));
95
+ registerPublicKey.mockResolvedValueOnce(undefined);
96
+
97
+ await expect(login(false, "/keys/dev")).resolves.toBeUndefined();
98
+
99
+ expect(authenticateWithKey).toHaveBeenCalledWith(
100
+ "org-1",
101
+ "https://api.example",
102
+ "/keys/dev"
103
+ );
104
+ expect(browserLogin).toHaveBeenCalledTimes(1);
105
+ expect(getOrCreatePublicKey).toHaveBeenCalledWith("/keys/dev");
106
+ });
107
+
108
+ it("does not attempt key authentication without a configured organization", async () => {
109
+ hasKeyPair.mockReturnValue(true);
110
+ getConfig.mockResolvedValue({ modelProviders: [] });
111
+ registerPublicKey.mockResolvedValueOnce(undefined);
112
+
113
+ await expect(login(false, "/keys/dev")).resolves.toBeUndefined();
114
+
115
+ expect(hasKeyPair).not.toHaveBeenCalled();
116
+ expect(authenticateWithKey).not.toHaveBeenCalled();
117
+ expect(browserLogin).toHaveBeenCalledTimes(1);
118
+ });
119
+ });
@@ -0,0 +1,38 @@
1
+ import { AgentSyncKnowhowWeb } from "../../../src/services/AgentSyncKnowhowWeb";
2
+
3
+ describe("AgentSyncKnowhowWeb sync error reporting", () => {
4
+ let errorSpy: jest.SpyInstance;
5
+
6
+ beforeEach(() => {
7
+ errorSpy = jest.spyOn(console, "error").mockImplementation(() => undefined);
8
+ });
9
+
10
+ afterEach(() => {
11
+ errorSpy.mockRestore();
12
+ });
13
+
14
+ it("reports only the first remote failure for a task", async () => {
15
+ const sync = new AgentSyncKnowhowWeb();
16
+ (sync as any).client = {
17
+ createChatTask: jest.fn().mockRejectedValue(new Error("offline")),
18
+ };
19
+
20
+ await sync.createChatTask({ messageId: "message-1", prompt: "test" });
21
+ await sync.createChatTask({ messageId: "message-1", prompt: "test" });
22
+
23
+ expect(errorSpy).toHaveBeenCalledTimes(1);
24
+ });
25
+
26
+ it("allows the next task to report its first failure after reset", async () => {
27
+ const sync = new AgentSyncKnowhowWeb();
28
+ (sync as any).client = {
29
+ createChatTask: jest.fn().mockRejectedValue(new Error("offline")),
30
+ };
31
+
32
+ await sync.createChatTask({ messageId: "message-1", prompt: "test" });
33
+ sync.reset();
34
+ await sync.createChatTask({ messageId: "message-2", prompt: "test" });
35
+
36
+ expect(errorSpy).toHaveBeenCalledTimes(2);
37
+ });
38
+ });
@@ -0,0 +1,53 @@
1
+ import { LazyToolsService } from "../../../src/services/LazyToolsService";
2
+ import { Tool } from "../../../src/clients/types";
3
+
4
+ function tool(name: string): Tool {
5
+ return {
6
+ type: "function",
7
+ function: {
8
+ name,
9
+ description: name,
10
+ parameters: { type: "object", properties: {}, required: [] },
11
+ },
12
+ };
13
+ }
14
+
15
+ describe("LazyToolsService persisted tool restoration", () => {
16
+ test("replaces pattern state with the exact saved tool list", () => {
17
+ const tools = new LazyToolsService();
18
+ tools.addTools([tool("alpha"), tool("beta"), tool("finalAnswer")]);
19
+ tools.enableTools(["*"]);
20
+ tools.disableTools(["alpha"]);
21
+
22
+ tools.restoreEnabledTools(["alpha", "finalAnswer"]);
23
+
24
+ expect(tools.getTools().map((entry) => entry.function.name).sort()).toEqual([
25
+ "alpha",
26
+ "finalAnswer",
27
+ ]);
28
+ });
29
+
30
+ test("calls a registered hidden tool without making it prompt-visible", async () => {
31
+ const tools = new LazyToolsService();
32
+ tools.addTools([tool("hiddenTool")]);
33
+ tools.setFunction("hiddenTool", ({ value }: { value: string }) => `got:${value}`);
34
+
35
+ expect(tools.getToolNames()).not.toContain("hiddenTool");
36
+ expect(tools.getFunctionNames()).toContain("hiddenTool");
37
+
38
+ const result = await tools.callTool({
39
+ id: "hidden-call",
40
+ type: "function",
41
+ function: {
42
+ name: "hiddenTool",
43
+ arguments: JSON.stringify({ value: "test" }),
44
+ },
45
+ });
46
+
47
+ expect(result.functionResp).toBe("got:test");
48
+ expect(tools.getToolNames()).not.toContain("hiddenTool");
49
+ expect(tools.listAvailableTools()).toMatchObject({
50
+ disabled: expect.arrayContaining(["hiddenTool"]),
51
+ });
52
+ });
53
+ });
@@ -0,0 +1,43 @@
1
+ import * as fs from "fs";
2
+ import * as os from "os";
3
+ import * as path from "path";
4
+ import { SessionManager } from "../../../src/services/SessionManager";
5
+ import { TaskInfo } from "../../../src/chat/types";
6
+
7
+ describe("SessionManager persisted run settings", () => {
8
+ let sessionsDir: string;
9
+
10
+ beforeEach(() => {
11
+ sessionsDir = fs.mkdtempSync(path.join(os.tmpdir(), "knowhow-sessions-"));
12
+ });
13
+
14
+ afterEach(() => {
15
+ fs.rmSync(sessionsDir, { recursive: true, force: true });
16
+ });
17
+
18
+ test("saves and updates the exact enabled tool list", () => {
19
+ const manager = new SessionManager(sessionsDir);
20
+ const taskInfo = {
21
+ taskId: "local-task",
22
+ agentName: "Patcher",
23
+ initialInput: "continue",
24
+ status: "running",
25
+ startTime: Date.now(),
26
+ totalCost: 0,
27
+ enabledTools: ["alpha", "finalAnswer"],
28
+ } as TaskInfo;
29
+
30
+ manager.saveSession(taskInfo.taskId, taskInfo, [[]]);
31
+ expect(manager.loadSession(taskInfo.taskId)?.enabledTools).toEqual([
32
+ "alpha",
33
+ "finalAnswer",
34
+ ]);
35
+
36
+ taskInfo.enabledTools = ["beta", "finalAnswer"];
37
+ manager.updateSession(taskInfo.taskId, taskInfo, [[]]);
38
+ expect(manager.loadSession(taskInfo.taskId)?.enabledTools).toEqual([
39
+ "beta",
40
+ "finalAnswer",
41
+ ]);
42
+ });
43
+ });
@@ -0,0 +1,45 @@
1
+ import { AgentSyncFs } from "../../../src/services/AgentSyncFs";
2
+ import { SyncerService } from "../../../src/services/SyncerService";
3
+
4
+ describe("SyncerService remote identity", () => {
5
+ beforeEach(() => {
6
+ jest
7
+ .spyOn(AgentSyncFs.prototype, "createTask")
8
+ .mockImplementation(async (options) => options.taskId);
9
+ jest
10
+ .spyOn(AgentSyncFs.prototype, "setRemoteIdentity")
11
+ .mockResolvedValue(undefined);
12
+ });
13
+
14
+ afterEach(() => {
15
+ jest.restoreAllMocks();
16
+ });
17
+
18
+ it("never attaches a local filesystem slug as a remote task", async () => {
19
+ const syncer = new SyncerService();
20
+ await syncer.createTask({
21
+ taskId: "1785905990-local-task-slug",
22
+ prompt: "resume",
23
+ syncRemote: true,
24
+ });
25
+
26
+ expect(syncer.getCreatedWebTaskId()).toBeUndefined();
27
+ expect(AgentSyncFs.prototype.setRemoteIdentity).not.toHaveBeenCalled();
28
+ });
29
+
30
+ it("attaches and persists an explicit remote task UUID", async () => {
31
+ const remoteTaskId = "123e4567-e89b-42d3-a456-426614174000";
32
+ const syncer = new SyncerService();
33
+ await syncer.createTask({
34
+ taskId: "1785905990-local-task-slug",
35
+ remoteTaskId,
36
+ prompt: "resume",
37
+ syncRemote: true,
38
+ });
39
+
40
+ expect(syncer.getCreatedWebTaskId()).toBe(remoteTaskId);
41
+ expect(AgentSyncFs.prototype.setRemoteIdentity).toHaveBeenCalledWith(
42
+ remoteTaskId
43
+ );
44
+ });
45
+ });
@@ -0,0 +1,119 @@
1
+ import { Trace, TraceAll } from "../../../src/util/Trace";
2
+ import {
3
+ SpanHandle,
4
+ TracerImpl,
5
+ TracingService,
6
+ } from "../../../src/services/TracingService";
7
+ import { ToolsService } from "../../../src/services/Tools";
8
+
9
+ class TestSpan implements SpanHandle {
10
+ ended = false;
11
+ error?: unknown;
12
+
13
+ setAttribute() {}
14
+
15
+ recordError(error: unknown) {
16
+ this.error = error;
17
+ }
18
+
19
+ end() {
20
+ this.ended = true;
21
+ }
22
+ }
23
+
24
+ class TestTracer implements TracerImpl {
25
+ spans: Array<{
26
+ name: string;
27
+ attributes?: Record<string, string | number | boolean>;
28
+ span: TestSpan;
29
+ }> = [];
30
+
31
+ startSpan(
32
+ name: string,
33
+ attributes?: Record<string, string | number | boolean>
34
+ ): SpanHandle {
35
+ const span = new TestSpan();
36
+ this.spans.push({ name, attributes, span });
37
+ return span;
38
+ }
39
+
40
+ withSpan<T>(_span: SpanHandle, fn: () => T): T {
41
+ return fn();
42
+ }
43
+ }
44
+
45
+ describe("tracing decorators", () => {
46
+ const tracer = new TestTracer();
47
+
48
+ beforeEach(() => {
49
+ tracer.spans = [];
50
+ TracingService.register(tracer);
51
+ });
52
+
53
+ afterAll(() => TracingService.reset());
54
+
55
+ it("traces and closes synchronous and asynchronous methods", async () => {
56
+ @TraceAll()
57
+ class Subject {
58
+ sync() {
59
+ return "sync";
60
+ }
61
+
62
+ async asyncMethod() {
63
+ return "async";
64
+ }
65
+ }
66
+
67
+ const subject = new Subject();
68
+ expect(subject.sync()).toBe("sync");
69
+ expect(await subject.asyncMethod()).toBe("async");
70
+
71
+ expect(tracer.spans.map(({ name }) => name)).toEqual([
72
+ "Subject.sync",
73
+ "Subject.asyncMethod",
74
+ ]);
75
+ expect(tracer.spans.every(({ span }) => span.ended)).toBe(true);
76
+ });
77
+
78
+ it("records rejected method errors before rethrowing them", async () => {
79
+ const failure = new Error("failed");
80
+
81
+ class Subject {
82
+ @Trace()
83
+ async fail() {
84
+ throw failure;
85
+ }
86
+ }
87
+
88
+ await expect(new Subject().fail()).rejects.toBe(failure);
89
+ expect(tracer.spans[0].span.error).toBe(failure);
90
+ expect(tracer.spans[0].span.ended).toBe(true);
91
+ });
92
+
93
+ it("traces the concrete ToolsService.callTool path", async () => {
94
+ const tools = new ToolsService();
95
+ tools.addTool({
96
+ type: "function",
97
+ function: {
98
+ name: "echo",
99
+ description: "Echo a value",
100
+ parameters: {
101
+ type: "object",
102
+ properties: { value: { type: "string" } },
103
+ required: ["value"],
104
+ },
105
+ },
106
+ });
107
+ tools.setFunction("echo", ({ value }: { value: string }) => value);
108
+
109
+ const result = await tools.callTool({
110
+ id: "trace-test",
111
+ type: "function",
112
+ function: { name: "echo", arguments: JSON.stringify({ value: "ok" }) },
113
+ });
114
+
115
+ expect(result.functionResp).toBe("ok");
116
+ expect(tracer.spans.some(({ name }) => name === "ToolsService.callTool"))
117
+ .toBe(true);
118
+ });
119
+ });
@@ -0,0 +1,43 @@
1
+ import http, { HTTP_UNAUTHORIZED_HANDLER, RefreshableHeaders } from "../../../src/utils/http";
2
+
3
+ describe("HTTP unauthorized renewal", () => {
4
+ const originalFetch = global.fetch;
5
+
6
+ afterEach(() => {
7
+ global.fetch = originalFetch;
8
+ jest.restoreAllMocks();
9
+ });
10
+
11
+ it("renews and retries a 401 once with the updated headers", async () => {
12
+ const headers: RefreshableHeaders = { Authorization: "Bearer old" };
13
+ const renew = jest.fn(async () => {
14
+ headers.Authorization = "Bearer new";
15
+ });
16
+ Object.defineProperty(headers, HTTP_UNAUTHORIZED_HANDLER, { value: renew });
17
+ global.fetch = jest
18
+ .fn()
19
+ .mockResolvedValueOnce(new Response("unauthorized", { status: 401 }))
20
+ .mockResolvedValueOnce(new Response(JSON.stringify({ ok: true }), { status: 200 }));
21
+
22
+ await expect(http.get("https://example.test", { headers })).resolves.toMatchObject({
23
+ data: { ok: true },
24
+ });
25
+ expect(renew).toHaveBeenCalledTimes(1);
26
+ expect(global.fetch).toHaveBeenNthCalledWith(
27
+ 2,
28
+ "https://example.test",
29
+ expect.objectContaining({ headers: { Authorization: "Bearer new" } })
30
+ );
31
+ });
32
+
33
+ it("does not renew or retry a second 401", async () => {
34
+ const headers: RefreshableHeaders = { Authorization: "Bearer old" };
35
+ const renew = jest.fn(async () => undefined);
36
+ Object.defineProperty(headers, HTTP_UNAUTHORIZED_HANDLER, { value: renew });
37
+ global.fetch = jest.fn().mockResolvedValue(new Response("unauthorized", { status: 401 }));
38
+
39
+ await expect(http.get("https://example.test", { headers })).rejects.toMatchObject({ status: 401 });
40
+ expect(renew).toHaveBeenCalledTimes(1);
41
+ expect(global.fetch).toHaveBeenCalledTimes(2);
42
+ });
43
+ });
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tyvm/knowhow",
3
- "version": "0.0.146",
3
+ "version": "0.0.148",
4
4
  "description": "ai cli with plugins and agents",
5
5
  "main": "ts_build/src/index.js",
6
6
  "bin": {
@@ -137,6 +137,7 @@ export declare abstract class BaseAgent implements IAgent {
137
137
  protected isTerminationResponse(content: string): boolean;
138
138
  getEnabledTools(): Tool[];
139
139
  getEnabledToolNames(): string[];
140
+ restoreEnabledTools(names: string[]): void;
140
141
  disableTool(toolName: string): void;
141
142
  isToolEnabled(toolName: string): boolean;
142
143
  enableTool(toolName: string): void;
@@ -191,5 +192,5 @@ export declare abstract class BaseAgent implements IAgent {
191
192
  content: string;
192
193
  } | null;
193
194
  getTaskBreakdown(messages: Message[]): Promise<string>;
194
- compressMessages(messages: Message[], startIndex: number, endIndex: number): Promise<Message[]>;
195
+ compressMessages(messages: Message[]): Promise<Message[]>;
195
196
  }