@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
package/src/commands/misc.ts
CHANGED
|
@@ -23,8 +23,9 @@ export function addLoginCommand(program: Command): void {
|
|
|
23
23
|
.command("login")
|
|
24
24
|
.description("Login to knowhow")
|
|
25
25
|
.option("--jwt", "Use manual JWT input instead of browser login")
|
|
26
|
-
.
|
|
27
|
-
|
|
26
|
+
.option("-i, --identity <path>", "Ed25519 private key to use for public-key login")
|
|
27
|
+
.action(async (options: { jwt?: boolean; identity?: string }) => {
|
|
28
|
+
await login(options.jwt, options.identity);
|
|
28
29
|
});
|
|
29
30
|
}
|
|
30
31
|
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import * as fs from "fs";
|
|
3
|
+
import * as path from "path";
|
|
4
|
+
import {
|
|
5
|
+
getProcessesDir, listManagedProcesses, ManagedProcess,
|
|
6
|
+
ManagedProcessInfo, processDirectory,
|
|
7
|
+
} from "../processes/ProcessManager";
|
|
8
|
+
import { runManagedProcessShim } from "../processes/managedProcessShim";
|
|
9
|
+
|
|
10
|
+
const terminal = new Set(["exited", "failed", "stopped"]);
|
|
11
|
+
function resolveProcess(value: string): ManagedProcess {
|
|
12
|
+
const root = getProcessesDir();
|
|
13
|
+
const matches = listManagedProcesses(root).filter((p) => p.id === value || p.id.includes(value));
|
|
14
|
+
if (!matches.length) throw new Error(`Managed process not found: ${value}`);
|
|
15
|
+
const exact = matches.find((p) => p.id === value);
|
|
16
|
+
if (!exact && matches.length > 1) throw new Error(`Ambiguous process id '${value}': ${matches.map((p) => p.id).join(", ")}`);
|
|
17
|
+
const item = exact || matches[0];
|
|
18
|
+
return new ManagedProcess(item.id, item.directory);
|
|
19
|
+
}
|
|
20
|
+
function commandOptions(options: any, command?: Command): any {
|
|
21
|
+
const fromOptions = typeof options?.optsWithGlobals === "function" ? options.optsWithGlobals() : options || {};
|
|
22
|
+
const fromCommand = typeof command?.optsWithGlobals === "function" ? command.optsWithGlobals() : {};
|
|
23
|
+
return { ...fromOptions, ...fromCommand };
|
|
24
|
+
}
|
|
25
|
+
function filtered(options: any): ManagedProcessInfo[] {
|
|
26
|
+
return listManagedProcesses().filter((item) => {
|
|
27
|
+
if (!options.all && terminal.has(item.status.state)) return false;
|
|
28
|
+
if (options.parent && item.status.parentProcessId !== options.parent) return false;
|
|
29
|
+
if (options.background !== undefined && item.status.background !== options.background) return false;
|
|
30
|
+
return true;
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
function printList(options: any): void {
|
|
34
|
+
const rows = filtered(options).map(({ id, status }) => ({
|
|
35
|
+
id, state: status.state, pid: status.pid || "-", background: status.background ? "yes" : "no",
|
|
36
|
+
parent: status.parentProcessId || "-", command: [status.command, ...status.args].join(" "),
|
|
37
|
+
}));
|
|
38
|
+
if (options.json) console.log(JSON.stringify(rows, null, 2));
|
|
39
|
+
else if (rows.length) console.table(rows);
|
|
40
|
+
else console.log("No managed processes.");
|
|
41
|
+
}
|
|
42
|
+
function addFilters(command: Command): Command {
|
|
43
|
+
return command.option("-a, --all", "include completed processes")
|
|
44
|
+
.option("--parent <id>", "filter by parent process/task id")
|
|
45
|
+
.option("--background", "show only background processes")
|
|
46
|
+
.option("--json", "emit JSON");
|
|
47
|
+
}
|
|
48
|
+
function tailText(file: string, lines: number): string {
|
|
49
|
+
if (!fs.existsSync(file)) return "";
|
|
50
|
+
return fs.readFileSync(file, "utf8").split("\n").slice(-lines - 1).join("\n");
|
|
51
|
+
}
|
|
52
|
+
async function followFiles(files: string[], fromEnd = true): Promise<void> {
|
|
53
|
+
const offsets = new Map<string, number>();
|
|
54
|
+
for (const file of files) offsets.set(file, fromEnd && fs.existsSync(file) ? fs.statSync(file).size : 0);
|
|
55
|
+
await new Promise<void>((resolve) => {
|
|
56
|
+
const poll = setInterval(() => {
|
|
57
|
+
for (const file of files) {
|
|
58
|
+
if (!fs.existsSync(file)) continue;
|
|
59
|
+
const size = fs.statSync(file).size; const offset = offsets.get(file) || 0;
|
|
60
|
+
if (size < offset) offsets.set(file, 0);
|
|
61
|
+
if (size > (offsets.get(file) || 0)) {
|
|
62
|
+
const start = offsets.get(file) || 0;
|
|
63
|
+
const fd = fs.openSync(file, "r"); const data = Buffer.alloc(size - start);
|
|
64
|
+
fs.readSync(fd, data, 0, data.length, start); fs.closeSync(fd);
|
|
65
|
+
process.stdout.write(data); offsets.set(file, size);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}, 150);
|
|
69
|
+
const done = () => { clearInterval(poll); resolve(); };
|
|
70
|
+
process.once("SIGINT", done); process.once("SIGTERM", done);
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function addProcessesCommand(program: Command): void {
|
|
75
|
+
const processes = addFilters(program.command("processes").description("inspect and control managed processes"));
|
|
76
|
+
processes.action((rawOptions, command) => printList(commandOptions(rawOptions, command)));
|
|
77
|
+
addFilters(processes.command("list").description("list managed processes"))
|
|
78
|
+
.action((rawOptions, command) => printList(commandOptions(rawOptions, command)));
|
|
79
|
+
|
|
80
|
+
processes.command("run").description("internal managed process shim").requiredOption("--config <path>")
|
|
81
|
+
.action(async ({ config }) => runManagedProcessShim(path.resolve(config)));
|
|
82
|
+
|
|
83
|
+
processes.command("logs <id>").alias("tail").option("-n, --lines <number>", "lines to show", "100")
|
|
84
|
+
.option("-f, --follow", "follow output").option("--stderr", "show stderr instead of stdout")
|
|
85
|
+
.option("--both", "show stdout and stderr").action(async (id, rawOptions, command) => {
|
|
86
|
+
const options = commandOptions(rawOptions, command);
|
|
87
|
+
const managed = resolveProcess(id);
|
|
88
|
+
const files = options.both ? [managed.stdoutPath, managed.stderrPath] : [options.stderr ? managed.stderrPath : managed.stdoutPath];
|
|
89
|
+
for (const file of files) process.stdout.write(tailText(file, Math.max(0, Number(options.lines) || 100)));
|
|
90
|
+
if (options.follow) await followFiles(files);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
processes.command("send <id> [input...]").option("-n, --no-newline", "do not append newline")
|
|
94
|
+
.action(async (id, input: string[], rawOptions, command) => {
|
|
95
|
+
const options = commandOptions(rawOptions, command);
|
|
96
|
+
const managed = resolveProcess(id);
|
|
97
|
+
await managed.write((input || []).join(" ") + (options.newline ? "\n" : ""));
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
processes.command("signal <id> [signal]").action((id, signal = "SIGTERM") => resolveProcess(id).signal(signal.toUpperCase() as NodeJS.Signals));
|
|
101
|
+
processes.command("stop <id>").action((id) => resolveProcess(id).signal("SIGTERM"));
|
|
102
|
+
processes.command("kill <id>").action((id) => resolveProcess(id).signal("SIGKILL"));
|
|
103
|
+
|
|
104
|
+
processes.command("attach <id>").description("attach terminal (Ctrl-] to detach)").action(async (id) => {
|
|
105
|
+
const managed = resolveProcess(id); let detached = false;
|
|
106
|
+
const raw = !!process.stdin.isTTY; if (raw) process.stdin.setRawMode(true);
|
|
107
|
+
process.stdin.resume();
|
|
108
|
+
const onData = async (data: Buffer) => {
|
|
109
|
+
if (data.includes(0x1d)) { detached = true; return; }
|
|
110
|
+
try { await managed.write(data.toString()); } catch (error) { console.error(String(error)); detached = true; }
|
|
111
|
+
};
|
|
112
|
+
process.stdin.on("data", onData);
|
|
113
|
+
const offsets = [managed.stdoutPath, managed.stderrPath].map((f) => fs.existsSync(f) ? fs.statSync(f).size : 0);
|
|
114
|
+
await new Promise<void>((resolve) => {
|
|
115
|
+
const timer = setInterval(() => {
|
|
116
|
+
[managed.stdoutPath, managed.stderrPath].forEach((file, i) => {
|
|
117
|
+
if (!fs.existsSync(file)) return; const size = fs.statSync(file).size;
|
|
118
|
+
if (size > offsets[i]) { const fd = fs.openSync(file, "r"); const b = Buffer.alloc(size - offsets[i]); fs.readSync(fd, b, 0, b.length, offsets[i]); fs.closeSync(fd); process.stdout.write(b); offsets[i] = size; }
|
|
119
|
+
});
|
|
120
|
+
if (detached || terminal.has(managed.status.state)) { clearInterval(timer); resolve(); }
|
|
121
|
+
}, 100);
|
|
122
|
+
});
|
|
123
|
+
process.stdin.off("data", onData); process.stdin.pause(); if (raw) process.stdin.setRawMode(false);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
processes.command("tree").option("-a, --all", "include completed processes").action((rawOptions, command) => {
|
|
127
|
+
const options = commandOptions(rawOptions, command);
|
|
128
|
+
const items = listManagedProcesses().filter((p) => options.all || !terminal.has(p.status.state));
|
|
129
|
+
const ids = new Set(items.map((p) => p.id));
|
|
130
|
+
const children = new Map<string | null, ManagedProcessInfo[]>();
|
|
131
|
+
for (const item of items) { const parent = item.status.parentProcessId && ids.has(item.status.parentProcessId) ? item.status.parentProcessId : null; children.set(parent, [...(children.get(parent) || []), item]); }
|
|
132
|
+
const render = (parent: string | null, prefix: string) => (children.get(parent) || []).forEach((item, index, array) => {
|
|
133
|
+
const last = index === array.length - 1; console.log(`${prefix}${last ? "└─" : "├─"}${item.id} [${item.status.state}] ${item.status.command}`);
|
|
134
|
+
render(item.id, prefix + (last ? " " : "│ "));
|
|
135
|
+
});
|
|
136
|
+
render(null, "");
|
|
137
|
+
});
|
|
138
|
+
}
|
package/src/commands/services.ts
CHANGED
|
@@ -3,7 +3,8 @@ import * as allTools from "../agents/tools";
|
|
|
3
3
|
import { LazyToolsService, services, MinimalToolsService, TracingService } from "../services";
|
|
4
4
|
import { agents } from "../agents";
|
|
5
5
|
import { ModulesService } from "../services/modules";
|
|
6
|
-
import { getConfig } from "../config";
|
|
6
|
+
import { getConfig, getConfigSync } from "../config";
|
|
7
|
+
import { authenticateWithKey } from "../auth/keyAuth";
|
|
7
8
|
|
|
8
9
|
/**
|
|
9
10
|
* Shared service setup used by commands that need full services (chat, agent, worker, etc.)
|
|
@@ -20,6 +21,7 @@ export async function setupServices() {
|
|
|
20
21
|
MediaProcessor,
|
|
21
22
|
Behaviors,
|
|
22
23
|
RuntimeReload,
|
|
24
|
+
Extensions,
|
|
23
25
|
} = services();
|
|
24
26
|
|
|
25
27
|
await Plugins.refreshConfiguredState();
|
|
@@ -76,6 +78,25 @@ export async function setupServices() {
|
|
|
76
78
|
`⚠ Some MCP servers failed to connect (continuing without them): ${msg}`
|
|
77
79
|
);
|
|
78
80
|
}
|
|
81
|
+
// Attempt silent JWT refresh via SSH identity before registering models.
|
|
82
|
+
// This handles the case where the JWT has expired but the user has a valid
|
|
83
|
+
// SSH identity registered — they shouldn't need to run `knowhow login` again
|
|
84
|
+
// unless they want to switch organizations.
|
|
85
|
+
try {
|
|
86
|
+
const startupConfig = getConfigSync();
|
|
87
|
+
if (startupConfig.orgId && !process.env.KNOWHOW_JWT) {
|
|
88
|
+
await authenticateWithKey(
|
|
89
|
+
startupConfig.orgId,
|
|
90
|
+
process.env.KNOWHOW_API_URL || "https://api.knowhow.tyvm.ai",
|
|
91
|
+
startupConfig.cliIdentityPath
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
} catch (_err) {
|
|
95
|
+
// Non-fatal: if SSH refresh fails (no key, revoked, network issue), we
|
|
96
|
+
// continue with the existing JWT (or no JWT). The user will see model
|
|
97
|
+
// warnings if no valid JWT is available.
|
|
98
|
+
}
|
|
99
|
+
|
|
79
100
|
console.log("Connecting to clients...");
|
|
80
101
|
await Clients.registerConfiguredModels();
|
|
81
102
|
console.log("✓ Services are set up and ready to go!");
|
|
@@ -92,6 +113,7 @@ export async function setupServices() {
|
|
|
92
113
|
Behaviors,
|
|
93
114
|
Events,
|
|
94
115
|
Tracing: TracingService,
|
|
116
|
+
Extensions,
|
|
95
117
|
};
|
|
96
118
|
|
|
97
119
|
// Call destroy() on all modules when the process is shutting down so they
|
package/src/index.ts
CHANGED
|
@@ -65,6 +65,7 @@ export * as embeddings from "./embeddings";
|
|
|
65
65
|
export * as types from "./types";
|
|
66
66
|
export * as processors from "./processors";
|
|
67
67
|
export * as ai from "./ai";
|
|
68
|
+
export * from "./processes/ProcessManager";
|
|
68
69
|
|
|
69
70
|
// Export module system types for external modules
|
|
70
71
|
export * from "./services/modules/types";
|
package/src/login.ts
CHANGED
|
@@ -1,13 +1,28 @@
|
|
|
1
1
|
import http from "./utils/http";
|
|
2
2
|
import fs from "fs";
|
|
3
3
|
import path from "path";
|
|
4
|
-
import { chmod } from "fs/promises";
|
|
5
4
|
import { ask } from "./utils";
|
|
6
5
|
import { getConfig, updateConfig } from "./config";
|
|
7
6
|
import { KNOWHOW_API_URL } from "./services/KnowhowClient";
|
|
8
7
|
import { BrowserLoginService } from "./auth/browserLogin";
|
|
9
|
-
|
|
10
|
-
|
|
8
|
+
import { authenticateWithKey, hasKeyPair, registerPublicKey } from "./auth/keyAuth";
|
|
9
|
+
import { getOrCreatePublicKey } from "./auth/keyManager";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Log in to Knowhow.
|
|
13
|
+
*
|
|
14
|
+
* Strategy (in order):
|
|
15
|
+
* 1. `--jwt` flag: prompt for a raw JWT and write it to disk.
|
|
16
|
+
* 2. Key auth: if an Ed25519 key and configured organization exist, sign a
|
|
17
|
+
* server challenge and exchange it for an organization-bound JWT.
|
|
18
|
+
* 3. Browser: open the browser approval flow (first-time login or key not yet registered).
|
|
19
|
+
*
|
|
20
|
+
* After any successful login the local config is updated with the current user's orgId.
|
|
21
|
+
*
|
|
22
|
+
* @param jwtFlag True when --jwt flag is passed.
|
|
23
|
+
* @param identityPath Optional path to a specific private key file (--identity flag).
|
|
24
|
+
*/
|
|
25
|
+
export async function login(jwtFlag?: boolean, identityPath?: string): Promise<void> {
|
|
11
26
|
if (!KNOWHOW_API_URL) {
|
|
12
27
|
throw new Error("Error: KNOWHOW_API_URL environment variable not set.");
|
|
13
28
|
}
|
|
@@ -15,9 +30,8 @@ export async function login(jwtFlag?: boolean): Promise<void> {
|
|
|
15
30
|
if (jwtFlag) {
|
|
16
31
|
const jwt = await ask("Enter your JWT: ");
|
|
17
32
|
|
|
18
|
-
// Update the JWT file
|
|
19
33
|
const configDir = path.join(process.cwd(), ".knowhow");
|
|
20
|
-
const jwtFile = path.join(
|
|
34
|
+
const jwtFile = path.join(configDir, ".jwt");
|
|
21
35
|
|
|
22
36
|
if (!fs.existsSync(configDir)) {
|
|
23
37
|
fs.mkdirSync(configDir, { recursive: true });
|
|
@@ -26,23 +40,69 @@ export async function login(jwtFlag?: boolean): Promise<void> {
|
|
|
26
40
|
fs.chmodSync(jwtFile, 0o600);
|
|
27
41
|
console.log("JWT updated successfully.");
|
|
28
42
|
} else {
|
|
29
|
-
|
|
30
|
-
|
|
43
|
+
const config = await getConfig();
|
|
44
|
+
const orgId = config.orgId;
|
|
45
|
+
|
|
46
|
+
// A local key is not proof of registration. Let the selected environment
|
|
47
|
+
// check it, but only when an organization is available to bind the attempt.
|
|
48
|
+
if (orgId && hasKeyPair(identityPath)) {
|
|
49
|
+
console.log("Found CLI identity — authenticating with public key...");
|
|
50
|
+
try {
|
|
51
|
+
const success = await authenticateWithKey(orgId, KNOWHOW_API_URL, identityPath);
|
|
52
|
+
if (success) {
|
|
53
|
+
console.log("✅ Successfully authenticated via public key!");
|
|
54
|
+
return await postLoginConfigUpdate(identityPath);
|
|
55
|
+
}
|
|
56
|
+
console.warn("Key authentication returned no JWT, falling back to browser login...");
|
|
57
|
+
} catch (error: unknown) {
|
|
58
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
59
|
+
console.warn(`Key authentication failed (${message}), falling back to browser login...`);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Fall back when no org/key is configured or when key auth is unavailable.
|
|
64
|
+
await doBrowserLogin(identityPath);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
await postLoginConfigUpdate(identityPath);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Complete browser PKCE, then best-effort bootstrap the selected identity. */
|
|
71
|
+
async function doBrowserLogin(identityPath?: string): Promise<void> {
|
|
72
|
+
console.log("Starting browser-based authentication...");
|
|
73
|
+
try {
|
|
74
|
+
const existingConfig = await getConfig();
|
|
75
|
+
const existingOrgId = existingConfig?.orgId;
|
|
76
|
+
const browserLoginService = new BrowserLoginService(undefined, existingOrgId);
|
|
77
|
+
await browserLoginService.login();
|
|
78
|
+
console.log("✅ Successfully authenticated via browser!");
|
|
79
|
+
|
|
31
80
|
try {
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
81
|
+
const keyPair = getOrCreatePublicKey(identityPath);
|
|
82
|
+
await registerPublicKey(
|
|
83
|
+
await loadJwt(),
|
|
84
|
+
keyPair.publicKeyBase64,
|
|
85
|
+
KNOWHOW_API_URL
|
|
86
|
+
);
|
|
87
|
+
console.log(`Registered CLI identity ${keyPair.fingerprint}`);
|
|
88
|
+
} catch (registrationError: unknown) {
|
|
89
|
+
// Browser login remains valid when key registration is unavailable (for
|
|
90
|
+
// example during a rolling backend deployment or for a partial identity).
|
|
91
|
+
const message = registrationError instanceof Error
|
|
92
|
+
? registrationError.message
|
|
93
|
+
: String(registrationError);
|
|
94
|
+
console.warn(`Could not register CLI identity (${message}). Browser login is still active.`);
|
|
42
95
|
}
|
|
96
|
+
} catch (error: unknown) {
|
|
97
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
98
|
+
console.error("Browser authentication failed:", msg);
|
|
99
|
+
console.log("You can try manual JWT login with: knowhow login --jwt");
|
|
100
|
+
throw error;
|
|
43
101
|
}
|
|
102
|
+
}
|
|
44
103
|
|
|
45
|
-
|
|
104
|
+
/** After any successful login, update the local config with the current user/org. */
|
|
105
|
+
async function postLoginConfigUpdate(identityPath?: string): Promise<void> {
|
|
46
106
|
try {
|
|
47
107
|
const storedJwt = await loadJwt();
|
|
48
108
|
const { user, currentOrg } = await checkJwt(storedJwt);
|
|
@@ -59,25 +119,26 @@ export async function login(jwtFlag?: boolean): Promise<void> {
|
|
|
59
119
|
}
|
|
60
120
|
|
|
61
121
|
const hasProvider = config.modelProviders.find(
|
|
62
|
-
(provider) => provider.provider === "knowhow"
|
|
122
|
+
(provider: { provider: string }) => provider.provider === "knowhow"
|
|
63
123
|
);
|
|
64
124
|
if (!hasProvider) {
|
|
65
|
-
config.modelProviders.push({
|
|
66
|
-
provider: "knowhow",
|
|
67
|
-
});
|
|
125
|
+
config.modelProviders.push({ provider: "knowhow" });
|
|
68
126
|
}
|
|
69
|
-
|
|
127
|
+
|
|
70
128
|
if (orgId) {
|
|
71
129
|
config.orgId = orgId;
|
|
72
130
|
}
|
|
73
131
|
|
|
132
|
+
if (identityPath) {
|
|
133
|
+
config.cliIdentityPath = path.resolve(identityPath);
|
|
134
|
+
}
|
|
135
|
+
|
|
74
136
|
await updateConfig(config);
|
|
75
|
-
} catch (error) {
|
|
137
|
+
} catch (error: unknown) {
|
|
76
138
|
if (http.isHttpError(error) && error.response) {
|
|
77
|
-
|
|
78
|
-
const errData = error.body ?? { message: "Unknown error" };
|
|
139
|
+
const errData = (error as any).body ?? { message: "Unknown error" };
|
|
79
140
|
throw new Error(
|
|
80
|
-
`Error: ${error.status} - ${errData?.message || "Unknown error"}`
|
|
141
|
+
`Error: ${(error as any).status} - ${errData?.message || "Unknown error"}`
|
|
81
142
|
);
|
|
82
143
|
}
|
|
83
144
|
console.log(
|
|
@@ -112,7 +173,7 @@ export async function checkJwt(storedJwt: string) {
|
|
|
112
173
|
const orgs = user.orgs;
|
|
113
174
|
const orgId = response.data.orgId;
|
|
114
175
|
|
|
115
|
-
const currentOrg = orgs.find((org) => {
|
|
176
|
+
const currentOrg = orgs.find((org: { organizationId: string }) => {
|
|
116
177
|
return org.organizationId === orgId;
|
|
117
178
|
});
|
|
118
179
|
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { spawn } from "child_process";
|
|
2
|
+
import * as fs from "fs";
|
|
3
|
+
import * as path from "path";
|
|
4
|
+
import { randomBytes } from "crypto";
|
|
5
|
+
|
|
6
|
+
export type ManagedProcessState = "starting" | "running" | "exited" | "failed" | "stopped";
|
|
7
|
+
export interface ManagedProcessStatus {
|
|
8
|
+
id: string; pid: number | null; shimPid: number | null; state: ManagedProcessState;
|
|
9
|
+
command: string; args: string[]; cwd: string; parentProcessId: string | null;
|
|
10
|
+
parentPid: number | null; background: boolean; startedAt: string; endedAt: string | null;
|
|
11
|
+
exitCode: number | null; signal: string | null; error?: string;
|
|
12
|
+
}
|
|
13
|
+
export interface SpawnManagedOptions {
|
|
14
|
+
id?: string; cwd?: string; env?: NodeJS.ProcessEnv; background?: boolean;
|
|
15
|
+
parentProcessId?: string; parentPid?: number; shell?: boolean; processesDir?: string;
|
|
16
|
+
}
|
|
17
|
+
export interface ManagedProcessInfo { id: string; directory: string; status: ManagedProcessStatus; }
|
|
18
|
+
|
|
19
|
+
export function getProcessesDir(cwd = process.cwd()): string {
|
|
20
|
+
return process.env.KNOWHOW_PROCESSES_DIR || path.join(cwd, ".knowhow", "processes");
|
|
21
|
+
}
|
|
22
|
+
export function processDirectory(id: string, processesDir = getProcessesDir()): string {
|
|
23
|
+
if (!/^[a-zA-Z0-9._-]+$/.test(id)) throw new Error(`Invalid process id: ${id}`);
|
|
24
|
+
return path.join(processesDir, id);
|
|
25
|
+
}
|
|
26
|
+
export function readManagedStatus(id: string, processesDir = getProcessesDir()): ManagedProcessStatus {
|
|
27
|
+
return JSON.parse(fs.readFileSync(path.join(processDirectory(id, processesDir), "status.json"), "utf8"));
|
|
28
|
+
}
|
|
29
|
+
export function listManagedProcesses(processesDir = getProcessesDir()): ManagedProcessInfo[] {
|
|
30
|
+
if (!fs.existsSync(processesDir)) return [];
|
|
31
|
+
return fs.readdirSync(processesDir, { withFileTypes: true })
|
|
32
|
+
.filter((entry) => entry.isDirectory() && fs.existsSync(path.join(processesDir, entry.name, "status.json")))
|
|
33
|
+
.flatMap((entry) => { try { return [{ id: entry.name, directory: path.join(processesDir, entry.name), status: readManagedStatus(entry.name, processesDir) }]; } catch { return []; } })
|
|
34
|
+
.sort((a, b) => b.status.startedAt.localeCompare(a.status.startedAt));
|
|
35
|
+
}
|
|
36
|
+
function makeId(command: string): string {
|
|
37
|
+
const name = path.basename(command).replace(/[^a-zA-Z0-9._-]/g, "-").slice(0, 28) || "process";
|
|
38
|
+
return `${name}-${Date.now()}-${randomBytes(3).toString("hex")}`;
|
|
39
|
+
}
|
|
40
|
+
function waitForFile(file: string, timeoutMs: number): Promise<void> {
|
|
41
|
+
const started = Date.now();
|
|
42
|
+
return new Promise((resolve, reject) => { const check = () => {
|
|
43
|
+
if (fs.existsSync(file)) return resolve();
|
|
44
|
+
if (Date.now() - started >= timeoutMs) return reject(new Error(`Managed process did not initialize: ${file}`));
|
|
45
|
+
setTimeout(check, 20);
|
|
46
|
+
}; check(); });
|
|
47
|
+
}
|
|
48
|
+
export class ManagedProcess {
|
|
49
|
+
constructor(public readonly id: string, public readonly directory: string) {}
|
|
50
|
+
get status(): ManagedProcessStatus { return readManagedStatus(this.id, path.dirname(this.directory)); }
|
|
51
|
+
get stdoutPath(): string { return path.join(this.directory, "stdout"); }
|
|
52
|
+
get stderrPath(): string { return path.join(this.directory, "stderr"); }
|
|
53
|
+
get inputLogPath(): string { return path.join(this.directory, "input.log"); }
|
|
54
|
+
get stdinPath(): string { return path.join(this.directory, "stdin"); }
|
|
55
|
+
async write(input: string): Promise<void> {
|
|
56
|
+
if (process.platform === "win32") {
|
|
57
|
+
await fs.promises.appendFile(this.stdinPath, input);
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
// A non-blocking open fails promptly with ENXIO if the shim is gone instead
|
|
61
|
+
// of leaving callers hung forever waiting for a FIFO reader.
|
|
62
|
+
const handle = await fs.promises.open(this.stdinPath, fs.constants.O_WRONLY | fs.constants.O_NONBLOCK);
|
|
63
|
+
try { await handle.writeFile(input); } finally { await handle.close(); }
|
|
64
|
+
}
|
|
65
|
+
signal(signal: NodeJS.Signals = "SIGTERM"): void {
|
|
66
|
+
const status = this.status;
|
|
67
|
+
if (!status.pid) throw new Error(`Process ${this.id} has no active pid`);
|
|
68
|
+
try { process.kill(-status.pid, signal); } catch { process.kill(status.pid, signal); }
|
|
69
|
+
}
|
|
70
|
+
async wait(timeoutMs?: number): Promise<ManagedProcessStatus> {
|
|
71
|
+
const started = Date.now();
|
|
72
|
+
while (true) {
|
|
73
|
+
const status = this.status;
|
|
74
|
+
if (!["starting", "running"].includes(status.state)) return status;
|
|
75
|
+
if (timeoutMs !== undefined && Date.now() - started >= timeoutMs) throw new Error(`Timed out waiting for managed process ${this.id} after ${timeoutMs}ms`);
|
|
76
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
export async function spawnManaged(command: string, args: string[] = [], options: SpawnManagedOptions = {}): Promise<ManagedProcess> {
|
|
81
|
+
const cwd = path.resolve(options.cwd || process.cwd());
|
|
82
|
+
const processesDir = options.processesDir || getProcessesDir(cwd);
|
|
83
|
+
const id = options.id || makeId(command);
|
|
84
|
+
const directory = processDirectory(id, processesDir);
|
|
85
|
+
fs.mkdirSync(processesDir, { recursive: true });
|
|
86
|
+
fs.mkdirSync(directory);
|
|
87
|
+
const configPath = path.join(directory, "launch.json");
|
|
88
|
+
fs.writeFileSync(configPath, JSON.stringify({ id, command, args, cwd, env: options.env || {}, background: options.background === true, parentProcessId: options.parentProcessId || null, parentPid: options.parentPid ?? process.pid, shell: options.shell === true, processesDir }, null, 2));
|
|
89
|
+
const cliPath = path.resolve(__dirname, "..", "cli.js");
|
|
90
|
+
const shim = spawn(process.execPath, [cliPath, "processes", "run", "--config", configPath], { cwd, detached: true, stdio: "ignore", env: process.env });
|
|
91
|
+
shim.unref();
|
|
92
|
+
try { await waitForFile(path.join(directory, "status.json"), 5000); }
|
|
93
|
+
catch (error) { try { fs.rmSync(directory, { recursive: true, force: true }); } catch {} throw error; }
|
|
94
|
+
return new ManagedProcess(id, directory);
|
|
95
|
+
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { spawn, spawnSync, ChildProcess } from "child_process";
|
|
2
|
+
import * as fs from "fs";
|
|
3
|
+
import * as path from "path";
|
|
4
|
+
import { ManagedProcessStatus } from "./ProcessManager";
|
|
5
|
+
|
|
6
|
+
interface LaunchConfig {
|
|
7
|
+
id: string; command: string; args: string[]; cwd: string; env: NodeJS.ProcessEnv;
|
|
8
|
+
background: boolean; parentProcessId: string | null; parentPid: number | null;
|
|
9
|
+
shell: boolean; processesDir: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function atomicJson(file: string, value: unknown): void {
|
|
13
|
+
const tmp = `${file}.${process.pid}.tmp`;
|
|
14
|
+
fs.writeFileSync(tmp, JSON.stringify(value, null, 2));
|
|
15
|
+
fs.renameSync(tmp, file);
|
|
16
|
+
}
|
|
17
|
+
function alive(pid: number): boolean {
|
|
18
|
+
try { process.kill(pid, 0); return true; } catch (error: any) { return error?.code === "EPERM"; }
|
|
19
|
+
}
|
|
20
|
+
function terminateGroup(child: ChildProcess, signal: NodeJS.Signals): void {
|
|
21
|
+
if (!child.pid) return;
|
|
22
|
+
try { process.kill(-child.pid, signal); } catch { try { child.kill(signal); } catch {} }
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export async function runManagedProcessShim(configPath: string): Promise<void> {
|
|
26
|
+
const config: LaunchConfig = JSON.parse(fs.readFileSync(configPath, "utf8"));
|
|
27
|
+
const dir = path.dirname(configPath);
|
|
28
|
+
const startedAt = new Date().toISOString();
|
|
29
|
+
const statusPath = path.join(dir, "status.json");
|
|
30
|
+
const stdoutPath = path.join(dir, "stdout");
|
|
31
|
+
const stderrPath = path.join(dir, "stderr");
|
|
32
|
+
const inputLogPath = path.join(dir, "input.log");
|
|
33
|
+
const fifoPath = path.join(dir, "stdin");
|
|
34
|
+
fs.closeSync(fs.openSync(stdoutPath, "a"));
|
|
35
|
+
fs.closeSync(fs.openSync(stderrPath, "a"));
|
|
36
|
+
fs.closeSync(fs.openSync(inputLogPath, "a"));
|
|
37
|
+
|
|
38
|
+
if (process.platform !== "win32") {
|
|
39
|
+
const result = spawnSync("mkfifo", [fifoPath]);
|
|
40
|
+
if (result.status !== 0 && !fs.existsSync(fifoPath)) throw new Error(`Unable to create stdin FIFO: ${result.stderr?.toString()}`);
|
|
41
|
+
} else {
|
|
42
|
+
// Windows has no POSIX FIFO. Keep the same path as an append-only inbox fallback.
|
|
43
|
+
fs.closeSync(fs.openSync(fifoPath, "a"));
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
let status: ManagedProcessStatus = {
|
|
47
|
+
id: config.id, pid: null, shimPid: process.pid, state: "starting", command: config.command,
|
|
48
|
+
args: config.args, cwd: config.cwd, parentProcessId: config.parentProcessId,
|
|
49
|
+
parentPid: config.parentPid, background: config.background, startedAt, endedAt: null,
|
|
50
|
+
exitCode: null, signal: null,
|
|
51
|
+
};
|
|
52
|
+
const writeStatus = () => atomicJson(statusPath, status);
|
|
53
|
+
atomicJson(path.join(dir, "metadata.json"), {
|
|
54
|
+
id: config.id, command: config.command, args: config.args, cwd: config.cwd,
|
|
55
|
+
parentProcessId: config.parentProcessId, parentPid: config.parentPid,
|
|
56
|
+
background: config.background, createdAt: startedAt, shimPid: process.pid,
|
|
57
|
+
});
|
|
58
|
+
fs.writeFileSync(path.join(dir, "pid"), "");
|
|
59
|
+
writeStatus();
|
|
60
|
+
|
|
61
|
+
const stdoutFd = fs.openSync(stdoutPath, "a");
|
|
62
|
+
const stderrFd = fs.openSync(stderrPath, "a");
|
|
63
|
+
const child = spawn(config.command, config.args, {
|
|
64
|
+
cwd: config.cwd, shell: config.shell, detached: process.platform !== "win32",
|
|
65
|
+
stdio: ["pipe", stdoutFd, stderrFd],
|
|
66
|
+
env: { ...process.env, ...config.env, KNOWHOW_MANAGED_PROCESS_ID: config.id, KNOWHOW_MANAGED_PROCESS_DIR: dir },
|
|
67
|
+
});
|
|
68
|
+
fs.closeSync(stdoutFd); fs.closeSync(stderrFd);
|
|
69
|
+
status = { ...status, pid: child.pid || null, state: "running" };
|
|
70
|
+
fs.writeFileSync(path.join(dir, "pid"), child.pid ? `${child.pid}\n` : "");
|
|
71
|
+
writeStatus();
|
|
72
|
+
|
|
73
|
+
let fifoFd: number | undefined;
|
|
74
|
+
let fifoStream: fs.ReadStream | undefined;
|
|
75
|
+
if (process.platform !== "win32") {
|
|
76
|
+
// O_RDWR prevents transient writers disconnecting from delivering EOF to the shim.
|
|
77
|
+
fifoFd = fs.openSync(fifoPath, fs.constants.O_RDWR);
|
|
78
|
+
fifoStream = fs.createReadStream("", { fd: fifoFd, autoClose: false });
|
|
79
|
+
fifoStream.on("data", (chunk) => {
|
|
80
|
+
fs.appendFileSync(inputLogPath, chunk);
|
|
81
|
+
if (child.stdin?.writable) child.stdin.write(chunk);
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
let stoppingForParent = false;
|
|
86
|
+
const parentTimer = !config.background && config.parentPid
|
|
87
|
+
? setInterval(() => {
|
|
88
|
+
if (!alive(config.parentPid!)) {
|
|
89
|
+
stoppingForParent = true;
|
|
90
|
+
terminateGroup(child, "SIGTERM");
|
|
91
|
+
setTimeout(() => { if (alive(child.pid || -1)) terminateGroup(child, "SIGKILL"); }, 3000).unref();
|
|
92
|
+
}
|
|
93
|
+
}, 500)
|
|
94
|
+
: undefined;
|
|
95
|
+
parentTimer?.unref();
|
|
96
|
+
|
|
97
|
+
const forward = (signal: NodeJS.Signals) => terminateGroup(child, signal);
|
|
98
|
+
process.once("SIGINT", () => forward("SIGINT"));
|
|
99
|
+
process.once("SIGTERM", () => forward("SIGTERM"));
|
|
100
|
+
process.once("SIGHUP", () => forward("SIGHUP"));
|
|
101
|
+
|
|
102
|
+
await new Promise<void>((resolve) => {
|
|
103
|
+
child.once("error", (error) => {
|
|
104
|
+
status = { ...status, state: "failed", error: error.message, endedAt: new Date().toISOString() };
|
|
105
|
+
writeStatus(); resolve();
|
|
106
|
+
});
|
|
107
|
+
child.once("exit", (code, signal) => {
|
|
108
|
+
status = { ...status, state: stoppingForParent || signal ? "stopped" : code === 0 ? "exited" : "failed", endedAt: new Date().toISOString(), exitCode: code, signal };
|
|
109
|
+
writeStatus(); resolve();
|
|
110
|
+
});
|
|
111
|
+
});
|
|
112
|
+
if (parentTimer) clearInterval(parentTimer);
|
|
113
|
+
fifoStream?.destroy();
|
|
114
|
+
if (fifoFd !== undefined) try { fs.closeSync(fifoFd); } catch {}
|
|
115
|
+
}
|
|
@@ -3,8 +3,10 @@ import { IAgent } from "../agents/interface";
|
|
|
3
3
|
import { EventService } from "./EventService";
|
|
4
4
|
import { ToolsService } from "./Tools";
|
|
5
5
|
import { ConfigAgent } from "../agents/configurable/ConfigAgent";
|
|
6
|
+
import { TraceAll } from "../util/Trace";
|
|
6
7
|
import { AgentContext } from "../agents/base/base";
|
|
7
8
|
|
|
9
|
+
@TraceAll()
|
|
8
10
|
export class AgentService {
|
|
9
11
|
private agents: Map<string, IAgent> = new Map();
|
|
10
12
|
private agentContext: AgentContext | null = null;
|