@tyvm/knowhow 0.0.54 → 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/base/base.ts +2 -2
- 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/clients/openai.ts +5 -0
- 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 +7 -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/base/base.js +2 -2
- 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/clients/openai.js +5 -0
- package/ts_build/src/clients/openai.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 +5 -0
- package/ts_build/src/types.js +4 -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/cli.ts
CHANGED
|
@@ -14,6 +14,12 @@ import * as allTools from "./agents/tools";
|
|
|
14
14
|
import { services } from "./services";
|
|
15
15
|
import { login } from "./login";
|
|
16
16
|
import { worker } from "./worker";
|
|
17
|
+
import {
|
|
18
|
+
startAllWorkers,
|
|
19
|
+
listWorkerPaths,
|
|
20
|
+
unregisterWorkerPath,
|
|
21
|
+
clearWorkerRegistry,
|
|
22
|
+
} from "./workerRegistry";
|
|
17
23
|
import { agents } from "./agents";
|
|
18
24
|
import { startChat } from "./chat";
|
|
19
25
|
import { askAI } from "./chat-old";
|
|
@@ -286,9 +292,56 @@ async function main() {
|
|
|
286
292
|
|
|
287
293
|
program
|
|
288
294
|
.command("worker")
|
|
289
|
-
.description("Start worker process")
|
|
290
|
-
.
|
|
291
|
-
|
|
295
|
+
.description("Start worker process and optionally register current directory")
|
|
296
|
+
.option("--register", "Register current directory as a worker path")
|
|
297
|
+
.option("--share", "Share this worker with your organization (allows other users to use it)")
|
|
298
|
+
.option("--unshare", "Make this worker private (only you can use it)")
|
|
299
|
+
.option("--sandbox", "Run worker in a Docker container for isolation")
|
|
300
|
+
.option("--no-sandbox", "Run worker directly on host (disable sandbox mode)")
|
|
301
|
+
.action(async (options) => {
|
|
302
|
+
await worker(options);
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
program
|
|
306
|
+
.command("workers")
|
|
307
|
+
.description("Manage and start all registered workers")
|
|
308
|
+
.option("--list", "List all registered worker paths")
|
|
309
|
+
.option("--unregister <path>", "Unregister a worker path")
|
|
310
|
+
.option("--clear", "Clear all registered worker paths")
|
|
311
|
+
.action(async (options) => {
|
|
312
|
+
try {
|
|
313
|
+
if (options.list) {
|
|
314
|
+
const workers = await listWorkerPaths();
|
|
315
|
+
if (workers.length === 0) {
|
|
316
|
+
console.log("No workers registered.");
|
|
317
|
+
console.log(
|
|
318
|
+
"\nTo register a worker, run 'knowhow worker --register' from the worker directory."
|
|
319
|
+
);
|
|
320
|
+
} else {
|
|
321
|
+
console.log(`Registered workers (${workers.length}):`);
|
|
322
|
+
workers.forEach((workerPath, index) => {
|
|
323
|
+
console.log(` ${index + 1}. ${workerPath}`);
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
if (options.unregister) {
|
|
330
|
+
await unregisterWorkerPath(options.unregister);
|
|
331
|
+
return;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
if (options.clear) {
|
|
335
|
+
await clearWorkerRegistry();
|
|
336
|
+
return;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// Default action: start all workers
|
|
340
|
+
await startAllWorkers();
|
|
341
|
+
} catch (error) {
|
|
342
|
+
console.error("Error managing workers:", error);
|
|
343
|
+
process.exit(1);
|
|
344
|
+
}
|
|
292
345
|
});
|
|
293
346
|
|
|
294
347
|
await program.parseAsync(process.argv);
|
package/src/clients/anthropic.ts
CHANGED
|
@@ -44,11 +44,7 @@ export class GenericAnthropicClient implements GenericClient {
|
|
|
44
44
|
const transformed = tools.map((tool) => ({
|
|
45
45
|
name: tool.function.name || "",
|
|
46
46
|
description: tool.function.description || "",
|
|
47
|
-
input_schema:
|
|
48
|
-
properties: tool.function.parameters.properties,
|
|
49
|
-
type: "object" as const,
|
|
50
|
-
required: tool.function.parameters.required || [],
|
|
51
|
-
},
|
|
47
|
+
input_schema: tool.function.parameters as any,
|
|
52
48
|
}));
|
|
53
49
|
|
|
54
50
|
this.handleToolCaching(transformed);
|
|
@@ -329,6 +325,12 @@ export class GenericAnthropicClient implements GenericClient {
|
|
|
329
325
|
|
|
330
326
|
pricesPerMillion() {
|
|
331
327
|
return {
|
|
328
|
+
[Models.anthropic.Opus4_5]: {
|
|
329
|
+
input: 5.0,
|
|
330
|
+
cache_write: 6.25,
|
|
331
|
+
cache_hit: 0.5,
|
|
332
|
+
output: 25.0,
|
|
333
|
+
},
|
|
332
334
|
[Models.anthropic.Opus4]: {
|
|
333
335
|
input: 15.0,
|
|
334
336
|
cache_write: 18.75,
|
package/src/clients/knowhow.ts
CHANGED
|
@@ -5,9 +5,9 @@ import {
|
|
|
5
5
|
EmbeddingOptions,
|
|
6
6
|
EmbeddingResponse,
|
|
7
7
|
} from "./types";
|
|
8
|
-
import { KnowhowSimpleClient } from "../services/KnowhowClient";
|
|
8
|
+
import { KnowhowSimpleClient, KNOWHOW_API_URL } from "../services/KnowhowClient";
|
|
9
9
|
|
|
10
|
-
const envUrl =
|
|
10
|
+
const envUrl = KNOWHOW_API_URL;
|
|
11
11
|
export class KnowhowGenericClient implements GenericClient {
|
|
12
12
|
private client: KnowhowSimpleClient;
|
|
13
13
|
|
package/src/clients/openai.ts
CHANGED
|
@@ -210,6 +210,11 @@ export class GenericOpenAiClient implements GenericClient {
|
|
|
210
210
|
cached_input: 0,
|
|
211
211
|
output: 10.0,
|
|
212
212
|
},
|
|
213
|
+
[Models.openai.GPT_5_2]: {
|
|
214
|
+
input: 1.75,
|
|
215
|
+
cached_input: 0.175,
|
|
216
|
+
output: 14,
|
|
217
|
+
},
|
|
213
218
|
[Models.openai.GPT_5_1]: {
|
|
214
219
|
input: 1.25,
|
|
215
220
|
cached_input: 0.125,
|
package/src/index.ts
CHANGED
|
@@ -253,12 +253,6 @@ export async function handleMultiOutputGeneration(
|
|
|
253
253
|
hashes[file] = { promptHash: "", fileHash: "" };
|
|
254
254
|
}
|
|
255
255
|
|
|
256
|
-
// summarize the file
|
|
257
|
-
console.log("Summarizing", file);
|
|
258
|
-
const summary = prompt
|
|
259
|
-
? await summarizeFile(file, prompt, model, agent)
|
|
260
|
-
: fileContent;
|
|
261
|
-
|
|
262
256
|
// write the summary to the output file
|
|
263
257
|
const { name, ext, dir } = path.parse(file);
|
|
264
258
|
const nestedFolder = inputPath ? (dir + "/").replace(inputPath, "") : "";
|
|
@@ -282,6 +276,12 @@ export async function handleMultiOutputGeneration(
|
|
|
282
276
|
continue;
|
|
283
277
|
}
|
|
284
278
|
|
|
279
|
+
// summarize the file
|
|
280
|
+
console.log("Summarizing", file);
|
|
281
|
+
const summary = prompt
|
|
282
|
+
? await summarizeFile(file, prompt, model, agent)
|
|
283
|
+
: fileContent;
|
|
284
|
+
|
|
285
285
|
console.log("Writing summary to", outputFile);
|
|
286
286
|
await writeFile(outputFile, summary);
|
|
287
287
|
|
package/src/microphone.ts
CHANGED
|
@@ -117,18 +117,26 @@ sudo apt-get install sox libsox-fmt-all
|
|
|
117
117
|
}
|
|
118
118
|
|
|
119
119
|
export async function voiceToText() {
|
|
120
|
-
|
|
121
|
-
"Press Enter to Start Recording, or exit to quit...: "
|
|
120
|
+
let input = await ask(
|
|
121
|
+
"Press Enter to Start Recording, or exit to quit...: ",
|
|
122
|
+
["exit"]
|
|
122
123
|
);
|
|
123
124
|
|
|
124
125
|
if (input === "exit") {
|
|
125
|
-
return "voice";
|
|
126
|
+
return "/voice";
|
|
126
127
|
}
|
|
127
128
|
|
|
128
129
|
const recording = await recordAudio();
|
|
129
130
|
console.log("Recording audio...");
|
|
130
|
-
await ask("Press Enter to Stop
|
|
131
|
+
input = await ask("Press Enter to Stop Recording, or exit to quit...: ", [
|
|
132
|
+
"exit",
|
|
133
|
+
]);
|
|
131
134
|
recording.stop();
|
|
132
135
|
console.log("Stopped recording");
|
|
136
|
+
|
|
137
|
+
if (input === "exit") {
|
|
138
|
+
return "/voice";
|
|
139
|
+
}
|
|
140
|
+
|
|
133
141
|
return convertAudioToText("/tmp/knowhow.wav", false);
|
|
134
142
|
}
|
|
@@ -0,0 +1,473 @@
|
|
|
1
|
+
import { execAsync } from "../utils";
|
|
2
|
+
import * as path from "path";
|
|
3
|
+
import * as os from "os";
|
|
4
|
+
import { Config } from "../types";
|
|
5
|
+
import { updateConfig } from "../config";
|
|
6
|
+
|
|
7
|
+
export interface DockerWorkerOptions {
|
|
8
|
+
workspaceDir: string;
|
|
9
|
+
jwt: string;
|
|
10
|
+
apiUrl: string;
|
|
11
|
+
config?: Config;
|
|
12
|
+
share?: boolean;
|
|
13
|
+
unshare?: boolean;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export class DockerService {
|
|
17
|
+
private static readonly IMAGE_NAME = "knowhow-worker";
|
|
18
|
+
private static readonly CONTAINER_PREFIX = "knowhow-worker";
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Check if Docker is installed and running
|
|
22
|
+
*/
|
|
23
|
+
async checkDockerAvailable(): Promise<boolean> {
|
|
24
|
+
try {
|
|
25
|
+
await execAsync("docker --version");
|
|
26
|
+
await execAsync("docker info");
|
|
27
|
+
return true;
|
|
28
|
+
} catch (error) {
|
|
29
|
+
console.log(error);
|
|
30
|
+
return false;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Get the path to the Dockerfile
|
|
36
|
+
*/
|
|
37
|
+
private getDockerfilePath(): string {
|
|
38
|
+
const buildContext = process.cwd();
|
|
39
|
+
return path.join(buildContext, ".knowhow", "Dockerfile.worker");
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Ensure the Dockerfile exists, creating it from template if needed
|
|
44
|
+
*/
|
|
45
|
+
private async ensureDockerfile(): Promise<string> {
|
|
46
|
+
const dockerfilePath = this.getDockerfilePath();
|
|
47
|
+
const fs = require("fs");
|
|
48
|
+
|
|
49
|
+
// Create .knowhow directory if it doesn't exist
|
|
50
|
+
const knowhowDir = path.dirname(dockerfilePath);
|
|
51
|
+
if (!fs.existsSync(knowhowDir)) {
|
|
52
|
+
fs.mkdirSync(knowhowDir, { recursive: true });
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Only write the default Dockerfile if it doesn't exist
|
|
56
|
+
if (!fs.existsSync(dockerfilePath)) {
|
|
57
|
+
console.log("📝 Creating default worker.Dockerfile...");
|
|
58
|
+
const dockerfile = this.generateDockerfile();
|
|
59
|
+
fs.writeFileSync(dockerfilePath, dockerfile);
|
|
60
|
+
console.log(`✓ Dockerfile created at ${dockerfilePath}`);
|
|
61
|
+
console.log(" You can customize this file to modify the worker image\n");
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return dockerfilePath;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Build the Docker image for the knowhow worker
|
|
69
|
+
*/
|
|
70
|
+
async buildWorkerImage(): Promise<void> {
|
|
71
|
+
console.log("🔨 Building Docker image for knowhow worker...");
|
|
72
|
+
|
|
73
|
+
const dockerfilePath = await this.ensureDockerfile();
|
|
74
|
+
const buildContext = process.cwd();
|
|
75
|
+
|
|
76
|
+
console.log("📦 Starting Docker build process...\n");
|
|
77
|
+
|
|
78
|
+
return new Promise<void>((resolve, reject) => {
|
|
79
|
+
const { spawn } = require("child_process");
|
|
80
|
+
const buildProcess = spawn(
|
|
81
|
+
"docker",
|
|
82
|
+
[
|
|
83
|
+
"build",
|
|
84
|
+
"-t",
|
|
85
|
+
DockerService.IMAGE_NAME,
|
|
86
|
+
"-f",
|
|
87
|
+
dockerfilePath,
|
|
88
|
+
buildContext,
|
|
89
|
+
],
|
|
90
|
+
{
|
|
91
|
+
stdio: ["inherit", "pipe", "pipe"],
|
|
92
|
+
}
|
|
93
|
+
);
|
|
94
|
+
|
|
95
|
+
buildProcess.stdout.on("data", (data: Buffer) => {
|
|
96
|
+
const output = data.toString();
|
|
97
|
+
// Add prefixes to make build steps more visible
|
|
98
|
+
const lines = output.split("\n").filter((line) => line.trim());
|
|
99
|
+
lines.forEach((line) => {
|
|
100
|
+
if (line.includes("Step ")) {
|
|
101
|
+
console.log(`🔧 ${line}`);
|
|
102
|
+
} else if (line.includes("---> ")) {
|
|
103
|
+
console.log(` ${line}`);
|
|
104
|
+
} else if (line.trim()) {
|
|
105
|
+
console.log(` ${line}`);
|
|
106
|
+
}
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
buildProcess.stderr.on("data", (data: Buffer) => {
|
|
111
|
+
const output = data.toString();
|
|
112
|
+
const lines = output.split("\n").filter((line) => line.trim());
|
|
113
|
+
lines.forEach((line) => {
|
|
114
|
+
console.log(`⚠️ ${line}`);
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
buildProcess.on("close", (code: number) => {
|
|
119
|
+
if (code === 0) {
|
|
120
|
+
console.log("\n🎉 Docker image built successfully!");
|
|
121
|
+
console.log(
|
|
122
|
+
`✅ Image '${DockerService.IMAGE_NAME}' is ready to use\n`
|
|
123
|
+
);
|
|
124
|
+
resolve();
|
|
125
|
+
} else {
|
|
126
|
+
reject(new Error(`Docker build failed with exit code ${code}`));
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
buildProcess.on("error", (error: Error) => {
|
|
131
|
+
reject(error);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
buildProcess.on("spawn", () => {
|
|
135
|
+
console.log("🚀 Docker build process started...\n");
|
|
136
|
+
});
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Check if the worker image exists
|
|
142
|
+
*/
|
|
143
|
+
async imageExists(): Promise<boolean> {
|
|
144
|
+
try {
|
|
145
|
+
const { stdout } = await execAsync(
|
|
146
|
+
`docker images -q ${DockerService.IMAGE_NAME}`
|
|
147
|
+
);
|
|
148
|
+
return stdout.trim().length > 0;
|
|
149
|
+
} catch (error) {
|
|
150
|
+
return false;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Expand ~ to home directory in paths
|
|
156
|
+
*/
|
|
157
|
+
private expandPath(pathStr: string): string {
|
|
158
|
+
if (pathStr.startsWith("~/") || pathStr === "~") {
|
|
159
|
+
return pathStr.replace("~", os.homedir());
|
|
160
|
+
}
|
|
161
|
+
return pathStr;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Parse and validate a volume mount specification
|
|
166
|
+
* Format: "host_path:container_path[:mode]"
|
|
167
|
+
*/
|
|
168
|
+
private parseVolumeMount(volumeSpec: string): {
|
|
169
|
+
hostPath: string;
|
|
170
|
+
containerPath: string;
|
|
171
|
+
mode?: string;
|
|
172
|
+
isValid: boolean;
|
|
173
|
+
error?: string;
|
|
174
|
+
} {
|
|
175
|
+
const parts = volumeSpec.split(":");
|
|
176
|
+
|
|
177
|
+
if (parts.length < 2) {
|
|
178
|
+
return {
|
|
179
|
+
hostPath: "",
|
|
180
|
+
containerPath: "",
|
|
181
|
+
isValid: false,
|
|
182
|
+
error: `Invalid volume format: "${volumeSpec}". Expected format: "host_path:container_path[:mode]"`,
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const hostPath = this.expandPath(parts[0]);
|
|
187
|
+
const containerPath = parts[1];
|
|
188
|
+
const mode = parts[2]; // optional, can be "ro" (read-only) or "rw" (read-write)
|
|
189
|
+
|
|
190
|
+
// Check if host path exists
|
|
191
|
+
const fs = require("fs");
|
|
192
|
+
if (!fs.existsSync(hostPath)) {
|
|
193
|
+
return {
|
|
194
|
+
hostPath,
|
|
195
|
+
containerPath,
|
|
196
|
+
mode,
|
|
197
|
+
isValid: false,
|
|
198
|
+
error: `Host path does not exist: ${hostPath} (from "${volumeSpec}")`,
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
return {
|
|
203
|
+
hostPath,
|
|
204
|
+
containerPath,
|
|
205
|
+
mode,
|
|
206
|
+
isValid: true,
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Validate and format volume mounts from config
|
|
212
|
+
*/
|
|
213
|
+
private processVolumeMounts(volumes: string[]): {
|
|
214
|
+
valid: string[];
|
|
215
|
+
errors: string[];
|
|
216
|
+
} {
|
|
217
|
+
const valid: string[] = [];
|
|
218
|
+
const errors: string[] = [];
|
|
219
|
+
|
|
220
|
+
for (const volumeSpec of volumes) {
|
|
221
|
+
const parsed = this.parseVolumeMount(volumeSpec);
|
|
222
|
+
|
|
223
|
+
if (!parsed.isValid) {
|
|
224
|
+
errors.push(parsed.error!);
|
|
225
|
+
} else {
|
|
226
|
+
// Rebuild the volume mount string with expanded paths
|
|
227
|
+
const mount = parsed.mode
|
|
228
|
+
? `${parsed.hostPath}:${parsed.containerPath}:${parsed.mode}`
|
|
229
|
+
: `${parsed.hostPath}:${parsed.containerPath}`;
|
|
230
|
+
valid.push(mount);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
return { valid, errors };
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Run the knowhow worker in a Docker container
|
|
239
|
+
*/
|
|
240
|
+
async runWorkerContainer(options: DockerWorkerOptions): Promise<string> {
|
|
241
|
+
const containerName = `${DockerService.CONTAINER_PREFIX}-${Date.now()}`;
|
|
242
|
+
const homedir = os.homedir();
|
|
243
|
+
const relativeWorkspace = options.workspaceDir.replace(homedir, "~");
|
|
244
|
+
const knowhowDir = path.join(homedir, ".knowhow");
|
|
245
|
+
const relativeKnowhowDir = "~/.knowhow";
|
|
246
|
+
|
|
247
|
+
let config = options.config;
|
|
248
|
+
|
|
249
|
+
// Ensure config has the default volumes that we always mount
|
|
250
|
+
const defaultVolumes = [
|
|
251
|
+
`${relativeWorkspace}:/workspace`,
|
|
252
|
+
`${relativeKnowhowDir}:/root/.knowhow`,
|
|
253
|
+
];
|
|
254
|
+
|
|
255
|
+
// Initialize worker.volumes if not present or merge with defaults
|
|
256
|
+
if (!config?.worker?.volumes || config.worker.volumes.length === 0) {
|
|
257
|
+
console.log("📝 Initializing default volume mounts in config...");
|
|
258
|
+
config = {
|
|
259
|
+
...config,
|
|
260
|
+
worker: {
|
|
261
|
+
...config?.worker,
|
|
262
|
+
volumes: defaultVolumes,
|
|
263
|
+
},
|
|
264
|
+
};
|
|
265
|
+
await updateConfig(config);
|
|
266
|
+
console.log("✓ Config updated with default volume mounts\n");
|
|
267
|
+
} else {
|
|
268
|
+
// Ensure default volumes are present in the config
|
|
269
|
+
const configVolumes = config.worker.volumes;
|
|
270
|
+
const missingDefaults = defaultVolumes.filter(
|
|
271
|
+
(dv) => !configVolumes.some((cv) => cv.split(":")[1] === dv.split(":")[1])
|
|
272
|
+
);
|
|
273
|
+
|
|
274
|
+
if (missingDefaults.length > 0) {
|
|
275
|
+
console.log("📝 Adding missing default volumes to config...");
|
|
276
|
+
config.worker.volumes = [...missingDefaults, ...configVolumes];
|
|
277
|
+
await updateConfig(config);
|
|
278
|
+
console.log("✓ Config updated with missing default volumes\n");
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
// Build docker run command from config volumes
|
|
284
|
+
const volumeMounts: string[] = [];
|
|
285
|
+
|
|
286
|
+
console.log(
|
|
287
|
+
`📁 Processing ${config.worker.volumes.length} volume mount(s)...`
|
|
288
|
+
);
|
|
289
|
+
|
|
290
|
+
const { valid, errors } = this.processVolumeMounts(config.worker.volumes);
|
|
291
|
+
|
|
292
|
+
// Report errors but continue with valid volumes
|
|
293
|
+
if (errors.length > 0) {
|
|
294
|
+
console.warn("⚠️ Some volume mounts could not be processed:");
|
|
295
|
+
errors.forEach((error) => console.warn(` ${error}`));
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
if (valid.length > 0) {
|
|
299
|
+
console.log(`✓ Mounting ${valid.length} volume(s):`);
|
|
300
|
+
valid.forEach((mount) => {
|
|
301
|
+
const parts = mount.split(":");
|
|
302
|
+
const mode = parts[2] ? ` (${parts[2]})` : "";
|
|
303
|
+
console.log(` ${parts[0]} → ${parts[1]}${mode}`);
|
|
304
|
+
volumeMounts.push(`-v "${mount}"`);
|
|
305
|
+
});
|
|
306
|
+
} else {
|
|
307
|
+
console.error("❌ No valid volume mounts available!");
|
|
308
|
+
throw new Error("Cannot start container without valid volume mounts");
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// Prepare hostname and root for display in Docker
|
|
312
|
+
const hostname = `${os.hostname()}.docker`;
|
|
313
|
+
const workspaceRoot = process.env.WORKER_ROOT || relativeWorkspace;
|
|
314
|
+
|
|
315
|
+
const envVars = [
|
|
316
|
+
`-e KNOWHOW_JWT="${options.jwt}"`,
|
|
317
|
+
`-e KNOWHOW_API_URL="${options.apiUrl}"`,
|
|
318
|
+
`-e WORKSPACE_ROOT="${relativeWorkspace}"`,
|
|
319
|
+
`-e WORKER_HOSTNAME="${hostname}"`,
|
|
320
|
+
`-e WORKER_ROOT="${workspaceRoot}"`,
|
|
321
|
+
];
|
|
322
|
+
|
|
323
|
+
if (options.share) {
|
|
324
|
+
envVars.push(`-e WORKER_SHARED="true"`);
|
|
325
|
+
} else if (options.unshare) {
|
|
326
|
+
envVars.push(`-e WORKER_SHARED="false"`);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// Handle envFile from config
|
|
330
|
+
const envFileArgs: string[] = [];
|
|
331
|
+
if (config?.worker?.envFile) {
|
|
332
|
+
const envFilePath = this.expandPath(config.worker.envFile);
|
|
333
|
+
const fs = require("fs");
|
|
334
|
+
|
|
335
|
+
if (fs.existsSync(envFilePath)) {
|
|
336
|
+
console.log(`📄 Using environment file: ${envFilePath}`);
|
|
337
|
+
envFileArgs.push(`--env-file "${envFilePath}"`);
|
|
338
|
+
} else {
|
|
339
|
+
console.warn(`⚠️ Environment file not found: ${envFilePath}`);
|
|
340
|
+
console.warn(` Container will run without the environment file.`);
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
const dockerCmd = [
|
|
345
|
+
"docker run",
|
|
346
|
+
"-d",
|
|
347
|
+
`--name ${containerName}`,
|
|
348
|
+
"--network host", // Use host network for easier API connectivity
|
|
349
|
+
...volumeMounts,
|
|
350
|
+
...envVars,
|
|
351
|
+
...envFileArgs,
|
|
352
|
+
`-w /workspace`,
|
|
353
|
+
DockerService.IMAGE_NAME,
|
|
354
|
+
].join(" ");
|
|
355
|
+
|
|
356
|
+
console.log("Starting Docker container...");
|
|
357
|
+
|
|
358
|
+
try {
|
|
359
|
+
const { stdout } = await execAsync(dockerCmd);
|
|
360
|
+
const containerId = stdout.trim();
|
|
361
|
+
|
|
362
|
+
console.log(
|
|
363
|
+
`✓ Container started: ${containerName} (${containerId.substring(
|
|
364
|
+
0,
|
|
365
|
+
12
|
|
366
|
+
)})`
|
|
367
|
+
);
|
|
368
|
+
console.log(` Workspace: ${options.workspaceDir}`);
|
|
369
|
+
console.log(` Container ID: ${containerId.substring(0, 12)}`);
|
|
370
|
+
|
|
371
|
+
return containerId;
|
|
372
|
+
} catch (error) {
|
|
373
|
+
console.error("Failed to start container:", error.message);
|
|
374
|
+
throw error;
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
/**
|
|
379
|
+
* Follow container logs
|
|
380
|
+
*/
|
|
381
|
+
async followContainerLogs(containerId: string): Promise<void> {
|
|
382
|
+
console.log("\nFollowing container logs (Ctrl+C to stop)...\n");
|
|
383
|
+
|
|
384
|
+
const { spawn } = require("child_process");
|
|
385
|
+
const logsProcess = spawn("docker", ["logs", "-f", containerId], {
|
|
386
|
+
stdio: "inherit",
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
return new Promise((resolve, reject) => {
|
|
390
|
+
logsProcess.on("error", reject);
|
|
391
|
+
|
|
392
|
+
// Handle Ctrl+C gracefully
|
|
393
|
+
process.on("SIGINT", () => {
|
|
394
|
+
logsProcess.kill();
|
|
395
|
+
resolve();
|
|
396
|
+
});
|
|
397
|
+
});
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
/**
|
|
401
|
+
* Stop and remove a container
|
|
402
|
+
*/
|
|
403
|
+
async stopContainer(containerId: string): Promise<void> {
|
|
404
|
+
try {
|
|
405
|
+
console.log(`\nStopping container ${containerId.substring(0, 12)}...`);
|
|
406
|
+
await execAsync(`docker stop ${containerId}`);
|
|
407
|
+
await execAsync(`docker rm ${containerId}`);
|
|
408
|
+
console.log("✓ Container stopped and removed");
|
|
409
|
+
} catch (error) {
|
|
410
|
+
console.error("Failed to stop container:", error.message);
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
/**
|
|
415
|
+
* Clean up all knowhow worker containers
|
|
416
|
+
*/
|
|
417
|
+
async cleanupAllWorkerContainers(): Promise<void> {
|
|
418
|
+
try {
|
|
419
|
+
const { stdout } = await execAsync(
|
|
420
|
+
`docker ps -a --filter "name=${DockerService.CONTAINER_PREFIX}" --format "{{.ID}}"`
|
|
421
|
+
);
|
|
422
|
+
|
|
423
|
+
const containerIds = stdout.trim().split("\n").filter(Boolean);
|
|
424
|
+
|
|
425
|
+
if (containerIds.length === 0) {
|
|
426
|
+
console.log("No worker containers to clean up");
|
|
427
|
+
return;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
console.log(`Cleaning up ${containerIds.length} worker container(s)...`);
|
|
431
|
+
|
|
432
|
+
for (const containerId of containerIds) {
|
|
433
|
+
await this.stopContainer(containerId);
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
console.log("✓ All worker containers cleaned up");
|
|
437
|
+
} catch (error) {
|
|
438
|
+
console.error("Failed to clean up containers:", error.message);
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
/**
|
|
443
|
+
* Generate Dockerfile content for the worker
|
|
444
|
+
*/
|
|
445
|
+
private generateDockerfile(): string {
|
|
446
|
+
return `FROM node:20
|
|
447
|
+
|
|
448
|
+
# Install necessary system dependencies
|
|
449
|
+
RUN apt-get update && apt-get install -y \
|
|
450
|
+
curl \
|
|
451
|
+
ffmpeg \
|
|
452
|
+
&& rm -rf /var/lib/apt/lists/*
|
|
453
|
+
|
|
454
|
+
# Install necessary packages
|
|
455
|
+
RUN apt-get install git python3 make g++ bash curl
|
|
456
|
+
|
|
457
|
+
# Install knowhow CLI globally
|
|
458
|
+
RUN npm install -g @tyvm/knowhow
|
|
459
|
+
|
|
460
|
+
# Create workspace directory
|
|
461
|
+
WORKDIR /workspace
|
|
462
|
+
|
|
463
|
+
RUN knowhow init
|
|
464
|
+
|
|
465
|
+
# Set environment variables
|
|
466
|
+
ENV NODE_ENV=production
|
|
467
|
+
ENV KNOWHOW_DOCKER=true
|
|
468
|
+
|
|
469
|
+
# Set the default command to run the worker
|
|
470
|
+
CMD ["knowhow", "worker"]
|
|
471
|
+
`;
|
|
472
|
+
}
|
|
473
|
+
}
|
|
@@ -50,7 +50,10 @@ export class KnowhowSimpleClient {
|
|
|
50
50
|
headers = {};
|
|
51
51
|
jwtValidated = false;
|
|
52
52
|
|
|
53
|
-
constructor(
|
|
53
|
+
constructor(
|
|
54
|
+
private baseUrl = KNOWHOW_API_URL,
|
|
55
|
+
private jwt = loadKnowhowJwt()
|
|
56
|
+
) {
|
|
54
57
|
this.setJwt(jwt);
|
|
55
58
|
}
|
|
56
59
|
|