@tyvm/knowhow 0.0.83 → 0.0.84
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/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/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 +25 -0
- 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/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/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/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 +19 -0
- 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/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,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,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
|
+
});
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import { PluginService } from "../../../src/plugins/plugins";
|
|
2
|
+
import { PluginContext } from "../../../src/plugins/types";
|
|
3
|
+
import { Config } from "../../../src/types";
|
|
4
|
+
|
|
5
|
+
// Minimal mock context for PluginService construction
|
|
6
|
+
// EmbeddingPlugin calls context.Events.on() in its constructor, so we must mock it.
|
|
7
|
+
function makeContext(): PluginContext {
|
|
8
|
+
return {
|
|
9
|
+
Events: {
|
|
10
|
+
on: jest.fn(),
|
|
11
|
+
emit: jest.fn(),
|
|
12
|
+
emitBlocking: jest.fn(),
|
|
13
|
+
emitNonBlocking: jest.fn(),
|
|
14
|
+
} as any,
|
|
15
|
+
} as PluginContext;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
describe("PluginService.loadPlugin", () => {
|
|
19
|
+
it("should register the plugin under its meta.key", async () => {
|
|
20
|
+
const service = new PluginService(makeContext());
|
|
21
|
+
const mockKey = "test-plugin";
|
|
22
|
+
|
|
23
|
+
// Mock loadPlugin to simulate what a real dynamic import would do
|
|
24
|
+
service.loadPlugin = jest.fn().mockImplementation(async (_spec: string) => {
|
|
25
|
+
const instance = {
|
|
26
|
+
meta: { key: mockKey, name: "Test Plugin" },
|
|
27
|
+
isEnabled: () => true,
|
|
28
|
+
enable: () => {},
|
|
29
|
+
disable: () => {},
|
|
30
|
+
call: () => Promise.resolve(""),
|
|
31
|
+
callMany: () => Promise.resolve(""),
|
|
32
|
+
embed: () => Promise.resolve([]),
|
|
33
|
+
};
|
|
34
|
+
(service as any).pluginMap.set(instance.meta.key, instance);
|
|
35
|
+
return instance.meta.key;
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
const key = await service.loadPlugin("some-package");
|
|
39
|
+
expect(key).toBe(mockKey);
|
|
40
|
+
expect(service.isPlugin(mockKey)).toBe(true);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it("should call getPlugin after loading", async () => {
|
|
44
|
+
const service = new PluginService(makeContext());
|
|
45
|
+
const mockKey = "my-plugin";
|
|
46
|
+
|
|
47
|
+
service.loadPlugin = jest.fn().mockImplementation(async (_spec: string) => {
|
|
48
|
+
const instance = {
|
|
49
|
+
meta: { key: mockKey, name: "My Plugin" },
|
|
50
|
+
isEnabled: () => true,
|
|
51
|
+
enable: () => {},
|
|
52
|
+
disable: () => {},
|
|
53
|
+
call: () => Promise.resolve("result"),
|
|
54
|
+
callMany: () => Promise.resolve("result"),
|
|
55
|
+
embed: () => Promise.resolve([]),
|
|
56
|
+
};
|
|
57
|
+
(service as any).pluginMap.set(instance.meta.key, instance);
|
|
58
|
+
return instance.meta.key;
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
await service.loadPlugin("./some-path");
|
|
62
|
+
const plugin = service.getPlugin(mockKey);
|
|
63
|
+
expect(plugin).toBeDefined();
|
|
64
|
+
expect(plugin!.meta.key).toBe(mockKey);
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
describe("PluginService.loadPluginsFromConfig", () => {
|
|
69
|
+
it("should load plugins listed in config.pluginPackages", async () => {
|
|
70
|
+
const service = new PluginService(makeContext());
|
|
71
|
+
const loadedSpecs: string[] = [];
|
|
72
|
+
|
|
73
|
+
service.loadPlugin = jest.fn().mockImplementation(async (spec: string) => {
|
|
74
|
+
loadedSpecs.push(spec);
|
|
75
|
+
const key = `plugin-from-${spec}`;
|
|
76
|
+
const instance = {
|
|
77
|
+
meta: { key, name: key },
|
|
78
|
+
isEnabled: () => true,
|
|
79
|
+
enable: () => {},
|
|
80
|
+
disable: () => {},
|
|
81
|
+
call: () => Promise.resolve(""),
|
|
82
|
+
callMany: () => Promise.resolve(""),
|
|
83
|
+
embed: () => Promise.resolve([]),
|
|
84
|
+
};
|
|
85
|
+
(service as any).pluginMap.set(key, instance);
|
|
86
|
+
return key;
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
const config = {
|
|
90
|
+
pluginPackages: {
|
|
91
|
+
asana: "@knowhow/plugin-asana",
|
|
92
|
+
linear: "@knowhow/plugin-linear",
|
|
93
|
+
},
|
|
94
|
+
} as unknown as Config;
|
|
95
|
+
|
|
96
|
+
await service.loadPluginsFromConfig(config);
|
|
97
|
+
|
|
98
|
+
expect(loadedSpecs).toContain("@knowhow/plugin-asana");
|
|
99
|
+
expect(loadedSpecs).toContain("@knowhow/plugin-linear");
|
|
100
|
+
expect(loadedSpecs.length).toBe(2);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it("should handle empty pluginPackages gracefully", async () => {
|
|
104
|
+
const service = new PluginService(makeContext());
|
|
105
|
+
service.loadPlugin = jest.fn();
|
|
106
|
+
|
|
107
|
+
const config = {} as Config;
|
|
108
|
+
await expect(service.loadPluginsFromConfig(config)).resolves.toBeUndefined();
|
|
109
|
+
expect(service.loadPlugin).not.toHaveBeenCalled();
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it("should log a warning and not crash when a plugin fails to load", async () => {
|
|
113
|
+
const service = new PluginService(makeContext());
|
|
114
|
+
const warnSpy = jest.spyOn(console, "warn").mockImplementation(() => {});
|
|
115
|
+
|
|
116
|
+
service.loadPlugin = jest.fn().mockRejectedValue(new Error("Module not found"));
|
|
117
|
+
|
|
118
|
+
const config = {
|
|
119
|
+
pluginPackages: {
|
|
120
|
+
broken: "non-existent-package",
|
|
121
|
+
},
|
|
122
|
+
} as unknown as Config;
|
|
123
|
+
|
|
124
|
+
await expect(service.loadPluginsFromConfig(config)).resolves.toBeUndefined();
|
|
125
|
+
expect(warnSpy).toHaveBeenCalledWith(
|
|
126
|
+
expect.stringContaining("broken"),
|
|
127
|
+
expect.any(String)
|
|
128
|
+
);
|
|
129
|
+
|
|
130
|
+
warnSpy.mockRestore();
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it("should load each plugin with the correct spec string", async () => {
|
|
134
|
+
const service = new PluginService(makeContext());
|
|
135
|
+
const loadedSpecs: string[] = [];
|
|
136
|
+
|
|
137
|
+
service.loadPlugin = jest.fn().mockImplementation(async (spec: string) => {
|
|
138
|
+
loadedSpecs.push(spec);
|
|
139
|
+
return spec;
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
const config = {
|
|
143
|
+
pluginPackages: {
|
|
144
|
+
valid: "@knowhow/valid-plugin",
|
|
145
|
+
},
|
|
146
|
+
} as unknown as Config;
|
|
147
|
+
|
|
148
|
+
await service.loadPluginsFromConfig(config);
|
|
149
|
+
expect(loadedSpecs).toEqual(["@knowhow/valid-plugin"]);
|
|
150
|
+
});
|
|
151
|
+
});
|
package/ts_build/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tyvm/knowhow",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.84",
|
|
4
4
|
"description": "ai cli with plugins and agents",
|
|
5
5
|
"main": "ts_build/src/index.js",
|
|
6
6
|
"bin": {
|
|
@@ -40,14 +40,15 @@
|
|
|
40
40
|
"dependencies": {
|
|
41
41
|
"@anthropic-ai/sdk": "^0.39.0",
|
|
42
42
|
"@aws-sdk/client-s3": "^3.588.0",
|
|
43
|
-
"@tyvm/knowhow-tunnel": "0.0.3",
|
|
44
43
|
"@google/genai": "^0.14.1",
|
|
45
44
|
"@inquirer/editor": "^4.2.18",
|
|
46
45
|
"@linear/sdk": "^12.0.0",
|
|
47
46
|
"@modelcontextprotocol/sdk": "^1.13.3",
|
|
48
47
|
"@notionhq/client": "^2.2.14",
|
|
49
48
|
"@octokit/rest": "^20.0.2",
|
|
49
|
+
"@simplewebauthn/server": "^13.3.0",
|
|
50
50
|
"@types/react": "^19.1.8",
|
|
51
|
+
"@tyvm/knowhow-tunnel": "0.0.3",
|
|
51
52
|
"asana": "^3.0.16",
|
|
52
53
|
"axios": "^1.5.0",
|
|
53
54
|
"cheerio": "^1.0.0",
|
|
@@ -61,6 +62,7 @@
|
|
|
61
62
|
"ink": "^6.0.1",
|
|
62
63
|
"isolated-vm": "^5.0.4",
|
|
63
64
|
"jira-client": "^8.2.2",
|
|
65
|
+
"jiti": "^2.6.1",
|
|
64
66
|
"marked": "^10.0.0",
|
|
65
67
|
"marked-terminal": "^6.2.0",
|
|
66
68
|
"minimatch": "^10.1.2",
|
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
/// <reference types="node" />
|
|
2
|
-
import { EventEmitter } from "events";
|
|
3
1
|
import { GenericClient, Message, MessageContent, Tool, ToolCall } from "../../clients/types";
|
|
4
2
|
import { IAgent } from "../interface";
|
|
5
3
|
import { ToolsService } from "../../services/Tools";
|
|
@@ -45,13 +43,15 @@ export declare abstract class BaseAgent implements IAgent {
|
|
|
45
43
|
protected taskBreakdown: string;
|
|
46
44
|
protected summaries: string[];
|
|
47
45
|
protected currentTaskId: string | null;
|
|
48
|
-
agentEvents:
|
|
46
|
+
agentEvents: EventService;
|
|
49
47
|
eventTypes: {
|
|
50
48
|
newThread: string;
|
|
51
49
|
threadUpdate: string;
|
|
52
50
|
costUpdate: string;
|
|
51
|
+
agentLog: string;
|
|
53
52
|
toolCall: string;
|
|
54
53
|
toolUsed: string;
|
|
54
|
+
agentStatus: string;
|
|
55
55
|
notStarted: string;
|
|
56
56
|
inProgress: string;
|
|
57
57
|
done: string;
|
|
@@ -70,6 +70,7 @@ export declare abstract class BaseAgent implements IAgent {
|
|
|
70
70
|
clientService: AIClient;
|
|
71
71
|
disabledTools: any[];
|
|
72
72
|
constructor(context: AgentContext);
|
|
73
|
+
protected log(message: string, level?: "info" | "warn" | "error"): void;
|
|
73
74
|
setMaxTurns(maxTurns: number | null): void;
|
|
74
75
|
setMaxSpend(maxSpend: number | null): void;
|
|
75
76
|
setMaxRunTime(maxRunTimeMs: number | null): void;
|