@tyvm/knowhow 0.0.146 → 0.0.148
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 +51 -23
- package/src/agents/historyRollback.ts +36 -0
- package/src/agents/tools/execCommand.ts +102 -320
- package/src/agents/tools/list.ts +2 -2
- package/src/agents/tools/startAgentTask.ts +91 -34
- package/src/ai.ts +4 -1
- 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 +23 -2
- package/src/chat/modules/AgentModule.ts +114 -33
- 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 +7 -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 +103 -34
- 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/modules/ToolCommandModule.ts +102 -0
- package/src/chat/modules/index.ts +1 -0
- package/src/chat/types.ts +3 -1
- package/src/cli.ts +10 -3
- package/src/clients/http.ts +67 -7
- package/src/clients/index.ts +4 -0
- package/src/commands/agent.ts +1 -38
- package/src/commands/agents.ts +455 -60
- package/src/commands/misc.ts +3 -2
- package/src/commands/processes.ts +138 -0
- package/src/commands/services.ts +23 -1
- package/src/index.ts +1 -0
- package/src/login.ts +89 -28
- package/src/processes/ProcessManager.ts +95 -0
- package/src/processes/managedProcessShim.ts +115 -0
- package/src/services/AgentService.ts +2 -0
- package/src/services/AgentSyncFs.ts +77 -1
- 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/ExtensionsService.ts +25 -0
- package/src/services/KnowhowClient.ts +68 -7
- package/src/services/LazyToolsService.ts +24 -20
- package/src/services/Mcp.ts +2 -0
- package/src/services/McpServer.ts +2 -0
- package/src/services/MessageProcessor.ts +2 -0
- package/src/services/MinimalToolsService.ts +2 -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 +12 -0
- package/src/services/index.ts +6 -0
- package/src/services/modules/index.ts +10 -1
- package/src/services/modules/types.ts +4 -0
- package/src/types.ts +2 -0
- package/src/util/Trace.ts +1 -1
- package/src/utils/http.ts +18 -2
- 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/ToolCommandModule.test.ts +71 -0
- package/tests/unit/chat/historyCommandArgs.test.ts +37 -0
- package/tests/unit/commands/agents.test.ts +111 -0
- package/tests/unit/login.test.ts +119 -0
- package/tests/unit/services/AgentSyncKnowhowWeb.test.ts +38 -0
- package/tests/unit/services/LazyToolsService.test.ts +53 -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 +2 -1
- package/ts_build/src/agents/base/base.js +47 -14
- 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/execCommand.d.ts +4 -1
- package/ts_build/src/agents/tools/execCommand.js +100 -223
- package/ts_build/src/agents/tools/execCommand.js.map +1 -1
- package/ts_build/src/agents/tools/list.js +2 -2
- package/ts_build/src/agents/tools/list.js.map +1 -1
- package/ts_build/src/agents/tools/startAgentTask.d.ts +16 -1
- package/ts_build/src/agents/tools/startAgentTask.js +71 -28
- package/ts_build/src/agents/tools/startAgentTask.js.map +1 -1
- package/ts_build/src/ai.js +2 -1
- package/ts_build/src/ai.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.d.ts +6 -0
- package/ts_build/src/chat/CliChatService.js +17 -2
- 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 +83 -24
- 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.d.ts +1 -0
- package/ts_build/src/chat/modules/InternalChatModule.js +21 -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 +113 -18
- 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/modules/ToolCommandModule.d.ts +10 -0
- package/ts_build/src/chat/modules/ToolCommandModule.js +86 -0
- package/ts_build/src/chat/modules/ToolCommandModule.js.map +1 -0
- package/ts_build/src/chat/modules/index.d.ts +1 -0
- package/ts_build/src/chat/modules/index.js +3 -1
- package/ts_build/src/chat/modules/index.js.map +1 -1
- package/ts_build/src/chat/types.d.ts +3 -1
- package/ts_build/src/cli.js +7 -3
- package/ts_build/src/cli.js.map +1 -1
- package/ts_build/src/clients/http.d.ts +4 -0
- package/ts_build/src/clients/http.js +68 -6
- package/ts_build/src/clients/http.js.map +1 -1
- package/ts_build/src/clients/index.js +23 -4
- package/ts_build/src/clients/index.js.map +1 -1
- package/ts_build/src/commands/agent.js +1 -21
- package/ts_build/src/commands/agent.js.map +1 -1
- package/ts_build/src/commands/agents.d.ts +2 -1
- package/ts_build/src/commands/agents.js +216 -23
- 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/processes.d.ts +2 -0
- package/ts_build/src/commands/processes.js +213 -0
- package/ts_build/src/commands/processes.js.map +1 -0
- package/ts_build/src/commands/services.js +11 -1
- package/ts_build/src/commands/services.js.map +1 -1
- package/ts_build/src/index.d.ts +1 -0
- package/ts_build/src/index.js +1 -0
- package/ts_build/src/index.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/processes/ProcessManager.d.ts +51 -0
- package/ts_build/src/processes/ProcessManager.js +160 -0
- package/ts_build/src/processes/ProcessManager.js.map +1 -0
- package/ts_build/src/processes/managedProcessShim.d.ts +1 -0
- package/ts_build/src/processes/managedProcessShim.js +161 -0
- package/ts_build/src/processes/managedProcessShim.js.map +1 -0
- 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 +68 -16
- 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/ExtensionsService.d.ts +10 -0
- package/ts_build/src/services/ExtensionsService.js +17 -0
- package/ts_build/src/services/ExtensionsService.js.map +1 -0
- 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 -9
- package/ts_build/src/services/LazyToolsService.js.map +1 -1
- package/ts_build/src/services/Mcp.js +12 -2
- 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/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 +19 -2
- package/ts_build/src/services/Tools.js.map +1 -1
- package/ts_build/src/services/index.d.ts +5 -0
- package/ts_build/src/services/index.js +7 -2
- package/ts_build/src/services/index.js.map +1 -1
- package/ts_build/src/services/modules/index.d.ts +4 -1
- package/ts_build/src/services/modules/index.js +6 -1
- 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 +1 -3
- 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/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/ToolCommandModule.test.d.ts +1 -0
- package/ts_build/tests/unit/chat/ToolCommandModule.test.js +61 -0
- package/ts_build/tests/unit/chat/ToolCommandModule.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 +119 -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/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 +47 -0
- package/ts_build/tests/unit/services/LazyToolsService.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
|
@@ -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.
|
|
@@ -32,6 +34,20 @@ export interface StartAgentTaskParams {
|
|
|
32
34
|
_ctx?: { caller?: any; taskId?: string; [key: string]: any };
|
|
33
35
|
}
|
|
34
36
|
|
|
37
|
+
export interface StartAgentTaskResult {
|
|
38
|
+
success: boolean;
|
|
39
|
+
status: "started" | "completed" | "failed";
|
|
40
|
+
taskId: string;
|
|
41
|
+
pid?: number;
|
|
42
|
+
syncFs: boolean;
|
|
43
|
+
syncReady: boolean;
|
|
44
|
+
agentDir?: string;
|
|
45
|
+
logPath: string;
|
|
46
|
+
parentTaskId?: string;
|
|
47
|
+
exitCode?: number | null;
|
|
48
|
+
error?: string;
|
|
49
|
+
}
|
|
50
|
+
|
|
35
51
|
const PROCESSES_DIR = path.join(process.cwd(), ".knowhow", "processes");
|
|
36
52
|
const AGENTS_DIR = path.join(process.cwd(), ".knowhow", "processes", "agents");
|
|
37
53
|
|
|
@@ -98,12 +114,14 @@ function subagentCapabilitiesNote(): string {
|
|
|
98
114
|
* .knowhow/processes/agents/{taskId}/input.txt
|
|
99
115
|
* The agent will pick up the new content and process it as a new message.
|
|
100
116
|
*/
|
|
101
|
-
export async function startAgentTask(params: StartAgentTaskParams): Promise<
|
|
117
|
+
export async function startAgentTask(params: StartAgentTaskParams): Promise<StartAgentTaskResult> {
|
|
102
118
|
const {
|
|
103
119
|
messageId,
|
|
104
120
|
prompt,
|
|
105
121
|
taskId: providedTaskId,
|
|
106
122
|
resume,
|
|
123
|
+
forkTaskId,
|
|
124
|
+
rollback = 0,
|
|
107
125
|
syncFs,
|
|
108
126
|
syncRemote,
|
|
109
127
|
provider,
|
|
@@ -118,6 +136,18 @@ export async function startAgentTask(params: StartAgentTaskParams): Promise<stri
|
|
|
118
136
|
if (!prompt) {
|
|
119
137
|
throw new Error("prompt is required to create a chat task");
|
|
120
138
|
}
|
|
139
|
+
if (resume && forkTaskId) {
|
|
140
|
+
throw new Error("resume and forkTaskId are mutually exclusive");
|
|
141
|
+
}
|
|
142
|
+
if (resume && !providedTaskId) {
|
|
143
|
+
throw new Error("taskId is required when resuming");
|
|
144
|
+
}
|
|
145
|
+
if (!Number.isInteger(rollback) || rollback < 0) {
|
|
146
|
+
throw new Error("rollback must be a non-negative integer");
|
|
147
|
+
}
|
|
148
|
+
if (rollback > 0 && !resume && !forkTaskId) {
|
|
149
|
+
throw new Error("rollback requires resume or forkTaskId");
|
|
150
|
+
}
|
|
121
151
|
|
|
122
152
|
// Default filesystem synchronization ON unless the caller explicitly opts out
|
|
123
153
|
// (syncFs: false) or is using a messageId-based web sync. This ensures spawned
|
|
@@ -134,7 +164,14 @@ export async function startAgentTask(params: StartAgentTaskParams): Promise<stri
|
|
|
134
164
|
explicitParentTaskId ?? _ctx?.taskId ?? (_ctx?.caller as any)?.currentTaskId;
|
|
135
165
|
|
|
136
166
|
// Build args array (no shell escaping needed - args are passed directly)
|
|
137
|
-
const
|
|
167
|
+
const isHistoryRun = !!resume || !!forkTaskId;
|
|
168
|
+
const args: string[] = resume
|
|
169
|
+
? ["agents", "resume", providedTaskId!]
|
|
170
|
+
: forkTaskId
|
|
171
|
+
? ["agents", "fork", forkTaskId]
|
|
172
|
+
: ["agent"];
|
|
173
|
+
|
|
174
|
+
if (forkTaskId) args.push("--task-id", taskId);
|
|
138
175
|
|
|
139
176
|
if (messageId) {
|
|
140
177
|
args.push("--message-id", messageId);
|
|
@@ -144,11 +181,11 @@ export async function startAgentTask(params: StartAgentTaskParams): Promise<stri
|
|
|
144
181
|
// When syncRemote is requested, pass it through so the spawned agent pushes
|
|
145
182
|
// its work to the remote task identified by --task-id rather than staying
|
|
146
183
|
// local-only.
|
|
147
|
-
if (syncRemote) {
|
|
184
|
+
if (syncRemote && !isHistoryRun) {
|
|
148
185
|
args.push("--sync-remote");
|
|
149
186
|
}
|
|
150
187
|
|
|
151
|
-
if (useSyncFs || providedTaskId) {
|
|
188
|
+
if (!isHistoryRun && (useSyncFs || providedTaskId)) {
|
|
152
189
|
// Pass --task-id whenever we have a known taskId (syncFs or explicit taskId)
|
|
153
190
|
args.push("--task-id", taskId);
|
|
154
191
|
}
|
|
@@ -165,20 +202,19 @@ export async function startAgentTask(params: StartAgentTaskParams): Promise<stri
|
|
|
165
202
|
args.push("--agent-name", agentName);
|
|
166
203
|
}
|
|
167
204
|
|
|
168
|
-
if (maxTimeLimit !== undefined) {
|
|
205
|
+
if (maxTimeLimit !== undefined && !isHistoryRun) {
|
|
169
206
|
args.push("--max-time-limit", String(maxTimeLimit));
|
|
170
207
|
}
|
|
171
208
|
|
|
172
|
-
if (maxSpendLimit !== undefined) {
|
|
209
|
+
if (maxSpendLimit !== undefined && !isHistoryRun) {
|
|
173
210
|
args.push("--max-spend-limit", String(maxSpendLimit));
|
|
174
211
|
}
|
|
175
|
-
if (parentTaskId) {
|
|
212
|
+
if (parentTaskId && !isHistoryRun) {
|
|
176
213
|
// Tell the child who spawned it so it can report back to the parent.
|
|
177
214
|
args.push("--parent-task-id", parentTaskId);
|
|
178
215
|
}
|
|
179
|
-
if (
|
|
180
|
-
|
|
181
|
-
args.push("--resume");
|
|
216
|
+
if (rollback > 0) {
|
|
217
|
+
args.push("--rollback", String(rollback));
|
|
182
218
|
}
|
|
183
219
|
|
|
184
220
|
const timeoutMs = maxTimeLimit ? maxTimeLimit * 60 * 1000 : 60 * 60 * 1000;
|
|
@@ -213,43 +249,54 @@ export async function startAgentTask(params: StartAgentTaskParams): Promise<stri
|
|
|
213
249
|
child.stdin!.write(promptWithCaps, "utf8");
|
|
214
250
|
child.stdin!.end();
|
|
215
251
|
|
|
216
|
-
return new Promise<
|
|
252
|
+
return new Promise<StartAgentTaskResult>((resolve) => {
|
|
217
253
|
let settled = false;
|
|
218
|
-
|
|
254
|
+
let exited = false;
|
|
255
|
+
const done = (result: StartAgentTaskResult) => {
|
|
219
256
|
if (settled) return;
|
|
220
257
|
settled = true;
|
|
221
258
|
try { fs.closeSync(fd); } catch {}
|
|
222
|
-
resolve(
|
|
259
|
+
resolve(result);
|
|
223
260
|
};
|
|
224
261
|
|
|
225
262
|
child.once("error", (e) => {
|
|
226
|
-
done(
|
|
263
|
+
done({
|
|
264
|
+
success: false, status: "failed", taskId, pid, syncFs: useSyncFs,
|
|
265
|
+
syncReady: false, agentDir: useSyncFs ? agentTaskDir : undefined,
|
|
266
|
+
logPath, parentTaskId, error: String(e),
|
|
267
|
+
});
|
|
227
268
|
});
|
|
228
269
|
|
|
229
|
-
const syncFsNote = useSyncFs
|
|
230
|
-
? `\nTask ID: ${taskId}\nAgent dir: ${agentTaskDir}\n` +
|
|
231
|
-
`To send agent messages, write to: ${agentTaskDir}/input.txt\n` +
|
|
232
|
-
`To check status, read: ${agentTaskDir}/status.txt\n`
|
|
233
|
-
: "";
|
|
234
|
-
|
|
235
270
|
child.once("exit", (code) => {
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
271
|
+
exited = true;
|
|
272
|
+
done({
|
|
273
|
+
success: code === 0, status: code === 0 ? "completed" : "failed",
|
|
274
|
+
taskId, pid, syncFs: useSyncFs,
|
|
275
|
+
syncReady: useSyncFs && fs.existsSync(path.join(agentTaskDir, "metadata.json")),
|
|
276
|
+
agentDir: useSyncFs ? agentTaskDir : undefined, logPath, parentTaskId,
|
|
277
|
+
exitCode: code, ...(code === 0 ? {} : { error: `Agent exited with code ${code}` }),
|
|
278
|
+
});
|
|
240
279
|
});
|
|
241
280
|
|
|
242
281
|
if (!waitForCompletion) {
|
|
243
|
-
//
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
282
|
+
// A spawn is not the same as an initialized fs-sync agent. Poll briefly
|
|
283
|
+
// so callers can distinguish those states without parsing log prose.
|
|
284
|
+
const deadline = Date.now() + (useSyncFs ? 5000 : 100);
|
|
285
|
+
const acknowledge = () => {
|
|
286
|
+
if (settled || exited) return;
|
|
287
|
+
const syncReady = useSyncFs && fs.existsSync(path.join(agentTaskDir, "metadata.json"));
|
|
288
|
+
if (!useSyncFs || syncReady || Date.now() >= deadline) {
|
|
289
|
+
try { child.unref(); } catch {}
|
|
290
|
+
done({
|
|
291
|
+
success: true, status: "started", taskId, pid, syncFs: useSyncFs,
|
|
292
|
+
syncReady, agentDir: useSyncFs ? agentTaskDir : undefined,
|
|
293
|
+
logPath, parentTaskId,
|
|
294
|
+
});
|
|
295
|
+
return;
|
|
296
|
+
}
|
|
297
|
+
setTimeout(acknowledge, 100);
|
|
298
|
+
};
|
|
299
|
+
setTimeout(acknowledge, 50);
|
|
253
300
|
}
|
|
254
301
|
});
|
|
255
302
|
}
|
|
@@ -320,6 +367,16 @@ export const startAgentTaskDefinition: Tool = {
|
|
|
320
367
|
description:
|
|
321
368
|
"Resume a previously started task from where it left off. Must be used together with taskId which identifies the task to resume.",
|
|
322
369
|
},
|
|
370
|
+
forkTaskId: {
|
|
371
|
+
type: "string",
|
|
372
|
+
description:
|
|
373
|
+
"Fork this existing task into a new task. The source remains unchanged; taskId optionally selects the new task ID.",
|
|
374
|
+
},
|
|
375
|
+
rollback: {
|
|
376
|
+
type: "number",
|
|
377
|
+
description:
|
|
378
|
+
"Discard this many newest agent interactions before resuming or forking. Must be a non-negative integer.",
|
|
379
|
+
},
|
|
323
380
|
parentTaskId: {
|
|
324
381
|
type: "string",
|
|
325
382
|
description:
|
package/src/ai.ts
CHANGED
|
@@ -65,13 +65,16 @@ export async function singlePrompt(
|
|
|
65
65
|
// This ensures generate agent runs produce readable output and are observable
|
|
66
66
|
// via `knowhow agents list/tail/status`, matching `knowhow agent --input` behaviour.
|
|
67
67
|
const { startAgentTask } = await import("./agents/tools/startAgentTask");
|
|
68
|
-
|
|
68
|
+
const result = await startAgentTask({
|
|
69
69
|
agentName: agent,
|
|
70
70
|
prompt: userPrompt,
|
|
71
71
|
model: model || undefined,
|
|
72
72
|
waitForCompletion: true,
|
|
73
73
|
...(agentOptions ?? {}),
|
|
74
74
|
});
|
|
75
|
+
// singlePrompt is a text API; keep generation pipelines compatible while
|
|
76
|
+
// the tool itself exposes a structured orchestration result.
|
|
77
|
+
return JSON.stringify(result);
|
|
75
78
|
}
|
|
76
79
|
|
|
77
80
|
if (!model) {
|
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
|
+
}
|
|
@@ -20,6 +20,12 @@ import fs from "fs";
|
|
|
20
20
|
import path from "path";
|
|
21
21
|
import { services } from "../services";
|
|
22
22
|
import { logger } from "../logger";
|
|
23
|
+
import { ModuleExtension } from "../services/ExtensionsService";
|
|
24
|
+
|
|
25
|
+
export interface ChatExtension extends ModuleExtension {
|
|
26
|
+
type: "chat";
|
|
27
|
+
commands: ChatCommand[];
|
|
28
|
+
}
|
|
23
29
|
|
|
24
30
|
export class CliChatService implements ChatService {
|
|
25
31
|
private context: ChatContext;
|
|
@@ -59,6 +65,7 @@ export class CliChatService implements ChatService {
|
|
|
59
65
|
}
|
|
60
66
|
},
|
|
61
67
|
});
|
|
68
|
+
this.refreshExtensions();
|
|
62
69
|
this.loadInputHistory();
|
|
63
70
|
|
|
64
71
|
// Set up callback to add entries to inputHistory immediately when user presses Enter
|
|
@@ -151,7 +158,12 @@ export class CliChatService implements ChatService {
|
|
|
151
158
|
}
|
|
152
159
|
|
|
153
160
|
registerCommand(command: ChatCommand): void {
|
|
154
|
-
this.commands.
|
|
161
|
+
const existing = this.commands.findIndex((item) => item.name === command.name);
|
|
162
|
+
if (existing >= 0) {
|
|
163
|
+
this.commands[existing] = command;
|
|
164
|
+
} else {
|
|
165
|
+
this.commands.push(command);
|
|
166
|
+
}
|
|
155
167
|
}
|
|
156
168
|
|
|
157
169
|
registerMode(mode: ChatMode): void {
|
|
@@ -163,9 +175,17 @@ export class CliChatService implements ChatService {
|
|
|
163
175
|
}
|
|
164
176
|
|
|
165
177
|
getCommands(): ChatCommand[] {
|
|
178
|
+
this.refreshExtensions();
|
|
166
179
|
return this.commands;
|
|
167
180
|
}
|
|
168
181
|
|
|
182
|
+
/** Re-read the registry so services created before module setup still work. */
|
|
183
|
+
refreshExtensions(): void {
|
|
184
|
+
for (const extension of services().Extensions.list<ChatExtension>("chat")) {
|
|
185
|
+
for (const command of extension.commands ?? []) this.registerCommand(command);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
169
189
|
/**
|
|
170
190
|
* Get commands available in the current mode
|
|
171
191
|
*/
|
|
@@ -176,6 +196,7 @@ export class CliChatService implements ChatService {
|
|
|
176
196
|
}
|
|
177
197
|
|
|
178
198
|
getCommandsForActiveModes(): ChatCommand[] {
|
|
199
|
+
this.refreshExtensions();
|
|
179
200
|
const activeModes = this.modes
|
|
180
201
|
.filter((mode) => mode.active)
|
|
181
202
|
.map((mode) => mode.name);
|
|
@@ -216,7 +237,7 @@ export class CliChatService implements ChatService {
|
|
|
216
237
|
const command = availableCommands.find((cmd) => cmd.name === commandName);
|
|
217
238
|
|
|
218
239
|
if (command) {
|
|
219
|
-
const result = await command.handler(args);
|
|
240
|
+
const result = await command.handler(args, this);
|
|
220
241
|
|
|
221
242
|
// If handler returns a CommandResult and it's not handled, pass to modules
|
|
222
243
|
if (result && typeof result === "object" && "handled" in result) {
|