@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.
Files changed (98) hide show
  1. package/docs/input-queue-manager.md +142 -0
  2. package/docs/multi-worker-management.md +142 -0
  3. package/package.json +1 -1
  4. package/scripts/README.md +119 -0
  5. package/scripts/restore_keys.sh +59 -0
  6. package/scripts/unset_keys.sh +60 -0
  7. package/src/agents/base/base.ts +2 -2
  8. package/src/agents/tools/askHuman.ts +2 -0
  9. package/src/agents/tools/startAgentTask.ts +2 -2
  10. package/src/ai.ts +3 -1
  11. package/src/chat/CliChatService.ts +2 -2
  12. package/src/chat/modules/AgentModule.ts +25 -2
  13. package/src/chat-old.ts +2 -2
  14. package/src/cli.ts +56 -3
  15. package/src/clients/anthropic.ts +7 -5
  16. package/src/clients/knowhow.ts +2 -2
  17. package/src/clients/openai.ts +5 -0
  18. package/src/index.ts +6 -6
  19. package/src/microphone.ts +12 -4
  20. package/src/services/DockerService.ts +473 -0
  21. package/src/services/KnowhowClient.ts +4 -1
  22. package/src/services/index.ts +5 -1
  23. package/src/types.ts +7 -0
  24. package/src/utils/InputQueueManager.ts +324 -0
  25. package/src/utils/index.ts +5 -152
  26. package/src/worker.ts +158 -9
  27. package/src/workerRegistry.ts +152 -0
  28. package/tests/clients/AIClient.test.ts +177 -92
  29. package/tests/manual/test-concurrent-ask.ts +43 -0
  30. package/tests/services/DockerService.test.ts +24 -0
  31. package/tests/unit/input-queue.test.ts +80 -0
  32. package/ts_build/package.json +1 -1
  33. package/ts_build/src/agents/base/base.js +2 -2
  34. package/ts_build/src/agents/tools/askHuman.d.ts +1 -1
  35. package/ts_build/src/agents/tools/askHuman.js.map +1 -1
  36. package/ts_build/src/agents/tools/startAgentTask.js +2 -1
  37. package/ts_build/src/agents/tools/startAgentTask.js.map +1 -1
  38. package/ts_build/src/ai.js +3 -1
  39. package/ts_build/src/ai.js.map +1 -1
  40. package/ts_build/src/chat/CliChatService.js +1 -1
  41. package/ts_build/src/chat/CliChatService.js.map +1 -1
  42. package/ts_build/src/chat/modules/AgentModule.js +11 -1
  43. package/ts_build/src/chat/modules/AgentModule.js.map +1 -1
  44. package/ts_build/src/chat-old.js +1 -1
  45. package/ts_build/src/chat-old.js.map +1 -1
  46. package/ts_build/src/cli.js +46 -3
  47. package/ts_build/src/cli.js.map +1 -1
  48. package/ts_build/src/clients/anthropic.js +7 -5
  49. package/ts_build/src/clients/anthropic.js.map +1 -1
  50. package/ts_build/src/clients/knowhow.js +1 -1
  51. package/ts_build/src/clients/knowhow.js.map +1 -1
  52. package/ts_build/src/clients/openai.js +5 -0
  53. package/ts_build/src/clients/openai.js.map +1 -1
  54. package/ts_build/src/dockerWorker.d.ts +22 -0
  55. package/ts_build/src/dockerWorker.js +210 -0
  56. package/ts_build/src/dockerWorker.js.map +1 -0
  57. package/ts_build/src/index.js +4 -4
  58. package/ts_build/src/index.js.map +1 -1
  59. package/ts_build/src/microphone.js +8 -3
  60. package/ts_build/src/microphone.js.map +1 -1
  61. package/ts_build/src/services/DockerService.d.ts +26 -0
  62. package/ts_build/src/services/DockerService.js +363 -0
  63. package/ts_build/src/services/DockerService.js.map +1 -0
  64. package/ts_build/src/services/KnowhowClient.d.ts +1 -1
  65. package/ts_build/src/services/KnowhowClient.js +1 -1
  66. package/ts_build/src/services/KnowhowClient.js.map +1 -1
  67. package/ts_build/src/services/index.d.ts +3 -0
  68. package/ts_build/src/services/index.js +4 -1
  69. package/ts_build/src/services/index.js.map +1 -1
  70. package/ts_build/src/types.d.ts +5 -0
  71. package/ts_build/src/types.js +4 -0
  72. package/ts_build/src/types.js.map +1 -1
  73. package/ts_build/src/utils/InputQueueManager.d.ts +19 -0
  74. package/ts_build/src/utils/InputQueueManager.js +234 -0
  75. package/ts_build/src/utils/InputQueueManager.js.map +1 -0
  76. package/ts_build/src/utils/index.d.ts +1 -3
  77. package/ts_build/src/utils/index.js +4 -114
  78. package/ts_build/src/utils/index.js.map +1 -1
  79. package/ts_build/src/worker-entrypoint.d.ts +2 -0
  80. package/ts_build/src/worker-entrypoint.js +39 -0
  81. package/ts_build/src/worker-entrypoint.js.map +1 -0
  82. package/ts_build/src/worker.d.ts +7 -1
  83. package/ts_build/src/worker.js +117 -9
  84. package/ts_build/src/worker.js.map +1 -1
  85. package/ts_build/src/workerRegistry.d.ts +11 -0
  86. package/ts_build/src/workerRegistry.js +143 -0
  87. package/ts_build/src/workerRegistry.js.map +1 -0
  88. package/ts_build/tests/clients/AIClient.test.js +88 -42
  89. package/ts_build/tests/clients/AIClient.test.js.map +1 -1
  90. package/ts_build/tests/manual/test-concurrent-ask.d.ts +1 -0
  91. package/ts_build/tests/manual/test-concurrent-ask.js +22 -0
  92. package/ts_build/tests/manual/test-concurrent-ask.js.map +1 -0
  93. package/ts_build/tests/services/DockerService.test.d.ts +1 -0
  94. package/ts_build/tests/services/DockerService.test.js +22 -0
  95. package/ts_build/tests/services/DockerService.test.js.map +1 -0
  96. package/ts_build/tests/unit/input-queue.test.d.ts +1 -0
  97. package/ts_build/tests/unit/input-queue.test.js +32 -0
  98. 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
- .action(async () => {
291
- await worker();
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);
@@ -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,
@@ -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 = process.env.KNOWHOW_API_URL;
10
+ const envUrl = KNOWHOW_API_URL;
11
11
  export class KnowhowGenericClient implements GenericClient {
12
12
  private client: KnowhowSimpleClient;
13
13
 
@@ -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
- const input = await ask(
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(private baseUrl, private jwt = loadKnowhowJwt()) {
53
+ constructor(
54
+ private baseUrl = KNOWHOW_API_URL,
55
+ private jwt = loadKnowhowJwt()
56
+ ) {
54
57
  this.setJwt(jwt);
55
58
  }
56
59