@tyvm/knowhow 0.0.145 → 0.0.147
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/autodoc/cli-reference.md +36 -7
- package/bin/knowhow.js +8 -6
- package/package.json +1 -1
- package/src/agents/base/base.ts +24 -0
- package/src/agents/historyRollback.ts +36 -0
- package/src/agents/tools/startAgentTask.ts +41 -9
- package/src/auth/browserLogin.ts +9 -3
- package/src/auth/keyAuth.ts +115 -0
- package/src/auth/keyManager.ts +166 -0
- package/src/chat/CliChatService.ts +16 -0
- package/src/chat/modules/AgentModule.ts +144 -56
- package/src/chat/modules/AskModule.ts +2 -0
- package/src/chat/modules/BaseChatModule.ts +0 -1
- package/src/chat/modules/CustomCommandsModule.ts +2 -0
- package/src/chat/modules/InternalChatModule.ts +2 -0
- package/src/chat/modules/RemoteSyncModule.ts +4 -0
- package/src/chat/modules/RendererModule.ts +2 -0
- package/src/chat/modules/SearchModule.ts +2 -0
- package/src/chat/modules/SessionsModule.ts +110 -39
- package/src/chat/modules/SetupModule.ts +2 -0
- package/src/chat/modules/ShellCommandModule.ts +2 -0
- package/src/chat/modules/SystemModule.ts +2 -0
- package/src/chat/types.ts +5 -2
- package/src/cli.ts +1 -1
- package/src/clients/anthropic.ts +24 -0
- package/src/clients/fireworks.ts +3 -0
- package/src/clients/gemini.ts +8 -0
- package/src/clients/index.ts +2 -0
- package/src/clients/openai.ts +7 -4
- package/src/clients/types.ts +30 -7
- package/src/clients/xai.ts +23 -4
- package/src/commands/agent.ts +11 -39
- package/src/commands/agents.ts +403 -53
- package/src/commands/misc.ts +3 -2
- package/src/commands/services.ts +49 -3
- package/src/login.ts +89 -28
- package/src/plugins/plugins.ts +16 -0
- package/src/processors/TokenCompressor.ts +40 -8
- package/src/processors/ToolResponseCache.ts +17 -8
- package/src/processors/tools/boundedToolResponse.ts +49 -0
- package/src/processors/tools/grepToolResponse.ts +164 -38
- package/src/processors/tools/index.ts +2 -0
- package/src/processors/tools/jqToolResponse.ts +40 -10
- package/src/processors/tools/listStoredToolResponses.ts +51 -10
- package/src/processors/tools/tailToolResponse.ts +85 -17
- package/src/services/AgentService.ts +2 -0
- package/src/services/AgentSyncFs.ts +65 -0
- package/src/services/AgentSyncKnowhowWeb.ts +22 -4
- package/src/services/BehaviorsService.ts +2 -0
- package/src/services/DockerService.ts +2 -0
- package/src/services/EmbeddingService.ts +2 -0
- package/src/services/EmbeddingsService.ts +3 -0
- package/src/services/KnowhowClient.ts +68 -7
- package/src/services/LazyToolsService.ts +16 -0
- package/src/services/Mcp.ts +11 -2
- package/src/services/McpServer.ts +2 -0
- package/src/services/MessageProcessor.ts +2 -0
- package/src/services/MinimalToolsService.ts +2 -0
- package/src/services/RuntimeReloadService.ts +31 -0
- package/src/services/SessionManager.ts +5 -0
- package/src/services/SkillsService.ts +2 -0
- package/src/services/SyncedAgentWatcher.ts +2 -0
- package/src/services/SyncerService.ts +23 -8
- package/src/services/TaskRegistry.ts +2 -0
- package/src/services/Tools.ts +13 -0
- package/src/services/TracingService.ts +116 -0
- package/src/services/index.ts +10 -0
- package/src/services/modules/index.ts +49 -3
- package/src/services/modules/types.ts +8 -0
- package/src/types.ts +2 -0
- package/src/util/Trace.ts +236 -0
- package/src/utils/http.ts +18 -2
- package/tests/clients/fireworks.test.ts +65 -0
- package/tests/clients/reasoningEffort.test.ts +79 -0
- package/tests/compressor/toolResponseCache.test.ts +2 -1
- package/tests/manual/clients/completions.test.ts +27 -10
- package/tests/processors/TokenCompressor.test.ts +17 -3
- package/tests/processors/tools/boundedRetrievalTools.test.ts +68 -0
- package/tests/processors/tools/grepToolResponse.test.ts +88 -1
- package/tests/unit/agents/historyRollback.test.ts +53 -0
- package/tests/unit/auth/keyAuth.test.ts +102 -0
- package/tests/unit/auth/keyManager.test.ts +67 -0
- package/tests/unit/chat/historyCommandArgs.test.ts +37 -0
- package/tests/unit/commands/agents.test.ts +68 -0
- package/tests/unit/login.test.ts +119 -0
- package/tests/unit/plugins/pluginLoading.test.ts +31 -0
- package/tests/unit/services/AgentSyncKnowhowWeb.test.ts +38 -0
- package/tests/unit/services/LazyToolsService.test.ts +29 -0
- package/tests/unit/services/RuntimeReloadService.test.ts +29 -0
- package/tests/unit/services/SessionManager.test.ts +43 -0
- package/tests/unit/services/SyncerService.test.ts +45 -0
- package/tests/unit/util/Trace.test.ts +119 -0
- package/tests/unit/utils/http.test.ts +43 -0
- package/ts_build/package.json +1 -1
- package/ts_build/src/agents/base/base.d.ts +1 -0
- package/ts_build/src/agents/base/base.js +31 -2
- package/ts_build/src/agents/base/base.js.map +1 -1
- package/ts_build/src/agents/historyRollback.d.ts +2 -0
- package/ts_build/src/agents/historyRollback.js +22 -0
- package/ts_build/src/agents/historyRollback.js.map +1 -0
- package/ts_build/src/agents/tools/startAgentTask.d.ts +2 -0
- package/ts_build/src/agents/tools/startAgentTask.js +36 -9
- package/ts_build/src/agents/tools/startAgentTask.js.map +1 -1
- package/ts_build/src/auth/browserLogin.d.ts +1 -0
- package/ts_build/src/auth/browserLogin.js +5 -2
- package/ts_build/src/auth/browserLogin.js.map +1 -1
- package/ts_build/src/auth/keyAuth.d.ts +5 -0
- package/ts_build/src/auth/keyAuth.js +79 -0
- package/ts_build/src/auth/keyAuth.js.map +1 -0
- package/ts_build/src/auth/keyManager.d.ts +14 -0
- package/ts_build/src/auth/keyManager.js +145 -0
- package/ts_build/src/auth/keyManager.js.map +1 -0
- package/ts_build/src/chat/CliChatService.js +15 -0
- package/ts_build/src/chat/CliChatService.js.map +1 -1
- package/ts_build/src/chat/modules/AgentModule.d.ts +13 -1
- package/ts_build/src/chat/modules/AgentModule.js +96 -43
- package/ts_build/src/chat/modules/AgentModule.js.map +1 -1
- package/ts_build/src/chat/modules/AskModule.js +12 -2
- package/ts_build/src/chat/modules/AskModule.js.map +1 -1
- package/ts_build/src/chat/modules/BaseChatModule.js.map +1 -1
- package/ts_build/src/chat/modules/CustomCommandsModule.js +12 -2
- package/ts_build/src/chat/modules/CustomCommandsModule.js.map +1 -1
- package/ts_build/src/chat/modules/InternalChatModule.js +16 -2
- package/ts_build/src/chat/modules/InternalChatModule.js.map +1 -1
- package/ts_build/src/chat/modules/RemoteSyncModule.js +19 -2
- package/ts_build/src/chat/modules/RemoteSyncModule.js.map +1 -1
- package/ts_build/src/chat/modules/RendererModule.js +17 -2
- package/ts_build/src/chat/modules/RendererModule.js.map +1 -1
- package/ts_build/src/chat/modules/SearchModule.js +12 -2
- package/ts_build/src/chat/modules/SearchModule.js.map +1 -1
- package/ts_build/src/chat/modules/SessionsModule.d.ts +6 -0
- package/ts_build/src/chat/modules/SessionsModule.js +114 -24
- package/ts_build/src/chat/modules/SessionsModule.js.map +1 -1
- package/ts_build/src/chat/modules/SetupModule.js +17 -2
- package/ts_build/src/chat/modules/SetupModule.js.map +1 -1
- package/ts_build/src/chat/modules/ShellCommandModule.js +12 -2
- package/ts_build/src/chat/modules/ShellCommandModule.js.map +1 -1
- package/ts_build/src/chat/modules/SystemModule.js +12 -2
- package/ts_build/src/chat/modules/SystemModule.js.map +1 -1
- package/ts_build/src/chat/types.d.ts +5 -2
- package/ts_build/src/cli.js +1 -1
- package/ts_build/src/cli.js.map +1 -1
- package/ts_build/src/clients/anthropic.d.ts +1 -0
- package/ts_build/src/clients/anthropic.js +12 -0
- package/ts_build/src/clients/anthropic.js.map +1 -1
- package/ts_build/src/clients/fireworks.js +1 -0
- package/ts_build/src/clients/fireworks.js.map +1 -1
- package/ts_build/src/clients/gemini.js +8 -0
- package/ts_build/src/clients/gemini.js.map +1 -1
- package/ts_build/src/clients/index.js +19 -4
- package/ts_build/src/clients/index.js.map +1 -1
- package/ts_build/src/clients/openai.js +4 -2
- package/ts_build/src/clients/openai.js.map +1 -1
- package/ts_build/src/clients/types.d.ts +4 -1
- package/ts_build/src/clients/types.js +14 -0
- package/ts_build/src/clients/types.js.map +1 -1
- package/ts_build/src/clients/xai.d.ts +1 -0
- package/ts_build/src/clients/xai.js +23 -4
- package/ts_build/src/clients/xai.js.map +1 -1
- package/ts_build/src/commands/agent.js +7 -22
- package/ts_build/src/commands/agent.js.map +1 -1
- package/ts_build/src/commands/agents.d.ts +1 -1
- package/ts_build/src/commands/agents.js +174 -16
- package/ts_build/src/commands/agents.js.map +1 -1
- package/ts_build/src/commands/misc.js +3 -2
- package/ts_build/src/commands/misc.js.map +1 -1
- package/ts_build/src/commands/services.d.ts +2 -0
- package/ts_build/src/commands/services.js +45 -3
- package/ts_build/src/commands/services.js.map +1 -1
- package/ts_build/src/login.d.ts +1 -1
- package/ts_build/src/login.js +53 -15
- package/ts_build/src/login.js.map +1 -1
- package/ts_build/src/plugins/plugins.d.ts +1 -0
- package/ts_build/src/plugins/plugins.js +14 -0
- package/ts_build/src/plugins/plugins.js.map +1 -1
- package/ts_build/src/processors/TokenCompressor.js +15 -6
- package/ts_build/src/processors/TokenCompressor.js.map +1 -1
- package/ts_build/src/processors/ToolResponseCache.d.ts +3 -3
- package/ts_build/src/processors/ToolResponseCache.js +8 -8
- package/ts_build/src/processors/ToolResponseCache.js.map +1 -1
- package/ts_build/src/processors/tools/boundedToolResponse.d.ts +7 -0
- package/ts_build/src/processors/tools/boundedToolResponse.js +27 -0
- package/ts_build/src/processors/tools/boundedToolResponse.js.map +1 -0
- package/ts_build/src/processors/tools/grepToolResponse.d.ts +4 -0
- package/ts_build/src/processors/tools/grepToolResponse.js +101 -25
- package/ts_build/src/processors/tools/grepToolResponse.js.map +1 -1
- package/ts_build/src/processors/tools/index.d.ts +2 -2
- package/ts_build/src/processors/tools/index.js.map +1 -1
- package/ts_build/src/processors/tools/jqToolResponse.d.ts +5 -1
- package/ts_build/src/processors/tools/jqToolResponse.js +26 -9
- package/ts_build/src/processors/tools/jqToolResponse.js.map +1 -1
- package/ts_build/src/processors/tools/listStoredToolResponses.d.ts +7 -1
- package/ts_build/src/processors/tools/listStoredToolResponses.js +39 -9
- package/ts_build/src/processors/tools/listStoredToolResponses.js.map +1 -1
- package/ts_build/src/processors/tools/tailToolResponse.d.ts +5 -0
- package/ts_build/src/processors/tools/tailToolResponse.js +56 -13
- package/ts_build/src/processors/tools/tailToolResponse.js.map +1 -1
- package/ts_build/src/services/AgentService.js +18 -2
- package/ts_build/src/services/AgentService.js.map +1 -1
- package/ts_build/src/services/AgentSyncFs.d.ts +3 -0
- package/ts_build/src/services/AgentSyncFs.js +62 -15
- package/ts_build/src/services/AgentSyncFs.js.map +1 -1
- package/ts_build/src/services/AgentSyncKnowhowWeb.d.ts +2 -0
- package/ts_build/src/services/AgentSyncKnowhowWeb.js +28 -6
- package/ts_build/src/services/AgentSyncKnowhowWeb.js.map +1 -1
- package/ts_build/src/services/BehaviorsService.js +12 -2
- package/ts_build/src/services/BehaviorsService.js.map +1 -1
- package/ts_build/src/services/DockerService.js +20 -8
- package/ts_build/src/services/DockerService.js.map +1 -1
- package/ts_build/src/services/EmbeddingService.js +16 -2
- package/ts_build/src/services/EmbeddingService.js.map +1 -1
- package/ts_build/src/services/EmbeddingsService.js +12 -2
- package/ts_build/src/services/EmbeddingsService.js.map +1 -1
- package/ts_build/src/services/KnowhowClient.d.ts +7 -2
- package/ts_build/src/services/KnowhowClient.js +108 -10
- package/ts_build/src/services/KnowhowClient.js.map +1 -1
- package/ts_build/src/services/LazyToolsService.d.ts +2 -0
- package/ts_build/src/services/LazyToolsService.js +26 -2
- package/ts_build/src/services/LazyToolsService.js.map +1 -1
- package/ts_build/src/services/Mcp.js +20 -4
- package/ts_build/src/services/Mcp.js.map +1 -1
- package/ts_build/src/services/McpServer.js +17 -2
- package/ts_build/src/services/McpServer.js.map +1 -1
- package/ts_build/src/services/MessageProcessor.js +16 -2
- package/ts_build/src/services/MessageProcessor.js.map +1 -1
- package/ts_build/src/services/MinimalToolsService.js +16 -2
- package/ts_build/src/services/MinimalToolsService.js.map +1 -1
- package/ts_build/src/services/RuntimeReloadService.d.ts +12 -0
- package/ts_build/src/services/RuntimeReloadService.js +26 -0
- package/ts_build/src/services/RuntimeReloadService.js.map +1 -0
- package/ts_build/src/services/SessionManager.js +19 -2
- package/ts_build/src/services/SessionManager.js.map +1 -1
- package/ts_build/src/services/SkillsService.js +16 -2
- package/ts_build/src/services/SkillsService.js.map +1 -1
- package/ts_build/src/services/SyncedAgentWatcher.js +16 -2
- package/ts_build/src/services/SyncedAgentWatcher.js.map +1 -1
- package/ts_build/src/services/SyncerService.d.ts +1 -0
- package/ts_build/src/services/SyncerService.js +27 -5
- package/ts_build/src/services/SyncerService.js.map +1 -1
- package/ts_build/src/services/TaskRegistry.js +12 -2
- package/ts_build/src/services/TaskRegistry.js.map +1 -1
- package/ts_build/src/services/Tools.d.ts +1 -0
- package/ts_build/src/services/Tools.js +21 -2
- package/ts_build/src/services/Tools.js.map +1 -1
- package/ts_build/src/services/TracingService.d.ts +16 -0
- package/ts_build/src/services/TracingService.js +35 -0
- package/ts_build/src/services/TracingService.js.map +1 -0
- package/ts_build/src/services/index.d.ts +8 -0
- package/ts_build/src/services/index.js +9 -1
- package/ts_build/src/services/index.js.map +1 -1
- package/ts_build/src/services/modules/index.d.ts +6 -0
- package/ts_build/src/services/modules/index.js +29 -3
- package/ts_build/src/services/modules/index.js.map +1 -1
- package/ts_build/src/services/modules/types.d.ts +3 -0
- package/ts_build/src/types.d.ts +1 -0
- package/ts_build/src/types.js.map +1 -1
- package/ts_build/src/util/Trace.d.ts +11 -0
- package/ts_build/src/util/Trace.js +107 -0
- package/ts_build/src/util/Trace.js.map +1 -0
- package/ts_build/src/utils/http.d.ts +6 -2
- package/ts_build/src/utils/http.js +8 -2
- package/ts_build/src/utils/http.js.map +1 -1
- package/ts_build/tests/clients/fireworks.test.d.ts +1 -0
- package/ts_build/tests/clients/fireworks.test.js +65 -0
- package/ts_build/tests/clients/fireworks.test.js.map +1 -0
- package/ts_build/tests/clients/reasoningEffort.test.d.ts +1 -0
- package/ts_build/tests/clients/reasoningEffort.test.js +68 -0
- package/ts_build/tests/clients/reasoningEffort.test.js.map +1 -0
- package/ts_build/tests/compressor/toolResponseCache.test.js +1 -1
- package/ts_build/tests/compressor/toolResponseCache.test.js.map +1 -1
- package/ts_build/tests/manual/clients/completions.test.js +19 -6
- package/ts_build/tests/manual/clients/completions.test.js.map +1 -1
- package/ts_build/tests/processors/TokenCompressor.test.js +11 -2
- package/ts_build/tests/processors/TokenCompressor.test.js.map +1 -1
- package/ts_build/tests/processors/tools/boundedRetrievalTools.test.d.ts +1 -0
- package/ts_build/tests/processors/tools/boundedRetrievalTools.test.js +57 -0
- package/ts_build/tests/processors/tools/boundedRetrievalTools.test.js.map +1 -0
- package/ts_build/tests/processors/tools/grepToolResponse.test.js +37 -0
- package/ts_build/tests/processors/tools/grepToolResponse.test.js.map +1 -1
- package/ts_build/tests/unit/agents/historyRollback.test.d.ts +1 -0
- package/ts_build/tests/unit/agents/historyRollback.test.js +42 -0
- package/ts_build/tests/unit/agents/historyRollback.test.js.map +1 -0
- package/ts_build/tests/unit/auth/keyAuth.test.d.ts +1 -0
- package/ts_build/tests/unit/auth/keyAuth.test.js +77 -0
- package/ts_build/tests/unit/auth/keyAuth.test.js.map +1 -0
- package/ts_build/tests/unit/auth/keyManager.test.d.ts +1 -0
- package/ts_build/tests/unit/auth/keyManager.test.js +52 -0
- package/ts_build/tests/unit/auth/keyManager.test.js.map +1 -0
- package/ts_build/tests/unit/chat/historyCommandArgs.test.d.ts +1 -0
- package/ts_build/tests/unit/chat/historyCommandArgs.test.js +29 -0
- package/ts_build/tests/unit/chat/historyCommandArgs.test.js.map +1 -0
- package/ts_build/tests/unit/commands/agents.test.d.ts +1 -0
- package/ts_build/tests/unit/commands/agents.test.js +91 -0
- package/ts_build/tests/unit/commands/agents.test.js.map +1 -0
- package/ts_build/tests/unit/login.test.d.ts +1 -0
- package/ts_build/tests/unit/login.test.js +97 -0
- package/ts_build/tests/unit/login.test.js.map +1 -0
- package/ts_build/tests/unit/plugins/pluginLoading.test.js +59 -0
- package/ts_build/tests/unit/plugins/pluginLoading.test.js.map +1 -1
- package/ts_build/tests/unit/services/AgentSyncKnowhowWeb.test.d.ts +1 -0
- package/ts_build/tests/unit/services/AgentSyncKnowhowWeb.test.js +32 -0
- package/ts_build/tests/unit/services/AgentSyncKnowhowWeb.test.js.map +1 -0
- package/ts_build/tests/unit/services/LazyToolsService.test.d.ts +1 -0
- package/ts_build/tests/unit/services/LazyToolsService.test.js +27 -0
- package/ts_build/tests/unit/services/LazyToolsService.test.js.map +1 -0
- package/ts_build/tests/unit/services/RuntimeReloadService.test.d.ts +1 -0
- package/ts_build/tests/unit/services/RuntimeReloadService.test.js +26 -0
- package/ts_build/tests/unit/services/RuntimeReloadService.test.js.map +1 -0
- package/ts_build/tests/unit/services/SessionManager.test.d.ts +1 -0
- package/ts_build/tests/unit/services/SessionManager.test.js +72 -0
- package/ts_build/tests/unit/services/SessionManager.test.js.map +1 -0
- package/ts_build/tests/unit/services/SyncerService.test.d.ts +1 -0
- package/ts_build/tests/unit/services/SyncerService.test.js +40 -0
- package/ts_build/tests/unit/services/SyncerService.test.js.map +1 -0
- package/ts_build/tests/unit/util/Trace.test.d.ts +1 -0
- package/ts_build/tests/unit/util/Trace.test.js +107 -0
- package/ts_build/tests/unit/util/Trace.test.js.map +1 -0
- package/ts_build/tests/unit/utils/http.test.d.ts +1 -0
- package/ts_build/tests/unit/utils/http.test.js +69 -0
- package/ts_build/tests/unit/utils/http.test.js.map +1 -0
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { GenericAnthropicClient } from "../../src/clients/anthropic";
|
|
2
|
+
import { GenericGeminiClient } from "../../src/clients/gemini";
|
|
3
|
+
import { GenericOpenAiClient } from "../../src/clients/openai";
|
|
4
|
+
import { AnthropicModels } from "../../src/clients/pricing";
|
|
5
|
+
import {
|
|
6
|
+
REASONING_EFFORTS,
|
|
7
|
+
ReasoningEffort,
|
|
8
|
+
isReasoningEffort,
|
|
9
|
+
} from "../../src/clients/types";
|
|
10
|
+
import {
|
|
11
|
+
GoogleThinkingBudgetModels,
|
|
12
|
+
GoogleThinkingLevelModels,
|
|
13
|
+
} from "../../src/types";
|
|
14
|
+
import { GenericXAIClient } from "../../src/clients/xai";
|
|
15
|
+
|
|
16
|
+
const options = (model: string, reasoning_effort: ReasoningEffort) => ({
|
|
17
|
+
model,
|
|
18
|
+
reasoning_effort,
|
|
19
|
+
messages: [],
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
describe("reasoning effort values", () => {
|
|
23
|
+
it("accepts the complete provider-neutral set and rejects other values", () => {
|
|
24
|
+
expect(REASONING_EFFORTS).toEqual([
|
|
25
|
+
"none",
|
|
26
|
+
"minimal",
|
|
27
|
+
"low",
|
|
28
|
+
"medium",
|
|
29
|
+
"high",
|
|
30
|
+
"xhigh",
|
|
31
|
+
"max",
|
|
32
|
+
]);
|
|
33
|
+
REASONING_EFFORTS.forEach((effort) => expect(isReasoningEffort(effort)).toBe(true));
|
|
34
|
+
expect(isReasoningEffort("extra-high")).toBe(false);
|
|
35
|
+
expect(isReasoningEffort(undefined)).toBe(false);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it("maps provider-neutral values to OpenAI legacy and Responses spellings", () => {
|
|
39
|
+
const client = new GenericOpenAiClient("fake-key");
|
|
40
|
+
expect(client.resolveReasoningEffort(options("unknown", "none"))).toBe("low");
|
|
41
|
+
expect(client.resolveReasoningEffort(options("unknown", "minimal"))).toBe("low");
|
|
42
|
+
expect(client.resolveReasoningEffort(options("unknown", "xhigh"))).toBe("high");
|
|
43
|
+
expect(client.resolveReasoningEffort(options("unknown", "max"))).toBe("high");
|
|
44
|
+
expect(client.resolveReasoningEffortForModel(options("unknown", "max"))).toBe("xhigh");
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it("maps all values to valid Gemini thinking levels and budgets", () => {
|
|
48
|
+
const client = new GenericGeminiClient("fake-key");
|
|
49
|
+
const levelModel = GoogleThinkingLevelModels[0];
|
|
50
|
+
const budgetModel = GoogleThinkingBudgetModels[0];
|
|
51
|
+
|
|
52
|
+
expect(client.buildThinkingConfig(options(levelModel, "none"))).toEqual({ thinkingLevel: "minimal" });
|
|
53
|
+
expect(client.buildThinkingConfig(options(levelModel, "minimal"))).toEqual({ thinkingLevel: "minimal" });
|
|
54
|
+
expect(client.buildThinkingConfig(options(levelModel, "xhigh"))).toEqual({ thinkingLevel: "high" });
|
|
55
|
+
expect(client.buildThinkingConfig(options(levelModel, "max"))).toEqual({ thinkingLevel: "high" });
|
|
56
|
+
expect(client.buildThinkingConfig(options(budgetModel, "none"))).toEqual({ thinkingBudget: 0 });
|
|
57
|
+
expect(client.buildThinkingConfig(options(budgetModel, "minimal"))).toEqual({ thinkingBudget: 512 });
|
|
58
|
+
expect(client.buildThinkingConfig(options(budgetModel, "medium"))).toEqual({ thinkingBudget: 8192 });
|
|
59
|
+
expect(client.buildThinkingConfig(options(budgetModel, "max"))).toEqual({ thinkingBudget: -1 });
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it("maps provider-neutral values to Anthropic output effort", () => {
|
|
63
|
+
const client = new GenericAnthropicClient("fake-key");
|
|
64
|
+
expect(client.getEffortOutputConfig(AnthropicModels.Sonnet5, "none")).toBeUndefined();
|
|
65
|
+
expect(client.getEffortOutputConfig(AnthropicModels.Sonnet5, "minimal")).toEqual({ effort: "low" });
|
|
66
|
+
expect(client.getEffortOutputConfig(AnthropicModels.Sonnet5, "xhigh")).toEqual({ effort: "xhigh" });
|
|
67
|
+
expect(client.getEffortOutputConfig(AnthropicModels.Sonnet5, "max")).toEqual({ effort: "max" });
|
|
68
|
+
expect(client.getEffortOutputConfig("unsupported", "high")).toBeUndefined();
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it("maps provider-neutral values to xAI legacy effort", () => {
|
|
72
|
+
const client = new GenericXAIClient("fake-key");
|
|
73
|
+
expect(client.resolveLegacyReasoningEffort("none")).toBeUndefined();
|
|
74
|
+
expect(client.resolveLegacyReasoningEffort("minimal")).toBe("low");
|
|
75
|
+
expect(client.resolveLegacyReasoningEffort("medium")).toBe("medium");
|
|
76
|
+
expect(client.resolveLegacyReasoningEffort("xhigh")).toBe("high");
|
|
77
|
+
expect(client.resolveLegacyReasoningEffort("max")).toBe("high");
|
|
78
|
+
});
|
|
79
|
+
});
|
|
@@ -111,7 +111,8 @@ describe("ToolResponseCache - MCP Format Support", () => {
|
|
|
111
111
|
it("should filter data array with ._data | map(select(.private == true))", async () => {
|
|
112
112
|
const result = await cache.queryToolResponse(
|
|
113
113
|
"github-repos",
|
|
114
|
-
"._data | map(select(.private == true))"
|
|
114
|
+
"._data | map(select(.private == true))",
|
|
115
|
+
{ maxCharacters: 50_000 }
|
|
115
116
|
);
|
|
116
117
|
|
|
117
118
|
// Parse and verify filtering worked
|
|
@@ -17,6 +17,7 @@ import { OpenAiChatModels, OpenAiReasoningModels, OpenAiResponsesOnlyModels } fr
|
|
|
17
17
|
import { AnthropicTextModels, AnthropicLimitedAvailabilityModels } from "../../../src/clients/pricing/anthropic";
|
|
18
18
|
import { GoogleTextModels } from "../../../src/clients/pricing/google";
|
|
19
19
|
import { XaiTextModels, XaiDeprecatedTextModels } from "../../../src/clients/pricing/xai";
|
|
20
|
+
import { FireworksTextPricing } from "../../../src/clients/pricing/fireworks";
|
|
20
21
|
|
|
21
22
|
const OUTPUT_FILE = path.join(__dirname, "completions.json");
|
|
22
23
|
const PROMPT = "Say hello in 5 words.";
|
|
@@ -29,6 +30,7 @@ interface ModelEntry {
|
|
|
29
30
|
provider: string;
|
|
30
31
|
model: string;
|
|
31
32
|
envKey: string;
|
|
33
|
+
reasoningEffort?: "low";
|
|
32
34
|
}
|
|
33
35
|
|
|
34
36
|
function openaiEntries(models: string[]): ModelEntry[] {
|
|
@@ -47,19 +49,35 @@ function xaiEntries(models: string[]): ModelEntry[] {
|
|
|
47
49
|
return models.map((model) => ({ provider: "xai", model, envKey: "XAI_API_KEY" }));
|
|
48
50
|
}
|
|
49
51
|
|
|
52
|
+
function fireworksEntries(): ModelEntry[] {
|
|
53
|
+
return Object.entries(FireworksTextPricing)
|
|
54
|
+
.filter(([, pricing]) => !pricing.deprecated)
|
|
55
|
+
.map(([model]) => ({
|
|
56
|
+
provider: "fireworks",
|
|
57
|
+
model,
|
|
58
|
+
envKey: "FIREWORKS_API_KEY",
|
|
59
|
+
// Fireworks model support for reasoning_effort is model-specific. This
|
|
60
|
+
// suite exercises the request shape used by Patcher without that flag.
|
|
61
|
+
}));
|
|
62
|
+
}
|
|
63
|
+
|
|
50
64
|
const TEST_MODELS: ModelEntry[] = [
|
|
51
65
|
// OpenAI — all active chat + reasoning models + responses-API models
|
|
52
66
|
// Deduplicate since some models appear in multiple lists
|
|
53
|
-
...openaiEntries([...new Set([...OpenAiChatModels, ...OpenAiReasoningModels, ...OpenAiResponsesOnlyModels])])
|
|
67
|
+
...openaiEntries([...new Set([...OpenAiChatModels, ...OpenAiReasoningModels, ...OpenAiResponsesOnlyModels])])
|
|
68
|
+
.map((entry) => ({ ...entry, reasoningEffort: "low" as const })),
|
|
54
69
|
|
|
55
70
|
// Anthropic — active text models (limited availability excluded)
|
|
56
|
-
...anthropicEntries(AnthropicTextModels),
|
|
71
|
+
...anthropicEntries(AnthropicTextModels).map((entry) => ({ ...entry, reasoningEffort: "low" as const })),
|
|
57
72
|
|
|
58
73
|
// Google — all active text models
|
|
59
|
-
...googleEntries(GoogleTextModels),
|
|
74
|
+
...googleEntries(GoogleTextModels).map((entry) => ({ ...entry, reasoningEffort: "low" as const })),
|
|
60
75
|
|
|
61
76
|
// XAI — active text models (deprecated grok-2 models excluded)
|
|
62
|
-
...xaiEntries(XaiTextModels),
|
|
77
|
+
...xaiEntries(XaiTextModels).map((entry) => ({ ...entry, reasoningEffort: "low" as const })),
|
|
78
|
+
|
|
79
|
+
// Fireworks — all active catalog models
|
|
80
|
+
...fireworksEntries(),
|
|
63
81
|
];
|
|
64
82
|
|
|
65
83
|
// ─── Persistence helpers ──────────────────────────────────────────────────────
|
|
@@ -118,7 +136,7 @@ describe("Text Completions – multi-provider model verification", () => {
|
|
|
118
136
|
console.log(` Total records: ${Object.keys(results).length}`);
|
|
119
137
|
});
|
|
120
138
|
|
|
121
|
-
for (const { provider, model, envKey } of TEST_MODELS) {
|
|
139
|
+
for (const { provider, model, envKey, reasoningEffort } of TEST_MODELS) {
|
|
122
140
|
const key = recordKey(provider, model);
|
|
123
141
|
|
|
124
142
|
test(`${provider} / ${model}`, async () => {
|
|
@@ -145,11 +163,10 @@ describe("Text Completions – multi-provider model verification", () => {
|
|
|
145
163
|
model,
|
|
146
164
|
messages: [{ role: "user", content: PROMPT }],
|
|
147
165
|
max_tokens: 2000,
|
|
148
|
-
//
|
|
149
|
-
//
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
reasoning_effort: "low",
|
|
166
|
+
// Exercise the summary option enabled by Patcher. Provider clients must
|
|
167
|
+
// translate or remove this internal option rather than forwarding it.
|
|
168
|
+
reasoning_summary: true,
|
|
169
|
+
reasoning_effort: reasoningEffort,
|
|
153
170
|
});
|
|
154
171
|
const durationMs = Date.now() - startMs;
|
|
155
172
|
|
|
@@ -446,7 +446,7 @@ describe("TokenCompressor", () => {
|
|
|
446
446
|
expect(result).toContain("Available keys:");
|
|
447
447
|
});
|
|
448
448
|
|
|
449
|
-
it("should auto-stitch a multi-chunk chain
|
|
449
|
+
it("should auto-stitch and paginate a multi-chunk chain", () => {
|
|
450
450
|
// Force a multi-chunk compression then expand the first key.
|
|
451
451
|
const content = "a".repeat(40000);
|
|
452
452
|
const placeholder = tokenCompressor.compressStringInChunks(content);
|
|
@@ -456,9 +456,23 @@ describe("TokenCompressor", () => {
|
|
|
456
456
|
const functions = toolsServiceCalls[0][0];
|
|
457
457
|
const result = functions.expandTokens(firstKey);
|
|
458
458
|
|
|
459
|
-
|
|
460
|
-
expect(result).
|
|
459
|
+
expect(result.length).toBeLessThanOrEqual(20_000);
|
|
460
|
+
expect(result).toContain("Repeat with characterOffset=");
|
|
461
461
|
expect(result).not.toContain("NEXT_CHUNK_KEY");
|
|
462
|
+
|
|
463
|
+
const pages = [result];
|
|
464
|
+
let offsetMatch = result.match(/characterOffset=(\d+)/);
|
|
465
|
+
while (offsetMatch) {
|
|
466
|
+
const page = functions.expandTokens(
|
|
467
|
+
firstKey,
|
|
468
|
+
undefined,
|
|
469
|
+
undefined,
|
|
470
|
+
Number(offsetMatch[1])
|
|
471
|
+
);
|
|
472
|
+
pages.push(page);
|
|
473
|
+
offsetMatch = page.match(/Repeat with characterOffset=(\d+)/);
|
|
474
|
+
}
|
|
475
|
+
expect(pages.reduce((sum, page) => sum + (page.match(/^a+/)?.[0].length || 0), 0)).toBe(content.length);
|
|
462
476
|
});
|
|
463
477
|
|
|
464
478
|
it("should support a ranged read with real line numbers", () => {
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import {
|
|
2
|
+
executeJqQuery,
|
|
3
|
+
jqToolResponseDefinition,
|
|
4
|
+
} from "../../../src/processors/tools/jqToolResponse";
|
|
5
|
+
import {
|
|
6
|
+
executeTail,
|
|
7
|
+
tailToolResponseDefinition,
|
|
8
|
+
} from "../../../src/processors/tools/tailToolResponse";
|
|
9
|
+
import {
|
|
10
|
+
executeListStoredToolResponses,
|
|
11
|
+
listStoredToolResponsesDefinition,
|
|
12
|
+
} from "../../../src/processors/tools/listStoredToolResponses";
|
|
13
|
+
|
|
14
|
+
describe("bounded retrieval tools", () => {
|
|
15
|
+
it("bounds and paginates JQ output", async () => {
|
|
16
|
+
const data = JSON.stringify(Array.from({ length: 100 }, (_, i) => `value-${i}-${"x".repeat(30)}`));
|
|
17
|
+
const first = await executeJqQuery(data, "jq-id", ".", ["jq-id"], {}, { maxCharacters: 1000 });
|
|
18
|
+
expect(first.length).toBeLessThanOrEqual(1000);
|
|
19
|
+
expect(first).toContain("Repeat with characterOffset=");
|
|
20
|
+
|
|
21
|
+
const offset = Number(first.match(/characterOffset=(\d+)/)?.[1]);
|
|
22
|
+
const next = await executeJqQuery(data, "jq-id", ".", ["jq-id"], {}, {
|
|
23
|
+
characterOffset: offset,
|
|
24
|
+
maxCharacters: 1000,
|
|
25
|
+
});
|
|
26
|
+
expect(next).not.toBe(first);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it("bounds tail line lengths and supports backward pagination", async () => {
|
|
30
|
+
const data = ["first", `second ${"a".repeat(5000)}`, "third"].join("\n");
|
|
31
|
+
const result = await executeTail(data, "tail-id", ["tail-id"], {
|
|
32
|
+
lines: 1,
|
|
33
|
+
endLine: 2,
|
|
34
|
+
maxLineCharacters: 1000,
|
|
35
|
+
maxCharacters: 2000,
|
|
36
|
+
});
|
|
37
|
+
expect(result.length).toBeLessThanOrEqual(2000);
|
|
38
|
+
expect(result).toContain("Earlier lines available");
|
|
39
|
+
expect(result).toContain("lineCharacterOffset=1000");
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it("paginates stored response results", async () => {
|
|
43
|
+
const storage = { a: "one", b: "two", c: "three" };
|
|
44
|
+
const metadata = {
|
|
45
|
+
a: { toolCallId: "a", originalLength: 3, storedAt: 1 },
|
|
46
|
+
b: { toolCallId: "b", originalLength: 3, storedAt: 2 },
|
|
47
|
+
c: { toolCallId: "c", originalLength: 5, storedAt: 3 },
|
|
48
|
+
};
|
|
49
|
+
const result = await executeListStoredToolResponses(storage, metadata, {}, {
|
|
50
|
+
maxResults: 1,
|
|
51
|
+
resultOffset: 1,
|
|
52
|
+
});
|
|
53
|
+
expect(result).toContain("Tool Call ID: b");
|
|
54
|
+
expect(result).not.toContain("Tool Call ID: c");
|
|
55
|
+
expect(result).toContain("resultOffset=2");
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it("exposes pagination controls in each tool schema", () => {
|
|
59
|
+
const jqOptions = (jqToolResponseDefinition.function.parameters.properties as any).options;
|
|
60
|
+
const tailOptions = (tailToolResponseDefinition.function.parameters.properties as any).options;
|
|
61
|
+
const listOptions = (listStoredToolResponsesDefinition.function.parameters.properties as any).options;
|
|
62
|
+
expect(jqOptions.properties.characterOffset).toBeDefined();
|
|
63
|
+
expect(tailOptions.properties.endLine).toBeDefined();
|
|
64
|
+
expect(tailOptions.properties.maxCharacters).toBeDefined();
|
|
65
|
+
expect(listOptions.properties.resultOffset).toBeDefined();
|
|
66
|
+
expect(listOptions.properties.maxCharacters).toBeDefined();
|
|
67
|
+
});
|
|
68
|
+
});
|
|
@@ -1,4 +1,7 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
executeGrep,
|
|
3
|
+
grepToolResponseDefinition,
|
|
4
|
+
} from "../../../src/processors/tools/grepToolResponse";
|
|
2
5
|
|
|
3
6
|
/**
|
|
4
7
|
* Verifies grepToolResponse operates on the decompressed/plain stored content
|
|
@@ -69,4 +72,88 @@ describe("executeGrep", () => {
|
|
|
69
72
|
|
|
70
73
|
expect(result).toContain("No matches found");
|
|
71
74
|
});
|
|
75
|
+
|
|
76
|
+
it("bounds the total response and advertises the next result page", async () => {
|
|
77
|
+
const manyMatches = Array.from(
|
|
78
|
+
{ length: 20 },
|
|
79
|
+
(_, index) => `match ${index} ${"x".repeat(200)}`
|
|
80
|
+
).join("\n");
|
|
81
|
+
|
|
82
|
+
const result = await executeGrep(
|
|
83
|
+
manyMatches,
|
|
84
|
+
toolCallId,
|
|
85
|
+
"match",
|
|
86
|
+
[toolCallId],
|
|
87
|
+
{ maxCharacters: 1000, maxResults: 20 }
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
expect(result.length).toBeLessThanOrEqual(1000);
|
|
91
|
+
expect(result).toContain("More matches available");
|
|
92
|
+
expect(result).toMatch(/resultOffset=\d+/);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it("keeps a large log grep under the default response limit", async () => {
|
|
96
|
+
const logMatches = Array.from(
|
|
97
|
+
{ length: 500 },
|
|
98
|
+
(_, index) => `${index}: ${JSON.stringify({ level: 30, message: "sandbox runner", detail: "x".repeat(500) })}`
|
|
99
|
+
).join("\n");
|
|
100
|
+
|
|
101
|
+
const result = await executeGrep(
|
|
102
|
+
logMatches,
|
|
103
|
+
toolCallId,
|
|
104
|
+
"runner|sandbox",
|
|
105
|
+
[toolCallId]
|
|
106
|
+
);
|
|
107
|
+
|
|
108
|
+
expect(result.length).toBeLessThanOrEqual(20_000);
|
|
109
|
+
expect(result).toContain("More matches available");
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it("paginates matches using resultOffset", async () => {
|
|
113
|
+
const result = await executeGrep(
|
|
114
|
+
["match zero", "match one", "match two"].join("\n"),
|
|
115
|
+
toolCallId,
|
|
116
|
+
"match",
|
|
117
|
+
[toolCallId],
|
|
118
|
+
{ maxResults: 1, resultOffset: 1 }
|
|
119
|
+
);
|
|
120
|
+
|
|
121
|
+
expect(result).not.toContain("match zero");
|
|
122
|
+
expect(result).toContain("match one");
|
|
123
|
+
expect(result).not.toContain("match two");
|
|
124
|
+
expect(result).toContain("resultOffset=2");
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it("slices very long matching lines and lets callers page within the line", async () => {
|
|
128
|
+
const longLine = `prefix-${"a".repeat(5000)}-suffix`;
|
|
129
|
+
const firstPage = await executeGrep(
|
|
130
|
+
longLine,
|
|
131
|
+
toolCallId,
|
|
132
|
+
"prefix",
|
|
133
|
+
[toolCallId]
|
|
134
|
+
);
|
|
135
|
+
|
|
136
|
+
expect(firstPage.length).toBeLessThan(10_000);
|
|
137
|
+
expect(firstPage).toContain("line 1 truncated");
|
|
138
|
+
expect(firstPage).toContain("lineCharacterOffset=4000");
|
|
139
|
+
expect(firstPage).not.toContain("-suffix");
|
|
140
|
+
|
|
141
|
+
const secondPage = await executeGrep(
|
|
142
|
+
longLine,
|
|
143
|
+
toolCallId,
|
|
144
|
+
"prefix",
|
|
145
|
+
[toolCallId],
|
|
146
|
+
{ lineCharacterOffset: 4000 }
|
|
147
|
+
);
|
|
148
|
+
|
|
149
|
+
expect(secondPage).toContain("-suffix");
|
|
150
|
+
expect(secondPage).toContain("End of matches");
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
it("exposes pagination controls in the tool schema", () => {
|
|
154
|
+
const options = (grepToolResponseDefinition.function.parameters.properties as any).options;
|
|
155
|
+
expect(options.properties.resultOffset).toBeDefined();
|
|
156
|
+
expect(options.properties.lineCharacterOffset).toBeDefined();
|
|
157
|
+
expect(options.properties.maxCharacters).toBeDefined();
|
|
158
|
+
});
|
|
72
159
|
});
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { rollbackAgentInteractions } from "../../../src/agents/historyRollback";
|
|
2
|
+
import { Message } from "../../../src/clients/types";
|
|
3
|
+
|
|
4
|
+
const message = (role: Message["role"], content: string): Message => ({ role, content });
|
|
5
|
+
|
|
6
|
+
describe("rollbackAgentInteractions", () => {
|
|
7
|
+
const threads: Message[][] = [[
|
|
8
|
+
message("user", "request"),
|
|
9
|
+
message("assistant", "first"),
|
|
10
|
+
message("tool", "first result"),
|
|
11
|
+
message("assistant", "second"),
|
|
12
|
+
message("tool", "second result"),
|
|
13
|
+
]];
|
|
14
|
+
|
|
15
|
+
it("clones history without changing it when rollback is zero", () => {
|
|
16
|
+
const result = rollbackAgentInteractions(threads, 0);
|
|
17
|
+
expect(result).toEqual(threads);
|
|
18
|
+
expect(result).not.toBe(threads);
|
|
19
|
+
expect(result[0]).not.toBe(threads[0]);
|
|
20
|
+
expect(result[0][0]).not.toBe(threads[0][0]);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it("removes the newest assistant and its following tool results", () => {
|
|
24
|
+
expect(rollbackAgentInteractions(threads, 1)).toEqual([threads[0].slice(0, 3)]);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it("can remove multiple interactions", () => {
|
|
28
|
+
expect(rollbackAgentInteractions(threads, 2)).toEqual([[threads[0][0]]]);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it("removes a trailing assistant without tools", () => {
|
|
32
|
+
const input = [[message("user", "request"), message("assistant", "tail")]];
|
|
33
|
+
expect(rollbackAgentInteractions(input, 1)).toEqual([[input[0][0]]]);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it.each([-1, 1.5])("rejects invalid rollback %p", (rollback) => {
|
|
37
|
+
expect(() => rollbackAgentInteractions(threads, rollback)).toThrow(
|
|
38
|
+
"rollback must be a non-negative integer"
|
|
39
|
+
);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it("rejects rollback beyond available interactions", () => {
|
|
43
|
+
expect(() => rollbackAgentInteractions(threads, 3)).toThrow(
|
|
44
|
+
"only has 2 assistant interaction(s)"
|
|
45
|
+
);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it("does not mutate input history", () => {
|
|
49
|
+
const before = JSON.stringify(threads);
|
|
50
|
+
rollbackAgentInteractions(threads, 1);
|
|
51
|
+
expect(JSON.stringify(threads)).toBe(before);
|
|
52
|
+
});
|
|
53
|
+
});
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
const post = jest.fn();
|
|
2
|
+
const publicKeyBase64 = Buffer.alloc(32, 7).toString("base64");
|
|
3
|
+
const keyPairExists = jest.fn(() => true);
|
|
4
|
+
const loadKeyPair = jest.fn(() => ({ publicKeyBase64 }));
|
|
5
|
+
const signMessage = jest.fn(() => "server-message-signature");
|
|
6
|
+
|
|
7
|
+
jest.mock("../../../src/utils/http", () => ({
|
|
8
|
+
__esModule: true,
|
|
9
|
+
default: {
|
|
10
|
+
post,
|
|
11
|
+
isHttpError: (error: unknown) =>
|
|
12
|
+
typeof error === "object" && error !== null && "status" in error,
|
|
13
|
+
},
|
|
14
|
+
}));
|
|
15
|
+
|
|
16
|
+
jest.mock("../../../src/auth/keyManager", () => ({
|
|
17
|
+
keyPairExists,
|
|
18
|
+
loadKeyPair,
|
|
19
|
+
signMessage,
|
|
20
|
+
}));
|
|
21
|
+
|
|
22
|
+
jest.mock("../../../src/auth/browserLogin", () => ({
|
|
23
|
+
getCliUserAgent: () => "test-agent",
|
|
24
|
+
}));
|
|
25
|
+
|
|
26
|
+
import {
|
|
27
|
+
authenticateWithKey,
|
|
28
|
+
exchangePublicKeyJwt,
|
|
29
|
+
hasKeyPair,
|
|
30
|
+
registerPublicKey,
|
|
31
|
+
} from "../../../src/auth/keyAuth";
|
|
32
|
+
|
|
33
|
+
describe("keyAuth", () => {
|
|
34
|
+
beforeEach(() => {
|
|
35
|
+
jest.clearAllMocks();
|
|
36
|
+
keyPairExists.mockReturnValue(true);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it("signs the exact server challenge for the selected key, org, and environment", async () => {
|
|
40
|
+
post
|
|
41
|
+
.mockResolvedValueOnce({
|
|
42
|
+
data: {
|
|
43
|
+
challengeId: "challenge_1",
|
|
44
|
+
message: "exact message from server",
|
|
45
|
+
expiresAt: "2099-01-01T00:00:00.000Z",
|
|
46
|
+
},
|
|
47
|
+
status: 201,
|
|
48
|
+
})
|
|
49
|
+
.mockResolvedValueOnce({ data: { jwt: "renewed-jwt" }, status: 200 });
|
|
50
|
+
|
|
51
|
+
expect(hasKeyPair("/keys/work")).toBe(true);
|
|
52
|
+
await expect(
|
|
53
|
+
exchangePublicKeyJwt("org-1", "https://work.example", "/keys/work")
|
|
54
|
+
).resolves.toBe("renewed-jwt");
|
|
55
|
+
|
|
56
|
+
expect(loadKeyPair).toHaveBeenCalledWith("/keys/work");
|
|
57
|
+
expect(signMessage).toHaveBeenCalledWith("exact message from server", "/keys/work");
|
|
58
|
+
expect(post.mock.calls).toEqual([
|
|
59
|
+
[
|
|
60
|
+
"https://work.example/api/public-keys/challenge",
|
|
61
|
+
{ publicKey: publicKeyBase64, orgId: "org-1" },
|
|
62
|
+
{ headers: { "User-Agent": "test-agent" } },
|
|
63
|
+
],
|
|
64
|
+
[
|
|
65
|
+
"https://work.example/api/public-keys/authenticate",
|
|
66
|
+
{ challengeId: "challenge_1", signature: "server-message-signature" },
|
|
67
|
+
{ headers: { "User-Agent": "test-agent" } },
|
|
68
|
+
],
|
|
69
|
+
]);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it("falls back when the key is unavailable without signing a challenge", async () => {
|
|
73
|
+
post.mockRejectedValueOnce({ status: 401 });
|
|
74
|
+
|
|
75
|
+
await expect(
|
|
76
|
+
authenticateWithKey("org-1", "https://work.example", "/keys/work")
|
|
77
|
+
).resolves.toBe(false);
|
|
78
|
+
|
|
79
|
+
expect(post).toHaveBeenCalledTimes(1);
|
|
80
|
+
expect(signMessage).not.toHaveBeenCalled();
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it("registers only server-owned Ed25519 key fields in the selected environment", async () => {
|
|
84
|
+
post.mockResolvedValueOnce({ data: {}, status: 201 });
|
|
85
|
+
|
|
86
|
+
await registerPublicKey("browser-jwt", publicKeyBase64, "https://dev.example");
|
|
87
|
+
|
|
88
|
+
expect(post).toHaveBeenCalledWith(
|
|
89
|
+
"https://dev.example/api/public-keys/register",
|
|
90
|
+
{
|
|
91
|
+
publicKey: publicKeyBase64,
|
|
92
|
+
label: expect.any(String),
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
headers: {
|
|
96
|
+
Authorization: "Bearer browser-jwt",
|
|
97
|
+
"User-Agent": "test-agent",
|
|
98
|
+
},
|
|
99
|
+
}
|
|
100
|
+
);
|
|
101
|
+
});
|
|
102
|
+
});
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import crypto from "crypto";
|
|
2
|
+
import fs from "fs";
|
|
3
|
+
import os from "os";
|
|
4
|
+
import path from "path";
|
|
5
|
+
import {
|
|
6
|
+
generateKeyPair,
|
|
7
|
+
loadKeyPair,
|
|
8
|
+
signMessage,
|
|
9
|
+
} from "../../../src/auth/keyManager";
|
|
10
|
+
|
|
11
|
+
describe("keyManager", () => {
|
|
12
|
+
let directory: string;
|
|
13
|
+
let identity: string;
|
|
14
|
+
|
|
15
|
+
beforeEach(() => {
|
|
16
|
+
directory = fs.mkdtempSync(path.join(os.tmpdir(), "knowhow-key-manager-"));
|
|
17
|
+
identity = path.join(directory, "identity");
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
afterEach(() => {
|
|
21
|
+
fs.rmSync(directory, { recursive: true, force: true });
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it("creates an Ed25519 identity with an OpenSSH-compatible public key", () => {
|
|
25
|
+
const generated = generateKeyPair(identity);
|
|
26
|
+
const publicKeyLine = fs.readFileSync(`${identity}.pub`, "utf8").trim();
|
|
27
|
+
|
|
28
|
+
expect(publicKeyLine).toMatch(/^ssh-ed25519 [A-Za-z0-9+/]+=* knowhow-cli$/);
|
|
29
|
+
expect(fs.statSync(identity).mode % 0o1000).toBe(0o600);
|
|
30
|
+
expect(loadKeyPair(identity)).toEqual(generated);
|
|
31
|
+
|
|
32
|
+
const challengeMessage = "exact server-provided challenge";
|
|
33
|
+
const signature = signMessage(challengeMessage, identity);
|
|
34
|
+
const publicKey = crypto.createPublicKey(fs.readFileSync(identity));
|
|
35
|
+
expect(
|
|
36
|
+
crypto.verify(
|
|
37
|
+
null,
|
|
38
|
+
Buffer.from(challengeMessage),
|
|
39
|
+
publicKey,
|
|
40
|
+
Buffer.from(signature, "base64")
|
|
41
|
+
)
|
|
42
|
+
).toBe(true);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it("loads legacy raw-base64 public keys but rejects non-Ed25519 identities", () => {
|
|
46
|
+
const generated = generateKeyPair(identity);
|
|
47
|
+
fs.writeFileSync(`${identity}.pub`, generated.publicKeyBase64);
|
|
48
|
+
expect(loadKeyPair(identity).publicKeyBase64).toBe(generated.publicKeyBase64);
|
|
49
|
+
|
|
50
|
+
const rsaIdentity = path.join(directory, "rsa");
|
|
51
|
+
const rsa = crypto.generateKeyPairSync("rsa", {
|
|
52
|
+
modulusLength: 2048,
|
|
53
|
+
privateKeyEncoding: { type: "pkcs8", format: "pem" },
|
|
54
|
+
publicKeyEncoding: { type: "spki", format: "pem" },
|
|
55
|
+
});
|
|
56
|
+
fs.writeFileSync(rsaIdentity, rsa.privateKey);
|
|
57
|
+
fs.writeFileSync(`${rsaIdentity}.pub`, generated.publicKeyBase64);
|
|
58
|
+
|
|
59
|
+
expect(() => loadKeyPair(rsaIdentity)).toThrow("must be an Ed25519 private key");
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it("does not overwrite a partial identity", () => {
|
|
63
|
+
fs.writeFileSync(identity, "existing key");
|
|
64
|
+
expect(() => generateKeyPair(identity)).toThrow("a key file already exists");
|
|
65
|
+
expect(fs.readFileSync(identity, "utf8")).toBe("existing key");
|
|
66
|
+
});
|
|
67
|
+
});
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { parseHistoryCommandArgs } from "../../../src/chat/modules/SessionsModule";
|
|
2
|
+
|
|
3
|
+
describe("parseHistoryCommandArgs", () => {
|
|
4
|
+
it("parses an id and rollback in either order", () => {
|
|
5
|
+
expect(parseHistoryCommandArgs(["task-1", "--rollback", "2"])).toEqual({
|
|
6
|
+
id: "task-1", rollback: 2, showAll: false,
|
|
7
|
+
});
|
|
8
|
+
expect(parseHistoryCommandArgs(["--rollback", "1", "task-1"])).toEqual({
|
|
9
|
+
id: "task-1", rollback: 1, showAll: false,
|
|
10
|
+
});
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it("supports interactive --all", () => {
|
|
14
|
+
expect(parseHistoryCommandArgs(["--all"])).toEqual({
|
|
15
|
+
id: undefined, rollback: 0, showAll: true,
|
|
16
|
+
});
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it.each([
|
|
20
|
+
[["--rollback"]],
|
|
21
|
+
[["--rollback", "-1"]],
|
|
22
|
+
[["--rollback", "1.5"]],
|
|
23
|
+
])(
|
|
24
|
+
"rejects invalid rollback arguments: %p",
|
|
25
|
+
(args) => {
|
|
26
|
+
expect(() => parseHistoryCommandArgs(args)).toThrow(
|
|
27
|
+
"--rollback must be a non-negative integer"
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
it("rejects unexpected arguments", () => {
|
|
33
|
+
expect(() => parseHistoryCommandArgs(["one", "two"])).toThrow(
|
|
34
|
+
"Unexpected argument: two"
|
|
35
|
+
);
|
|
36
|
+
});
|
|
37
|
+
});
|