@tyvm/knowhow 0.0.83 → 0.0.85

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 (237) hide show
  1. package/package.json +4 -2
  2. package/src/agents/base/base.ts +72 -62
  3. package/src/agents/index.ts +30 -14
  4. package/src/agents/researcher/researcher.ts +1 -2
  5. package/src/agents/tools/aiClient.ts +48 -0
  6. package/src/agents/tools/list.ts +57 -0
  7. package/src/agents/tools/startAgentTask.ts +3 -1
  8. package/src/chat/CliChatService.ts +20 -4
  9. package/src/chat/modules/AgentModule.ts +399 -357
  10. package/src/chat/modules/CustomCommandsModule.ts +0 -1
  11. package/src/chat/modules/InternalChatModule.ts +18 -2
  12. package/src/chat/modules/RendererModule.ts +109 -0
  13. package/src/chat/modules/SessionsModule.ts +854 -0
  14. package/src/chat/modules/SetupModule.ts +6 -8
  15. package/src/chat/modules/index.ts +1 -0
  16. package/src/chat/renderer/CompactRenderer.ts +209 -0
  17. package/src/chat/renderer/ConsoleRenderer.ts +141 -0
  18. package/src/chat/renderer/FancyRenderer.ts +421 -0
  19. package/src/chat/renderer/index.ts +5 -0
  20. package/src/chat/renderer/loadRenderer.ts +314 -0
  21. package/src/chat/renderer/messagesToRenderEvents.ts +96 -0
  22. package/src/chat/renderer/types.ts +88 -0
  23. package/src/chat/types.ts +5 -0
  24. package/src/chat.ts +69 -5
  25. package/src/cli.ts +24 -5
  26. package/src/clients/index.ts +91 -0
  27. package/src/clients/pricing/google.ts +81 -2
  28. package/src/clients/pricing/openai.ts +68 -0
  29. package/src/config.ts +15 -0
  30. package/src/plugins/AgentsMdPlugin.ts +1 -1
  31. package/src/plugins/GitPlugin.ts +20 -20
  32. package/src/plugins/PluginBase.ts +11 -0
  33. package/src/plugins/SkillsPlugin.ts +150 -0
  34. package/src/plugins/asana.ts +4 -4
  35. package/src/plugins/embedding.ts +3 -5
  36. package/src/plugins/exec.ts +3 -3
  37. package/src/plugins/figma.ts +3 -7
  38. package/src/plugins/github.ts +18 -29
  39. package/src/plugins/jira.ts +2 -2
  40. package/src/plugins/language.ts +4 -4
  41. package/src/plugins/linear.ts +4 -4
  42. package/src/plugins/notion.ts +6 -8
  43. package/src/plugins/plugins.ts +29 -3
  44. package/src/plugins/url.ts +2 -2
  45. package/src/plugins/vim.ts +4 -3
  46. package/src/services/AgentService.ts +17 -0
  47. package/src/services/AgentSyncFs.ts +3 -0
  48. package/src/services/EventService.ts +168 -27
  49. package/src/services/KnowhowClient.ts +1 -0
  50. package/src/services/SessionManager.ts +51 -1
  51. package/src/services/SyncedAgentWatcher.ts +397 -0
  52. package/src/services/SyncerService.ts +147 -0
  53. package/src/services/index.ts +2 -0
  54. package/src/services/modules/index.ts +14 -3
  55. package/src/types.ts +103 -5
  56. package/src/worker.ts +80 -2
  57. package/src/workers/auth/PasskeySetup.ts +185 -0
  58. package/src/workers/auth/WorkerPasskeyAuth.ts +190 -0
  59. package/src/workers/auth/types.ts +58 -0
  60. package/src/workers/tools/getChallenge.ts +33 -0
  61. package/src/workers/tools/index.ts +8 -0
  62. package/src/workers/tools/lock.ts +31 -0
  63. package/src/workers/tools/unlock.ts +116 -0
  64. package/tests/clients/pricing.test.ts +144 -0
  65. package/tests/unit/modules/moduleLoading.test.ts +226 -0
  66. package/tests/unit/plugins/pluginLoading.test.ts +151 -0
  67. package/ts_build/package.json +4 -2
  68. package/ts_build/src/agents/base/base.d.ts +4 -3
  69. package/ts_build/src/agents/base/base.js +54 -30
  70. package/ts_build/src/agents/base/base.js.map +1 -1
  71. package/ts_build/src/agents/index.d.ts +3 -0
  72. package/ts_build/src/agents/index.js +21 -11
  73. package/ts_build/src/agents/index.js.map +1 -1
  74. package/ts_build/src/agents/researcher/researcher.js +1 -1
  75. package/ts_build/src/agents/researcher/researcher.js.map +1 -1
  76. package/ts_build/src/agents/tools/aiClient.d.ts +3 -0
  77. package/ts_build/src/agents/tools/aiClient.js +31 -1
  78. package/ts_build/src/agents/tools/aiClient.js.map +1 -1
  79. package/ts_build/src/agents/tools/list.js +48 -0
  80. package/ts_build/src/agents/tools/list.js.map +1 -1
  81. package/ts_build/src/agents/tools/startAgentTask.js +2 -1
  82. package/ts_build/src/agents/tools/startAgentTask.js.map +1 -1
  83. package/ts_build/src/chat/CliChatService.js +16 -5
  84. package/ts_build/src/chat/CliChatService.js.map +1 -1
  85. package/ts_build/src/chat/modules/AgentModule.d.ts +34 -17
  86. package/ts_build/src/chat/modules/AgentModule.js +248 -258
  87. package/ts_build/src/chat/modules/AgentModule.js.map +1 -1
  88. package/ts_build/src/chat/modules/CustomCommandsModule.js.map +1 -1
  89. package/ts_build/src/chat/modules/InternalChatModule.d.ts +3 -0
  90. package/ts_build/src/chat/modules/InternalChatModule.js +16 -1
  91. package/ts_build/src/chat/modules/InternalChatModule.js.map +1 -1
  92. package/ts_build/src/chat/modules/RendererModule.d.ts +16 -0
  93. package/ts_build/src/chat/modules/RendererModule.js +76 -0
  94. package/ts_build/src/chat/modules/RendererModule.js.map +1 -0
  95. package/ts_build/src/chat/modules/SessionsModule.d.ts +33 -0
  96. package/ts_build/src/chat/modules/SessionsModule.js +582 -0
  97. package/ts_build/src/chat/modules/SessionsModule.js.map +1 -0
  98. package/ts_build/src/chat/modules/SetupModule.d.ts +3 -3
  99. package/ts_build/src/chat/modules/SetupModule.js +4 -6
  100. package/ts_build/src/chat/modules/SetupModule.js.map +1 -1
  101. package/ts_build/src/chat/modules/index.d.ts +1 -0
  102. package/ts_build/src/chat/modules/index.js +3 -1
  103. package/ts_build/src/chat/modules/index.js.map +1 -1
  104. package/ts_build/src/chat/renderer/CompactRenderer.d.ts +23 -0
  105. package/ts_build/src/chat/renderer/CompactRenderer.js +167 -0
  106. package/ts_build/src/chat/renderer/CompactRenderer.js.map +1 -0
  107. package/ts_build/src/chat/renderer/ConsoleRenderer.d.ts +22 -0
  108. package/ts_build/src/chat/renderer/ConsoleRenderer.js +110 -0
  109. package/ts_build/src/chat/renderer/ConsoleRenderer.js.map +1 -0
  110. package/ts_build/src/chat/renderer/FancyRenderer.d.ts +23 -0
  111. package/ts_build/src/chat/renderer/FancyRenderer.js +328 -0
  112. package/ts_build/src/chat/renderer/FancyRenderer.js.map +1 -0
  113. package/ts_build/src/chat/renderer/index.d.ts +5 -0
  114. package/ts_build/src/chat/renderer/index.js +29 -0
  115. package/ts_build/src/chat/renderer/index.js.map +1 -0
  116. package/ts_build/src/chat/renderer/loadRenderer.d.ts +4 -0
  117. package/ts_build/src/chat/renderer/loadRenderer.js +246 -0
  118. package/ts_build/src/chat/renderer/loadRenderer.js.map +1 -0
  119. package/ts_build/src/chat/renderer/messagesToRenderEvents.d.ts +15 -0
  120. package/ts_build/src/chat/renderer/messagesToRenderEvents.js +72 -0
  121. package/ts_build/src/chat/renderer/messagesToRenderEvents.js.map +1 -0
  122. package/ts_build/src/chat/renderer/types.d.ts +75 -0
  123. package/ts_build/src/chat/renderer/types.js +3 -0
  124. package/ts_build/src/chat/renderer/types.js.map +1 -0
  125. package/ts_build/src/chat/types.d.ts +5 -0
  126. package/ts_build/src/chat.js +46 -4
  127. package/ts_build/src/chat.js.map +1 -1
  128. package/ts_build/src/cli.js +18 -5
  129. package/ts_build/src/cli.js.map +1 -1
  130. package/ts_build/src/clients/gemini.d.ts +10 -10
  131. package/ts_build/src/clients/index.d.ts +10 -0
  132. package/ts_build/src/clients/index.js +58 -0
  133. package/ts_build/src/clients/index.js.map +1 -1
  134. package/ts_build/src/clients/pricing/google.d.ts +10 -10
  135. package/ts_build/src/clients/pricing/google.js +74 -2
  136. package/ts_build/src/clients/pricing/google.js.map +1 -1
  137. package/ts_build/src/clients/pricing/openai.js +65 -0
  138. package/ts_build/src/clients/pricing/openai.js.map +1 -1
  139. package/ts_build/src/config.d.ts +1 -0
  140. package/ts_build/src/config.js +17 -1
  141. package/ts_build/src/config.js.map +1 -1
  142. package/ts_build/src/plugins/AgentsMdPlugin.js +1 -1
  143. package/ts_build/src/plugins/AgentsMdPlugin.js.map +1 -1
  144. package/ts_build/src/plugins/GitPlugin.js +20 -20
  145. package/ts_build/src/plugins/GitPlugin.js.map +1 -1
  146. package/ts_build/src/plugins/PluginBase.d.ts +1 -0
  147. package/ts_build/src/plugins/PluginBase.js +13 -0
  148. package/ts_build/src/plugins/PluginBase.js.map +1 -1
  149. package/ts_build/src/plugins/SkillsPlugin.d.ts +13 -0
  150. package/ts_build/src/plugins/SkillsPlugin.js +149 -0
  151. package/ts_build/src/plugins/SkillsPlugin.js.map +1 -0
  152. package/ts_build/src/plugins/asana.js +4 -4
  153. package/ts_build/src/plugins/asana.js.map +1 -1
  154. package/ts_build/src/plugins/embedding.js +3 -3
  155. package/ts_build/src/plugins/embedding.js.map +1 -1
  156. package/ts_build/src/plugins/exec.js +3 -3
  157. package/ts_build/src/plugins/exec.js.map +1 -1
  158. package/ts_build/src/plugins/figma.js +3 -3
  159. package/ts_build/src/plugins/figma.js.map +1 -1
  160. package/ts_build/src/plugins/github.js +18 -18
  161. package/ts_build/src/plugins/github.js.map +1 -1
  162. package/ts_build/src/plugins/jira.js +2 -2
  163. package/ts_build/src/plugins/jira.js.map +1 -1
  164. package/ts_build/src/plugins/language.js +4 -4
  165. package/ts_build/src/plugins/language.js.map +1 -1
  166. package/ts_build/src/plugins/linear.js +4 -4
  167. package/ts_build/src/plugins/linear.js.map +1 -1
  168. package/ts_build/src/plugins/notion.js +6 -6
  169. package/ts_build/src/plugins/notion.js.map +1 -1
  170. package/ts_build/src/plugins/plugins.d.ts +3 -0
  171. package/ts_build/src/plugins/plugins.js +18 -3
  172. package/ts_build/src/plugins/plugins.js.map +1 -1
  173. package/ts_build/src/plugins/url.js +2 -2
  174. package/ts_build/src/plugins/url.js.map +1 -1
  175. package/ts_build/src/plugins/vim.js +2 -2
  176. package/ts_build/src/plugins/vim.js.map +1 -1
  177. package/ts_build/src/services/AgentService.d.ts +3 -0
  178. package/ts_build/src/services/AgentService.js +7 -0
  179. package/ts_build/src/services/AgentService.js.map +1 -1
  180. package/ts_build/src/services/AgentSyncFs.d.ts +1 -0
  181. package/ts_build/src/services/AgentSyncFs.js +2 -0
  182. package/ts_build/src/services/AgentSyncFs.js.map +1 -1
  183. package/ts_build/src/services/EventService.d.ts +25 -2
  184. package/ts_build/src/services/EventService.js +92 -14
  185. package/ts_build/src/services/EventService.js.map +1 -1
  186. package/ts_build/src/services/KnowhowClient.d.ts +1 -0
  187. package/ts_build/src/services/KnowhowClient.js.map +1 -1
  188. package/ts_build/src/services/SessionManager.d.ts +6 -0
  189. package/ts_build/src/services/SessionManager.js +39 -1
  190. package/ts_build/src/services/SessionManager.js.map +1 -1
  191. package/ts_build/src/services/SyncedAgentWatcher.d.ts +101 -0
  192. package/ts_build/src/services/SyncedAgentWatcher.js +312 -0
  193. package/ts_build/src/services/SyncedAgentWatcher.js.map +1 -0
  194. package/ts_build/src/services/SyncerService.d.ts +30 -0
  195. package/ts_build/src/services/SyncerService.js +72 -0
  196. package/ts_build/src/services/SyncerService.js.map +1 -0
  197. package/ts_build/src/services/index.d.ts +2 -0
  198. package/ts_build/src/services/index.js +2 -0
  199. package/ts_build/src/services/index.js.map +1 -1
  200. package/ts_build/src/services/modules/index.js +10 -2
  201. package/ts_build/src/services/modules/index.js.map +1 -1
  202. package/ts_build/src/types.d.ts +51 -2
  203. package/ts_build/src/types.js +73 -5
  204. package/ts_build/src/types.js.map +1 -1
  205. package/ts_build/src/worker.d.ts +2 -0
  206. package/ts_build/src/worker.js +59 -4
  207. package/ts_build/src/worker.js.map +1 -1
  208. package/ts_build/src/workers/auth/PasskeySetup.d.ts +10 -0
  209. package/ts_build/src/workers/auth/PasskeySetup.js +131 -0
  210. package/ts_build/src/workers/auth/PasskeySetup.js.map +1 -0
  211. package/ts_build/src/workers/auth/WorkerPasskeyAuth.d.ts +35 -0
  212. package/ts_build/src/workers/auth/WorkerPasskeyAuth.js +129 -0
  213. package/ts_build/src/workers/auth/WorkerPasskeyAuth.js.map +1 -0
  214. package/ts_build/src/workers/auth/types.d.ts +36 -0
  215. package/ts_build/src/workers/auth/types.js +3 -0
  216. package/ts_build/src/workers/auth/types.js.map +1 -0
  217. package/ts_build/src/workers/tools/getChallenge.d.ts +9 -0
  218. package/ts_build/src/workers/tools/getChallenge.js +27 -0
  219. package/ts_build/src/workers/tools/getChallenge.js.map +1 -0
  220. package/ts_build/src/workers/tools/index.d.ts +6 -0
  221. package/ts_build/src/workers/tools/index.js +10 -0
  222. package/ts_build/src/workers/tools/index.js.map +1 -1
  223. package/ts_build/src/workers/tools/lock.d.ts +9 -0
  224. package/ts_build/src/workers/tools/lock.js +27 -0
  225. package/ts_build/src/workers/tools/lock.js.map +1 -0
  226. package/ts_build/src/workers/tools/unlock.d.ts +18 -0
  227. package/ts_build/src/workers/tools/unlock.js +78 -0
  228. package/ts_build/src/workers/tools/unlock.js.map +1 -0
  229. package/ts_build/tests/clients/pricing.test.d.ts +1 -0
  230. package/ts_build/tests/clients/pricing.test.js +90 -0
  231. package/ts_build/tests/clients/pricing.test.js.map +1 -0
  232. package/ts_build/tests/unit/modules/moduleLoading.test.d.ts +1 -0
  233. package/ts_build/tests/unit/modules/moduleLoading.test.js +187 -0
  234. package/ts_build/tests/unit/modules/moduleLoading.test.js.map +1 -0
  235. package/ts_build/tests/unit/plugins/pluginLoading.test.d.ts +1 -0
  236. package/ts_build/tests/unit/plugins/pluginLoading.test.js +123 -0
  237. package/ts_build/tests/unit/plugins/pluginLoading.test.js.map +1 -0
