@tyvm/knowhow 0.0.55 → 0.0.56
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/docs/input-queue-manager.md +142 -0
- package/docs/multi-worker-management.md +142 -0
- package/package.json +1 -1
- package/scripts/README.md +119 -0
- package/scripts/restore_keys.sh +59 -0
- package/scripts/unset_keys.sh +60 -0
- package/src/agents/tools/askHuman.ts +2 -0
- package/src/agents/tools/startAgentTask.ts +2 -2
- package/src/ai.ts +3 -1
- package/src/chat/CliChatService.ts +2 -2
- package/src/chat/modules/AgentModule.ts +25 -2
- package/src/chat-old.ts +2 -2
- package/src/cli.ts +56 -3
- package/src/clients/anthropic.ts +7 -5
- package/src/clients/knowhow.ts +2 -2
- package/src/index.ts +6 -6
- package/src/microphone.ts +12 -4
- package/src/services/DockerService.ts +473 -0
- package/src/services/KnowhowClient.ts +4 -1
- package/src/services/index.ts +5 -1
- package/src/types.ts +6 -0
- package/src/utils/InputQueueManager.ts +324 -0
- package/src/utils/index.ts +5 -152
- package/src/worker.ts +158 -9
- package/src/workerRegistry.ts +152 -0
- package/tests/clients/AIClient.test.ts +177 -92
- package/tests/manual/test-concurrent-ask.ts +43 -0
- package/tests/services/DockerService.test.ts +24 -0
- package/tests/unit/input-queue.test.ts +80 -0
- package/ts_build/package.json +1 -1
- package/ts_build/src/agents/tools/askHuman.d.ts +1 -1
- package/ts_build/src/agents/tools/askHuman.js.map +1 -1
- package/ts_build/src/agents/tools/startAgentTask.js +2 -1
- package/ts_build/src/agents/tools/startAgentTask.js.map +1 -1
- package/ts_build/src/ai.js +3 -1
- package/ts_build/src/ai.js.map +1 -1
- package/ts_build/src/chat/CliChatService.js +1 -1
- package/ts_build/src/chat/CliChatService.js.map +1 -1
- package/ts_build/src/chat/modules/AgentModule.js +11 -1
- package/ts_build/src/chat/modules/AgentModule.js.map +1 -1
- package/ts_build/src/chat-old.js +1 -1
- package/ts_build/src/chat-old.js.map +1 -1
- package/ts_build/src/cli.js +46 -3
- package/ts_build/src/cli.js.map +1 -1
- package/ts_build/src/clients/anthropic.js +7 -5
- package/ts_build/src/clients/anthropic.js.map +1 -1
- package/ts_build/src/clients/knowhow.js +1 -1
- package/ts_build/src/clients/knowhow.js.map +1 -1
- package/ts_build/src/dockerWorker.d.ts +22 -0
- package/ts_build/src/dockerWorker.js +210 -0
- package/ts_build/src/dockerWorker.js.map +1 -0
- package/ts_build/src/index.js +4 -4
- package/ts_build/src/index.js.map +1 -1
- package/ts_build/src/microphone.js +8 -3
- package/ts_build/src/microphone.js.map +1 -1
- package/ts_build/src/services/DockerService.d.ts +26 -0
- package/ts_build/src/services/DockerService.js +363 -0
- package/ts_build/src/services/DockerService.js.map +1 -0
- package/ts_build/src/services/KnowhowClient.d.ts +1 -1
- package/ts_build/src/services/KnowhowClient.js +1 -1
- package/ts_build/src/services/KnowhowClient.js.map +1 -1
- package/ts_build/src/services/index.d.ts +3 -0
- package/ts_build/src/services/index.js +4 -1
- package/ts_build/src/services/index.js.map +1 -1
- package/ts_build/src/types.d.ts +4 -0
- package/ts_build/src/types.js +3 -0
- package/ts_build/src/types.js.map +1 -1
- package/ts_build/src/utils/InputQueueManager.d.ts +19 -0
- package/ts_build/src/utils/InputQueueManager.js +234 -0
- package/ts_build/src/utils/InputQueueManager.js.map +1 -0
- package/ts_build/src/utils/index.d.ts +1 -3
- package/ts_build/src/utils/index.js +4 -114
- package/ts_build/src/utils/index.js.map +1 -1
- package/ts_build/src/worker-entrypoint.d.ts +2 -0
- package/ts_build/src/worker-entrypoint.js +39 -0
- package/ts_build/src/worker-entrypoint.js.map +1 -0
- package/ts_build/src/worker.d.ts +7 -1
- package/ts_build/src/worker.js +117 -9
- package/ts_build/src/worker.js.map +1 -1
- package/ts_build/src/workerRegistry.d.ts +11 -0
- package/ts_build/src/workerRegistry.js +143 -0
- package/ts_build/src/workerRegistry.js.map +1 -0
- package/ts_build/tests/clients/AIClient.test.js +88 -42
- package/ts_build/tests/clients/AIClient.test.js.map +1 -1
- package/ts_build/tests/manual/test-concurrent-ask.d.ts +1 -0
- package/ts_build/tests/manual/test-concurrent-ask.js +22 -0
- package/ts_build/tests/manual/test-concurrent-ask.js.map +1 -0
- package/ts_build/tests/services/DockerService.test.d.ts +1 -0
- package/ts_build/tests/services/DockerService.test.js +22 -0
- package/ts_build/tests/services/DockerService.test.js.map +1 -0
- package/ts_build/tests/unit/input-queue.test.d.ts +1 -0
- package/ts_build/tests/unit/input-queue.test.js +32 -0
- package/ts_build/tests/unit/input-queue.test.js.map +1 -0
package/src/worker.ts
CHANGED
|
@@ -7,15 +7,141 @@ import { McpServerService } from "./services/Mcp";
|
|
|
7
7
|
import * as allTools from "./agents/tools";
|
|
8
8
|
import { wait } from "./utils";
|
|
9
9
|
import { getConfig, updateConfig } from "./config";
|
|
10
|
+
import { KNOWHOW_API_URL } from "./services/KnowhowClient";
|
|
11
|
+
import { registerWorkerPath } from "./workerRegistry";
|
|
10
12
|
|
|
11
|
-
const API_URL =
|
|
13
|
+
const API_URL = KNOWHOW_API_URL;
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Run the worker in a Docker sandbox
|
|
17
|
+
*/
|
|
18
|
+
async function runWorkerInSandbox(
|
|
19
|
+
options: { share?: boolean; unshare?: boolean; sandbox?: boolean },
|
|
20
|
+
config: any
|
|
21
|
+
) {
|
|
22
|
+
const { Docker } = services();
|
|
23
|
+
|
|
24
|
+
console.log("🐳 Starting knowhow worker in Docker sandbox mode...\n");
|
|
25
|
+
|
|
26
|
+
// Check if Docker is available
|
|
27
|
+
const dockerAvailable = await Docker.checkDockerAvailable();
|
|
28
|
+
|
|
29
|
+
if (!dockerAvailable) {
|
|
30
|
+
console.error("❌ Docker is not installed or not running.");
|
|
31
|
+
console.error("\nTo use --sandbox mode, you need to:");
|
|
32
|
+
console.error(" 1. Install Docker: https://docs.docker.com/get-docker/");
|
|
33
|
+
console.error(" 2. Start the Docker daemon");
|
|
34
|
+
console.error(" 3. Run this command again with --sandbox\n");
|
|
35
|
+
process.exit(1);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
console.log("✓ Docker is available");
|
|
39
|
+
|
|
40
|
+
// Always rebuild the image to ensure it's up to date with any Dockerfile changes
|
|
41
|
+
console.log("🔄 Building Docker image (ensuring latest version)...");
|
|
42
|
+
try {
|
|
43
|
+
await Docker.buildWorkerImage();
|
|
44
|
+
} catch (error) {
|
|
45
|
+
console.error("❌ Failed to build Docker image:", error.message);
|
|
46
|
+
console.error("\nPlease check the .knowhow/Dockerfile.worker for errors");
|
|
47
|
+
console.error(" You can edit this file to customize the worker image\n");
|
|
48
|
+
process.exit(1);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Get JWT token
|
|
52
|
+
const jwt = await loadJwt();
|
|
53
|
+
|
|
54
|
+
// Run the container
|
|
55
|
+
let containerId: string;
|
|
56
|
+
|
|
57
|
+
console.log("🚀 Starting Docker container...");
|
|
58
|
+
try {
|
|
59
|
+
containerId = await Docker.runWorkerContainer({
|
|
60
|
+
workspaceDir: process.cwd(),
|
|
61
|
+
jwt,
|
|
62
|
+
apiUrl: API_URL,
|
|
63
|
+
config,
|
|
64
|
+
share: options?.share,
|
|
65
|
+
unshare: options?.unshare,
|
|
66
|
+
});
|
|
67
|
+
} catch (error) {
|
|
68
|
+
console.error("❌ Failed to start Docker container:", error.message);
|
|
69
|
+
process.exit(1);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Follow logs and handle cleanup
|
|
73
|
+
try {
|
|
74
|
+
await Docker.followContainerLogs(containerId);
|
|
75
|
+
} finally {
|
|
76
|
+
await Docker.stopContainer(containerId);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export async function worker(options?: {
|
|
81
|
+
register?: boolean;
|
|
82
|
+
share?: boolean;
|
|
83
|
+
unshare?: boolean;
|
|
84
|
+
sandbox?: boolean;
|
|
85
|
+
noSandbox?: boolean;
|
|
86
|
+
}) {
|
|
87
|
+
const config = await getConfig();
|
|
88
|
+
|
|
89
|
+
// Determine sandbox mode with priority: command line flags > config > default (false)
|
|
90
|
+
let shouldUseSandbox = false;
|
|
91
|
+
let sandboxSource = "";
|
|
92
|
+
|
|
93
|
+
if (options?.sandbox) {
|
|
94
|
+
shouldUseSandbox = true;
|
|
95
|
+
sandboxSource = "command line (--sandbox)";
|
|
96
|
+
|
|
97
|
+
// Save sandbox preference to config
|
|
98
|
+
const updatedConfig = {
|
|
99
|
+
...config,
|
|
100
|
+
worker: {
|
|
101
|
+
...config.worker,
|
|
102
|
+
sandbox: true,
|
|
103
|
+
},
|
|
104
|
+
};
|
|
105
|
+
await updateConfig(updatedConfig);
|
|
106
|
+
console.log("💾 Sandbox mode preference saved to config");
|
|
107
|
+
} else if (options?.noSandbox) {
|
|
108
|
+
shouldUseSandbox = false;
|
|
109
|
+
sandboxSource = "command line (--no-sandbox)";
|
|
110
|
+
|
|
111
|
+
// Save no-sandbox preference to config
|
|
112
|
+
const updatedConfig = {
|
|
113
|
+
...config,
|
|
114
|
+
worker: {
|
|
115
|
+
...config.worker,
|
|
116
|
+
sandbox: false,
|
|
117
|
+
},
|
|
118
|
+
};
|
|
119
|
+
await updateConfig(updatedConfig);
|
|
120
|
+
console.log("💾 No-sandbox mode preference saved to config");
|
|
121
|
+
} else {
|
|
122
|
+
// Use config preference or default to false
|
|
123
|
+
shouldUseSandbox = config.worker?.sandbox ?? false;
|
|
124
|
+
sandboxSource =
|
|
125
|
+
config.worker?.sandbox !== undefined
|
|
126
|
+
? `config (${shouldUseSandbox ? "sandbox" : "no-sandbox"})`
|
|
127
|
+
: "default (no-sandbox)";
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (shouldUseSandbox) {
|
|
131
|
+
console.log(`🐳 Using sandbox mode (${sandboxSource})`);
|
|
132
|
+
return runWorkerInSandbox(options, config);
|
|
133
|
+
}
|
|
12
134
|
|
|
13
|
-
export async function worker() {
|
|
14
135
|
const { Tools } = services();
|
|
15
136
|
const mcpServer = new McpServerService(Tools);
|
|
16
137
|
const clientName = "knowhow-worker";
|
|
17
138
|
const clientVersion = "1.1.1";
|
|
18
|
-
|
|
139
|
+
|
|
140
|
+
if (!shouldUseSandbox) {
|
|
141
|
+
console.log(`🖥️ Using host mode (${sandboxSource})`);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Use the config we already loaded above
|
|
19
145
|
|
|
20
146
|
if (!config.worker || !config.worker.allowedTools) {
|
|
21
147
|
console.log(
|
|
@@ -30,6 +156,12 @@ export async function worker() {
|
|
|
30
156
|
return;
|
|
31
157
|
}
|
|
32
158
|
|
|
159
|
+
// Handle registration flag
|
|
160
|
+
if (options?.register) {
|
|
161
|
+
await registerWorkerPath(process.cwd());
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
|
|
33
165
|
const toolsToUse = Tools.getToolsByNames(config.worker.allowedTools);
|
|
34
166
|
mcpServer.createServer(clientName, clientVersion).withTools(toolsToUse);
|
|
35
167
|
|
|
@@ -41,13 +173,30 @@ export async function worker() {
|
|
|
41
173
|
|
|
42
174
|
const dir = process.cwd();
|
|
43
175
|
const homedir = os.homedir();
|
|
44
|
-
|
|
176
|
+
|
|
177
|
+
// Use environment variables if available (set by Docker), otherwise compute defaults
|
|
178
|
+
const hostname = process.env.WORKER_HOSTNAME || os.hostname();
|
|
179
|
+
const root = process.env.WORKER_ROOT || (dir === homedir ? "~" : dir.replace(homedir, "~"));
|
|
180
|
+
|
|
181
|
+
const headers: Record<string, string> = {
|
|
182
|
+
Authorization: `Bearer ${jwt}`,
|
|
183
|
+
"User-Agent": `${clientName}/${clientVersion}/${hostname}`,
|
|
184
|
+
Root: root,
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
// Add shared header based on flags
|
|
188
|
+
if (options?.share) {
|
|
189
|
+
headers.Shared = "true";
|
|
190
|
+
console.log("🔓 Worker shared with organization");
|
|
191
|
+
} else if (options?.unshare) {
|
|
192
|
+
headers.Shared = "false";
|
|
193
|
+
console.log("🔒 Worker is now private (unshared)");
|
|
194
|
+
} else {
|
|
195
|
+
console.log("🔒 Worker is private (only you can use it)");
|
|
196
|
+
}
|
|
197
|
+
|
|
45
198
|
const ws = new WebSocket(`${API_URL}/ws/worker`, {
|
|
46
|
-
headers
|
|
47
|
-
Authorization: `Bearer ${jwt}`,
|
|
48
|
-
"User-Agent": `${clientName}/${clientVersion}/${os.hostname()}`,
|
|
49
|
-
Root: `${root}`,
|
|
50
|
-
},
|
|
199
|
+
headers,
|
|
51
200
|
});
|
|
52
201
|
|
|
53
202
|
ws.on("open", () => {
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import * as fs from "fs";
|
|
2
|
+
import * as path from "path";
|
|
3
|
+
import * as os from "os";
|
|
4
|
+
import { spawn, ChildProcess } from "child_process";
|
|
5
|
+
|
|
6
|
+
interface WorkerRegistry {
|
|
7
|
+
workers: string[];
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function getWorkerRegistryPath(): string {
|
|
11
|
+
return path.join(os.homedir(), ".knowhow", "workers.json");
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export async function loadWorkerRegistry(): Promise<WorkerRegistry> {
|
|
15
|
+
const registryPath = getWorkerRegistryPath();
|
|
16
|
+
|
|
17
|
+
try {
|
|
18
|
+
if (fs.existsSync(registryPath)) {
|
|
19
|
+
const content = await fs.promises.readFile(registryPath, "utf8");
|
|
20
|
+
return JSON.parse(content);
|
|
21
|
+
}
|
|
22
|
+
} catch (error) {
|
|
23
|
+
console.warn("Failed to load worker registry:", error);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
return { workers: [] };
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export async function saveWorkerRegistry(registry: WorkerRegistry): Promise<void> {
|
|
30
|
+
const registryPath = getWorkerRegistryPath();
|
|
31
|
+
const dir = path.dirname(registryPath);
|
|
32
|
+
|
|
33
|
+
// Ensure the directory exists
|
|
34
|
+
if (!fs.existsSync(dir)) {
|
|
35
|
+
await fs.promises.mkdir(dir, { recursive: true });
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
await fs.promises.writeFile(
|
|
39
|
+
registryPath,
|
|
40
|
+
JSON.stringify(registry, null, 2),
|
|
41
|
+
"utf8"
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export async function registerWorkerPath(workerPath: string): Promise<void> {
|
|
46
|
+
const registry = await loadWorkerRegistry();
|
|
47
|
+
const normalizedPath = path.resolve(workerPath);
|
|
48
|
+
|
|
49
|
+
if (!registry.workers.includes(normalizedPath)) {
|
|
50
|
+
registry.workers.push(normalizedPath);
|
|
51
|
+
await saveWorkerRegistry(registry);
|
|
52
|
+
console.log(`✓ Registered worker path: ${normalizedPath}`);
|
|
53
|
+
} else {
|
|
54
|
+
console.log(`Worker path already registered: ${normalizedPath}`);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export async function unregisterWorkerPath(workerPath: string): Promise<void> {
|
|
59
|
+
const registry = await loadWorkerRegistry();
|
|
60
|
+
const normalizedPath = path.resolve(workerPath);
|
|
61
|
+
|
|
62
|
+
const index = registry.workers.indexOf(normalizedPath);
|
|
63
|
+
if (index !== -1) {
|
|
64
|
+
registry.workers.splice(index, 1);
|
|
65
|
+
await saveWorkerRegistry(registry);
|
|
66
|
+
console.log(`✓ Unregistered worker path: ${normalizedPath}`);
|
|
67
|
+
} else {
|
|
68
|
+
console.log(`Worker path not found in registry: ${normalizedPath}`);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export async function listWorkerPaths(): Promise<string[]> {
|
|
73
|
+
const registry = await loadWorkerRegistry();
|
|
74
|
+
return registry.workers;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export async function clearWorkerRegistry(): Promise<void> {
|
|
78
|
+
await saveWorkerRegistry({ workers: [] });
|
|
79
|
+
console.log("✓ Cleared all registered worker paths");
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
interface WorkerProcess {
|
|
83
|
+
path: string;
|
|
84
|
+
process: ChildProcess;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export async function startAllWorkers(): Promise<void> {
|
|
88
|
+
const workerPaths = await listWorkerPaths();
|
|
89
|
+
|
|
90
|
+
if (workerPaths.length === 0) {
|
|
91
|
+
console.log("No workers registered. Use 'knowhow worker --register' to register workers.");
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
console.log(`Starting ${workerPaths.length} worker(s)...`);
|
|
96
|
+
|
|
97
|
+
const processes: WorkerProcess[] = [];
|
|
98
|
+
|
|
99
|
+
for (const workerPath of workerPaths) {
|
|
100
|
+
if (!fs.existsSync(workerPath)) {
|
|
101
|
+
console.warn(`⚠ Worker path does not exist: ${workerPath}`);
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
console.log(`Starting worker at: ${workerPath}`);
|
|
106
|
+
|
|
107
|
+
const workerProcess = spawn("knowhow", ["worker"], {
|
|
108
|
+
cwd: workerPath,
|
|
109
|
+
stdio: "inherit",
|
|
110
|
+
detached: false,
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
workerProcess.on("error", (error) => {
|
|
114
|
+
console.error(`Error in worker at ${workerPath}:`, error);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
workerProcess.on("exit", (code, signal) => {
|
|
118
|
+
console.log(`Worker at ${workerPath} exited with code ${code} and signal ${signal}`);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
processes.push({ path: workerPath, process: workerProcess });
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
console.log(`\n✓ Started ${processes.length} worker(s)`);
|
|
125
|
+
console.log("Press Ctrl+C to stop all workers\n");
|
|
126
|
+
|
|
127
|
+
// Handle graceful shutdown
|
|
128
|
+
const shutdownHandler = () => {
|
|
129
|
+
console.log("\nShutting down all workers...");
|
|
130
|
+
|
|
131
|
+
for (const { path: workerPath, process: proc } of processes) {
|
|
132
|
+
console.log(`Stopping worker at: ${workerPath}`);
|
|
133
|
+
proc.kill("SIGTERM");
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Give processes time to shut down gracefully
|
|
137
|
+
setTimeout(() => {
|
|
138
|
+
for (const { process: proc } of processes) {
|
|
139
|
+
if (!proc.killed) {
|
|
140
|
+
proc.kill("SIGKILL");
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
process.exit(0);
|
|
144
|
+
}, 5000);
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
process.on("SIGINT", shutdownHandler);
|
|
148
|
+
process.on("SIGTERM", shutdownHandler);
|
|
149
|
+
|
|
150
|
+
// Keep the process alive
|
|
151
|
+
await new Promise(() => {});
|
|
152
|
+
}
|