@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.
Files changed (271) hide show
  1. package/autodoc/cli-reference.md +36 -7
  2. package/bin/knowhow.js +8 -6
  3. package/package.json +1 -1
  4. package/src/agents/base/base.ts +51 -23
  5. package/src/agents/historyRollback.ts +36 -0
  6. package/src/agents/tools/execCommand.ts +102 -320
  7. package/src/agents/tools/list.ts +2 -2
  8. package/src/agents/tools/startAgentTask.ts +91 -34
  9. package/src/ai.ts +4 -1
  10. package/src/auth/browserLogin.ts +9 -3
  11. package/src/auth/keyAuth.ts +115 -0
  12. package/src/auth/keyManager.ts +166 -0
  13. package/src/chat/CliChatService.ts +23 -2
  14. package/src/chat/modules/AgentModule.ts +114 -33
  15. package/src/chat/modules/AskModule.ts +2 -0
  16. package/src/chat/modules/BaseChatModule.ts +0 -1
  17. package/src/chat/modules/CustomCommandsModule.ts +2 -0
  18. package/src/chat/modules/InternalChatModule.ts +7 -0
  19. package/src/chat/modules/RemoteSyncModule.ts +4 -0
  20. package/src/chat/modules/RendererModule.ts +2 -0
  21. package/src/chat/modules/SearchModule.ts +2 -0
  22. package/src/chat/modules/SessionsModule.ts +103 -34
  23. package/src/chat/modules/SetupModule.ts +2 -0
  24. package/src/chat/modules/ShellCommandModule.ts +2 -0
  25. package/src/chat/modules/SystemModule.ts +2 -0
  26. package/src/chat/modules/ToolCommandModule.ts +102 -0
  27. package/src/chat/modules/index.ts +1 -0
  28. package/src/chat/types.ts +3 -1
  29. package/src/cli.ts +10 -3
  30. package/src/clients/http.ts +67 -7
  31. package/src/clients/index.ts +4 -0
  32. package/src/commands/agent.ts +1 -38
  33. package/src/commands/agents.ts +455 -60
  34. package/src/commands/misc.ts +3 -2
  35. package/src/commands/processes.ts +138 -0
  36. package/src/commands/services.ts +23 -1
  37. package/src/index.ts +1 -0
  38. package/src/login.ts +89 -28
  39. package/src/processes/ProcessManager.ts +95 -0
  40. package/src/processes/managedProcessShim.ts +115 -0
  41. package/src/services/AgentService.ts +2 -0
  42. package/src/services/AgentSyncFs.ts +77 -1
  43. package/src/services/AgentSyncKnowhowWeb.ts +22 -4
  44. package/src/services/BehaviorsService.ts +2 -0
  45. package/src/services/DockerService.ts +2 -0
  46. package/src/services/EmbeddingService.ts +2 -0
  47. package/src/services/EmbeddingsService.ts +3 -0
  48. package/src/services/ExtensionsService.ts +25 -0
  49. package/src/services/KnowhowClient.ts +68 -7
  50. package/src/services/LazyToolsService.ts +24 -20
  51. package/src/services/Mcp.ts +2 -0
  52. package/src/services/McpServer.ts +2 -0
  53. package/src/services/MessageProcessor.ts +2 -0
  54. package/src/services/MinimalToolsService.ts +2 -0
  55. package/src/services/SessionManager.ts +5 -0
  56. package/src/services/SkillsService.ts +2 -0
  57. package/src/services/SyncedAgentWatcher.ts +2 -0
  58. package/src/services/SyncerService.ts +23 -8
  59. package/src/services/TaskRegistry.ts +2 -0
  60. package/src/services/Tools.ts +12 -0
  61. package/src/services/index.ts +6 -0
  62. package/src/services/modules/index.ts +10 -1
  63. package/src/services/modules/types.ts +4 -0
  64. package/src/types.ts +2 -0
  65. package/src/util/Trace.ts +1 -1
  66. package/src/utils/http.ts +18 -2
  67. package/tests/unit/agents/historyRollback.test.ts +53 -0
  68. package/tests/unit/auth/keyAuth.test.ts +102 -0
  69. package/tests/unit/auth/keyManager.test.ts +67 -0
  70. package/tests/unit/chat/ToolCommandModule.test.ts +71 -0
  71. package/tests/unit/chat/historyCommandArgs.test.ts +37 -0
  72. package/tests/unit/commands/agents.test.ts +111 -0
  73. package/tests/unit/login.test.ts +119 -0
  74. package/tests/unit/services/AgentSyncKnowhowWeb.test.ts +38 -0
  75. package/tests/unit/services/LazyToolsService.test.ts +53 -0
  76. package/tests/unit/services/SessionManager.test.ts +43 -0
  77. package/tests/unit/services/SyncerService.test.ts +45 -0
  78. package/tests/unit/util/Trace.test.ts +119 -0
  79. package/tests/unit/utils/http.test.ts +43 -0
  80. package/ts_build/package.json +1 -1
  81. package/ts_build/src/agents/base/base.d.ts +2 -1
  82. package/ts_build/src/agents/base/base.js +47 -14
  83. package/ts_build/src/agents/base/base.js.map +1 -1
  84. package/ts_build/src/agents/historyRollback.d.ts +2 -0
  85. package/ts_build/src/agents/historyRollback.js +22 -0
  86. package/ts_build/src/agents/historyRollback.js.map +1 -0
  87. package/ts_build/src/agents/tools/execCommand.d.ts +4 -1
  88. package/ts_build/src/agents/tools/execCommand.js +100 -223
  89. package/ts_build/src/agents/tools/execCommand.js.map +1 -1
  90. package/ts_build/src/agents/tools/list.js +2 -2
  91. package/ts_build/src/agents/tools/list.js.map +1 -1
  92. package/ts_build/src/agents/tools/startAgentTask.d.ts +16 -1
  93. package/ts_build/src/agents/tools/startAgentTask.js +71 -28
  94. package/ts_build/src/agents/tools/startAgentTask.js.map +1 -1
  95. package/ts_build/src/ai.js +2 -1
  96. package/ts_build/src/ai.js.map +1 -1
  97. package/ts_build/src/auth/browserLogin.d.ts +1 -0
  98. package/ts_build/src/auth/browserLogin.js +5 -2
  99. package/ts_build/src/auth/browserLogin.js.map +1 -1
  100. package/ts_build/src/auth/keyAuth.d.ts +5 -0
  101. package/ts_build/src/auth/keyAuth.js +79 -0
  102. package/ts_build/src/auth/keyAuth.js.map +1 -0
  103. package/ts_build/src/auth/keyManager.d.ts +14 -0
  104. package/ts_build/src/auth/keyManager.js +145 -0
  105. package/ts_build/src/auth/keyManager.js.map +1 -0
  106. package/ts_build/src/chat/CliChatService.d.ts +6 -0
  107. package/ts_build/src/chat/CliChatService.js +17 -2
  108. package/ts_build/src/chat/CliChatService.js.map +1 -1
  109. package/ts_build/src/chat/modules/AgentModule.d.ts +13 -1
  110. package/ts_build/src/chat/modules/AgentModule.js +83 -24
  111. package/ts_build/src/chat/modules/AgentModule.js.map +1 -1
  112. package/ts_build/src/chat/modules/AskModule.js +12 -2
  113. package/ts_build/src/chat/modules/AskModule.js.map +1 -1
  114. package/ts_build/src/chat/modules/BaseChatModule.js.map +1 -1
  115. package/ts_build/src/chat/modules/CustomCommandsModule.js +12 -2
  116. package/ts_build/src/chat/modules/CustomCommandsModule.js.map +1 -1
  117. package/ts_build/src/chat/modules/InternalChatModule.d.ts +1 -0
  118. package/ts_build/src/chat/modules/InternalChatModule.js +21 -2
  119. package/ts_build/src/chat/modules/InternalChatModule.js.map +1 -1
  120. package/ts_build/src/chat/modules/RemoteSyncModule.js +19 -2
  121. package/ts_build/src/chat/modules/RemoteSyncModule.js.map +1 -1
  122. package/ts_build/src/chat/modules/RendererModule.js +17 -2
  123. package/ts_build/src/chat/modules/RendererModule.js.map +1 -1
  124. package/ts_build/src/chat/modules/SearchModule.js +12 -2
  125. package/ts_build/src/chat/modules/SearchModule.js.map +1 -1
  126. package/ts_build/src/chat/modules/SessionsModule.d.ts +6 -0
  127. package/ts_build/src/chat/modules/SessionsModule.js +113 -18
  128. package/ts_build/src/chat/modules/SessionsModule.js.map +1 -1
  129. package/ts_build/src/chat/modules/SetupModule.js +17 -2
  130. package/ts_build/src/chat/modules/SetupModule.js.map +1 -1
  131. package/ts_build/src/chat/modules/ShellCommandModule.js +12 -2
  132. package/ts_build/src/chat/modules/ShellCommandModule.js.map +1 -1
  133. package/ts_build/src/chat/modules/SystemModule.js +12 -2
  134. package/ts_build/src/chat/modules/SystemModule.js.map +1 -1
  135. package/ts_build/src/chat/modules/ToolCommandModule.d.ts +10 -0
  136. package/ts_build/src/chat/modules/ToolCommandModule.js +86 -0
  137. package/ts_build/src/chat/modules/ToolCommandModule.js.map +1 -0
  138. package/ts_build/src/chat/modules/index.d.ts +1 -0
  139. package/ts_build/src/chat/modules/index.js +3 -1
  140. package/ts_build/src/chat/modules/index.js.map +1 -1
  141. package/ts_build/src/chat/types.d.ts +3 -1
  142. package/ts_build/src/cli.js +7 -3
  143. package/ts_build/src/cli.js.map +1 -1
  144. package/ts_build/src/clients/http.d.ts +4 -0
  145. package/ts_build/src/clients/http.js +68 -6
  146. package/ts_build/src/clients/http.js.map +1 -1
  147. package/ts_build/src/clients/index.js +23 -4
  148. package/ts_build/src/clients/index.js.map +1 -1
  149. package/ts_build/src/commands/agent.js +1 -21
  150. package/ts_build/src/commands/agent.js.map +1 -1
  151. package/ts_build/src/commands/agents.d.ts +2 -1
  152. package/ts_build/src/commands/agents.js +216 -23
  153. package/ts_build/src/commands/agents.js.map +1 -1
  154. package/ts_build/src/commands/misc.js +3 -2
  155. package/ts_build/src/commands/misc.js.map +1 -1
  156. package/ts_build/src/commands/processes.d.ts +2 -0
  157. package/ts_build/src/commands/processes.js +213 -0
  158. package/ts_build/src/commands/processes.js.map +1 -0
  159. package/ts_build/src/commands/services.js +11 -1
  160. package/ts_build/src/commands/services.js.map +1 -1
  161. package/ts_build/src/index.d.ts +1 -0
  162. package/ts_build/src/index.js +1 -0
  163. package/ts_build/src/index.js.map +1 -1
  164. package/ts_build/src/login.d.ts +1 -1
  165. package/ts_build/src/login.js +53 -15
  166. package/ts_build/src/login.js.map +1 -1
  167. package/ts_build/src/processes/ProcessManager.d.ts +51 -0
  168. package/ts_build/src/processes/ProcessManager.js +160 -0
  169. package/ts_build/src/processes/ProcessManager.js.map +1 -0
  170. package/ts_build/src/processes/managedProcessShim.d.ts +1 -0
  171. package/ts_build/src/processes/managedProcessShim.js +161 -0
  172. package/ts_build/src/processes/managedProcessShim.js.map +1 -0
  173. package/ts_build/src/services/AgentService.js +18 -2
  174. package/ts_build/src/services/AgentService.js.map +1 -1
  175. package/ts_build/src/services/AgentSyncFs.d.ts +3 -0
  176. package/ts_build/src/services/AgentSyncFs.js +68 -16
  177. package/ts_build/src/services/AgentSyncFs.js.map +1 -1
  178. package/ts_build/src/services/AgentSyncKnowhowWeb.d.ts +2 -0
  179. package/ts_build/src/services/AgentSyncKnowhowWeb.js +28 -6
  180. package/ts_build/src/services/AgentSyncKnowhowWeb.js.map +1 -1
  181. package/ts_build/src/services/BehaviorsService.js +12 -2
  182. package/ts_build/src/services/BehaviorsService.js.map +1 -1
  183. package/ts_build/src/services/DockerService.js +20 -8
  184. package/ts_build/src/services/DockerService.js.map +1 -1
  185. package/ts_build/src/services/EmbeddingService.js +16 -2
  186. package/ts_build/src/services/EmbeddingService.js.map +1 -1
  187. package/ts_build/src/services/EmbeddingsService.js +12 -2
  188. package/ts_build/src/services/EmbeddingsService.js.map +1 -1
  189. package/ts_build/src/services/ExtensionsService.d.ts +10 -0
  190. package/ts_build/src/services/ExtensionsService.js +17 -0
  191. package/ts_build/src/services/ExtensionsService.js.map +1 -0
  192. package/ts_build/src/services/KnowhowClient.d.ts +7 -2
  193. package/ts_build/src/services/KnowhowClient.js +108 -10
  194. package/ts_build/src/services/KnowhowClient.js.map +1 -1
  195. package/ts_build/src/services/LazyToolsService.d.ts +2 -0
  196. package/ts_build/src/services/LazyToolsService.js +26 -9
  197. package/ts_build/src/services/LazyToolsService.js.map +1 -1
  198. package/ts_build/src/services/Mcp.js +12 -2
  199. package/ts_build/src/services/Mcp.js.map +1 -1
  200. package/ts_build/src/services/McpServer.js +17 -2
  201. package/ts_build/src/services/McpServer.js.map +1 -1
  202. package/ts_build/src/services/MessageProcessor.js +16 -2
  203. package/ts_build/src/services/MessageProcessor.js.map +1 -1
  204. package/ts_build/src/services/MinimalToolsService.js +16 -2
  205. package/ts_build/src/services/MinimalToolsService.js.map +1 -1
  206. package/ts_build/src/services/SessionManager.js +19 -2
  207. package/ts_build/src/services/SessionManager.js.map +1 -1
  208. package/ts_build/src/services/SkillsService.js +16 -2
  209. package/ts_build/src/services/SkillsService.js.map +1 -1
  210. package/ts_build/src/services/SyncedAgentWatcher.js +16 -2
  211. package/ts_build/src/services/SyncedAgentWatcher.js.map +1 -1
  212. package/ts_build/src/services/SyncerService.d.ts +1 -0
  213. package/ts_build/src/services/SyncerService.js +27 -5
  214. package/ts_build/src/services/SyncerService.js.map +1 -1
  215. package/ts_build/src/services/TaskRegistry.js +12 -2
  216. package/ts_build/src/services/TaskRegistry.js.map +1 -1
  217. package/ts_build/src/services/Tools.d.ts +1 -0
  218. package/ts_build/src/services/Tools.js +19 -2
  219. package/ts_build/src/services/Tools.js.map +1 -1
  220. package/ts_build/src/services/index.d.ts +5 -0
  221. package/ts_build/src/services/index.js +7 -2
  222. package/ts_build/src/services/index.js.map +1 -1
  223. package/ts_build/src/services/modules/index.d.ts +4 -1
  224. package/ts_build/src/services/modules/index.js +6 -1
  225. package/ts_build/src/services/modules/index.js.map +1 -1
  226. package/ts_build/src/services/modules/types.d.ts +3 -0
  227. package/ts_build/src/types.d.ts +1 -0
  228. package/ts_build/src/types.js.map +1 -1
  229. package/ts_build/src/util/Trace.d.ts +1 -3
  230. package/ts_build/src/utils/http.d.ts +6 -2
  231. package/ts_build/src/utils/http.js +8 -2
  232. package/ts_build/src/utils/http.js.map +1 -1
  233. package/ts_build/tests/unit/agents/historyRollback.test.d.ts +1 -0
  234. package/ts_build/tests/unit/agents/historyRollback.test.js +42 -0
  235. package/ts_build/tests/unit/agents/historyRollback.test.js.map +1 -0
  236. package/ts_build/tests/unit/auth/keyAuth.test.d.ts +1 -0
  237. package/ts_build/tests/unit/auth/keyAuth.test.js +77 -0
  238. package/ts_build/tests/unit/auth/keyAuth.test.js.map +1 -0
  239. package/ts_build/tests/unit/auth/keyManager.test.d.ts +1 -0
  240. package/ts_build/tests/unit/auth/keyManager.test.js +52 -0
  241. package/ts_build/tests/unit/auth/keyManager.test.js.map +1 -0
  242. package/ts_build/tests/unit/chat/ToolCommandModule.test.d.ts +1 -0
  243. package/ts_build/tests/unit/chat/ToolCommandModule.test.js +61 -0
  244. package/ts_build/tests/unit/chat/ToolCommandModule.test.js.map +1 -0
  245. package/ts_build/tests/unit/chat/historyCommandArgs.test.d.ts +1 -0
  246. package/ts_build/tests/unit/chat/historyCommandArgs.test.js +29 -0
  247. package/ts_build/tests/unit/chat/historyCommandArgs.test.js.map +1 -0
  248. package/ts_build/tests/unit/commands/agents.test.d.ts +1 -0
  249. package/ts_build/tests/unit/commands/agents.test.js +119 -0
  250. package/ts_build/tests/unit/commands/agents.test.js.map +1 -0
  251. package/ts_build/tests/unit/login.test.d.ts +1 -0
  252. package/ts_build/tests/unit/login.test.js +97 -0
  253. package/ts_build/tests/unit/login.test.js.map +1 -0
  254. package/ts_build/tests/unit/services/AgentSyncKnowhowWeb.test.d.ts +1 -0
  255. package/ts_build/tests/unit/services/AgentSyncKnowhowWeb.test.js +32 -0
  256. package/ts_build/tests/unit/services/AgentSyncKnowhowWeb.test.js.map +1 -0
  257. package/ts_build/tests/unit/services/LazyToolsService.test.d.ts +1 -0
  258. package/ts_build/tests/unit/services/LazyToolsService.test.js +47 -0
  259. package/ts_build/tests/unit/services/LazyToolsService.test.js.map +1 -0
  260. package/ts_build/tests/unit/services/SessionManager.test.d.ts +1 -0
  261. package/ts_build/tests/unit/services/SessionManager.test.js +72 -0
  262. package/ts_build/tests/unit/services/SessionManager.test.js.map +1 -0
  263. package/ts_build/tests/unit/services/SyncerService.test.d.ts +1 -0
  264. package/ts_build/tests/unit/services/SyncerService.test.js +40 -0
  265. package/ts_build/tests/unit/services/SyncerService.test.js.map +1 -0
  266. package/ts_build/tests/unit/util/Trace.test.d.ts +1 -0
  267. package/ts_build/tests/unit/util/Trace.test.js +107 -0
  268. package/ts_build/tests/unit/util/Trace.test.js.map +1 -0
  269. package/ts_build/tests/unit/utils/http.test.d.ts +1 -0
  270. package/ts_build/tests/unit/utils/http.test.js +69 -0
  271. package/ts_build/tests/unit/utils/http.test.js.map +1 -0
