@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.
- package/package.json +4 -2
- package/src/agents/base/base.ts +72 -62
- package/src/agents/index.ts +30 -14
- package/src/agents/researcher/researcher.ts +1 -2
- package/src/agents/tools/aiClient.ts +48 -0
- package/src/agents/tools/list.ts +57 -0
- package/src/agents/tools/startAgentTask.ts +3 -1
- package/src/chat/CliChatService.ts +20 -4
- package/src/chat/modules/AgentModule.ts +399 -357
- package/src/chat/modules/CustomCommandsModule.ts +0 -1
- package/src/chat/modules/InternalChatModule.ts +18 -2
- package/src/chat/modules/RendererModule.ts +109 -0
- package/src/chat/modules/SessionsModule.ts +854 -0
- package/src/chat/modules/SetupModule.ts +6 -8
- package/src/chat/modules/index.ts +1 -0
- package/src/chat/renderer/CompactRenderer.ts +209 -0
- package/src/chat/renderer/ConsoleRenderer.ts +141 -0
- package/src/chat/renderer/FancyRenderer.ts +421 -0
- package/src/chat/renderer/index.ts +5 -0
- package/src/chat/renderer/loadRenderer.ts +314 -0
- package/src/chat/renderer/messagesToRenderEvents.ts +96 -0
- package/src/chat/renderer/types.ts +88 -0
- package/src/chat/types.ts +5 -0
- package/src/chat.ts +69 -5
- package/src/cli.ts +24 -5
- package/src/clients/index.ts +91 -0
- package/src/clients/pricing/google.ts +81 -2
- package/src/clients/pricing/openai.ts +68 -0
- package/src/config.ts +15 -0
- package/src/plugins/AgentsMdPlugin.ts +1 -1
- package/src/plugins/GitPlugin.ts +20 -20
- package/src/plugins/PluginBase.ts +11 -0
- package/src/plugins/SkillsPlugin.ts +150 -0
- package/src/plugins/asana.ts +4 -4
- package/src/plugins/embedding.ts +3 -5
- package/src/plugins/exec.ts +3 -3
- package/src/plugins/figma.ts +3 -7
- package/src/plugins/github.ts +18 -29
- package/src/plugins/jira.ts +2 -2
- package/src/plugins/language.ts +4 -4
- package/src/plugins/linear.ts +4 -4
- package/src/plugins/notion.ts +6 -8
- package/src/plugins/plugins.ts +29 -3
- package/src/plugins/url.ts +2 -2
- package/src/plugins/vim.ts +4 -3
- package/src/services/AgentService.ts +17 -0
- package/src/services/AgentSyncFs.ts +3 -0
- package/src/services/EventService.ts +168 -27
- package/src/services/KnowhowClient.ts +1 -0
- package/src/services/SessionManager.ts +51 -1
- package/src/services/SyncedAgentWatcher.ts +397 -0
- package/src/services/SyncerService.ts +147 -0
- package/src/services/index.ts +2 -0
- package/src/services/modules/index.ts +14 -3
- package/src/types.ts +103 -5
- package/src/worker.ts +80 -2
- package/src/workers/auth/PasskeySetup.ts +185 -0
- package/src/workers/auth/WorkerPasskeyAuth.ts +190 -0
- package/src/workers/auth/types.ts +58 -0
- package/src/workers/tools/getChallenge.ts +33 -0
- package/src/workers/tools/index.ts +8 -0
- package/src/workers/tools/lock.ts +31 -0
- package/src/workers/tools/unlock.ts +116 -0
- package/tests/clients/pricing.test.ts +144 -0
- package/tests/unit/modules/moduleLoading.test.ts +226 -0
- package/tests/unit/plugins/pluginLoading.test.ts +151 -0
- package/ts_build/package.json +4 -2
- package/ts_build/src/agents/base/base.d.ts +4 -3
- package/ts_build/src/agents/base/base.js +54 -30
- package/ts_build/src/agents/base/base.js.map +1 -1
- package/ts_build/src/agents/index.d.ts +3 -0
- package/ts_build/src/agents/index.js +21 -11
- package/ts_build/src/agents/index.js.map +1 -1
- package/ts_build/src/agents/researcher/researcher.js +1 -1
- package/ts_build/src/agents/researcher/researcher.js.map +1 -1
- package/ts_build/src/agents/tools/aiClient.d.ts +3 -0
- package/ts_build/src/agents/tools/aiClient.js +31 -1
- package/ts_build/src/agents/tools/aiClient.js.map +1 -1
- package/ts_build/src/agents/tools/list.js +48 -0
- package/ts_build/src/agents/tools/list.js.map +1 -1
- package/ts_build/src/agents/tools/startAgentTask.js +2 -1
- package/ts_build/src/agents/tools/startAgentTask.js.map +1 -1
- package/ts_build/src/chat/CliChatService.js +16 -5
- package/ts_build/src/chat/CliChatService.js.map +1 -1
- package/ts_build/src/chat/modules/AgentModule.d.ts +34 -17
- package/ts_build/src/chat/modules/AgentModule.js +248 -258
- package/ts_build/src/chat/modules/AgentModule.js.map +1 -1
- package/ts_build/src/chat/modules/CustomCommandsModule.js.map +1 -1
- package/ts_build/src/chat/modules/InternalChatModule.d.ts +3 -0
- package/ts_build/src/chat/modules/InternalChatModule.js +16 -1
- package/ts_build/src/chat/modules/InternalChatModule.js.map +1 -1
- package/ts_build/src/chat/modules/RendererModule.d.ts +16 -0
- package/ts_build/src/chat/modules/RendererModule.js +76 -0
- package/ts_build/src/chat/modules/RendererModule.js.map +1 -0
- package/ts_build/src/chat/modules/SessionsModule.d.ts +33 -0
- package/ts_build/src/chat/modules/SessionsModule.js +582 -0
- package/ts_build/src/chat/modules/SessionsModule.js.map +1 -0
- package/ts_build/src/chat/modules/SetupModule.d.ts +3 -3
- package/ts_build/src/chat/modules/SetupModule.js +4 -6
- package/ts_build/src/chat/modules/SetupModule.js.map +1 -1
- package/ts_build/src/chat/modules/index.d.ts +1 -0
- package/ts_build/src/chat/modules/index.js +3 -1
- package/ts_build/src/chat/modules/index.js.map +1 -1
- package/ts_build/src/chat/renderer/CompactRenderer.d.ts +23 -0
- package/ts_build/src/chat/renderer/CompactRenderer.js +167 -0
- package/ts_build/src/chat/renderer/CompactRenderer.js.map +1 -0
- package/ts_build/src/chat/renderer/ConsoleRenderer.d.ts +22 -0
- package/ts_build/src/chat/renderer/ConsoleRenderer.js +110 -0
- package/ts_build/src/chat/renderer/ConsoleRenderer.js.map +1 -0
- package/ts_build/src/chat/renderer/FancyRenderer.d.ts +23 -0
- package/ts_build/src/chat/renderer/FancyRenderer.js +328 -0
- package/ts_build/src/chat/renderer/FancyRenderer.js.map +1 -0
- package/ts_build/src/chat/renderer/index.d.ts +5 -0
- package/ts_build/src/chat/renderer/index.js +29 -0
- package/ts_build/src/chat/renderer/index.js.map +1 -0
- package/ts_build/src/chat/renderer/loadRenderer.d.ts +4 -0
- package/ts_build/src/chat/renderer/loadRenderer.js +246 -0
- package/ts_build/src/chat/renderer/loadRenderer.js.map +1 -0
- package/ts_build/src/chat/renderer/messagesToRenderEvents.d.ts +15 -0
- package/ts_build/src/chat/renderer/messagesToRenderEvents.js +72 -0
- package/ts_build/src/chat/renderer/messagesToRenderEvents.js.map +1 -0
- package/ts_build/src/chat/renderer/types.d.ts +75 -0
- package/ts_build/src/chat/renderer/types.js +3 -0
- package/ts_build/src/chat/renderer/types.js.map +1 -0
- package/ts_build/src/chat/types.d.ts +5 -0
- package/ts_build/src/chat.js +46 -4
- package/ts_build/src/chat.js.map +1 -1
- package/ts_build/src/cli.js +18 -5
- package/ts_build/src/cli.js.map +1 -1
- package/ts_build/src/clients/gemini.d.ts +10 -10
- package/ts_build/src/clients/index.d.ts +10 -0
- package/ts_build/src/clients/index.js +58 -0
- package/ts_build/src/clients/index.js.map +1 -1
- package/ts_build/src/clients/pricing/google.d.ts +10 -10
- package/ts_build/src/clients/pricing/google.js +74 -2
- package/ts_build/src/clients/pricing/google.js.map +1 -1
- package/ts_build/src/clients/pricing/openai.js +65 -0
- package/ts_build/src/clients/pricing/openai.js.map +1 -1
- package/ts_build/src/config.d.ts +1 -0
- package/ts_build/src/config.js +17 -1
- package/ts_build/src/config.js.map +1 -1
- package/ts_build/src/plugins/AgentsMdPlugin.js +1 -1
- package/ts_build/src/plugins/AgentsMdPlugin.js.map +1 -1
- package/ts_build/src/plugins/GitPlugin.js +20 -20
- package/ts_build/src/plugins/GitPlugin.js.map +1 -1
- package/ts_build/src/plugins/PluginBase.d.ts +1 -0
- package/ts_build/src/plugins/PluginBase.js +13 -0
- package/ts_build/src/plugins/PluginBase.js.map +1 -1
- package/ts_build/src/plugins/SkillsPlugin.d.ts +13 -0
- package/ts_build/src/plugins/SkillsPlugin.js +149 -0
- package/ts_build/src/plugins/SkillsPlugin.js.map +1 -0
- package/ts_build/src/plugins/asana.js +4 -4
- package/ts_build/src/plugins/asana.js.map +1 -1
- package/ts_build/src/plugins/embedding.js +3 -3
- package/ts_build/src/plugins/embedding.js.map +1 -1
- package/ts_build/src/plugins/exec.js +3 -3
- package/ts_build/src/plugins/exec.js.map +1 -1
- package/ts_build/src/plugins/figma.js +3 -3
- package/ts_build/src/plugins/figma.js.map +1 -1
- package/ts_build/src/plugins/github.js +18 -18
- package/ts_build/src/plugins/github.js.map +1 -1
- package/ts_build/src/plugins/jira.js +2 -2
- package/ts_build/src/plugins/jira.js.map +1 -1
- package/ts_build/src/plugins/language.js +4 -4
- package/ts_build/src/plugins/language.js.map +1 -1
- package/ts_build/src/plugins/linear.js +4 -4
- package/ts_build/src/plugins/linear.js.map +1 -1
- package/ts_build/src/plugins/notion.js +6 -6
- package/ts_build/src/plugins/notion.js.map +1 -1
- package/ts_build/src/plugins/plugins.d.ts +3 -0
- package/ts_build/src/plugins/plugins.js +18 -3
- package/ts_build/src/plugins/plugins.js.map +1 -1
- package/ts_build/src/plugins/url.js +2 -2
- package/ts_build/src/plugins/url.js.map +1 -1
- package/ts_build/src/plugins/vim.js +2 -2
- package/ts_build/src/plugins/vim.js.map +1 -1
- package/ts_build/src/services/AgentService.d.ts +3 -0
- package/ts_build/src/services/AgentService.js +7 -0
- package/ts_build/src/services/AgentService.js.map +1 -1
- package/ts_build/src/services/AgentSyncFs.d.ts +1 -0
- package/ts_build/src/services/AgentSyncFs.js +2 -0
- package/ts_build/src/services/AgentSyncFs.js.map +1 -1
- package/ts_build/src/services/EventService.d.ts +25 -2
- package/ts_build/src/services/EventService.js +92 -14
- package/ts_build/src/services/EventService.js.map +1 -1
- package/ts_build/src/services/KnowhowClient.d.ts +1 -0
- package/ts_build/src/services/KnowhowClient.js.map +1 -1
- package/ts_build/src/services/SessionManager.d.ts +6 -0
- package/ts_build/src/services/SessionManager.js +39 -1
- package/ts_build/src/services/SessionManager.js.map +1 -1
- package/ts_build/src/services/SyncedAgentWatcher.d.ts +101 -0
- package/ts_build/src/services/SyncedAgentWatcher.js +312 -0
- package/ts_build/src/services/SyncedAgentWatcher.js.map +1 -0
- package/ts_build/src/services/SyncerService.d.ts +30 -0
- package/ts_build/src/services/SyncerService.js +72 -0
- package/ts_build/src/services/SyncerService.js.map +1 -0
- package/ts_build/src/services/index.d.ts +2 -0
- package/ts_build/src/services/index.js +2 -0
- package/ts_build/src/services/index.js.map +1 -1
- package/ts_build/src/services/modules/index.js +10 -2
- package/ts_build/src/services/modules/index.js.map +1 -1
- package/ts_build/src/types.d.ts +51 -2
- package/ts_build/src/types.js +73 -5
- package/ts_build/src/types.js.map +1 -1
- package/ts_build/src/worker.d.ts +2 -0
- package/ts_build/src/worker.js +59 -4
- package/ts_build/src/worker.js.map +1 -1
- package/ts_build/src/workers/auth/PasskeySetup.d.ts +10 -0
- package/ts_build/src/workers/auth/PasskeySetup.js +131 -0
- package/ts_build/src/workers/auth/PasskeySetup.js.map +1 -0
- package/ts_build/src/workers/auth/WorkerPasskeyAuth.d.ts +35 -0
- package/ts_build/src/workers/auth/WorkerPasskeyAuth.js +129 -0
- package/ts_build/src/workers/auth/WorkerPasskeyAuth.js.map +1 -0
- package/ts_build/src/workers/auth/types.d.ts +36 -0
- package/ts_build/src/workers/auth/types.js +3 -0
- package/ts_build/src/workers/auth/types.js.map +1 -0
- package/ts_build/src/workers/tools/getChallenge.d.ts +9 -0
- package/ts_build/src/workers/tools/getChallenge.js +27 -0
- package/ts_build/src/workers/tools/getChallenge.js.map +1 -0
- package/ts_build/src/workers/tools/index.d.ts +6 -0
- package/ts_build/src/workers/tools/index.js +10 -0
- package/ts_build/src/workers/tools/index.js.map +1 -1
- package/ts_build/src/workers/tools/lock.d.ts +9 -0
- package/ts_build/src/workers/tools/lock.js +27 -0
- package/ts_build/src/workers/tools/lock.js.map +1 -0
- package/ts_build/src/workers/tools/unlock.d.ts +18 -0
- package/ts_build/src/workers/tools/unlock.js +78 -0
- package/ts_build/src/workers/tools/unlock.js.map +1 -0
- package/ts_build/tests/clients/pricing.test.d.ts +1 -0
- package/ts_build/tests/clients/pricing.test.js +90 -0
- package/ts_build/tests/clients/pricing.test.js.map +1 -0
- package/ts_build/tests/unit/modules/moduleLoading.test.d.ts +1 -0
- package/ts_build/tests/unit/modules/moduleLoading.test.js +187 -0
- package/ts_build/tests/unit/modules/moduleLoading.test.js.map +1 -0
- package/ts_build/tests/unit/plugins/pluginLoading.test.d.ts +1 -0
- package/ts_build/tests/unit/plugins/pluginLoading.test.js +123 -0
- 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
|
+
});
|