@tyvm/knowhow 0.0.146 → 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 (214) 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/modules/AgentModule.ts +114 -33
  11. package/src/chat/modules/AskModule.ts +2 -0
  12. package/src/chat/modules/BaseChatModule.ts +0 -1
  13. package/src/chat/modules/CustomCommandsModule.ts +2 -0
  14. package/src/chat/modules/InternalChatModule.ts +2 -0
  15. package/src/chat/modules/RemoteSyncModule.ts +4 -0
  16. package/src/chat/modules/RendererModule.ts +2 -0
  17. package/src/chat/modules/SearchModule.ts +2 -0
  18. package/src/chat/modules/SessionsModule.ts +103 -34
  19. package/src/chat/modules/SetupModule.ts +2 -0
  20. package/src/chat/modules/ShellCommandModule.ts +2 -0
  21. package/src/chat/modules/SystemModule.ts +2 -0
  22. package/src/chat/types.ts +2 -0
  23. package/src/cli.ts +1 -1
  24. package/src/clients/index.ts +2 -0
  25. package/src/commands/agent.ts +1 -38
  26. package/src/commands/agents.ts +403 -53
  27. package/src/commands/misc.ts +3 -2
  28. package/src/login.ts +89 -28
  29. package/src/services/AgentService.ts +2 -0
  30. package/src/services/AgentSyncFs.ts +65 -0
  31. package/src/services/AgentSyncKnowhowWeb.ts +22 -4
  32. package/src/services/BehaviorsService.ts +2 -0
  33. package/src/services/DockerService.ts +2 -0
  34. package/src/services/EmbeddingService.ts +2 -0
  35. package/src/services/EmbeddingsService.ts +3 -0
  36. package/src/services/KnowhowClient.ts +68 -7
  37. package/src/services/LazyToolsService.ts +9 -0
  38. package/src/services/Mcp.ts +2 -0
  39. package/src/services/McpServer.ts +2 -0
  40. package/src/services/MessageProcessor.ts +2 -0
  41. package/src/services/MinimalToolsService.ts +2 -0
  42. package/src/services/SessionManager.ts +5 -0
  43. package/src/services/SkillsService.ts +2 -0
  44. package/src/services/SyncedAgentWatcher.ts +2 -0
  45. package/src/services/SyncerService.ts +23 -8
  46. package/src/services/TaskRegistry.ts +2 -0
  47. package/src/services/Tools.ts +2 -0
  48. package/src/services/index.ts +2 -0
  49. package/src/services/modules/index.ts +4 -1
  50. package/src/types.ts +2 -0
  51. package/src/util/Trace.ts +1 -1
  52. package/src/utils/http.ts +18 -2
  53. package/tests/unit/agents/historyRollback.test.ts +53 -0
  54. package/tests/unit/auth/keyAuth.test.ts +102 -0
  55. package/tests/unit/auth/keyManager.test.ts +67 -0
  56. package/tests/unit/chat/historyCommandArgs.test.ts +37 -0
  57. package/tests/unit/commands/agents.test.ts +68 -0
  58. package/tests/unit/login.test.ts +119 -0
  59. package/tests/unit/services/AgentSyncKnowhowWeb.test.ts +38 -0
  60. package/tests/unit/services/LazyToolsService.test.ts +29 -0
  61. package/tests/unit/services/SessionManager.test.ts +43 -0
  62. package/tests/unit/services/SyncerService.test.ts +45 -0
  63. package/tests/unit/util/Trace.test.ts +119 -0
  64. package/tests/unit/utils/http.test.ts +43 -0
  65. package/ts_build/package.json +1 -1
  66. package/ts_build/src/agents/base/base.d.ts +1 -0
  67. package/ts_build/src/agents/base/base.js +31 -2
  68. package/ts_build/src/agents/base/base.js.map +1 -1
  69. package/ts_build/src/agents/historyRollback.d.ts +2 -0
  70. package/ts_build/src/agents/historyRollback.js +22 -0
  71. package/ts_build/src/agents/historyRollback.js.map +1 -0
  72. package/ts_build/src/agents/tools/startAgentTask.d.ts +2 -0
  73. package/ts_build/src/agents/tools/startAgentTask.js +36 -9
  74. package/ts_build/src/agents/tools/startAgentTask.js.map +1 -1
  75. package/ts_build/src/auth/browserLogin.d.ts +1 -0
  76. package/ts_build/src/auth/browserLogin.js +5 -2
  77. package/ts_build/src/auth/browserLogin.js.map +1 -1
  78. package/ts_build/src/auth/keyAuth.d.ts +5 -0
  79. package/ts_build/src/auth/keyAuth.js +79 -0
  80. package/ts_build/src/auth/keyAuth.js.map +1 -0
  81. package/ts_build/src/auth/keyManager.d.ts +14 -0
  82. package/ts_build/src/auth/keyManager.js +145 -0
  83. package/ts_build/src/auth/keyManager.js.map +1 -0
  84. package/ts_build/src/chat/modules/AgentModule.d.ts +13 -1
  85. package/ts_build/src/chat/modules/AgentModule.js +83 -24
  86. package/ts_build/src/chat/modules/AgentModule.js.map +1 -1
  87. package/ts_build/src/chat/modules/AskModule.js +12 -2
  88. package/ts_build/src/chat/modules/AskModule.js.map +1 -1
  89. package/ts_build/src/chat/modules/BaseChatModule.js.map +1 -1
  90. package/ts_build/src/chat/modules/CustomCommandsModule.js +12 -2
  91. package/ts_build/src/chat/modules/CustomCommandsModule.js.map +1 -1
  92. package/ts_build/src/chat/modules/InternalChatModule.js +16 -2
  93. package/ts_build/src/chat/modules/InternalChatModule.js.map +1 -1
  94. package/ts_build/src/chat/modules/RemoteSyncModule.js +19 -2
  95. package/ts_build/src/chat/modules/RemoteSyncModule.js.map +1 -1
  96. package/ts_build/src/chat/modules/RendererModule.js +17 -2
  97. package/ts_build/src/chat/modules/RendererModule.js.map +1 -1
  98. package/ts_build/src/chat/modules/SearchModule.js +12 -2
  99. package/ts_build/src/chat/modules/SearchModule.js.map +1 -1
  100. package/ts_build/src/chat/modules/SessionsModule.d.ts +6 -0
  101. package/ts_build/src/chat/modules/SessionsModule.js +113 -18
  102. package/ts_build/src/chat/modules/SessionsModule.js.map +1 -1
  103. package/ts_build/src/chat/modules/SetupModule.js +17 -2
  104. package/ts_build/src/chat/modules/SetupModule.js.map +1 -1
  105. package/ts_build/src/chat/modules/ShellCommandModule.js +12 -2
  106. package/ts_build/src/chat/modules/ShellCommandModule.js.map +1 -1
  107. package/ts_build/src/chat/modules/SystemModule.js +12 -2
  108. package/ts_build/src/chat/modules/SystemModule.js.map +1 -1
  109. package/ts_build/src/chat/types.d.ts +2 -0
  110. package/ts_build/src/cli.js +1 -1
  111. package/ts_build/src/cli.js.map +1 -1
  112. package/ts_build/src/clients/index.js +19 -4
  113. package/ts_build/src/clients/index.js.map +1 -1
  114. package/ts_build/src/commands/agent.js +1 -21
  115. package/ts_build/src/commands/agent.js.map +1 -1
  116. package/ts_build/src/commands/agents.d.ts +1 -1
  117. package/ts_build/src/commands/agents.js +174 -16
  118. package/ts_build/src/commands/agents.js.map +1 -1
  119. package/ts_build/src/commands/misc.js +3 -2
  120. package/ts_build/src/commands/misc.js.map +1 -1
  121. package/ts_build/src/login.d.ts +1 -1
  122. package/ts_build/src/login.js +53 -15
  123. package/ts_build/src/login.js.map +1 -1
  124. package/ts_build/src/services/AgentService.js +18 -2
  125. package/ts_build/src/services/AgentService.js.map +1 -1
  126. package/ts_build/src/services/AgentSyncFs.d.ts +3 -0
  127. package/ts_build/src/services/AgentSyncFs.js +62 -15
  128. package/ts_build/src/services/AgentSyncFs.js.map +1 -1
  129. package/ts_build/src/services/AgentSyncKnowhowWeb.d.ts +2 -0
  130. package/ts_build/src/services/AgentSyncKnowhowWeb.js +28 -6
  131. package/ts_build/src/services/AgentSyncKnowhowWeb.js.map +1 -1
  132. package/ts_build/src/services/BehaviorsService.js +12 -2
  133. package/ts_build/src/services/BehaviorsService.js.map +1 -1
  134. package/ts_build/src/services/DockerService.js +20 -8
  135. package/ts_build/src/services/DockerService.js.map +1 -1
  136. package/ts_build/src/services/EmbeddingService.js +16 -2
  137. package/ts_build/src/services/EmbeddingService.js.map +1 -1
  138. package/ts_build/src/services/EmbeddingsService.js +12 -2
  139. package/ts_build/src/services/EmbeddingsService.js.map +1 -1
  140. package/ts_build/src/services/KnowhowClient.d.ts +7 -2
  141. package/ts_build/src/services/KnowhowClient.js +108 -10
  142. package/ts_build/src/services/KnowhowClient.js.map +1 -1
  143. package/ts_build/src/services/LazyToolsService.d.ts +1 -0
  144. package/ts_build/src/services/LazyToolsService.js +21 -2
  145. package/ts_build/src/services/LazyToolsService.js.map +1 -1
  146. package/ts_build/src/services/Mcp.js +12 -2
  147. package/ts_build/src/services/Mcp.js.map +1 -1
  148. package/ts_build/src/services/McpServer.js +17 -2
  149. package/ts_build/src/services/McpServer.js.map +1 -1
  150. package/ts_build/src/services/MessageProcessor.js +16 -2
  151. package/ts_build/src/services/MessageProcessor.js.map +1 -1
  152. package/ts_build/src/services/MinimalToolsService.js +16 -2
  153. package/ts_build/src/services/MinimalToolsService.js.map +1 -1
  154. package/ts_build/src/services/SessionManager.js +19 -2
  155. package/ts_build/src/services/SessionManager.js.map +1 -1
  156. package/ts_build/src/services/SkillsService.js +16 -2
  157. package/ts_build/src/services/SkillsService.js.map +1 -1
  158. package/ts_build/src/services/SyncedAgentWatcher.js +16 -2
  159. package/ts_build/src/services/SyncedAgentWatcher.js.map +1 -1
  160. package/ts_build/src/services/SyncerService.d.ts +1 -0
  161. package/ts_build/src/services/SyncerService.js +27 -5
  162. package/ts_build/src/services/SyncerService.js.map +1 -1
  163. package/ts_build/src/services/TaskRegistry.js +12 -2
  164. package/ts_build/src/services/TaskRegistry.js.map +1 -1
  165. package/ts_build/src/services/Tools.js +16 -2
  166. package/ts_build/src/services/Tools.js.map +1 -1
  167. package/ts_build/src/services/index.d.ts +2 -0
  168. package/ts_build/src/services/index.js +4 -2
  169. package/ts_build/src/services/index.js.map +1 -1
  170. package/ts_build/src/services/modules/index.d.ts +2 -1
  171. package/ts_build/src/services/modules/index.js +1 -1
  172. package/ts_build/src/services/modules/index.js.map +1 -1
  173. package/ts_build/src/types.d.ts +1 -0
  174. package/ts_build/src/types.js.map +1 -1
  175. package/ts_build/src/util/Trace.d.ts +1 -3
  176. package/ts_build/src/utils/http.d.ts +6 -2
  177. package/ts_build/src/utils/http.js +8 -2
  178. package/ts_build/src/utils/http.js.map +1 -1
  179. package/ts_build/tests/unit/agents/historyRollback.test.d.ts +1 -0
  180. package/ts_build/tests/unit/agents/historyRollback.test.js +42 -0
  181. package/ts_build/tests/unit/agents/historyRollback.test.js.map +1 -0
  182. package/ts_build/tests/unit/auth/keyAuth.test.d.ts +1 -0
  183. package/ts_build/tests/unit/auth/keyAuth.test.js +77 -0
  184. package/ts_build/tests/unit/auth/keyAuth.test.js.map +1 -0
  185. package/ts_build/tests/unit/auth/keyManager.test.d.ts +1 -0
  186. package/ts_build/tests/unit/auth/keyManager.test.js +52 -0
  187. package/ts_build/tests/unit/auth/keyManager.test.js.map +1 -0
  188. package/ts_build/tests/unit/chat/historyCommandArgs.test.d.ts +1 -0
  189. package/ts_build/tests/unit/chat/historyCommandArgs.test.js +29 -0
  190. package/ts_build/tests/unit/chat/historyCommandArgs.test.js.map +1 -0
  191. package/ts_build/tests/unit/commands/agents.test.d.ts +1 -0
  192. package/ts_build/tests/unit/commands/agents.test.js +91 -0
  193. package/ts_build/tests/unit/commands/agents.test.js.map +1 -0
  194. package/ts_build/tests/unit/login.test.d.ts +1 -0
  195. package/ts_build/tests/unit/login.test.js +97 -0
  196. package/ts_build/tests/unit/login.test.js.map +1 -0
  197. package/ts_build/tests/unit/services/AgentSyncKnowhowWeb.test.d.ts +1 -0
  198. package/ts_build/tests/unit/services/AgentSyncKnowhowWeb.test.js +32 -0
  199. package/ts_build/tests/unit/services/AgentSyncKnowhowWeb.test.js.map +1 -0
  200. package/ts_build/tests/unit/services/LazyToolsService.test.d.ts +1 -0
  201. package/ts_build/tests/unit/services/LazyToolsService.test.js +27 -0
  202. package/ts_build/tests/unit/services/LazyToolsService.test.js.map +1 -0
  203. package/ts_build/tests/unit/services/SessionManager.test.d.ts +1 -0
  204. package/ts_build/tests/unit/services/SessionManager.test.js +72 -0
  205. package/ts_build/tests/unit/services/SessionManager.test.js.map +1 -0
  206. package/ts_build/tests/unit/services/SyncerService.test.d.ts +1 -0
  207. package/ts_build/tests/unit/services/SyncerService.test.js +40 -0
  208. package/ts_build/tests/unit/services/SyncerService.test.js.map +1 -0
  209. package/ts_build/tests/unit/util/Trace.test.d.ts +1 -0
  210. package/ts_build/tests/unit/util/Trace.test.js +107 -0
  211. package/ts_build/tests/unit/util/Trace.test.js.map +1 -0
  212. package/ts_build/tests/unit/utils/http.test.d.ts +1 -0
  213. package/ts_build/tests/unit/utils/http.test.js +69 -0
  214. package/ts_build/tests/unit/utils/http.test.js.map +1 -0