@@ -0,0 +1,58 @@
1
+ /**
2
+ * Auth message types for the worker WebSocket authentication protocol.
3
+ */
4
+
5
+ // Worker → Client: challenge
6
+ export interface AuthChallengeMessage {
7
+ type: "auth:challenge";
8
+ challenge: string; // base64-encoded 32 random bytes
9
+ timestamp: number; // epoch seconds
10
+ }
11
+
12
+ // Client → Worker: response
13
+ export interface AuthResponseMessage {
14
+ type: "auth:response";
15
+ challenge: string; // echo back
16
+ signature: string; // base64-encoded WebAuthn assertion signature
17
+ credentialId: string; // base64-encoded credential ID
18
+ authenticatorData: string; // base64-encoded authenticator data
19
+ clientDataJSON: string; // base64-encoded client data JSON
20
+ }
21
+
22
+ // Worker → Client: success
23
+ export interface AuthSuccessMessage {
24
+ type: "auth:success";
25
+ token: string; // opaque session token
26
+ expiresAt: number; // epoch seconds
27
+ }
28
+
29
+ // Worker → Client: failure
30
+ export interface AuthFailureMessage {
31
+ type: "auth:failure";
32
+ reason: "invalid_signature" | "expired" | "unknown_credential";
33
+ }
34
+
35
+ export type AuthMessage =
36
+ | AuthChallengeMessage
37
+ | AuthResponseMessage
38
+ | AuthSuccessMessage
39
+ | AuthFailureMessage;
40
+
41
+ // Passkey credential stored in config
42
+ export interface PasskeyCredential {
43
+ publicKey: string; // base64-encoded public key
44
+ credentialId: string; // base64-encoded credential ID
45
+ algorithm: string; // e.g. "ES256"
46
+ }
47
+
48
+ // Setup session returned by knowhow-web
49
+ export interface PasskeySetupSession {
50
+ sessionId: string;
51
+ browserUrl: string;
52
+ }
53
+
54
+ // Status of a passkey setup session
55
+ export interface PasskeySetupStatus {
56
+ status: "pending" | "complete" | "expired";
57
+ credential?: PasskeyCredential;
58
+ }
@@ -0,0 +1,33 @@
1
+ import { Tool } from "../../clients/types";
2
+ import { WorkerPasskeyAuthService } from "../auth/WorkerPasskeyAuth";
3
+
4
+ /**
5
+ * Returns a challenge for the client to sign with their passkey.
6
+ * The challenge must be signed and passed to the `unlock` tool.
7
+ */
8
+ export function makeGetChallengeTool(authService: WorkerPasskeyAuthService) {
9
+ async function getChallenge(): Promise<{ challenge: string; message: string }> {
10
+ const challenge = authService.generateChallenge();
11
+ return {
12
+ challenge,
13
+ message:
14
+ "Sign this challenge with your passkey and call the `unlock` tool with the assertion data.",
15
+ };
16
+ }
17
+
18
+ const getChallengeDefinition: Tool = {
19
+ type: "function" as const,
20
+ function: {
21
+ name: "getChallenge",
22
+ description:
23
+ "Get a challenge string to sign with your passkey. Required before calling `unlock`. Returns a base64url-encoded challenge.",
24
+ parameters: {
25
+ type: "object",
26
+ properties: {},
27
+ required: [],
28
+ },
29
+ },
30
+ };
31
+
32
+ return { getChallenge, getChallengeDefinition };
33
+ }
@@ -1,9 +1,17 @@
1
1
  export * from "./listAllowedPorts";
