@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
package/autodoc/cli-reference.md
CHANGED
|
@@ -86,7 +86,7 @@ knowhow chat
|
|
|
86
86
|
---
|
|
87
87
|
|
|
88
88
|
### `knowhow agent`
|
|
89
|
-
**Purpose:**
|
|
89
|
+
**Purpose:** Start a new one-shot agent task directly from the CLI (default agent: `Patcher`).
|
|
90
90
|
|
|
91
91
|
**Usage:**
|
|
92
92
|
```bash
|
|
@@ -114,8 +114,6 @@ knowhow agent [options]
|
|
|
114
114
|
Custom prompt template file with a `{text}` placeholder
|
|
115
115
|
- `--input <text>`
|
|
116
116
|
Task input (fallback to stdin if not provided)
|
|
117
|
-
- `--resume`
|
|
118
|
-
Resume a previously started task using `--task-id` (local FS or remote)
|
|
119
117
|
- `--renderer <name>`
|
|
120
118
|
Renderer to use: `basic`, `compact`, `fancy`, or a path/package
|
|
121
119
|
(default: from config or `basic`)
|
|
@@ -136,14 +134,45 @@ Use a custom prompt template file:
|
|
|
136
134
|
knowhow agent --prompt-file .knowhow/prompts/BasicCodeDocumenter.mdx --input "src/index.ts"
|
|
137
135
|
```
|
|
138
136
|
|
|
139
|
-
|
|
137
|
+
Choose provider + renderer:
|
|
140
138
|
```bash
|
|
141
|
-
knowhow agent --
|
|
139
|
+
knowhow agent --provider openai --model gpt-4.1-mini --renderer fancy --input "Fix the failing tests"
|
|
142
140
|
```
|
|
143
141
|
|
|
144
|
-
|
|
142
|
+
---
|
|
143
|
+
|
|
144
|
+
### `knowhow agents resume` / `knowhow agents fork`
|
|
145
|
+
**Purpose:** Continue an existing agent task, or branch its history into a new task without modifying the source.
|
|
146
|
+
|
|
147
|
+
**Usage:**
|
|
145
148
|
```bash
|
|
146
|
-
knowhow
|
|
149
|
+
knowhow agents resume [taskId] [options]
|
|
150
|
+
knowhow agents fork [sourceTaskId] [options]
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
Both commands accept a full or partial task ID. Use `-i <number>` instead to select the corresponding row from `knowhow agents list`.
|
|
154
|
+
|
|
155
|
+
#### Options
|
|
156
|
+
- `-i, --index <number>` — Select a task by row number from `knowhow agents list`
|
|
157
|
+
- `--rollback <count>` — Discard the newest N agent interactions before continuing (default: `0`)
|
|
158
|
+
- `--input <text>` — Message to send after restoring history
|
|
159
|
+
- `--message-id <messageId>` — Knowhow message ID for remote task lookup
|
|
160
|
+
- `--sync-fs` — Enable filesystem-based synchronization
|
|
161
|
+
- `--provider <provider>` — Override the saved AI provider
|
|
162
|
+
- `--model <model>` — Override the saved model
|
|
163
|
+
- `--agent-name <name>` — Override the saved agent
|
|
164
|
+
- `--reasoning-effort <effort>` — Override the saved reasoning effort
|
|
165
|
+
- `--renderer <name>` — Renderer to use (default: from config or `basic`)
|
|
166
|
+
- `--task-id <taskId>` — Explicit destination task ID (`fork` only)
|
|
167
|
+
|
|
168
|
+
Resume the first listed task after removing its newest assistant interaction:
|
|
169
|
+
```bash
|
|
170
|
+
knowhow agents resume -i 1 --rollback 1
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
Fork the third listed task into an explicit destination task ID:
|
|
174
|
+
```bash
|
|
175
|
+
knowhow agents fork -i 3 --rollback 1 --task-id my-new-task
|
|
147
176
|
```
|
|
148
177
|
|
|
149
178
|
---
|
package/bin/knowhow.js
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
1
|
+
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// knowhow CLI entry point.
|
|
4
4
|
//
|
|
5
|
-
// --no-node-snapshot is
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
5
|
+
// --no-node-snapshot is NOT needed here. The script module (knowhow-module-script)
|
|
6
|
+
// forks a child process with --no-node-snapshot only when executeScript is actually
|
|
7
|
+
// called. This means:
|
|
8
|
+
// - The shebang works on Linux (GNU env) and macOS without any argument-splitting tricks.
|
|
9
|
+
// - Ghostty (or whichever terminal launched knowhow) remains the TCC subject for
|
|
10
|
+
// macOS Screen Recording / Accessibility permissions — computer-use features work
|
|
11
|
+
// without having to grant those permissions to the node binary itself.
|
|
10
12
|
|
|
11
13
|
const path = require("node:path");
|
|
12
14
|
const cliJs = path.join(__dirname, "../ts_build/src/cli.js");
|
package/package.json
CHANGED
package/src/agents/base/base.ts
CHANGED
|
@@ -22,6 +22,7 @@ import { Models } from "../../ai";
|
|
|
22
22
|
import { MessageProcessor } from "../../services/MessageProcessor";
|
|
23
23
|
import { DEFAULT_CONTEXT_LIMIT } from "../../clients/contextLimits";
|
|
24
24
|
import { Marked } from "../../utils";
|
|
25
|
+
import { TraceAll } from "../../util/Trace";
|
|
25
26
|
|
|
26
27
|
export { Message, Tool, ToolCall };
|
|
27
28
|
export interface ModelPreference {
|
|
@@ -83,6 +84,7 @@ interface ActiveObservation {
|
|
|
83
84
|
expiry?: NodeJS.Timeout;
|
|
84
85
|
}
|
|
85
86
|
|
|
87
|
+
@TraceAll()
|
|
86
88
|
export abstract class BaseAgent implements IAgent {
|
|
87
89
|
abstract name: string;
|
|
88
90
|
abstract description: string;
|
|
@@ -471,6 +473,28 @@ export abstract class BaseAgent implements IAgent {
|
|
|
471
473
|
return this.getEnabledTools().map((t) => t.function.name);
|
|
472
474
|
}
|
|
473
475
|
|
|
476
|
+
/** Restore the exact request-visible tool list persisted for a prior run. */
|
|
477
|
+
restoreEnabledTools(names: string[]) {
|
|
478
|
+
const uniqueNames = [...new Set(names)];
|
|
479
|
+
const lazyTools = this.tools as ToolsService & {
|
|
480
|
+
enableTools?: (patterns: string[]) => unknown;
|
|
481
|
+
restoreEnabledTools?: (names: string[]) => void;
|
|
482
|
+
};
|
|
483
|
+
|
|
484
|
+
// LazyToolsService only exposes explicitly enabled tools. Materialize the
|
|
485
|
+
// saved concrete names before applying the agent-level exact-set filter.
|
|
486
|
+
if (lazyTools.restoreEnabledTools) {
|
|
487
|
+
lazyTools.restoreEnabledTools(uniqueNames);
|
|
488
|
+
} else {
|
|
489
|
+
lazyTools.enableTools?.(uniqueNames);
|
|
490
|
+
}
|
|
491
|
+
const savedNames = new Set(uniqueNames);
|
|
492
|
+
this.disabledTools = this.tools
|
|
493
|
+
.getTools()
|
|
494
|
+
.map((tool) => tool.function.name)
|
|
495
|
+
.filter((name) => !savedNames.has(name));
|
|
496
|
+
}
|
|
497
|
+
|
|
474
498
|
disableTool(toolName: string) {
|
|
475
499
|
this.disabledTools.push(toolName);
|
|
476
500
|
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { Message } from "../clients/types";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Remove the newest agent interactions from the final persisted thread.
|
|
5
|
+
*
|
|
6
|
+
* An interaction starts with an assistant message and includes every following
|
|
7
|
+
* tool result up to (but not including) the next assistant message. Cutting at
|
|
8
|
+
* the assistant boundary keeps tool_call/tool-result pairs valid and makes
|
|
9
|
+
* `rollback: 1` useful for recovering from a failed final tool-use turn.
|
|
10
|
+
*/
|
|
11
|
+
export function rollbackAgentInteractions(
|
|
12
|
+
threads: Message[][],
|
|
13
|
+
rollback: number = 0
|
|
14
|
+
): Message[][] {
|
|
15
|
+
if (!Number.isInteger(rollback) || rollback < 0) {
|
|
16
|
+
throw new Error("rollback must be a non-negative integer");
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const cloned = threads.map((thread) => thread.map((message) => ({ ...message })));
|
|
20
|
+
if (rollback === 0 || cloned.length === 0) return cloned;
|
|
21
|
+
|
|
22
|
+
const lastThread = cloned[cloned.length - 1];
|
|
23
|
+
const assistantIndexes = lastThread
|
|
24
|
+
.map((message, index) => (message.role === "assistant" ? index : -1))
|
|
25
|
+
.filter((index) => index >= 0);
|
|
26
|
+
|
|
27
|
+
if (rollback > assistantIndexes.length) {
|
|
28
|
+
throw new Error(
|
|
29
|
+
`Cannot rollback ${rollback} interaction(s); the latest thread only has ${assistantIndexes.length} assistant interaction(s).`
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const cutIndex = assistantIndexes[assistantIndexes.length - rollback];
|
|
34
|
+
cloned[cloned.length - 1] = lastThread.slice(0, cutIndex);
|
|
35
|
+
return cloned;
|
|
36
|
+
}
|
|
@@ -8,6 +8,8 @@ export interface StartAgentTaskParams {
|
|
|
8
8
|
syncFs?: boolean;
|
|
9
9
|
taskId?: string;
|
|
10
10
|
resume?: boolean;
|
|
11
|
+
forkTaskId?: string;
|
|
12
|
+
rollback?: number;
|
|
11
13
|
prompt: string;
|
|
12
14
|
/**
|
|
13
15
|
* Push this agent's work to a remote Knowhow task identified by taskId.
|
|
@@ -104,6 +106,8 @@ export async function startAgentTask(params: StartAgentTaskParams): Promise<stri
|
|
|
104
106
|
prompt,
|
|
105
107
|
taskId: providedTaskId,
|
|
106
108
|
resume,
|
|
109
|
+
forkTaskId,
|
|
110
|
+
rollback = 0,
|
|
107
111
|
syncFs,
|
|
108
112
|
syncRemote,
|
|
109
113
|
provider,
|
|
@@ -118,6 +122,18 @@ export async function startAgentTask(params: StartAgentTaskParams): Promise<stri
|
|
|
118
122
|
if (!prompt) {
|
|
119
123
|
throw new Error("prompt is required to create a chat task");
|
|
120
124
|
}
|
|
125
|
+
if (resume && forkTaskId) {
|
|
126
|
+
throw new Error("resume and forkTaskId are mutually exclusive");
|
|
127
|
+
}
|
|
128
|
+
if (resume && !providedTaskId) {
|
|
129
|
+
throw new Error("taskId is required when resuming");
|
|
130
|
+
}
|
|
131
|
+
if (!Number.isInteger(rollback) || rollback < 0) {
|
|
132
|
+
throw new Error("rollback must be a non-negative integer");
|
|
133
|
+
}
|
|
134
|
+
if (rollback > 0 && !resume && !forkTaskId) {
|
|
135
|
+
throw new Error("rollback requires resume or forkTaskId");
|
|
136
|
+
}
|
|
121
137
|
|
|
122
138
|
// Default filesystem synchronization ON unless the caller explicitly opts out
|
|
123
139
|
// (syncFs: false) or is using a messageId-based web sync. This ensures spawned
|
|
@@ -134,7 +150,14 @@ export async function startAgentTask(params: StartAgentTaskParams): Promise<stri
|
|
|
134
150
|
explicitParentTaskId ?? _ctx?.taskId ?? (_ctx?.caller as any)?.currentTaskId;
|
|
135
151
|
|
|
136
152
|
// Build args array (no shell escaping needed - args are passed directly)
|
|
137
|
-
const
|
|
153
|
+
const isHistoryRun = !!resume || !!forkTaskId;
|
|
154
|
+
const args: string[] = resume
|
|
155
|
+
? ["agents", "resume", providedTaskId!]
|
|
156
|
+
: forkTaskId
|
|
157
|
+
? ["agents", "fork", forkTaskId]
|
|
158
|
+
: ["agent"];
|
|
159
|
+
|
|
160
|
+
if (forkTaskId) args.push("--task-id", taskId);
|
|
138
161
|
|
|
139
162
|
if (messageId) {
|
|
140
163
|
args.push("--message-id", messageId);
|
|
@@ -144,11 +167,11 @@ export async function startAgentTask(params: StartAgentTaskParams): Promise<stri
|
|
|
144
167
|
// When syncRemote is requested, pass it through so the spawned agent pushes
|
|
145
168
|
// its work to the remote task identified by --task-id rather than staying
|
|
146
169
|
// local-only.
|
|
147
|
-
if (syncRemote) {
|
|
170
|
+
if (syncRemote && !isHistoryRun) {
|
|
148
171
|
args.push("--sync-remote");
|
|
149
172
|
}
|
|
150
173
|
|
|
151
|
-
if (useSyncFs || providedTaskId) {
|
|
174
|
+
if (!isHistoryRun && (useSyncFs || providedTaskId)) {
|
|
152
175
|
// Pass --task-id whenever we have a known taskId (syncFs or explicit taskId)
|
|
153
176
|
args.push("--task-id", taskId);
|
|
154
177
|
}
|
|
@@ -165,20 +188,19 @@ export async function startAgentTask(params: StartAgentTaskParams): Promise<stri
|
|
|
165
188
|
args.push("--agent-name", agentName);
|
|
166
189
|
}
|
|
167
190
|
|
|
168
|
-
if (maxTimeLimit !== undefined) {
|
|
191
|
+
if (maxTimeLimit !== undefined && !isHistoryRun) {
|
|
169
192
|
args.push("--max-time-limit", String(maxTimeLimit));
|
|
170
193
|
}
|
|
171
194
|
|
|
172
|
-
if (maxSpendLimit !== undefined) {
|
|
195
|
+
if (maxSpendLimit !== undefined && !isHistoryRun) {
|
|
173
196
|
args.push("--max-spend-limit", String(maxSpendLimit));
|
|
174
197
|
}
|
|
175
|
-
if (parentTaskId) {
|
|
198
|
+
if (parentTaskId && !isHistoryRun) {
|
|
176
199
|
// Tell the child who spawned it so it can report back to the parent.
|
|
177
200
|
args.push("--parent-task-id", parentTaskId);
|
|
178
201
|
}
|
|
179
|
-
if (
|
|
180
|
-
|
|
181
|
-
args.push("--resume");
|
|
202
|
+
if (rollback > 0) {
|
|
203
|
+
args.push("--rollback", String(rollback));
|
|
182
204
|
}
|
|
183
205
|
|
|
184
206
|
const timeoutMs = maxTimeLimit ? maxTimeLimit * 60 * 1000 : 60 * 60 * 1000;
|
|
@@ -320,6 +342,16 @@ export const startAgentTaskDefinition: Tool = {
|
|
|
320
342
|
description:
|
|
321
343
|
"Resume a previously started task from where it left off. Must be used together with taskId which identifies the task to resume.",
|
|
322
344
|
},
|
|
345
|
+
forkTaskId: {
|
|
346
|
+
type: "string",
|
|
347
|
+
description:
|
|
348
|
+
"Fork this existing task into a new task. The source remains unchanged; taskId optionally selects the new task ID.",
|
|
349
|
+
},
|
|
350
|
+
rollback: {
|
|
351
|
+
type: "number",
|
|
352
|
+
description:
|
|
353
|
+
"Discard this many newest agent interactions before resuming or forking. Must be a non-negative integer.",
|
|
354
|
+
},
|
|
323
355
|
parentTaskId: {
|
|
324
356
|
type: "string",
|
|
325
357
|
description:
|
package/src/auth/browserLogin.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import http from "../utils/http";
|
|
2
2
|
import { exec } from "child_process";
|
|
3
3
|
import { promisify } from "util";
|
|
4
|
+
import crypto from "crypto";
|
|
4
5
|
import * as os from "os";
|
|
5
6
|
import * as fs from "fs";
|
|
6
7
|
import * as path from "path";
|
|
@@ -28,6 +29,8 @@ interface RetrieveTokenResponse {
|
|
|
28
29
|
|
|
29
30
|
export class BrowserLoginService {
|
|
30
31
|
private baseUrl: string;
|
|
32
|
+
/** PKCE verifier — generated once per login attempt, never sent in a URL */
|
|
33
|
+
private verifier: string = crypto.randomBytes(32).toString("hex");
|
|
31
34
|
|
|
32
35
|
constructor(baseUrl: string = KNOWHOW_API_URL, private orgId?: string) {
|
|
33
36
|
if (!baseUrl) {
|
|
@@ -100,9 +103,10 @@ export class BrowserLoginService {
|
|
|
100
103
|
spinner.stop();
|
|
101
104
|
spinner.start("Authentication successful! Retrieving token");
|
|
102
105
|
|
|
103
|
-
// Step 4:
|
|
106
|
+
// Step 4: Redeem session using PKCE verifier (never sent in URL)
|
|
104
107
|
const tokenResponse = await http.post(
|
|
105
|
-
`${this.baseUrl}/api/cli-login/session/${sessionData.sessionId}/
|
|
108
|
+
`${this.baseUrl}/api/cli-login/session/${sessionData.sessionId}/redeem`,
|
|
109
|
+
{ verifier: this.verifier }
|
|
106
110
|
);
|
|
107
111
|
|
|
108
112
|
const tokenData = tokenResponse.data as RetrieveTokenResponse;
|
|
@@ -170,9 +174,11 @@ export class BrowserLoginService {
|
|
|
170
174
|
*/
|
|
171
175
|
private async createSession(): Promise<CreateSessionResponse> {
|
|
172
176
|
try {
|
|
177
|
+
// Compute the PKCE challenge (sha256 of verifier) to store server-side
|
|
178
|
+
const challenge = crypto.createHash("sha256").update(this.verifier, "utf8").digest("hex");
|
|
173
179
|
const response = await http.post<CreateSessionResponse>(
|
|
174
180
|
`${this.baseUrl}/api/cli-login/session`,
|
|
175
|
-
{},
|
|
181
|
+
{ challenge },
|
|
176
182
|
{ headers: { "User-Agent": getCliUserAgent() } }
|
|
177
183
|
);
|
|
178
184
|
return response.data;
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import os from "os";
|
|
3
|
+
import path from "path";
|
|
4
|
+
import http from "../utils/http";
|
|
5
|
+
import { getCliUserAgent } from "./browserLogin";
|
|
6
|
+
import { keyPairExists, loadKeyPair, signMessage } from "./keyManager";
|
|
7
|
+
|
|
8
|
+
const JWT_FILE_PATH = path.join(process.cwd(), ".knowhow", ".jwt");
|
|
9
|
+
const DEFAULT_API_URL = process.env.KNOWHOW_API_URL || "https://api.knowhow.tyvm.ai";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* This is intentionally based only on the selected identity. Registration is
|
|
13
|
+
* server-side and scoped to its environment/user/org; a local `.registered`
|
|
14
|
+
* marker cannot safely represent that state. An unknown or revoked key is
|
|
15
|
+
* cheaply rejected by the selected API and login then falls back to PKCE.
|
|
16
|
+
*/
|
|
17
|
+
export function hasKeyPair(privateKeyPath?: string): boolean {
|
|
18
|
+
return keyPairExists(privateKeyPath);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Exchange a registered Ed25519 identity for a new short-lived JWT. */
|
|
22
|
+
export async function exchangePublicKeyJwt(
|
|
23
|
+
orgId: string,
|
|
24
|
+
apiUrl: string = DEFAULT_API_URL,
|
|
25
|
+
privateKeyPath?: string
|
|
26
|
+
): Promise<string> {
|
|
27
|
+
if (!keyPairExists(privateKeyPath)) {
|
|
28
|
+
throw new Error("CLI identity is unavailable");
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const keyPair = loadKeyPair(privateKeyPath);
|
|
32
|
+
const headers = { "User-Agent": getCliUserAgent() };
|
|
33
|
+
const challengeResponse = await http.post<{
|
|
34
|
+
challengeId: string;
|
|
35
|
+
message: string;
|
|
36
|
+
expiresAt: string;
|
|
37
|
+
}>(
|
|
38
|
+
`${apiUrl}/api/public-keys/challenge`,
|
|
39
|
+
{ publicKey: keyPair.publicKeyBase64, orgId },
|
|
40
|
+
{ headers }
|
|
41
|
+
);
|
|
42
|
+
const { challengeId, message } = challengeResponse.data;
|
|
43
|
+
if (
|
|
44
|
+
typeof challengeId !== "string" ||
|
|
45
|
+
challengeId.length === 0 ||
|
|
46
|
+
challengeId.length > 128 ||
|
|
47
|
+
!/^[A-Za-z0-9_-]+$/.test(challengeId) ||
|
|
48
|
+
typeof message !== "string" ||
|
|
49
|
+
message.length === 0
|
|
50
|
+
) {
|
|
51
|
+
throw new Error("Public-key challenge response is invalid");
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const response = await http.post<{ jwt: string }>(
|
|
55
|
+
`${apiUrl}/api/public-keys/authenticate`,
|
|
56
|
+
{ challengeId, signature: signMessage(message, privateKeyPath) },
|
|
57
|
+
{ headers }
|
|
58
|
+
);
|
|
59
|
+
if (!response.data.jwt) {
|
|
60
|
+
throw new Error("Public-key authentication returned no JWT");
|
|
61
|
+
}
|
|
62
|
+
return response.data.jwt;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export async function authenticateWithKey(
|
|
66
|
+
orgId: string,
|
|
67
|
+
apiUrl: string = DEFAULT_API_URL,
|
|
68
|
+
privateKeyPath?: string
|
|
69
|
+
): Promise<boolean> {
|
|
70
|
+
if (!keyPairExists(privateKeyPath)) return false;
|
|
71
|
+
|
|
72
|
+
try {
|
|
73
|
+
storeJwt(await exchangePublicKeyJwt(orgId, apiUrl, privateKeyPath));
|
|
74
|
+
return true;
|
|
75
|
+
} catch (error: unknown) {
|
|
76
|
+
// The identity is not registered for this environment/org (or was revoked).
|
|
77
|
+
if (http.isHttpError(error) && (error.status === 401 || error.status === 404)) {
|
|
78
|
+
return false;
|
|
79
|
+
}
|
|
80
|
+
throw error;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** Register an Ed25519 CLI-auth key using the JWT obtained by browser PKCE. */
|
|
85
|
+
export async function registerPublicKey(
|
|
86
|
+
jwt: string,
|
|
87
|
+
publicKeyBase64: string,
|
|
88
|
+
apiUrl: string = DEFAULT_API_URL
|
|
89
|
+
): Promise<void> {
|
|
90
|
+
const rawPublicKey = Buffer.from(publicKeyBase64, "base64");
|
|
91
|
+
if (rawPublicKey.length !== 32 || rawPublicKey.toString("base64") !== publicKeyBase64) {
|
|
92
|
+
throw new Error("CLI authentication requires a 32-byte Ed25519 public key");
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
await http.post(
|
|
96
|
+
`${apiUrl}/api/public-keys/register`,
|
|
97
|
+
{
|
|
98
|
+
publicKey: publicKeyBase64,
|
|
99
|
+
label: os.hostname(),
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
headers: {
|
|
103
|
+
Authorization: `Bearer ${jwt}`,
|
|
104
|
+
"User-Agent": getCliUserAgent(),
|
|
105
|
+
},
|
|
106
|
+
}
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export function storeJwt(jwt: string): void {
|
|
111
|
+
const directory = path.dirname(JWT_FILE_PATH);
|
|
112
|
+
fs.mkdirSync(directory, { recursive: true });
|
|
113
|
+
fs.writeFileSync(JWT_FILE_PATH, jwt, { mode: 0o600 });
|
|
114
|
+
fs.chmodSync(JWT_FILE_PATH, 0o600);
|
|
115
|
+
}
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import crypto from "crypto";
|
|
2
|
+
import fs from "fs";
|
|
3
|
+
import os from "os";
|
|
4
|
+
import path from "path";
|
|
5
|
+
|
|
6
|
+
const ED25519_KEY_TYPE = "ssh-ed25519";
|
|
7
|
+
const ED25519_RAW_KEY_LENGTH = 32;
|
|
8
|
+
const ED25519_SPKI_PREFIX = Buffer.from("302a300506032b6570032100", "hex");
|
|
9
|
+
|
|
10
|
+
export interface KeyPair {
|
|
11
|
+
privateKeyPath: string;
|
|
12
|
+
publicKeyPath: string;
|
|
13
|
+
/** Raw 32-byte Ed25519 key, base64 encoded for the authentication API. */
|
|
14
|
+
publicKeyBase64: string;
|
|
15
|
+
/** OpenSSH SHA-256 fingerprint (hashes the SSH wire-format public key). */
|
|
16
|
+
fingerprint: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function getKeyDir(): string {
|
|
20
|
+
return path.join(os.homedir(), ".knowhow", "keys");
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function getDefaultPrivateKeyPath(): string {
|
|
24
|
+
return path.join(getKeyDir(), "id_ed25519");
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function getDefaultPublicKeyPath(): string {
|
|
28
|
+
return `${getDefaultPrivateKeyPath()}.pub`;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function keyPairExists(privateKeyPath?: string): boolean {
|
|
32
|
+
const privatePath = privateKeyPath ?? getDefaultPrivateKeyPath();
|
|
33
|
+
return fs.existsSync(privatePath) && fs.existsSync(`${privatePath}.pub`);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Generate a CLI-only Ed25519 key and an OpenSSH-compatible public-key file. */
|
|
37
|
+
export function generateKeyPair(privateKeyPath?: string): KeyPair {
|
|
38
|
+
const privatePath = privateKeyPath ?? getDefaultPrivateKeyPath();
|
|
39
|
+
const publicPath = `${privatePath}.pub`;
|
|
40
|
+
|
|
41
|
+
fs.mkdirSync(path.dirname(privatePath), { recursive: true, mode: 0o700 });
|
|
42
|
+
const { privateKey, publicKey } = crypto.generateKeyPairSync("ed25519", {
|
|
43
|
+
privateKeyEncoding: { type: "pkcs8", format: "pem" },
|
|
44
|
+
publicKeyEncoding: { type: "spki", format: "der" },
|
|
45
|
+
});
|
|
46
|
+
const rawPublicKey = extractRawPublicKey(publicKey);
|
|
47
|
+
|
|
48
|
+
// Refuse to overwrite either half of an identity. Partial identities should be
|
|
49
|
+
// repaired explicitly rather than silently replacing a user's key.
|
|
50
|
+
if (fs.existsSync(privatePath) || fs.existsSync(publicPath)) {
|
|
51
|
+
throw new Error(`Cannot generate identity: a key file already exists at ${privatePath} or ${publicPath}`);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
fs.writeFileSync(privatePath, privateKey, { mode: 0o600, flag: "wx" });
|
|
55
|
+
try {
|
|
56
|
+
fs.writeFileSync(publicPath, formatOpenSshPublicKey(rawPublicKey), {
|
|
57
|
+
mode: 0o644,
|
|
58
|
+
flag: "wx",
|
|
59
|
+
});
|
|
60
|
+
} catch (error) {
|
|
61
|
+
fs.rmSync(privatePath, { force: true });
|
|
62
|
+
throw error;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return keyPairMetadata(privatePath, rawPublicKey);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** Load and validate an Ed25519 identity. Legacy raw-base64 .pub files remain supported. */
|
|
69
|
+
export function loadKeyPair(privateKeyPath?: string): KeyPair {
|
|
70
|
+
const privatePath = privateKeyPath ?? getDefaultPrivateKeyPath();
|
|
71
|
+
const publicPath = `${privatePath}.pub`;
|
|
72
|
+
if (!fs.existsSync(privatePath)) throw new Error(`Private key not found at: ${privatePath}`);
|
|
73
|
+
if (!fs.existsSync(publicPath)) throw new Error(`Public key not found at: ${publicPath}`);
|
|
74
|
+
|
|
75
|
+
const privateKey = readEd25519PrivateKey(privatePath);
|
|
76
|
+
const publicKey = parsePublicKey(fs.readFileSync(publicPath, "utf8"));
|
|
77
|
+
const derivedPublicKey = extractRawPublicKey(
|
|
78
|
+
crypto.createPublicKey(privateKey).export({ type: "spki", format: "der" })
|
|
79
|
+
);
|
|
80
|
+
if (!crypto.timingSafeEqual(publicKey, derivedPublicKey)) {
|
|
81
|
+
throw new Error(`Public key does not match private key: ${publicPath}`);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return keyPairMetadata(privatePath, publicKey);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function getOrCreatePublicKey(privateKeyPath?: string): KeyPair {
|
|
88
|
+
return keyPairExists(privateKeyPath)
|
|
89
|
+
? loadKeyPair(privateKeyPath)
|
|
90
|
+
: generateKeyPair(privateKeyPath);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function signMessage(message: string, privateKeyPath?: string): string {
|
|
94
|
+
const privatePath = privateKeyPath ?? getDefaultPrivateKeyPath();
|
|
95
|
+
if (!fs.existsSync(privatePath)) throw new Error(`Private key not found at: ${privatePath}`);
|
|
96
|
+
return crypto.sign(null, Buffer.from(message, "utf8"), readEd25519PrivateKey(privatePath)).toString("base64");
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function readEd25519PrivateKey(privatePath: string): crypto.KeyObject {
|
|
100
|
+
let key: crypto.KeyObject;
|
|
101
|
+
try {
|
|
102
|
+
key = crypto.createPrivateKey(fs.readFileSync(privatePath));
|
|
103
|
+
} catch {
|
|
104
|
+
throw new Error(`Identity at ${privatePath} is not a supported PEM Ed25519 private key`);
|
|
105
|
+
}
|
|
106
|
+
if (key.asymmetricKeyType !== "ed25519") {
|
|
107
|
+
throw new Error(`Identity at ${privatePath} must be an Ed25519 private key`);
|
|
108
|
+
}
|
|
109
|
+
return key;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function extractRawPublicKey(spkiDer: Buffer): Buffer {
|
|
113
|
+
if (
|
|
114
|
+
spkiDer.length !== ED25519_SPKI_PREFIX.length + ED25519_RAW_KEY_LENGTH ||
|
|
115
|
+
!spkiDer.subarray(0, ED25519_SPKI_PREFIX.length).equals(ED25519_SPKI_PREFIX)
|
|
116
|
+
) {
|
|
117
|
+
throw new Error("Invalid Ed25519 public key");
|
|
118
|
+
}
|
|
119
|
+
return spkiDer.subarray(ED25519_SPKI_PREFIX.length);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function sshPublicKeyBlob(rawPublicKey: Buffer): Buffer {
|
|
123
|
+
const type = Buffer.from(ED25519_KEY_TYPE, "ascii");
|
|
124
|
+
const typeLength = Buffer.alloc(4);
|
|
125
|
+
const keyLength = Buffer.alloc(4);
|
|
126
|
+
typeLength.writeUInt32BE(type.length);
|
|
127
|
+
keyLength.writeUInt32BE(rawPublicKey.length);
|
|
128
|
+
return Buffer.concat([typeLength, type, keyLength, rawPublicKey]);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function formatOpenSshPublicKey(rawPublicKey: Buffer): string {
|
|
132
|
+
return `${ED25519_KEY_TYPE} ${sshPublicKeyBlob(rawPublicKey).toString("base64")} knowhow-cli\n`;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function parsePublicKey(value: string): Buffer {
|
|
136
|
+
const trimmed = value.trim();
|
|
137
|
+
if (trimmed.startsWith(`${ED25519_KEY_TYPE} `)) {
|
|
138
|
+
const parts = trimmed.split(/\s+/);
|
|
139
|
+
const blob = Buffer.from(parts[1] ?? "", "base64");
|
|
140
|
+
const expected = sshPublicKeyBlob(blob.subarray(blob.length - ED25519_RAW_KEY_LENGTH));
|
|
141
|
+
if (blob.length !== expected.length || !blob.equals(expected)) {
|
|
142
|
+
throw new Error("Invalid OpenSSH Ed25519 public key");
|
|
143
|
+
}
|
|
144
|
+
return blob.subarray(blob.length - ED25519_RAW_KEY_LENGTH);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Compatibility with key files written by early CLI builds.
|
|
148
|
+
if (!/^[A-Za-z0-9+/]+={0,2}$/.test(trimmed)) {
|
|
149
|
+
throw new Error("Public key must be an OpenSSH Ed25519 key");
|
|
150
|
+
}
|
|
151
|
+
const raw = Buffer.from(trimmed, "base64");
|
|
152
|
+
if (raw.length !== ED25519_RAW_KEY_LENGTH || raw.toString("base64") !== trimmed) {
|
|
153
|
+
throw new Error("Ed25519 public key must contain exactly 32 bytes");
|
|
154
|
+
}
|
|
155
|
+
return raw;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function keyPairMetadata(privatePath: string, rawPublicKey: Buffer): KeyPair {
|
|
159
|
+
const hash = crypto.createHash("sha256").update(sshPublicKeyBlob(rawPublicKey)).digest("base64").replace(/=+$/, "");
|
|
160
|
+
return {
|
|
161
|
+
privateKeyPath: privatePath,
|
|
162
|
+
publicKeyPath: `${privatePath}.pub`,
|
|
163
|
+
publicKeyBase64: rawPublicKey.toString("base64"),
|
|
164
|
+
fingerprint: `SHA256:${hash}`,
|
|
165
|
+
};
|
|
166
|
+
}
|
|
@@ -43,6 +43,22 @@ export class CliChatService implements ChatService {
|
|
|
43
43
|
chatHistory: this.chatHistory,
|
|
44
44
|
plugins,
|
|
45
45
|
};
|
|
46
|
+
this.registerCommand({
|
|
47
|
+
name: "reload",
|
|
48
|
+
description: "Reload configuration, tools, MCP connections, and modules",
|
|
49
|
+
handler: async () => {
|
|
50
|
+
try {
|
|
51
|
+
console.log("Reloading configuration...");
|
|
52
|
+
const result = await services().RuntimeReload.reload();
|
|
53
|
+
console.log(
|
|
54
|
+
`✓ Reloaded ${result.tools} tools, ${result.mcps} MCP servers, and ${result.modules} modules`
|
|
55
|
+
);
|
|
56
|
+
} catch (error) {
|
|
57
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
58
|
+
console.error(`⚠ Reload failed: ${message}`);
|
|
59
|
+
}
|
|
60
|
+
},
|
|
61
|
+
});
|
|
46
62
|
this.loadInputHistory();
|
|
47
63
|
|
|
48
64
|
// Set up callback to add entries to inputHistory immediately when user presses Enter
|