@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.
Files changed (93) 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/tools/askHuman.ts +2 -0
  8. package/src/agents/tools/startAgentTask.ts +2 -2
  9. package/src/ai.ts +3 -1
  10. package/src/chat/CliChatService.ts +2 -2
  11. package/src/chat/modules/AgentModule.ts +25 -2
  12. package/src/chat-old.ts +2 -2
  13. package/src/cli.ts +56 -3
  14. package/src/clients/anthropic.ts +7 -5
  15. package/src/clients/knowhow.ts +2 -2
  16. package/src/index.ts +6 -6
  17. package/src/microphone.ts +12 -4
  18. package/src/services/DockerService.ts +473 -0
  19. package/src/services/KnowhowClient.ts +4 -1
  20. package/src/services/index.ts +5 -1
  21. package/src/types.ts +6 -0
  22. package/src/utils/InputQueueManager.ts +324 -0
  23. package/src/utils/index.ts +5 -152
  24. package/src/worker.ts +158 -9
  25. package/src/workerRegistry.ts +152 -0
  26. package/tests/clients/AIClient.test.ts +177 -92
  27. package/tests/manual/test-concurrent-ask.ts +43 -0
  28. package/tests/services/DockerService.test.ts +24 -0
  29. package/tests/unit/input-queue.test.ts +80 -0
  30. package/ts_build/package.json +1 -1
  31. package/ts_build/src/agents/tools/askHuman.d.ts +1 -1
  32. package/ts_build/src/agents/tools/askHuman.js.map +1 -1
  33. package/ts_build/src/agents/tools/startAgentTask.js +2 -1
  34. package/ts_build/src/agents/tools/startAgentTask.js.map +1 -1
  35. package/ts_build/src/ai.js +3 -1
  36. package/ts_build/src/ai.js.map +1 -1
  37. package/ts_build/src/chat/CliChatService.js +1 -1
  38. package/ts_build/src/chat/CliChatService.js.map +1 -1
  39. package/ts_build/src/chat/modules/AgentModule.js +11 -1
  40. package/ts_build/src/chat/modules/AgentModule.js.map +1 -1
  41. package/ts_build/src/chat-old.js +1 -1
  42. package/ts_build/src/chat-old.js.map +1 -1
  43. package/ts_build/src/cli.js +46 -3
  44. package/ts_build/src/cli.js.map +1 -1
  45. package/ts_build/src/clients/anthropic.js +7 -5
  46. package/ts_build/src/clients/anthropic.js.map +1 -1
  47. package/ts_build/src/clients/knowhow.js +1 -1
  48. package/ts_build/src/clients/knowhow.js.map +1 -1
  49. package/ts_build/src/dockerWorker.d.ts +22 -0
  50. package/ts_build/src/dockerWorker.js +210 -0
  51. package/ts_build/src/dockerWorker.js.map +1 -0
  52. package/ts_build/src/index.js +4 -4
  53. package/ts_build/src/index.js.map +1 -1
  54. package/ts_build/src/microphone.js +8 -3
  55. package/ts_build/src/microphone.js.map +1 -1
  56. package/ts_build/src/services/DockerService.d.ts +26 -0
  57. package/ts_build/src/services/DockerService.js +363 -0
  58. package/ts_build/src/services/DockerService.js.map +1 -0
  59. package/ts_build/src/services/KnowhowClient.d.ts +1 -1
  60. package/ts_build/src/services/KnowhowClient.js +1 -1
  61. package/ts_build/src/services/KnowhowClient.js.map +1 -1
  62. package/ts_build/src/services/index.d.ts +3 -0
  63. package/ts_build/src/services/index.js +4 -1
  64. package/ts_build/src/services/index.js.map +1 -1
  65. package/ts_build/src/types.d.ts +4 -0
  66. package/ts_build/src/types.js +3 -0
  67. package/ts_build/src/types.js.map +1 -1
  68. package/ts_build/src/utils/InputQueueManager.d.ts +19 -0
  69. package/ts_build/src/utils/InputQueueManager.js +234 -0
  70. package/ts_build/src/utils/InputQueueManager.js.map +1 -0
  71. package/ts_build/src/utils/index.d.ts +1 -3
  72. package/ts_build/src/utils/index.js +4 -114
  73. package/ts_build/src/utils/index.js.map +1 -1
  74. package/ts_build/src/worker-entrypoint.d.ts +2 -0
  75. package/ts_build/src/worker-entrypoint.js +39 -0
  76. package/ts_build/src/worker-entrypoint.js.map +1 -0
  77. package/ts_build/src/worker.d.ts +7 -1
  78. package/ts_build/src/worker.js +117 -9
  79. package/ts_build/src/worker.js.map +1 -1
  80. package/ts_build/src/workerRegistry.d.ts +11 -0
  81. package/ts_build/src/workerRegistry.js +143 -0
  82. package/ts_build/src/workerRegistry.js.map +1 -0
  83. package/ts_build/tests/clients/AIClient.test.js +88 -42
  84. package/ts_build/tests/clients/AIClient.test.js.map +1 -1
  85. package/ts_build/tests/manual/test-concurrent-ask.d.ts +1 -0
  86. package/ts_build/tests/manual/test-concurrent-ask.js +22 -0
  87. package/ts_build/tests/manual/test-concurrent-ask.js.map +1 -0
  88. package/ts_build/tests/services/DockerService.test.d.ts +1 -0
  89. package/ts_build/tests/services/DockerService.test.js +22 -0
  90. package/ts_build/tests/services/DockerService.test.js.map +1 -0
  91. package/ts_build/tests/unit/input-queue.test.d.ts +1 -0
  92. package/ts_build/tests/unit/input-queue.test.js +32 -0
  93. 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 = process.env.KNOWHOW_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
- const config = await getConfig();
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
- const root = dir === homedir ? "~" : dir.replace(homedir, "~");
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
+ }