2
+ export * from "./getChallenge";
3
+ export * from "./unlock";
4
+ export * from "./lock";
5
+
2
6
  import {
3
7
  listAllowedPorts,
4
8
  listAllowedPortsDefinition,
5
9
  } from "./listAllowedPorts";
6
10
 
11
+ export { makeGetChallengeTool } from "./getChallenge";
12
+ export { makeUnlockTool } from "./unlock";
13
+ export { makeLockTool } from "./lock";
14
+
7
15
  export default {
8
16
  tools: { listAllowedPorts },
9
17
  definitions: [listAllowedPortsDefinition],
@@ -0,0 +1,31 @@
1
+ import { Tool } from "../../clients/types";
2
+ import { WorkerPasskeyAuthService } from "../auth/WorkerPasskeyAuth";
3
+
4
+ /**
5
+ * Re-locks the worker, requiring passkey authentication again for further tool access.
6
+ */
7
+ export function makeLockTool(authService: WorkerPasskeyAuthService) {
8
+ async function lock(): Promise<{ success: boolean; message: string }> {
9
+ authService.lock();
10
+ return {
11
+ success: true,
12
+ message: "Worker locked. Call getChallenge and unlock to regain access.",
13
+ };
14
+ }
15
+
16
+ const lockDefinition: Tool = {
17
+ type: "function" as const,
18
+ function: {
19
+ name: "lock",
20
+ description:
21
+ "Lock the worker. After locking, only getChallenge, unlock, and lock tools will be accessible until the worker is unlocked again with a valid passkey assertion.",
22
+ parameters: {
23
+ type: "object",
24
+ properties: {},
25
+ required: [],
26
+ },
27
+ },
28
+ };
29
+
30
+ return { lock, lockDefinition };
31
+ }
@@ -0,0 +1,116 @@
1
+ import { Tool } from "../../clients/types";
2
+ import { WorkerPasskeyAuthService } from "../auth/WorkerPasskeyAuth";
3
+
4
+ export interface UnlockToolParams {
5
+ /** base64url-encoded signature from WebAuthn assertion */
6
+ signature?: string;
7
+ /** base64url-encoded credential ID */
8
+ credentialId?: string;
9
+ /** base64url-encoded authenticatorData */
10
+ authenticatorData?: string;
11
+ /** base64url-encoded clientDataJSON */
12
+ clientDataJSON?: string;
13
+ /** The challenge that was signed (base64url) */
14
+ challenge?: string;
15
+ }
16
+
17
+ /**
18
+ * Combined challenge+unlock tool.
19
+ *
20
+ * - Called with NO params (or missing assertion fields): generates and returns a challenge.
21
+ * - Called WITH all assertion fields: verifies the WebAuthn assertion and unlocks the worker.
22
+ *
23
+ * This two-step flow lets the frontend call the tool once to get a challenge,
24
+ * do the browser WebAuthn assertion, then call again with the signed data.
25
+ */
26
+ export function makeUnlockTool(authService: WorkerPasskeyAuthService) {
27
+ async function unlock(
28
+ params: UnlockToolParams = {}
29
+ ): Promise<{ success: boolean; message: string; challenge?: string; credentialId?: string }> {
30
+ const hasAssertion =
31
+ params.signature &&
32
+ params.credentialId &&
33
+ params.authenticatorData &&
34
+ params.clientDataJSON &&
35
+ params.challenge;
36
+
37
+ // Step 1: no assertion data → generate and return a challenge
38
+ if (!hasAssertion) {
39
+ const challenge = authService.generateChallenge();
40
+ return {
41
+ success: false,
42
+ challenge,
43
+ credentialId: authService.getCredentialId(),
44
+ message:
45
+ "Sign this challenge with your passkey and call unlock again with the assertion data.",
46
+ };
47
+ }
48
+
49
+ // Step 2: verify assertion and unlock
50
+ const result = await authService.unlock({
51
+ signature: params.signature!,
52
+ credentialId: params.credentialId!,
53
+ authenticatorData: params.authenticatorData!,
54
+ clientDataJSON: params.clientDataJSON!,
55
+ challenge: params.challenge!,
56
+ });
57
+
58
+ if (result.success) {
59
+ const info = authService.getSessionInfo();
60
+ return {
61
+ success: true,
62
+ message: `Worker unlocked successfully. Session expires at ${info.expiresAt}.`,
63
+ };
64
+ }
65
+
66
+ return {
67
+ success: false,
68
+ message: `Unlock failed: ${result.reason}`,
69
+ };
70
+ }
71
+
72
+ const unlockDefinition: Tool = {
73
+ type: "function" as const,
74
+ function: {
75
+ name: "unlock",
76
+ description:
77
+ "Unlock the worker using a passkey. " +
78
+ "Call with NO parameters to receive a challenge string. " +
79
+ "Then sign the challenge via the browser WebAuthn API (navigator.credentials.get) and call again with the assertion data to unlock. " +
80
+ "All other tools are blocked until the worker is unlocked.",
81
+ parameters: {
82
+ type: "object",
83
+ properties: {
84
+ signature: {
85
+ type: "string",
86
+ description:
87
+ "base64url-encoded signature from WebAuthn assertion response.signature",
88
+ },
89
+ credentialId: {
90
+ type: "string",
91
+ description:
92
+ "base64url-encoded credential ID from WebAuthn assertion",
93
+ },
94
+ authenticatorData: {
95
+ type: "string",
96
+ description:
97
+ "base64url-encoded authenticatorData from WebAuthn assertion response.authenticatorData",
98
+ },
99
+ clientDataJSON: {
100
+ type: "string",
101
+ description:
102
+ "base64url-encoded clientDataJSON from WebAuthn assertion response.clientDataJSON",
103
+ },
104
+ challenge: {
105
+ type: "string",
106
+ description:
107
+ "The challenge string returned by the first unlock() call (base64url)",
108
+ },
109
+ },
110
+ required: [],
111
+ },
112
+ },
113
+ };
114
+
115
+ return { unlock, unlockDefinition };
116
+ }
@@ -0,0 +1,144 @@
1
+ /**
2
+ * Test that verifies every model supported by the Clients has a corresponding
3
+ * pricing entry. This ensures we never release model support without knowing the price.
4
+ *
5
+ * Models that are image-only, video-only, TTS, transcription, realtime, or live streaming
6
+ * are exempt from text pricing requirements — they should have their own pricing entries
7
+ * in the appropriate pricing tables (image, video, audio, etc.).
8
+ */
9
+
10
+ import {
11
+ Models,
12
+ EmbeddingModels,
13
+ GoogleImageModels,
14
+ GoogleVideoModels,
15
+ GoogleTTSModels,
16
+ OpenAiImageModels,
17
+ OpenAiVideoModels,
18
+ OpenAiTTSModels,
19
+ OpenAiTranscriptionModels,
20
+ OpenAiRealtimeModels,
21
+ XaiImageModels,
22
+ XaiVideoModels,
23
+ } from "../../src/types";
24
+
25
+ import {
26
+ OpenAiTextPricing,
27
+ GeminiTextPricing,
28
+ AnthropicTextPricing,
29
+ XaiTextPricing,
30
+ XaiImagePricing,
31
+ XaiVideoPricing,
32
+ } from "../../src/clients/pricing";
33
+
34
+ describe("Model Pricing Coverage", () => {
35
+ /**
36
+ * Models that are exclusively non-text modalities (image, video, TTS, transcription,
37
+ * realtime/live audio) and therefore do NOT need a text token pricing entry.
38
+ * They still must appear in their own modality-specific pricing table (see separate tests below).
39
+ */
40
+ const nonTextModels = new Set<string>([
41
+ ...GoogleImageModels,
42
+ ...GoogleVideoModels,
43
+ ...GoogleTTSModels,
44
+ ...OpenAiImageModels,
45
+ ...OpenAiVideoModels,
46
+ ...OpenAiTTSModels,
47
+ ...OpenAiTranscriptionModels,
48
+ ...OpenAiRealtimeModels,
49
+ ...XaiImageModels,
50
+ ...XaiVideoModels,
51
+ // Live streaming model — not a standard text completion model
52
+ Models.google.Gemini_20_Flash_Live,
53
+ ]);
54
+
55
+ const allTextPricing: Record<string, unknown> = {
56
+ ...OpenAiTextPricing,
57
+ ...GeminiTextPricing,
58
+ ...AnthropicTextPricing,
59
+ ...XaiTextPricing,
60
+ };
61
+
62
+ describe("Text completion models have pricing", () => {
63
+ for (const [provider, providerModels] of Object.entries(Models)) {
64
+ for (const [modelKey, modelId] of Object.entries(
65
+ providerModels as Record<string, string>
66
+ )) {
67
+ if (nonTextModels.has(modelId)) {
68
+ // Skip — covered by modality-specific tests below
69
+ continue;
70
+ }
71
+
72
+ it(`${provider}.${modelKey} (${modelId}) has text pricing`, () => {
73
+ const entry = allTextPricing[modelId];
74
+ expect(entry).toBeDefined();
75
+ expect(entry).toEqual(
76
+ expect.objectContaining({ input: expect.any(Number) })
77
+ );
78
+ });
79
+ }
80
+ }
81
+ });
82
+
83
+ describe("Embedding models have pricing", () => {
84
+ const allEmbeddingPricing: Record<string, unknown> = {
85
+ ...OpenAiTextPricing, // OpenAI embeddings are in the text pricing table
86
+ ...GeminiTextPricing, // Google embeddings are in the Gemini pricing table
87
+ };
88
+
89
+ for (const [provider, providerModels] of Object.entries(EmbeddingModels)) {
90
+ for (const [modelKey, modelId] of Object.entries(
91
+ providerModels as Record<string, string>
92
+ )) {
93
+ it(`EmbeddingModels.${provider}.${modelKey} (${modelId}) has pricing`, () => {
94
+ const entry = allEmbeddingPricing[modelId];
95
+ expect(entry).toBeDefined();
96
+ });
97
+ }
98
+ }
99
+ });
100
+
101
+ describe("XAI image models have image pricing", () => {
102
+ const xaiImagePricing = XaiImagePricing as Record<string, unknown>;
103
+
104
+ for (const modelId of XaiImageModels) {
105
+ it(`XAI image model (${modelId}) has image pricing`, () => {
106
+ const entry = xaiImagePricing[modelId];
107
+ expect(entry).toBeDefined();
108
+ });
109
+ }
110
+ });
111
+
112
+ describe("XAI video models have video pricing", () => {
113
+ const xaiVideoPricing = XaiVideoPricing as Record<string, unknown>;
114
+
115
+ for (const modelId of XaiVideoModels) {
116
+ it(`XAI video model (${modelId}) has video pricing`, () => {
117
+ const entry = xaiVideoPricing[modelId];
118
+ expect(entry).toBeDefined();
119
+ });
120
+ }
121
+ });
122
+
123
+ describe("Google image models have pricing", () => {
124
+ const geminiPricing = GeminiTextPricing as Record<string, unknown>;
125
+
126
+ for (const modelId of GoogleImageModels) {
127
+ it(`Google image model (${modelId}) has pricing in GeminiTextPricing`, () => {
128
+ const entry = geminiPricing[modelId];
129
+ expect(entry).toBeDefined();
130
+ });
131
+ }
132
+ });
133
+
134
+ describe("Google video models have pricing", () => {
135
+ const geminiPricing = GeminiTextPricing as Record<string, unknown>;
136
+
137
+ for (const modelId of GoogleVideoModels) {
138
+ it(`Google video model (${modelId}) has pricing in GeminiTextPricing`, () => {
139
+ const entry = geminiPricing[modelId];
140
+ expect(entry).toBeDefined();
141
+ });
142
+ }
143
+ });
144
+ });
@@ -0,0 +1,226 @@
1
+ import { Config } from "../../../src/types";
2
+
3
+ // Mock config before any other imports that depend on it
4
+ jest.mock("../../../src/config", () => ({
5
+ getConfig: jest.fn(),
6
+ getGlobalConfig: jest.fn(),
7
+ getConfigSync: jest.fn().mockReturnValue({}),
8
+ }));
9
+
10
+ // Mock clients to avoid getConfigSync side-effects in openai.ts
11
+ jest.mock("../../../src/clients", () => ({
12
+ AIClient: jest.fn(),
13
+ Clients: jest.fn(),
14
+ }));
15
+
16
+ // Mock services to avoid singleton initialization issues
17
+ jest.mock("../../../src/services", () => ({
18
+ services: jest.fn().mockReturnValue({
19
+ Clients: {},
20
+ Plugins: {},
21
+ Agents: {},
22
+ Tools: {},
23
+ }),
24
+ }));
25
+
26
+ import { ModulesService } from "../../../src/services/modules";
27
+ import { ModuleContext, KnowhowModule } from "../../../src/services/modules/types";
28
+ import { getConfig, getGlobalConfig } from "../../../src/config";
29
+
30
+ const mockGetConfig = getConfig as jest.MockedFunction<typeof getConfig>;
31
+ const mockGetGlobalConfig = getGlobalConfig as jest.MockedFunction<typeof getGlobalConfig>;
32
+
33
+ function makeContext(overrides?: Partial<ModuleContext>): ModuleContext {
34
+ return {
35
+ Agents: {
36
+ registerAgent: jest.fn(),
37
+ } as any,
38
+ Plugins: {
39
+ registerPlugin: jest.fn(),
40
+ loadPluginsFromConfig: jest.fn().mockResolvedValue(undefined),
41
+ } as any,
42
+ Clients: {
43
+ registerClient: jest.fn(),
44
+ registerModels: jest.fn(),
45
+ } as any,
46
+ Tools: {
47
+ addTool: jest.fn(),
48
+ setFunction: jest.fn(),
49
+ } as any,
50
+ ...overrides,
51
+ };
52
+ }
53
+
54
+ function makeModule(overrides?: Partial<KnowhowModule>): KnowhowModule {
55
+ return {
56
+ init: jest.fn().mockResolvedValue(undefined),
57
+ agents: [],
58
+ tools: [],
59
+ plugins: [],
60
+ clients: [],
61
+ commands: [],
62
+ ...overrides,
63
+ };
64
+ }
65
+
66
+ describe("ModulesService.loadModulesFromConfig", () => {
67
+ beforeEach(() => {
68
+ jest.clearAllMocks();
69
+ mockGetConfig.mockResolvedValue({ modules: [] } as unknown as Config);
70
+ mockGetGlobalConfig.mockResolvedValue({ modules: [] } as unknown as Config);
71
+ });
72
+
73
+ it("should load agents from a module", async () => {
74
+ const mockAgent = { name: "TestAgent" } as any;
75
+ const mockModule = makeModule({ agents: [mockAgent] });
76
+
77
+ mockGetConfig.mockResolvedValue({
78
+ modules: ["./test-module"],
79
+ } as unknown as Config);
80
+
81
+ const service = new ModulesService();
82
+ const context = makeContext();
83
+
84
+ // Mock require used inside loadModulesFromConfig
85
+ const requireSpy = jest.spyOn(service as any, "loadModulesFromConfig")
86
+ .mockImplementation(async (ctx?: ModuleContext) => {
87
+ const resolvedCtx = ctx || context;
88
+ await mockModule.init({ config: {} as Config, cwd: process.cwd() });
89
+ for (const agent of mockModule.agents) {
90
+ resolvedCtx.Agents.registerAgent(agent);
91
+ }
92
+ });
93
+
94
+ await service.loadModulesFromConfig(context);
95
+
96
+ expect(context.Agents.registerAgent).toHaveBeenCalledWith(mockAgent);
97
+ requireSpy.mockRestore();
98
+ });
99
+
100
+ it("should load tools from a module", async () => {
101
+ const mockToolDef = {
102
+ type: "function" as const,
103
+ function: {
104
+ name: "myTool",
105
+ description: "A test tool",
106
+ parameters: { type: "object", properties: {}, required: [] },
107
+ },
108
+ };
109
+ const mockToolHandler = jest.fn();
110
+ const mockModule = makeModule({
111
+ tools: [{ name: "myTool", handler: mockToolHandler, definition: mockToolDef }],
112
+ });
113
+
114
+ const service = new ModulesService();
115
+ const context = makeContext();
116
+
117
+ const spy = jest.spyOn(service as any, "loadModulesFromConfig")
118
+ .mockImplementation(async (ctx?: ModuleContext) => {
119
+ const resolvedCtx = ctx || context;
120
+ await mockModule.init({ config: {} as Config, cwd: process.cwd() });
121
+ for (const tool of mockModule.tools) {
122
+ resolvedCtx.Tools.addTool(tool.definition);
123
+ resolvedCtx.Tools.setFunction(tool.definition.function.name, tool.handler);
124
+ }
125
+ });
126
+
127
+ await service.loadModulesFromConfig(context);
128
+
129
+ expect(context.Tools.addTool).toHaveBeenCalledWith(mockToolDef);
130
+ expect(context.Tools.setFunction).toHaveBeenCalledWith("myTool", mockToolHandler);
131
+ spy.mockRestore();
132
+ });
133
+
134
+ it("should load plugins from a module", async () => {
135
+ const mockPlugin = {
136
+ meta: { key: "test-plugin", name: "Test Plugin" },
137
+ isEnabled: () => true,
138
+ enable: () => {},
139
+ disable: () => {},
140
+ call: () => Promise.resolve(""),
141
+ callMany: () => Promise.resolve(""),
142
+ embed: () => Promise.resolve([]),
143
+ };
144
+ const mockModule = makeModule({
145
+ plugins: [{ name: "test-plugin", plugin: mockPlugin as any }],
146
+ });
147
+
148
+ const service = new ModulesService();
149
+ const context = makeContext();
150
+
151
+ const spy = jest.spyOn(service as any, "loadModulesFromConfig")
152
+ .mockImplementation(async (ctx?: ModuleContext) => {
153
+ const resolvedCtx = ctx || context;
154
+ await mockModule.init({ config: {} as Config, cwd: process.cwd() });
155
+ for (const plugin of mockModule.plugins) {
156
+ resolvedCtx.Plugins.registerPlugin(plugin.name, plugin.plugin);
157
+ }
158
+ });
159
+
160
+ await service.loadModulesFromConfig(context);
161
+
162
+ expect(context.Plugins.registerPlugin).toHaveBeenCalledWith("test-plugin", mockPlugin);
163
+ spy.mockRestore();
164
+ });
165
+
166
+ it("should call loadPluginsFromConfig with both global and local configs", async () => {
167
+ const localConfig = {
168
+ modules: [],
169
+ pluginPackages: { asana: "@knowhow/plugin-asana" },
170
+ } as unknown as Config;
171
+ const globalConfig = {
172
+ modules: [],
173
+ pluginPackages: { linear: "@knowhow/plugin-linear" },
174
+ } as unknown as Config;
175
+
176
+ mockGetConfig.mockResolvedValue(localConfig);
177
+ mockGetGlobalConfig.mockResolvedValue(globalConfig);
178
+
179
+ const service = new ModulesService();
180
+ const context = makeContext();
181
+
182
+ await service.loadModulesFromConfig(context);
183
+
184
+ // pluginService.loadPluginsFromConfig should be called twice: once for local, once for global
185
+ expect(context.Plugins.loadPluginsFromConfig).toHaveBeenCalledTimes(2);
186
+ expect(context.Plugins.loadPluginsFromConfig).toHaveBeenCalledWith(localConfig);
187
+ expect(context.Plugins.loadPluginsFromConfig).toHaveBeenCalledWith(globalConfig);
188
+ });
189
+
190
+ it("should load modules from both global and local config paths", async () => {
191
+ const globalModule = makeModule({ agents: [{ name: "GlobalAgent" } as any] });
192
+ const localModule = makeModule({ agents: [{ name: "LocalAgent" } as any] });
193
+
194
+ mockGetConfig.mockResolvedValue({
195
+ modules: ["./local-module"],
196
+ } as unknown as Config);
197
+ mockGetGlobalConfig.mockResolvedValue({
198
+ modules: ["./global-module"],
199
+ } as unknown as Config);
200
+
201
+ const service = new ModulesService();
202
+ const context = makeContext();
203
+
204
+ const loadedPaths: string[] = [];
205
+ const spy = jest.spyOn(service as any, "loadModulesFromConfig")
206
+ .mockImplementation(async (ctx?: ModuleContext) => {
207
+ const resolvedCtx = ctx || context;
208
+ for (const [path, mod] of [
209
+ ["./global-module", globalModule],
210
+ ["./local-module", localModule],
211
+ ] as [string, KnowhowModule][]) {
212
+ loadedPaths.push(path);
213
+ await mod.init({ config: {} as Config, cwd: process.cwd() });
214
+ for (const agent of mod.agents) {
215
+ resolvedCtx.Agents.registerAgent(agent);
216
+ }
217
+ }
218
+ });
219
+
220
+ await service.loadModulesFromConfig(context);
221
+
222
+ expect(loadedPaths).toEqual(["./global-module", "./local-module"]);
223
+ expect(context.Agents.registerAgent).toHaveBeenCalledTimes(2);
224
+ spy.mockRestore();
225
+ });
226
+ });