@@ -1,17 +1,17 @@
1
- import { exec, spawn, ExecException } from "child_process";
2
- import { promisify } from "util";
3
1
  import * as fs from "fs";
4
- import * as path from "path";
5
- import * as os from "os";
2
+ import { exec } from "child_process";
3
+ import { promisify } from "util";
6
4
  import { services, ToolsService } from "../../services";
5
+ import { getProcessesDir, ManagedProcess, processDirectory, spawnManaged } from "../../processes/ProcessManager";
7
6
 
8
7
  export const execAsync = promisify(exec);
9
8
 
10
9
  export interface ExecCommandOptions {
11
- timeout?: number; // ms; -1 = wait indefinitely
12
- continueInBackground?: boolean; // allow to keep running on timeout
13
- maxBuffer?: number; // for exec()
14
- logFileName?: string; // custom log file name for background tasks (without path or extension)
10
+ timeout?: number;
11
+ continueInBackground?: boolean;
12
+ maxBuffer?: number;
13
+ logFileName?: string;
14
+ parentProcessId?: string;
15
15
  }
16
16
 
17
17
  type ExecResult = {
@@ -20,361 +20,143 @@ type ExecResult = {
20
20
  timedOut: boolean;
21
21
  killed: boolean;
22
22
  pid?: number;
23
- logPath?: string;
23
+ processId: string;
24
+ processDirectory: string;
25
+ logPath: string;
24
26
  };
25
27
 
26
- const PROCESSES_DIR = path.join(process.cwd(), ".knowhow", "processes");
27
- fs.mkdirSync(PROCESSES_DIR, { recursive: true });
28
-
29
- const STARTED_PIDS = new Set<number>();
30
- setupProcessCleanup();
31
-
32
- // ---------- utils ----------
33
- function stripTrailingAmp(cmd: string) {
34
- const t = cmd.trim();
35
- return t.endsWith("&") ? t.replace(/&\s*$/, "").trim() : t;
28
+ function stripTrailingAmp(command: string): string {
29
+ const trimmed = command.trim();
30
+ return trimmed.endsWith("&") ? trimmed.replace(/&\s*$/, "").trim() : trimmed;
36
31
  }
37
-
38
- function commandNameFrom(cmd: string) {
39
- const cleaned = stripTrailingAmp(cmd);
40
- const first = cleaned.split(/\s+/)[0] || "command";
41
- return first.replace(/[^\w.-]+/g, "_");
42
- }
43
-
44
- function makeLogPath(cmd: string, customFileName?: string) {
45
- // Use custom filename if provided, otherwise derive from command
46
- let baseName = customFileName
47
- ? customFileName.replace(/[^\w.-]+/g, "_")
48
- : commandNameFrom(cmd);
49
-
50
- let logPath = path.join(PROCESSES_DIR, `${baseName}.txt`);
51
-
52
- // If file already exists, append epoch seconds to ensure uniqueness
53
- if (fs.existsSync(logPath)) {
54
- const epochSeconds = Math.floor(Date.now() / 1000);
55
- baseName = `${baseName}_${epochSeconds}`;
56
- logPath = path.join(PROCESSES_DIR, `${baseName}.txt`);
57
- }
58
-
59
- return logPath;
60
- }
61
-
62
- function setupProcessCleanup() {
63
- const killAll = () => {
64
- for (const pid of STARTED_PIDS) {
65
- try {
66
- if (os.platform() === "win32") {
67
- spawn("taskkill", ["/PID", String(pid), "/T", "/F"], {
68
- stdio: "ignore",
69
- detached: true,
70
- }).unref();
71
- } else {
72
- try {
73
- process.kill(-pid, "SIGTERM");
74
- } catch {
75
- try {
76
- process.kill(pid, "SIGTERM");
77
- } catch {}
78
- }
79
- }
80
- } catch {}
81
- }
32
+ function readOutput(managed: ManagedProcess, maxBuffer: number): { stdout: string; stderr: string } {
33
+ const read = (file: string) => {
34
+ try {
35
+ const value = fs.readFileSync(file, "utf8");
36
+ return value.length > maxBuffer
37
+ ? `${value.slice(0, maxBuffer)}\n[output truncated after ${maxBuffer} characters]`
38
+ : value;
39
+ } catch { return ""; }
82
40
  };
83
- process.once("exit", killAll);
84
- process.once("SIGINT", () => {
85
- killAll();
86
- process.exit(130);
87
- });
88
- process.once("SIGTERM", () => {
89
- killAll();
90
- process.exit(143);
91
- });
92
- process.once("uncaughtException", (e) => {
93
- console.error(e);
94
- killAll();
95
- process.exit(1);
96
- });
97
- process.once("unhandledRejection", (e: any) => {
98
- console.error(e);
99
- killAll();
100
- process.exit(1);
101
- });
41
+ return { stdout: read(managed.stdoutPath), stderr: read(managed.stderrPath) };
42
+ }
43
+ async function stopAfterTimeout(managed: ManagedProcess): Promise<void> {
44
+ try { managed.signal("SIGTERM"); } catch {}
45
+ try { await managed.wait(3000); return; } catch {}
46
+ try { managed.signal("SIGKILL"); } catch {}
47
+ try { await managed.wait(1000); } catch {}
102
48
  }
103
49
 
104
- // ---------- core ----------
105
50
  const execWithTimeout = async (
106
51
  command: string,
107
- opts: ExecCommandOptions = {}
52
+ options: ExecCommandOptions = {}
108
53
  ): Promise<ExecResult> => {
109
- const {
110
- timeout = 5000,
111
- continueInBackground = false,
112
- maxBuffer = 1024 * 1024 * 16,
113
- } = opts;
114
-
115
- const cleaned = stripTrailingAmp(command);
116
- const shouldBg = continueInBackground || command.trim().endsWith("&");
117
-
118
- // Foreground, indefinite wait → stream with spawn
119
- if (timeout === -1 && !shouldBg) {
120
- return new Promise<ExecResult>((resolve) => {
121
- let out = "";
122
- let err = "";
123
- const child = spawn(cleaned, { shell: true });
124
- child.stdout?.on("data", (d) => (out += d.toString()));
125
- child.stderr?.on("data", (d) => (err += d.toString()));
126
- child.once("error", (e) =>
127
- resolve({
128
- stdout: out,
129
- stderr: err || String(e),
130
- timedOut: false,
131
- killed: false,
132
- pid: child.pid ?? undefined,
133
- })
134
- );
135
- child.once("exit", () =>
136
- resolve({
137
- stdout: out,
138
- stderr: err,
139
- timedOut: false,
140
- killed: false,
141
- pid: child.pid ?? undefined,
142
- })
143
- );
144
- });
54
+ const timeout = options.timeout ?? 5000;
55
+ const maxBuffer = options.maxBuffer ?? 16 * 1024 * 1024;
56
+ const background = options.continueInBackground || command.trim().endsWith("&");
57
+ let requestedId = options.logFileName?.replace(/[^a-zA-Z0-9._-]/g, "-");
58
+ if (requestedId) {
59
+ const base = requestedId;
60
+ let suffix = 0;
61
+ while (fs.existsSync(processDirectory(requestedId, getProcessesDir()))) requestedId = `${base}-${Date.now()}-${++suffix}`;
145
62
  }
63
+ const managed = await spawnManaged(stripTrailingAmp(command), [], {
64
+ id: requestedId,
65
+ shell: true,
66
+ background,
67
+ parentPid: process.pid,
68
+ parentProcessId: options.parentProcessId,
69
+ });
70
+ const initial = managed.status;
71
+ const common = {
72
+ pid: initial.pid || undefined,
73
+ processId: managed.id,
74
+ processDirectory: managed.directory,
75
+ logPath: managed.stdoutPath,
76
+ };
146
77
 
147
- if (shouldBg) {
148
- // --- BACKGROUND MODE WITH CHILD-OWNED LOG FD ---
149
- const logPath = makeLogPath(cleaned, opts.logFileName);
150
-
151
- // Open the log file now; we'll pass this FD to the child so it writes directly.
152
- // Use 'w' to truncate old logs and guarantee our header goes first.
153
- const fd = fs.openSync(logPath, "w");
154
-
155
- // Spawn detached; bind stdout/stderr to the same file.
156
- const child = spawn(cleaned, {
157
- shell: true,
158
- detached: true,
159
- stdio: ["ignore", fd, fd], // child writes directly to file for its entire lifetime
160
- });
161
-
162
- // Immediately write header (first line includes PID), then close *our* fd.
163
- // The child keeps its own duplicated handle open.
164
- const pid = child.pid!;
165
- const header =
166
- `PID: ${pid}\n` +
167
- `CMD: ${cleaned}\n` +
168
- `START: ${new Date().toISOString()}\n` +
169
- `---\n`;
170
- fs.writeSync(fd, header);
171
- fs.fsyncSync(fd); // flush header before we let go
172
- try {
173
- fs.closeSync(fd);
174
- } catch {}
175
-
176
- // We only wait 'timeout' ms to return; process keeps running/logging after that.
177
- return await new Promise<ExecResult>((resolve) => {
178
- let settled = false;
179
-
180
- const done = (res: ExecResult) => {
181
- if (settled) return;
182
- settled = true;
183
- resolve({ ...res, pid, logPath });
184
- };
185
-
186
- child.once("error", (e) =>
187
- done({
188
- stdout: "",
189
- stderr: `Failed to start command: ${String(e)}`,
190
- timedOut: false,
191
- killed: false,
192
- })
193
- );
194
-
195
- const tid = setTimeout(() => {
196
- // fully detach from our side
197
- try {
198
- child.unref();
199
- } catch {}
200
- STARTED_PIDS.add(pid);
201
- done({
202
- stdout: "",
203
- stderr:
204
- `Command timed out after ${timeout}ms but continues in background\n` +
205
- `Logs: ${logPath}\n` +
206
- `Tip: read first line for PID; kill by PID for cleanup.\n`,
207
- timedOut: true,
208
- killed: false,
209
- });
210
- }, timeout);
211
-
212
- // If it finishes early, report success and avoid “timed out” messaging
213
- child.once("exit", () => {
214
- clearTimeout(tid);
215
- done({
216
- stdout: "",
217
- stderr: `Process finished before timeout. Logs: ${logPath}\n`,
218
- timedOut: false,
219
- killed: false,
220
- });
221
- });
222
- });
78
+ if (background) {
79
+ return { stdout: "", stderr: "", timedOut: false, killed: false, ...common };
223
80
  }
224
81
 
225
- // Foreground with timeout → use exec (buffered) or switch to spawn+manual timer if you prefer
226
- return new Promise<ExecResult>((resolve) => {
227
- let out = "";
228
- let err = "";
229
- const child = exec(
230
- cleaned,
231
- { timeout, maxBuffer },
232
- (error: ExecException | null, stdout: string, stderr: string) => {
233
- out = out || stdout;
234
- err = err || stderr;
235
- if (error) {
236
- resolve({
237
- stdout: out,
238
- stderr: err || error.message,
239
- timedOut: (error as any).killed ?? false,
240
- killed: !!(error as any).killed,
241
- pid: (child as any).pid ?? undefined,
242
- });
243
- } else {
244
- resolve({
245
- stdout: out,
246
- stderr: err,
247
- timedOut: false,
248
- killed: false,
249
- pid: (child as any).pid ?? undefined,
250
- });
251
- }
252
- }
253
- );
254
- child.stdout?.on("data", (d) => (out += d.toString()));
255
- child.stderr?.on("data", (d) => (err += d.toString()));
256
- });
82
+ let timedOut = false;
83
+ if (timeout === -1) {
84
+ await managed.wait();
85
+ } else {
86
+ try { await managed.wait(timeout); }
87
+ catch {
88
+ timedOut = true;
89
+ await stopAfterTimeout(managed);
90
+ }
91
+ }
92
+ const output = readOutput(managed, maxBuffer);
93
+ return { ...output, timedOut, killed: timedOut, ...common };
257
94
  };
258
95
 
259
- // Public tool
96
+ /** Execute a shell command through the durable managed-process subsystem. */
260
97
  export async function execCommand(
261
98
  command: string,
262
99
  timeout?: number | string,
263
100
  continueInBackground?: boolean,
264
- logFileName?: string
101
+ logFileName?: string,
102
+ _ctx?: { taskId?: string }
265
103
  ): Promise<string> {
266
104
  if (!command || typeof command !== "string") {
267
105
  throw new Error("Invalid command. We received a non-string value. Please ensure you are sending strings of 4k tokens or less.");
268
106
  }
269
107
 
270
- // Get context from bound ToolsService (same pattern as writeFile)
271
- const toolService = (
272
- this instanceof ToolsService ? this : services().Tools
273
- ) as ToolsService;
274
-
275
- // Coerce timeout to a number. Agents (and some tool-call serializers) often
276
- // pass numeric arguments as strings (e.g. "120000"). Node's exec()/setTimeout
277
- // reject non-integer values with "The value of \"timeout\" is out of range.
278
- // It must be an unsigned integer", so normalize here before any use.
108
+ const toolService = (this instanceof ToolsService ? this : services().Tools) as ToolsService;
279
109
  let normalizedTimeout: number | undefined;
280
110
  const rawTimeout = typeof timeout === "string" ? Number(timeout.trim()) : timeout;
281
- if (typeof rawTimeout !== "number" || !Number.isFinite(rawTimeout)) {
282
- normalizedTimeout = undefined;
283
- } else if (rawTimeout === -1) {
284
- // -1 is the sentinel for "wait indefinitely" — leave as-is.
285
- normalizedTimeout = -1;
286
- } else {
287
- // Clamp to a non-negative integer so the underlying APIs never throw.
288
- normalizedTimeout = Math.max(0, Math.floor(rawTimeout));
289
- }
111
+ if (typeof rawTimeout !== "number" || !Number.isFinite(rawTimeout)) normalizedTimeout = undefined;
112
+ else if (rawTimeout === -1) normalizedTimeout = -1;
113
+ else normalizedTimeout = Math.max(0, Math.floor(rawTimeout));
290
114
 
291
- // Detect suspiciously small timeout values — agents often pass seconds instead of ms.
292
- // Any value < 100 is almost certainly a mistake (100ms is still very fast for a shell cmd).
293
- // We auto-correct to seconds and include a warning in the output so the agent learns.
294
115
  let correctedTimeout = normalizedTimeout;
295
116
  let timeoutWarning = "";
296
- if (
297
- normalizedTimeout !== undefined &&
298
- normalizedTimeout !== -1 &&
299
- normalizedTimeout < 100
300
- ) {
117
+ if (normalizedTimeout !== undefined && normalizedTimeout !== -1 && normalizedTimeout < 100) {
301
118
  correctedTimeout = normalizedTimeout * 1000;
302
- timeoutWarning =
303
- `⚠️ Warning: timeout was ${normalizedTimeout}ms which is extremely small and likely a mistake. ` +
304
- `The timeout unit is milliseconds, not seconds. ` +
305
- `Auto-corrected to ${correctedTimeout}ms (${normalizedTimeout}s).\n\n`;
119
+ timeoutWarning = `⚠️ Warning: timeout was ${normalizedTimeout}ms which is extremely small and likely a mistake. The timeout unit is milliseconds, not seconds. Auto-corrected to ${correctedTimeout}ms (${normalizedTimeout}s).\n\n`;
306
120
  }
307
121
 
308
122
  const context = toolService.getContext();
309
-
310
- // Emit pre-run blocking event — handlers can throw to block the command
311
123
  if (context.Events) {
312
- await context.Events.emitBlocking("exec:pre-run", {
313
- command,
314
- timeout,
315
- continueInBackground,
316
- logFileName,
317
- });
124
+ await context.Events.emitBlocking("exec:pre-run", { command, timeout, continueInBackground, logFileName });
318
125
  }
319
126
 
320
- const { stdout, stderr, timedOut, killed, pid, logPath } =
321
- await execWithTimeout(command, {
322
- timeout: correctedTimeout,
323
- continueInBackground,
324
- logFileName,
325
- });
127
+
128
+ const parentProcessId = process.env.KNOWHOW_MANAGED_PROCESS_ID || _ctx?.taskId;
129
+ const result = await execWithTimeout(command, {
130
+ timeout: correctedTimeout,
131
+ continueInBackground,
132
+ logFileName,
133
+ parentProcessId,
134
+ });
135
+ const { stdout, stderr, timedOut, killed, pid, processId, processDirectory, logPath } = result;
326
136
 
327
137
  let output = "";
328
- if (stderr) output += stderr + "\n";
138
+ if (stderr) output += stderr + (stderr.endsWith("\n") ? "" : "\n");
329
139
  if (stdout) output += stdout;
330
-
331
- const statusMsg = timedOut
332
- ? killed
140
+ const background = continueInBackground || command.trim().endsWith("&");
141
+ const statusMsg = background
142
+ ? ` (managed background process, id=${processId}, pid=${pid || "starting"})`
143
+ : timedOut
333
144
  ? " (killed due to timeout)"
334
- : ` (timed out, still running${pid ? `, pid=${pid}` : ""}${
335
- logPath ? `, logs=${logPath}` : ""
336
- })`
337
- : "";
338
-
339
- /*
340
- *const lines = output.split("\n");
341
- *const maxLines = 1000;
342
- *const maxChars = 40000;
343
- *const trimmed = (lines.length > maxLines ? lines.slice(0, maxLines) : lines)
344
- * .join("\n")
345
- * .slice(0, maxChars);
346
- *const trimmedMsg =
347
- * lines.length > maxLines
348
- * ? ` (${lines.length - maxLines} results trimmed)`
349
- * : "";
350
- */
351
-
352
- const result = `$ ${command}${statusMsg}\n${output}`;
145
+ : "";
146
+ if (background) {
147
+ output += `Process directory: ${processDirectory}\nStdout: ${logPath}\nStderr: ${result.processDirectory}/stderr\n`;
148
+ }
149
+ const rendered = `$ ${command}${statusMsg}\n${output}`;
353
150
 
354
- // Emit post-run blocking event — handlers can append extra context
355
151
  let eventResults: any[] = [];
356
152
  if (context.Events) {
357
153
  eventResults = await context.Events.emitBlocking("exec:post-run", {
358
- command,
359
- timeout,
360
- continueInBackground,
361
- logFileName,
362
- stdout,
363
- stderr,
364
- timedOut,
365
- killed,
366
- pid,
367
- logPath,
368
- output,
154
+ command, timeout, continueInBackground, logFileName,
155
+ stdout, stderr, timedOut, killed, pid, logPath, processId, processDirectory, output,
369
156
  });
370
157
  }
371
-
372
- // Append any additional context returned by post-run handlers
373
- let eventResultsText = "";
374
- if (eventResults && eventResults.length > 0) {
375
- eventResultsText =
376
- "\n\nAdditional Information:\n" + JSON.stringify(eventResults, null, 2);
377
- }
378
-
379
- return timeoutWarning + result + eventResultsText;
380
- };
158
+ const eventResultsText = eventResults?.length
159
+ ? "\n\nAdditional Information:\n" + JSON.stringify(eventResults, null, 2)
160
+ : "";
161
+ return timeoutWarning + rendered + eventResultsText;
162
+ }
@@ -88,7 +88,7 @@ export const includedTools = [
88
88
  function: {
89
89
  name: "execCommand",
90
90
  description:
91
- "Execute a command in the system's command line interface. Use this to run tests and things in the terminal. Supports timeout functionality. Use timeout: -1 to wait indefinitely. Commands ending with '&' or with continueInBackground=true will run in the background and write logs to .knowhow/processes/<command_name>.txt with PID in the first line for cleanup. You can optionally specify a custom log file name for background tasks.",
91
+ "Execute a command as a durable managed process. Use this to run tests and terminal tasks. Use timeout: -1 to wait indefinitely. Commands ending with '&' or with continueInBackground=true survive the calling agent and return a process ID plus a .knowhow/processes/<process-id>/ directory containing status, stdin, and output logs. Use `knowhow processes` to list, inspect, send input, attach, or stop them.",
92
92
  parameters: {
93
93
  type: "object",
94
94
  positional: true,
@@ -110,7 +110,7 @@ export const includedTools = [
110
110
  logFileName: {
111
111
  type: "string",
112
112
  description:
113
- "Optional custom log file name for background tasks (without path or extension). If not provided, a sanitized version of the command will be used. If the file already exists, epoch seconds will be appended to ensure uniqueness.",
113
+ "Optional preferred managed process ID (sanitized for use as a directory name). If it already exists, a timestamp suffix is added.",
114
114
  },
115
115
  },
116
116
  required: ["command"],