package/src/login.ts CHANGED
@@ -1,13 +1,28 @@
1
1
  import http from "./utils/http";
2
2
  import fs from "fs";
3
3
  import path from "path";
4
- import { chmod } from "fs/promises";
5
4
  import { ask } from "./utils";
6
5
  import { getConfig, updateConfig } from "./config";
7
6
  import { KNOWHOW_API_URL } from "./services/KnowhowClient";
8
7
  import { BrowserLoginService } from "./auth/browserLogin";
9
-
10
- export async function login(jwtFlag?: boolean): Promise<void> {
8
+ import { authenticateWithKey, hasKeyPair, registerPublicKey } from "./auth/keyAuth";
9
+ import { getOrCreatePublicKey } from "./auth/keyManager";
10
+
11
+ /**
12
+ * Log in to Knowhow.
13
+ *
14
+ * Strategy (in order):
15
+ * 1. `--jwt` flag: prompt for a raw JWT and write it to disk.
16
+ * 2. Key auth: if an Ed25519 key and configured organization exist, sign a
17
+ * server challenge and exchange it for an organization-bound JWT.
18
+ * 3. Browser: open the browser approval flow (first-time login or key not yet registered).
19
+ *
20
+ * After any successful login the local config is updated with the current user's orgId.
21
+ *
22
+ * @param jwtFlag True when --jwt flag is passed.
23
+ * @param identityPath Optional path to a specific private key file (--identity flag).
24
+ */
25
+ export async function login(jwtFlag?: boolean, identityPath?: string): Promise<void> {
11
26
  if (!KNOWHOW_API_URL) {
12
27
  throw new Error("Error: KNOWHOW_API_URL environment variable not set.");
13
28
  }
@@ -15,9 +30,8 @@ export async function login(jwtFlag?: boolean): Promise<void> {
15
30
  if (jwtFlag) {
16
31
  const jwt = await ask("Enter your JWT: ");
17
32
 
18
- // Update the JWT file
19
33
  const configDir = path.join(process.cwd(), ".knowhow");
20
- const jwtFile = path.join(process.cwd(), ".knowhow", ".jwt");
34
+ const jwtFile = path.join(configDir, ".jwt");
21
35
 
22
36
  if (!fs.existsSync(configDir)) {
23
37
  fs.mkdirSync(configDir, { recursive: true });
@@ -26,23 +40,69 @@ export async function login(jwtFlag?: boolean): Promise<void> {
26
40
  fs.chmodSync(jwtFile, 0o600);
27
41
  console.log("JWT updated successfully.");
28
42
  } else {
29
- // Use browser login as default method
30
- console.log("Starting browser-based authentication...");
43
+ const config = await getConfig();
44
+ const orgId = config.orgId;
45
+
46
+ // A local key is not proof of registration. Let the selected environment
47
+ // check it, but only when an organization is available to bind the attempt.
48
+ if (orgId && hasKeyPair(identityPath)) {
49
+ console.log("Found CLI identity — authenticating with public key...");
50
+ try {
51
+ const success = await authenticateWithKey(orgId, KNOWHOW_API_URL, identityPath);
52
+ if (success) {
53
+ console.log("✅ Successfully authenticated via public key!");
54
+ return await postLoginConfigUpdate(identityPath);
55
+ }
56
+ console.warn("Key authentication returned no JWT, falling back to browser login...");
57
+ } catch (error: unknown) {
58
+ const message = error instanceof Error ? error.message : String(error);
59
+ console.warn(`Key authentication failed (${message}), falling back to browser login...`);
60
+ }
61
+ }
62
+
63
+ // Fall back when no org/key is configured or when key auth is unavailable.
64
+ await doBrowserLogin(identityPath);
65
+ }
66
+
67
+ await postLoginConfigUpdate(identityPath);
68
+ }
69
+
70
+ /** Complete browser PKCE, then best-effort bootstrap the selected identity. */
71
+ async function doBrowserLogin(identityPath?: string): Promise<void> {
72
+ console.log("Starting browser-based authentication...");
73
+ try {
74
+ const existingConfig = await getConfig();
75
+ const existingOrgId = existingConfig?.orgId;
76
+ const browserLoginService = new BrowserLoginService(undefined, existingOrgId);
77
+ await browserLoginService.login();
78
+ console.log("✅ Successfully authenticated via browser!");
79
+
31
80
  try {
32
- // Pass existing orgId from config (if any) so the browser pre-selects the right org
33
- const existingConfig = await getConfig();
34
- const existingOrgId = existingConfig?.orgId;
35
- const browserLoginService = new BrowserLoginService(undefined, existingOrgId);
36
- await browserLoginService.login();
37
- console.log("Successfully authenticated via browser!");
38
- } catch (error) {
39
- console.error("Browser authentication failed:", error.message);
40
- console.log("You can try manual JWT login with: knowhow login --jwt");
41
- throw error;
81
+ const keyPair = getOrCreatePublicKey(identityPath);
82
+ await registerPublicKey(
83
+ await loadJwt(),
84
+ keyPair.publicKeyBase64,
85
+ KNOWHOW_API_URL
86
+ );
87
+ console.log(`Registered CLI identity ${keyPair.fingerprint}`);
88
+ } catch (registrationError: unknown) {
89
+ // Browser login remains valid when key registration is unavailable (for
90
+ // example during a rolling backend deployment or for a partial identity).
91
+ const message = registrationError instanceof Error
92
+ ? registrationError.message
93
+ : String(registrationError);
94
+ console.warn(`Could not register CLI identity (${message}). Browser login is still active.`);
42
95
  }
96
+ } catch (error: unknown) {
97
+ const msg = error instanceof Error ? error.message : String(error);
98
+ console.error("Browser authentication failed:", msg);
99
+ console.log("You can try manual JWT login with: knowhow login --jwt");
100
+ throw error;
43
101
  }
102
+ }
44
103
 
45
- // Get current user/org information
104
+ /** After any successful login, update the local config with the current user/org. */
105
+ async function postLoginConfigUpdate(identityPath?: string): Promise<void> {
46
106
  try {
47
107
  const storedJwt = await loadJwt();
48
108
  const { user, currentOrg } = await checkJwt(storedJwt);
@@ -59,25 +119,26 @@ export async function login(jwtFlag?: boolean): Promise<void> {
59
119
  }
60
120
 
61
121
  const hasProvider = config.modelProviders.find(
62
- (provider) => provider.provider === "knowhow"
122
+ (provider: { provider: string }) => provider.provider === "knowhow"
63
123
  );
64
124
  if (!hasProvider) {
65
- config.modelProviders.push({
66
- provider: "knowhow",
67
- });
125
+ config.modelProviders.push({ provider: "knowhow" });
68
126
  }
69
- // Save orgId to config so sync:remote and other features use the correct org
127
+
70
128
  if (orgId) {
71
129
  config.orgId = orgId;
72
130
  }
73
131
 
132
+ if (identityPath) {
133
+ config.cliIdentityPath = path.resolve(identityPath);
134
+ }
135
+
74
136
  await updateConfig(config);
75
- } catch (error) {
137
+ } catch (error: unknown) {
76
138
  if (http.isHttpError(error) && error.response) {
77
- // error.body is the parsed JSON response body (set by http.ts when response is not ok)
78
- const errData = error.body ?? { message: "Unknown error" };
139
+ const errData = (error as any).body ?? { message: "Unknown error" };
79
140
  throw new Error(
80
- `Error: ${error.status} - ${errData?.message || "Unknown error"}`
141
+ `Error: ${(error as any).status} - ${errData?.message || "Unknown error"}`
81
142
  );
82
143
  }
83
144
  console.log(
@@ -112,7 +173,7 @@ export async function checkJwt(storedJwt: string) {
112
173
  const orgs = user.orgs;
113
174
  const orgId = response.data.orgId;
114
175
 
115
- const currentOrg = orgs.find((org) => {
176
+ const currentOrg = orgs.find((org: { organizationId: string }) => {
116
177
  return org.organizationId === orgId;
117
178
  });
118
179
 
@@ -3,8 +3,10 @@ import { IAgent } from "../agents/interface";
3
3
  import { EventService } from "./EventService";
4
4
  import { ToolsService } from "./Tools";
5
5
  import { ConfigAgent } from "../agents/configurable/ConfigAgent";
6
+ import { TraceAll } from "../util/Trace";
6
7
  import { AgentContext } from "../agents/base/base";
7
8
 
9
+ @TraceAll()
8
10
  export class AgentService {
9
11
  private agents: Map<string, IAgent> = new Map();
10
12
  private agentContext: AgentContext | null = null;
@@ -3,6 +3,7 @@
3
3
  * Handles synchronization via filesystem files in .knowhow/processes/agents/taskId/
4
4
  */
5
5
  import { BaseAgent } from "../agents/base/base";
6
+ import { TraceAll } from "../util/Trace";
6
7
  import { promises as fs } from "fs";
7
8
  import * as path from "path";
8
9
  import { watch } from "fs";
@@ -19,6 +20,7 @@ export interface FsSyncOptions {
19
20
  * AgentSyncFs handles filesystem-based synchronization for agent tasks
20
21
  * Creates files in .knowhow/processes/agents/{taskId}/ for status and input
21
22
  */
23
+ @TraceAll()
22
24
  export class AgentSyncFs {
23
25
  /** Shared cleanup interval across all instances to avoid duplicate cleanup runs */
24
26
  private static sharedCleanupInterval: NodeJS.Timeout | null = null;
@@ -128,6 +130,66 @@ export class AgentSyncFs {
128
130
  }
129
131
  }
130
132
 
133
+ /**
134
+ * Persist the remote identity alongside the local task. Keeping the local
135
+ * slug and remote UUID as separate fields lets resume reconnect without ever
136
+ * mistaking one for the other.
137
+ */
138
+ static async persistRemoteIdentity(
139
+ taskId: string,
140
+ remoteTaskId: string,
141
+ remoteMessageId?: string
142
+ ): Promise<void> {
143
+ const taskPath = path.join(AgentSyncFs.sharedBasePath, taskId);
144
+ await AgentSyncFs.writeRemoteIdentity(
145
+ taskPath,
146
+ remoteTaskId,
147
+ remoteMessageId
148
+ );
149
+ }
150
+
151
+ async setRemoteIdentity(
152
+ remoteTaskId: string,
153
+ remoteMessageId?: string
154
+ ): Promise<void> {
155
+ if (!this.taskPath) return;
156
+ await AgentSyncFs.writeRemoteIdentity(this.taskPath, remoteTaskId, remoteMessageId);
157
+ }
158
+
159
+ private static async writeRemoteIdentity(
160
+ taskPath: string,
161
+ remoteTaskId: string,
162
+ remoteMessageId?: string
163
+ ): Promise<void> {
164
+ try {
165
+ const metadataPath = path.join(taskPath, "metadata.json");
166
+ let metadata: any = {};
167
+ try {
168
+ metadata = JSON.parse(await fs.readFile(metadataPath, "utf8"));
169
+ } catch {
170
+ // The normal path already created metadata, but recover if it was
171
+ // removed or malformed while the task was running.
172
+ }
173
+
174
+ metadata.remoteTaskId = remoteTaskId;
175
+ // Preserve the established session field name for compatibility with
176
+ // callers that already read knowhowTaskId.
177
+ metadata.knowhowTaskId = remoteTaskId;
178
+ if (remoteMessageId) {
179
+ metadata.remoteMessageId = remoteMessageId;
180
+ metadata.knowhowMessageId = remoteMessageId;
181
+ }
182
+
183
+ await fs.writeFile(
184
+ metadataPath,
185
+ JSON.stringify(metadata, null, 2),
186
+ "utf8"
187
+ );
188
+ } catch (error) {
189
+ console.error(`❌ Failed to persist remote task identity:`, error);
190
+ }
191
+ }
192
+
131
193
  /**
132
194
  * Append a token usage entry (including the full message chain sent for
133
195
  * that call) to usage.json. This is intended for debugging cache-hit
@@ -217,6 +279,9 @@ export class AgentSyncFs {
217
279
  // thread messages via `_reasoning_details` / `reasoning_summary`.)
218
280
  metadata.reasoningEffort = agent.getReasoningEffort?.();
219
281
  metadata.summarizeReasoning = agent.getSummarizeReasoning?.();
282
+ // Tool schemas are part of the provider cache prefix. Preserve the exact
283
+ // request-visible list so resume/fork can recreate the same request.
284
+ metadata.enabledTools = agent.getEnabledToolNames();
220
285
  metadata.inProgress = inProgress;
221
286
  metadata.lastUpdate = new Date().toISOString();
222
287
 
@@ -10,6 +10,7 @@ import {
10
10
  } from "./KnowhowClient";
11
11
  import { BaseAgent } from "../agents/base/base";
12
12
  import { wait } from "../utils";
13
+ import { TraceAll } from "../util/Trace";
13
14
 
14
15
  export interface SyncOptions {
15
16
  messageId?: string;
@@ -27,6 +28,7 @@ export interface TaskSyncState {
27
28
  * AgentSynchronization handles all communication with the Knowhow API
28
29
  * for task creation, updates, status polling, and message synchronization
29
30
  */
31
+ @TraceAll()
30
32
  export class AgentSyncKnowhowWeb {
31
33
  private client: KnowhowSimpleClient;
32
34
  private baseUrl: string;
@@ -36,6 +38,7 @@ export class AgentSyncKnowhowWeb {
36
38
  private agent: BaseAgent | undefined;
37
39
  private threadUpdateHandler: ((...args: any[]) => void) | undefined;
38
40
  private doneHandler: ((...args: any[]) => void) | undefined;
41
+ private syncErrorLogged = false;
39
42
  /**
40
43
  * Tracks the most recent in-flight thread update API call.
41
44
  * The done handler awaits this before sending the finalization call,
@@ -48,6 +51,17 @@ export class AgentSyncKnowhowWeb {
48
51
  this.client = new KnowhowSimpleClient(baseUrl);
49
52
  }
50
53
 
54
+ /**
55
+ * Remote outages can make both the update and pending-message requests fail
56
+ * on every thread event. Report the first failure for this task, then stay
57
+ * quiet so an unavailable optional sync service does not flood the chat.
58
+ */
59
+ private reportSyncErrorOnce(message: string, error: unknown): void {
60
+ if (this.syncErrorLogged) return;
61
+ this.syncErrorLogged = true;
62
+ console.error(message, error);
63
+ }
64
+
51
65
  /**
52
66
  * Create a new chat task in Knowhow
53
67
  */
@@ -68,7 +82,7 @@ export class AgentSyncKnowhowWeb {
68
82
  console.log(`✅ Created Knowhow chat task: ${knowhowTaskId}`);
69
83
  return knowhowTaskId;
70
84
  } catch (error) {
71
- console.error(`❌ Failed to create Knowhow chat task:`, error);
85
+ this.reportSyncErrorOnce(`❌ Failed to create Knowhow chat task:`, error);
72
86
  return undefined;
73
87
  }
74
88
  }
@@ -95,7 +109,7 @@ export class AgentSyncKnowhowWeb {
95
109
  });
96
110
  console.log(`✅ Updated Knowhow chat task: ${knowhowTaskId}`);
97
111
  } catch (error) {
98
- console.error(`❌ Failed to update Knowhow chat task:`, error);
112
+ this.reportSyncErrorOnce(`❌ Failed to update Knowhow chat task:`, error);
99
113
  }
100
114
  }
101
115
 
@@ -167,7 +181,10 @@ export class AgentSyncKnowhowWeb {
167
181
  await this.client.markMessagesAsProcessed(knowhowTaskId, messageIds);
168
182
  console.log(`✅ Marked ${messageIds.length} message(s) as processed`);
169
183
  } catch (error) {
170
- console.error(`❌ Error checking/processing pending messages:`, error);
184
+ this.reportSyncErrorOnce(
185
+ `❌ Error checking/processing pending messages:`,
186
+ error
187
+ );
171
188
  // Continue execution even if synchronization fails
172
189
  }
173
190
  }
@@ -226,7 +243,7 @@ export class AgentSyncKnowhowWeb {
226
243
  }
227
244
  // Still paused, continue waiting
228
245
  } catch (error) {
229
- console.error(`❌ Error polling task status:`, error);
246
+ this.reportSyncErrorOnce(`❌ Error polling task status:`, error);
230
247
  // Continue polling even on errors
231
248
  }
232
249
  }
@@ -350,6 +367,7 @@ export class AgentSyncKnowhowWeb {
350
367
  this.eventHandlersSetup = false;
351
368
  this.finalizationPromise = null;
352
369
  this.pendingThreadUpdatePromise = null;
370
+ this.syncErrorLogged = false;
353
371
  }
354
372
 
355
373
  /**
@@ -4,6 +4,7 @@ import * as os from "os";
4
4
  import { KnowhowSimpleClient } from "./KnowhowClient";
5
5
  import { createHash } from "crypto";
6
6
  import { getConfig } from "../config";
7
+ import { TraceAll } from "../util/Trace";
7
8
 
8
9
  const BEHAVIORS_DIR = ".knowhow/behaviors";
9
10
  const LOCAL_SKILLS_DIRS = [".agents/skills", ".knowhow/skills"];
@@ -30,6 +31,7 @@ export interface Behavior {
30
31
  * In-memory singleton for behaviors/skills.
31
32
  * Call initFromDisk() once at startup; tools then read from memory.
32
33
  */
34
+ @TraceAll()
33
35
  export class BehaviorsService {
34
36
  private behaviors: Behavior[] = [];
35
37
  private skills: Behavior[] = [];
@@ -4,6 +4,7 @@ import * as os from "os";
4
4
  import { Config } from "../types";
5
5
  import { updateConfig } from "../config";
6
6
  import * as crypto from "crypto";
7
+ import { TraceAll } from "../util/Trace";
7
8
 
8
9
  export interface DockerWorkerOptions {
9
10
  workspaceDir: string;
@@ -14,6 +15,7 @@ export interface DockerWorkerOptions {
14
15
  unshare?: boolean;
15
16
  }
16
17
 
18
+ @TraceAll()
17
19
  export class DockerService {
18
20
  private static readonly IMAGE_NAME = "knowhow-worker";
19
21
  private static readonly CONTAINER_PREFIX = "knowhow-worker";
@@ -1,4 +1,5 @@
1
1
  import * as path from "path";
2
+ import { TraceAll } from "../util/Trace";
2
3
  import { globSync } from "glob";
3
4
 
4
5
  import {
@@ -38,6 +39,7 @@ interface EmbeddingServiceContext {
38
39
  plugins: PluginService;
39
40
  }
40
41
 
42
+ @TraceAll()
41
43
  export class EmbeddingService {
42
44
  private config: Config;
43
45
  private embeddingClient: GenericClient;
@@ -20,6 +20,9 @@ export interface EmbeddingsResolver {
20
20
  * Modules can register a resolver for a given remoteType (e.g. "github", "s3")
21
21
  * via registerResolver(), and the core upload()/download() functions delegate to it.
22
22
  */
23
+ import { TraceAll } from "../util/Trace";
24
+
25
+ @TraceAll()
23
26
  export class EmbeddingsService {
24
27
  private resolvers = new Map<string, EmbeddingsResolver>();
25
28
 
@@ -1,6 +1,10 @@
1
1
  import { createHash } from "crypto";
2
- import http from "../utils/http";
2
+ import { TraceAll } from "../util/Trace";
3
+ import http, { HTTP_UNAUTHORIZED_HANDLER, RefreshableHeaders } from "../utils/http";
3
4
  import fs from "fs";
5
+ import { getConfigSync } from "../config";
6
+ import { exchangePublicKeyJwt, storeJwt } from "../auth/keyAuth";
7
+ import { keyPairExists } from "../auth/keyManager";
4
8
  import { Message } from "../clients/types";
5
9
  import path from "path";
6
10
  import {
@@ -141,17 +145,32 @@ export function loadKnowhowJwt(): string {
141
145
  export const KNOWHOW_API_URL =
142
146
  process.env.KNOWHOW_API_URL || "https://api.knowhow.tyvm.ai";
143
147
 
148
+ @TraceAll()
144
149
  export class KnowhowSimpleClient {
145
- headers: Record<string, string> = {};
150
+ headers: RefreshableHeaders = {};
146
151
  jwtValidated = false;
152
+ private readonly canRefreshJwt: boolean;
153
+ private refreshPromise?: Promise<void>;
147
154
 
148
155
  constructor(
149
156
  private baseUrl = KNOWHOW_API_URL,
150
- private jwt = loadKnowhowJwt()
157
+ jwt?: string
151
158
  ) {
152
- this.setJwt(jwt);
159
+ // Explicit tokens (including KNOWHOW_JWT worker credentials) have no
160
+ // corresponding local identity and must never trigger interactive renewal.
161
+ this.canRefreshJwt = jwt === undefined && !process.env.KNOWHOW_JWT;
162
+ this.jwt = jwt ?? loadKnowhowJwt();
163
+ this.setJwt(this.jwt);
164
+ if (this.canRefreshJwt) {
165
+ Object.defineProperty(this.headers, HTTP_UNAUTHORIZED_HANDLER, {
166
+ enumerable: false,
167
+ value: () => this.renewJwt(true),
168
+ });
169
+ }
153
170
  }
154
171
 
172
+ private jwt: string;
173
+
155
174
  /**
156
175
  * Reload the JWT from disk (useful after login refreshes the token).
157
176
  */
@@ -165,9 +184,45 @@ export class KnowhowSimpleClient {
165
184
 
166
185
  setJwt(jwt: string) {
167
186
  this.jwt = jwt;
168
- this.headers = {
169
- Authorization: `Bearer ${this.jwt}`,
170
- };
187
+ // Keep this object stable: the HTTP wrapper retries with the same reference.
188
+ this.headers.Authorization = `Bearer ${this.jwt}`;
189
+ }
190
+
191
+ private jwtExpiresSoon(): boolean {
192
+ try {
193
+ const payload = JSON.parse(Buffer.from(this.jwt.split(".")[1], "base64url").toString("utf8"));
194
+ return typeof payload.exp === "number" && payload.exp * 1000 - Date.now() < 5 * 60 * 1000;
195
+ } catch {
196
+ return false;
197
+ }
198
+ }
199
+
200
+ private async renewJwt(force = false): Promise<void> {
201
+ if (!this.canRefreshJwt) return;
202
+ if (!force && !this.jwtExpiresSoon()) return;
203
+ if (this.refreshPromise) return this.refreshPromise;
204
+
205
+ this.refreshPromise = (async () => {
206
+ const config = getConfigSync();
207
+ const identityPath = config.cliIdentityPath;
208
+ if (!config.orgId || !keyPairExists(identityPath)) {
209
+ throw new Error("JWT expired and the CLI identity is unavailable. Please run `knowhow login` again.");
210
+ }
211
+ try {
212
+ const freshJwt = await exchangePublicKeyJwt(config.orgId, this.baseUrl, identityPath);
213
+ storeJwt(freshJwt);
214
+ this.setJwt(freshJwt);
215
+ this.jwtValidated = true;
216
+ } catch (error) {
217
+ throw new Error(
218
+ "Unable to renew the CLI session; the identity may have been revoked. Please run `knowhow login` again.",
219
+ { cause: error }
220
+ );
221
+ }
222
+ })().finally(() => {
223
+ this.refreshPromise = undefined;
224
+ });
225
+ return this.refreshPromise;
171
226
  }
172
227
 
173
228
  async checkJwt() {
@@ -175,6 +230,8 @@ export class KnowhowSimpleClient {
175
230
  throw new Error("No JWT found. Please login first.");
176
231
  }
177
232
 
233
+ await this.renewJwt();
234
+
178
235
  if (!this.jwtValidated) {
179
236
  try {
180
237
  this.jwtValidated = true;
@@ -186,6 +243,10 @@ export class KnowhowSimpleClient {
186
243
  return org.organizationId === orgId;
187
244
  });
188
245
  } catch (error) {
246
+ this.jwtValidated = false;
247
+ if (error instanceof Error && error.message.includes("renew the CLI session")) {
248
+ throw error;
249
+ }
189
250
  throw new Error("Invalid JWT. Please login again.");
190
251
  }
191
252
  }
@@ -1,4 +1,5 @@
1
1
  import { minimatch } from "minimatch";
2
+ import { TraceAll } from "../util/Trace";
2
3
  import { ToolsService, ToolContext, ToolCallContext } from "./Tools";
3
4
  import { Tool } from "../clients/types";
4
5
  import { ToolCall } from "../clients/types";
@@ -21,6 +22,7 @@ import { definitions } from "../agents/tools/lazy/definitions";
21
22
  * In web sessions where you get a new ToolService each interaction, then the cache busting is going to happen with each enable
22
23
  *
23
24
  */
25
+ @TraceAll()
24
26
  export class LazyToolsService extends ToolsService {
25
27
  private allTools: Tool[] = [];
26
28
  // Start with lazy tools enabled by default
@@ -104,6 +106,13 @@ export class LazyToolsService extends ToolsService {
104
106
  };
105
107
  }
106
108
 
109
+ /** Replace glob state with an exact persisted request-visible tool list. */
110
+ restoreEnabledTools(names: string[]): void {
111
+ this.enabledPatterns = [...new Set(names)];
112
+ this.disabledPatterns = [];
113
+ this.updateVisibleTools();
114
+ }
115
+
107
116
  // Disable tools matching glob patterns
108
117
  disableTools(patterns: string[]) {
109
118
  for (const pattern of patterns) {
@@ -6,6 +6,7 @@ import fs from "fs";
6
6
  import { McpConfig } from "../types";
7
7
  import { Tool } from "../clients";
8
8
  import { getConfig } from "../config";
9
+ import { TraceAll } from "../util/Trace";
9
10
  import { ToolsService } from "./Tools";
10
11
  import { Transport } from "@modelcontextprotocol/sdk/shared/transport.js";
11
12
  import { CallToolResultSchema } from "@modelcontextprotocol/sdk/types.js";
@@ -36,6 +37,7 @@ export * from "./McpWebsocketTransport";
36
37
  * Each of the tools are namespaced with a prefix: mcp_index_servername_toolName
37
38
  * This services handles calls made to the namespaced function name, and finds the proper client to call the tool on.
38
39
  */
40
+ @TraceAll()
39
41
  export class McpService {
40
42
  connected = [];
41
43
  transports: Transport[] = [];
@@ -6,9 +6,11 @@ import { Tool, ToolProp } from "../clients/types";
6
6
  import { ToolsService } from "./Tools";
7
7
  import { MCPWebSocketTransport } from "./McpWebsocketTransport";
8
8
  import { WebSocket } from "ws";
9
+ import { TraceAll } from "../util/Trace";
9
10
 
10
11
  export type McpServerConfig = { name: string; version: string };
11
12
 
13
+ @TraceAll()
12
14
  export class McpServerService {
13
15
  server: McpServer | null = null;
14
16
  constructor(private toolsService: ToolsService) {}
@@ -1,4 +1,5 @@
1
1
  import { Message } from "../clients/types";
2
+ import { TraceAll } from "../util/Trace";
2
3
 
3
4
  export type ProcessorLifecycle =
4
5
  | "initial_call"
@@ -17,6 +18,7 @@ export interface ProcessorRegistration {
17
18
  priority: number;
18
19
  }
19
20
 
21
+ @TraceAll()
20
22
  export class MessageProcessor {
21
23
  private processors: Map<ProcessorLifecycle, ProcessorRegistration[]> =
22
24
  new Map();
@@ -5,6 +5,7 @@ import { inspectTools } from "../agents/tools/minimal/inspectTools";
5
5
  import { minimalDefinitions } from "../agents/tools/minimal/definitions";
6
6
  import { finalAnswer } from "../agents/tools/finalAnswer";
7
7
  import { includedTools } from "../agents/tools/list";
8
+ import { TraceAll } from "../util/Trace";
8
9
 
9
10
  /*
10
11
  * MinimalToolsService
@@ -19,6 +20,7 @@ import { includedTools } from "../agents/tools/list";
19
20
  * - Works on all providers and older models (no provider-specific defer_loading needed).
20
21
  * - Agents discover tools via inspectTools() and call them via callTool().
21
22
  */
23
+ @TraceAll()
22
24
  export class MinimalToolsService extends ToolsService {
23
25
  /** Full catalog of every registered tool (base + extended). */
24
26
  private allToolsCatalog: Tool[] = [];
@@ -5,10 +5,12 @@ import * as fs from "fs";
5
5
  import * as fsPromises from "fs/promises";
6
6
  import * as path from "path";
7
7
  import { TaskInfo, ChatSession } from "../chat/types";
8
+ import { TraceAll } from "../util/Trace";
8
9
 
9
10
  /**
10
11
  * SessionManager handles saving, loading, and managing agent sessions
11
12
  */
13
+ @TraceAll()
12
14
  export class SessionManager {
13
15
  private sessionsDir: string;
14
16
 
@@ -69,6 +71,7 @@ export class SessionManager {
69
71
  provider: taskInfo.provider,
70
72
  reasoningEffort: taskInfo.reasoningEffort,
71
73
  summarizeReasoning: taskInfo.summarizeReasoning,
74
+ enabledTools: taskInfo.enabledTools,
72
75
  };
73
76
 
74
77
  fs.writeFileSync(sessionPath, JSON.stringify(session, null, 2));
@@ -115,6 +118,8 @@ export class SessionManager {
115
118
  session.reasoningEffort = taskInfo.reasoningEffort;
116
119
  if (taskInfo.summarizeReasoning !== undefined)
117
120
  session.summarizeReasoning = taskInfo.summarizeReasoning;
121
+ if (taskInfo.enabledTools !== undefined)
122
+ session.enabledTools = taskInfo.enabledTools;
118
123
  }
119
124
 
120
125
  fs.writeFileSync(sessionPath, JSON.stringify(session, null, 2));
@@ -4,6 +4,7 @@ import * as os from "os";
4
4
  import { createHash } from "crypto";
5
5
  import * as https from "https";
6
6
  import { KnowhowSimpleClient } from "./KnowhowClient";
7
+ import { TraceAll } from "../util/Trace";
7
8
 
8
9
  // Default local install dir (relative to cwd)
9
10
  const LOCAL_SKILLS_DIR = ".knowhow/skills";
@@ -233,6 +234,7 @@ export function parseSkillRef(
233
234
  return { owner, repo, skill, sourceUrl };
234
235
  }
235
236
 
237
+ @TraceAll()
236
238
  export class SkillsService {
237
239
  private cwd: string;
238
240
  private isGlobal: boolean;