@tyvm/knowhow 0.0.138 → 0.0.141

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 (173) hide show
  1. package/autodoc/chat-guide.md +1 -0
  2. package/package.json +5 -2
  3. package/scripts/build-for-node.sh +4 -4
  4. package/src/agents/base/base.ts +254 -29
  5. package/src/agents/patcher/patcher.ts +1 -1
  6. package/src/agents/researcher/researcher.ts +2 -2
  7. package/src/agents/tools/agentCall.ts +6 -0
  8. package/src/agents/tools/connectAgent.ts +353 -0
  9. package/src/agents/tools/index.ts +6 -0
  10. package/src/agents/tools/list.ts +48 -0
  11. package/src/agents/tools/observe.ts +245 -0
  12. package/src/agents/tools/replyToParent.ts +122 -0
  13. package/src/agents/tools/runGenerate.ts +209 -0
  14. package/src/agents/tools/sendAgentMessage.ts +138 -0
  15. package/src/agents/tools/startAgentTask.ts +92 -5
  16. package/src/agents/tools/waitForAgentCompleted.ts +269 -0
  17. package/src/ai.ts +1 -0
  18. package/src/chat/modules/AgentModule.ts +89 -27
  19. package/src/chat/modules/SessionsModule.ts +6 -10
  20. package/src/clients/contextLimits.ts +26 -0
  21. package/src/clients/pricing/anthropic.ts +12 -0
  22. package/src/commands/agent.ts +22 -0
  23. package/src/commands/agents.ts +83 -0
  24. package/src/commands/misc.ts +6 -0
  25. package/src/commands/modules.ts +159 -7
  26. package/src/commands/services.ts +14 -0
  27. package/src/generate.ts +25 -3
  28. package/src/services/AgentService.ts +21 -3
  29. package/src/services/AgentSyncFs.ts +56 -0
  30. package/src/services/AgentSyncKnowhowWeb.ts +1 -1
  31. package/src/services/EventService.ts +4 -1
  32. package/src/services/KnowhowClient.ts +7 -0
  33. package/src/services/LazyToolsService.ts +7 -3
  34. package/src/services/MinimalToolsService.ts +15 -7
  35. package/src/services/SyncedAgentWatcher.ts +2 -0
  36. package/src/services/SyncerService.ts +35 -6
  37. package/src/services/Tools.ts +43 -3
  38. package/src/services/modules/index.ts +127 -46
  39. package/src/types.ts +5 -0
  40. package/tests/agents/interrupt.test.ts +11 -18
  41. package/tests/agents/observe-base.test.ts +183 -0
  42. package/tests/agents/tools/connectAgent.test.ts +138 -0
  43. package/tests/agents/tools/observe.test.ts +179 -0
  44. package/tests/agents/tools/replyToParent.test.ts +125 -0
  45. package/tests/agents/tools/sendAgentMessage.test.ts +99 -0
  46. package/tests/agents/tools/waitForAgentCompleted.test.ts +147 -0
  47. package/tests/chat/detach-attach-rendering.test.ts +315 -0
  48. package/tests/clients/contextLimits.test.ts +23 -1
  49. package/tests/clients/pricing.test.ts +20 -1
  50. package/tests/services/Tools.test.ts +172 -9
  51. package/tests/unit/modules/moduleLoading.test.ts +69 -0
  52. package/ts_build/package.json +5 -2
  53. package/ts_build/src/agents/base/base.d.ts +24 -1
  54. package/ts_build/src/agents/base/base.js +111 -16
  55. package/ts_build/src/agents/base/base.js.map +1 -1
  56. package/ts_build/src/agents/patcher/patcher.js +1 -1
  57. package/ts_build/src/agents/patcher/patcher.js.map +1 -1
  58. package/ts_build/src/agents/researcher/researcher.js +2 -2
  59. package/ts_build/src/agents/researcher/researcher.js.map +1 -1
  60. package/ts_build/src/agents/tools/agentCall.js.map +1 -1
  61. package/ts_build/src/agents/tools/connectAgent.d.ts +21 -0
  62. package/ts_build/src/agents/tools/connectAgent.js +284 -0
  63. package/ts_build/src/agents/tools/connectAgent.js.map +1 -0
  64. package/ts_build/src/agents/tools/index.d.ts +6 -0
  65. package/ts_build/src/agents/tools/index.js +6 -0
  66. package/ts_build/src/agents/tools/index.js.map +1 -1
  67. package/ts_build/src/agents/tools/list.js +40 -0
  68. package/ts_build/src/agents/tools/list.js.map +1 -1
  69. package/ts_build/src/agents/tools/observe.d.ts +20 -0
  70. package/ts_build/src/agents/tools/observe.js +144 -0
  71. package/ts_build/src/agents/tools/observe.js.map +1 -0
  72. package/ts_build/src/agents/tools/replyToParent.d.ts +4 -0
  73. package/ts_build/src/agents/tools/replyToParent.js +118 -0
  74. package/ts_build/src/agents/tools/replyToParent.js.map +1 -0
  75. package/ts_build/src/agents/tools/runGenerate.d.ts +19 -0
  76. package/ts_build/src/agents/tools/runGenerate.js +138 -0
  77. package/ts_build/src/agents/tools/runGenerate.js.map +1 -0
  78. package/ts_build/src/agents/tools/sendAgentMessage.d.ts +11 -0
  79. package/ts_build/src/agents/tools/sendAgentMessage.js +118 -0
  80. package/ts_build/src/agents/tools/sendAgentMessage.js.map +1 -0
  81. package/ts_build/src/agents/tools/startAgentTask.d.ts +7 -0
  82. package/ts_build/src/agents/tools/startAgentTask.js +53 -6
  83. package/ts_build/src/agents/tools/startAgentTask.js.map +1 -1
  84. package/ts_build/src/agents/tools/waitForAgentCompleted.d.ts +10 -0
  85. package/ts_build/src/agents/tools/waitForAgentCompleted.js +229 -0
  86. package/ts_build/src/agents/tools/waitForAgentCompleted.js.map +1 -0
  87. package/ts_build/src/ai.d.ts +1 -0
  88. package/ts_build/src/ai.js.map +1 -1
  89. package/ts_build/src/chat/modules/AgentModule.d.ts +3 -1
  90. package/ts_build/src/chat/modules/AgentModule.js +46 -21
  91. package/ts_build/src/chat/modules/AgentModule.js.map +1 -1
  92. package/ts_build/src/chat/modules/SessionsModule.js +1 -6
  93. package/ts_build/src/chat/modules/SessionsModule.js.map +1 -1
  94. package/ts_build/src/clients/contextLimits.js +21 -0
  95. package/ts_build/src/clients/contextLimits.js.map +1 -1
  96. package/ts_build/src/clients/pricing/anthropic.d.ts +2 -0
  97. package/ts_build/src/clients/pricing/anthropic.js +9 -0
  98. package/ts_build/src/clients/pricing/anthropic.js.map +1 -1
  99. package/ts_build/src/commands/agent.js +11 -0
  100. package/ts_build/src/commands/agent.js.map +1 -1
  101. package/ts_build/src/commands/agents.js +61 -0
  102. package/ts_build/src/commands/agents.js.map +1 -1
  103. package/ts_build/src/commands/misc.js +2 -0
  104. package/ts_build/src/commands/misc.js.map +1 -1
  105. package/ts_build/src/commands/modules.js +86 -7
  106. package/ts_build/src/commands/modules.js.map +1 -1
  107. package/ts_build/src/commands/services.js +10 -0
  108. package/ts_build/src/commands/services.js.map +1 -1
  109. package/ts_build/src/generate.d.ts +2 -0
  110. package/ts_build/src/generate.js +11 -3
  111. package/ts_build/src/generate.js.map +1 -1
  112. package/ts_build/src/services/AgentService.d.ts +4 -1
  113. package/ts_build/src/services/AgentService.js +9 -2
  114. package/ts_build/src/services/AgentService.js.map +1 -1
  115. package/ts_build/src/services/AgentSyncFs.d.ts +2 -0
  116. package/ts_build/src/services/AgentSyncFs.js +35 -0
  117. package/ts_build/src/services/AgentSyncFs.js.map +1 -1
  118. package/ts_build/src/services/AgentSyncKnowhowWeb.d.ts +1 -1
  119. package/ts_build/src/services/EventService.d.ts +4 -1
  120. package/ts_build/src/services/EventService.js.map +1 -1
  121. package/ts_build/src/services/KnowhowClient.js +3 -0
  122. package/ts_build/src/services/KnowhowClient.js.map +1 -1
  123. package/ts_build/src/services/LazyToolsService.d.ts +2 -2
  124. package/ts_build/src/services/LazyToolsService.js +2 -2
  125. package/ts_build/src/services/LazyToolsService.js.map +1 -1
  126. package/ts_build/src/services/MinimalToolsService.d.ts +2 -2
  127. package/ts_build/src/services/MinimalToolsService.js +6 -6
  128. package/ts_build/src/services/MinimalToolsService.js.map +1 -1
  129. package/ts_build/src/services/SyncedAgentWatcher.d.ts +1 -0
  130. package/ts_build/src/services/SyncedAgentWatcher.js.map +1 -1
  131. package/ts_build/src/services/SyncerService.d.ts +2 -1
  132. package/ts_build/src/services/SyncerService.js +8 -2
  133. package/ts_build/src/services/SyncerService.js.map +1 -1
  134. package/ts_build/src/services/Tools.d.ts +6 -1
  135. package/ts_build/src/services/Tools.js +9 -3
  136. package/ts_build/src/services/Tools.js.map +1 -1
  137. package/ts_build/src/services/modules/index.d.ts +1 -1
  138. package/ts_build/src/services/modules/index.js +67 -27
  139. package/ts_build/src/services/modules/index.js.map +1 -1
  140. package/ts_build/src/types.d.ts +3 -0
  141. package/ts_build/src/types.js.map +1 -1
  142. package/ts_build/tests/agents/interrupt.test.d.ts +0 -1
  143. package/ts_build/tests/agents/interrupt.test.js +3 -13
  144. package/ts_build/tests/agents/interrupt.test.js.map +1 -1
  145. package/ts_build/tests/agents/observe-base.test.d.ts +1 -0
  146. package/ts_build/tests/agents/observe-base.test.js +149 -0
  147. package/ts_build/tests/agents/observe-base.test.js.map +1 -0
  148. package/ts_build/tests/agents/tools/connectAgent.test.d.ts +1 -0
  149. package/ts_build/tests/agents/tools/connectAgent.test.js +142 -0
  150. package/ts_build/tests/agents/tools/connectAgent.test.js.map +1 -0
  151. package/ts_build/tests/agents/tools/observe.test.d.ts +1 -0
  152. package/ts_build/tests/agents/tools/observe.test.js +148 -0
  153. package/ts_build/tests/agents/tools/observe.test.js.map +1 -0
  154. package/ts_build/tests/agents/tools/replyToParent.test.d.ts +1 -0
  155. package/ts_build/tests/agents/tools/replyToParent.test.js +116 -0
  156. package/ts_build/tests/agents/tools/replyToParent.test.js.map +1 -0
  157. package/ts_build/tests/agents/tools/sendAgentMessage.test.d.ts +1 -0
  158. package/ts_build/tests/agents/tools/sendAgentMessage.test.js +112 -0
  159. package/ts_build/tests/agents/tools/sendAgentMessage.test.js.map +1 -0
  160. package/ts_build/tests/agents/tools/waitForAgentCompleted.test.d.ts +1 -0
  161. package/ts_build/tests/agents/tools/waitForAgentCompleted.test.js +147 -0
  162. package/ts_build/tests/agents/tools/waitForAgentCompleted.test.js.map +1 -0
  163. package/ts_build/tests/chat/detach-attach-rendering.test.d.ts +1 -0
  164. package/ts_build/tests/chat/detach-attach-rendering.test.js +168 -0
  165. package/ts_build/tests/chat/detach-attach-rendering.test.js.map +1 -0
  166. package/ts_build/tests/clients/contextLimits.test.js +11 -1
  167. package/ts_build/tests/clients/contextLimits.test.js.map +1 -1
  168. package/ts_build/tests/clients/pricing.test.js +10 -1
  169. package/ts_build/tests/clients/pricing.test.js.map +1 -1
  170. package/ts_build/tests/services/Tools.test.js +126 -8
  171. package/ts_build/tests/services/Tools.test.js.map +1 -1
  172. package/ts_build/tests/unit/modules/moduleLoading.test.js +84 -0
  173. package/ts_build/tests/unit/modules/moduleLoading.test.js.map +1 -1
@@ -493,6 +493,76 @@ async function cmdAttach(taskIdOrIndex: string | undefined, opts: { index?: stri
493
493
  await startChat(`/attach ${resolvedId}`);
494
494
  }
495
495
 
496
+ /**
497
+ * Print the final answer (the result of the agent's finalAnswer tool call) for
498
+ * a completed task. Reads `metadata.result`, falling back to the last assistant
499
+ * message in the thread if `result` isn't present. This lets a human read what
500
+ * a finished agent concluded with, even if they never attached before it ended.
501
+ */
502
+ async function cmdAnswer(
503
+ taskIdOrIndex: string | undefined,
504
+ opts: { index?: string; raw: boolean }
505
+ ): Promise<void> {
506
+ const value = opts.index ?? taskIdOrIndex;
507
+ if (!value) {
508
+ console.error("Provide a task ID or use -i <number> to select by index.");
509
+ process.exit(1);
510
+ }
511
+
512
+ const resolvedId = resolveTaskId(value, true);
513
+ if (!resolvedId) {
514
+ console.error(`Task not found: ${value}`);
515
+ const rows = buildRows(true);
516
+ if (rows.length > 0) {
517
+ console.error("\nAvailable tasks:");
518
+ rows.forEach((r, i) => console.error(` ${i + 1}. ${r.displayId}`));
519
+ }
520
+ process.exit(1);
521
+ return;
522
+ }
523
+
524
+ const meta = loadMeta(resolvedId);
525
+ if (!meta) {
526
+ console.error(`No metadata found for task: ${resolvedId}`);
527
+ process.exit(1);
528
+ return;
529
+ }
530
+
531
+ // Prefer the stored finalAnswer result; fall back to the last assistant message.
532
+ let answer: string | undefined =
533
+ typeof meta.result === "string" && meta.result.trim()
534
+ ? meta.result
535
+ : undefined;
536
+
537
+ if (!answer) {
538
+ const threads: any[][] = meta.threads ?? [];
539
+ if (threads.length > 0) {
540
+ answer = lastAssistantMessage(threads[threads.length - 1]);
541
+ }
542
+ }
543
+
544
+ if (opts.raw) {
545
+ console.log(answer ?? "");
546
+ return;
547
+ }
548
+
549
+ const status = getStatus(resolvedId, meta);
550
+ if (!answer) {
551
+ console.log(`\n📭 No final answer available for task: ${resolvedId}`);
552
+ console.log(` Status: ${status}`);
553
+ if (status !== "completed") {
554
+ console.log(` The agent hasn't finished yet — attach with 'knowhow agents attach' to follow it.`);
555
+ }
556
+ return;
557
+ }
558
+
559
+ console.log(`\n💬 Final answer for task: ${resolvedId}`);
560
+ console.log(` Agent: ${meta.agentName ?? "?"} | Status: ${status} | Cost: ${meta.totalCostUsd != null ? "$" + (meta.totalCostUsd as number).toFixed(4) : "?"}`);
561
+ console.log("─".repeat(80));
562
+ console.log(answer);
563
+ console.log("─".repeat(80) + "\n");
564
+ }
565
+
496
566
 
497
567
 
498
568
  // ---------------------------------------------------------------------------
@@ -553,4 +623,17 @@ export function addAgentsCommand(program: Command): void {
553
623
  .action(async (taskId: string | undefined, opts: { index?: string }) => {
554
624
  await cmdAttach(taskId, opts);
555
625
  });
626
+
627
+ agents
628
+ .command("answer [taskId]")
629
+ .description(
630
+ "Print the final answer (finalAnswer result) of a completed agent task, formatted for reading. " +
631
+ "Reads metadata.result, falling back to the last assistant message. " +
632
+ "Accepts a full task ID, partial substring, or -i <number> for row index from 'agents list'."
633
+ )
634
+ .option("-i, --index <number>", "Select task by row number from 'agents list'")
635
+ .option("--raw", "Print only the raw answer text (no header/formatting)", false)
636
+ .action(async (taskId: string | undefined, opts: { index?: string; raw: boolean }) => {
637
+ await cmdAnswer(taskId, opts);
638
+ });
556
639
  }
@@ -66,6 +66,10 @@ export function addGenerateCommand(program: Command): void {
66
66
  "--sync-fs",
67
67
  "Enable filesystem sync for agent-driven sources (creates .knowhow/processes/agents/<taskId>/ so tasks appear in `knowhow agents list`)"
68
68
  )
69
+ .option(
70
+ "--sync-remote",
71
+ "Push agent-driven sources' work to a remote Knowhow task (identified by the source's taskId)"
72
+ )
69
73
  .option(
70
74
  "--force",
71
75
  "Skip hash checks and regenerate all matching sources regardless of whether inputs/outputs have changed"
@@ -83,6 +87,7 @@ export function addGenerateCommand(program: Command): void {
83
87
  name?: string;
84
88
  filter?: string;
85
89
  syncFs?: boolean;
90
+ syncRemote?: boolean;
86
91
  force?: boolean;
87
92
  concurrency?: number;
88
93
  plan?: boolean;
@@ -99,6 +104,7 @@ export function addGenerateCommand(program: Command): void {
99
104
  name: options.name,
100
105
  filter: options.filter,
101
106
  syncFs: options.syncFs,
107
+ syncRemote: options.syncRemote,
102
108
  force: options.force,
103
109
  concurrency: options.concurrency,
104
110
  });
@@ -39,15 +39,146 @@ function ensureKnowhowPackageJson(knowhowDir: string): void {
39
39
  }
40
40
  }
41
41
 
42
+ /**
43
+ * Options that control how a module is installed via npm.
44
+ */
45
+ interface InstallOptions {
46
+ /**
47
+ * When true, force npm to run the module's install/build lifecycle scripts
48
+ * (preinstall/install/postinstall) in the foreground.
49
+ *
50
+ * This matters for native modules like `isolated-vm`, whose `install` script
51
+ * (`node-gyp-build || node-gyp rebuild`) selects/copies the correct prebuilt
52
+ * binary (or compiles from source). npm 11 and some org/CI policies block
53
+ * install scripts by default, which leaves native deps in a broken/mismatched
54
+ * state that later aborts the process at require-time (e.g. the
55
+ * `IsolateSpecificSize` assertion). Passing this flag ensures those scripts
56
+ * actually run so the right binary is in place.
57
+ */
58
+ allowScripts?: boolean;
59
+ }
60
+
42
61
  /**
43
62
  * Run `npm install --prefix <knowhowDir> <mod>` so that modules land in
44
63
  * .knowhow/node_modules rather than the project's node_modules.
64
+ *
65
+ * When `allowScripts` is set, we explicitly force install lifecycle scripts to
66
+ * run (`--foreground-scripts --ignore-scripts=false`) so native modules build
67
+ * correctly even under npm 11 / restrictive org policies that skip scripts by
68
+ * default.
45
69
  */
46
- function npmInstallToKnowhow(mod: string, knowhowDir: string): void {
47
- execSync(`npm install --prefix "${knowhowDir}" ${mod}`, {
70
+ function npmInstallToKnowhow(
71
+ mod: string,
72
+ knowhowDir: string,
73
+ options: InstallOptions = {}
74
+ ): void {
75
+ const scriptFlags = options.allowScripts
76
+ ? " --foreground-scripts --ignore-scripts=false"
77
+ : "";
78
+ execSync(`npm install --prefix "${knowhowDir}" ${mod}${scriptFlags}`, {
48
79
  stdio: "inherit",
49
80
  encoding: "utf-8",
50
81
  });
82
+
83
+ // When scripts weren't explicitly allowed, npm 11+ (and restrictive org/CI
84
+ // policies) silently *skip* package install/build scripts. For native addons
85
+ // like isolated-vm this leaves the package present but its native binary
86
+ // unbuilt — which later aborts at runtime with a cryptic error. Detect that
87
+ // situation now and tell the user exactly how to fix it (re-run with
88
+ // --allow-scripts) rather than letting them hit the runtime crash.
89
+ if (!options.allowScripts) {
90
+ warnIfInstallScriptsSkipped(knowhowDir);
91
+ }
92
+ }
93
+
94
+ /**
95
+ * Names of lifecycle scripts that run during `npm install` and typically build
96
+ * native code. If a freshly-installed package declares any of these AND we
97
+ * installed without allowing scripts, they were skipped.
98
+ */
99
+ const INSTALL_LIFECYCLE_SCRIPTS = ["preinstall", "install", "postinstall"];
100
+
101
+ /**
102
+ * Walk the installed dependency tree under <knowhowDir>/node_modules and return
103
+ * the names of packages that declare native/build install-lifecycle scripts.
104
+ *
105
+ * This reads the *actually installed* package.json files (rather than querying
106
+ * the registry) so it works offline, reflects the real resolved tree, and
107
+ * catches transitive dependencies (e.g. isolated-vm pulled in by a module).
108
+ */
109
+ function findPackagesWithInstallScripts(knowhowDir: string): string[] {
110
+ const nodeModules = path.join(knowhowDir, "node_modules");
111
+ const found = new Set<string>();
112
+
113
+ const readPkgScripts = (pkgDir: string): void => {
114
+ const pkgJsonPath = path.join(pkgDir, "package.json");
115
+ if (!fs.existsSync(pkgJsonPath)) return;
116
+ try {
117
+ const pkg = JSON.parse(fs.readFileSync(pkgJsonPath, "utf-8"));
118
+ const scripts = pkg.scripts ?? {};
119
+ const hasInstallScript = INSTALL_LIFECYCLE_SCRIPTS.some(
120
+ (s) => typeof scripts[s] === "string" && scripts[s].trim().length > 0
121
+ );
122
+ if (hasInstallScript && pkg.name) {
123
+ found.add(pkg.name);
124
+ }
125
+ } catch {
126
+ // ignore malformed package.json
127
+ }
128
+ };
129
+
130
+ const scanDir = (dir: string): void => {
131
+ if (!fs.existsSync(dir)) return;
132
+ let entries: fs.Dirent[] = [];
133
+ try {
134
+ entries = fs.readdirSync(dir, { withFileTypes: true });
135
+ } catch {
136
+ return;
137
+ }
138
+ for (const entry of entries) {
139
+ if (!entry.isDirectory()) continue;
140
+ if (entry.name === ".bin" || entry.name === ".cache") continue;
141
+ const full = path.join(dir, entry.name);
142
+ if (entry.name.startsWith("@")) {
143
+ // scope directory — recurse one level into the scoped packages
144
+ scanDir(full);
145
+ continue;
146
+ }
147
+ readPkgScripts(full);
148
+ // Recurse into nested node_modules for hoisting edge cases.
149
+ const nested = path.join(full, "node_modules");
150
+ if (fs.existsSync(nested)) scanDir(nested);
151
+ }
152
+ };
153
+
154
+ scanDir(nodeModules);
155
+ return Array.from(found).sort();
156
+ }
157
+
158
+ /**
159
+ * Check whether the just-installed tree contains packages with install scripts
160
+ * (that were skipped because --allow-scripts wasn't passed) and, if so, print a
161
+ * clear warning telling the user to re-run with --allow-scripts.
162
+ */
163
+ function warnIfInstallScriptsSkipped(knowhowDir: string): void {
164
+ let pkgsWithScripts: string[] = [];
165
+ try {
166
+ pkgsWithScripts = findPackagesWithInstallScripts(knowhowDir);
167
+ } catch {
168
+ return;
169
+ }
170
+ if (pkgsWithScripts.length === 0) return;
171
+
172
+ console.warn(
173
+ "\n⚠️ Some installed packages have native install/build scripts that may have\n" +
174
+ " been skipped (npm 11+ blocks install scripts by default):\n" +
175
+ pkgsWithScripts.map((p) => ` • ${p}`).join("\n") +
176
+ "\n\n If a module fails to load at runtime (e.g. isolated-vm), re-run the\n" +
177
+ " install with --allow-scripts so these build steps can run:\n\n" +
178
+ " knowhow modules install <module> --allow-scripts\n" +
179
+ " # or\n" +
180
+ " knowhow modules setup --allow-scripts\n"
181
+ );
51
182
  }
52
183
 
53
184
  /**
@@ -289,9 +420,14 @@ export function addModulesCommand(program: Command): void {
289
420
  "Add default built-in modules to your config and install them into .knowhow/node_modules"
290
421
  )
291
422
  .option("--global", "Use the global config (~/.knowhow/knowhow.json)")
423
+ .option(
424
+ "--allow-scripts",
425
+ "Allow modules' native install/build scripts to run (needed for native deps like isolated-vm under npm 11)"
426
+ )
292
427
  .action(async (opts) => {
293
428
  try {
294
429
  const isGlobal: boolean = opts.global ?? false;
430
+ const allowScripts: boolean = opts.allowScripts ?? false;
295
431
  const cfg = isGlobal ? await getGlobalConfig() : await getConfig();
296
432
  const configLabel = isGlobal
297
433
  ? "~/.knowhow/knowhow.json"
@@ -307,10 +443,16 @@ export function addModulesCommand(program: Command): void {
307
443
  let anyChanges = false;
308
444
  for (const mod of BUILTIN_MODULES) {
309
445
  if (!mod.startsWith(".") && !mod.startsWith("/")) {
310
- if (!isModuleInstalled(mod, knowhowDir)) {
446
+ // Reinstall if the module isn't installed yet, OR if the user
447
+ // explicitly passed --allow-scripts. In the latter case the whole
448
+ // point is to (re-)run the native install/build scripts (e.g.
449
+ // isolated-vm's node-gyp build), so skipping because the package
450
+ // is "already installed" would silently do nothing — which is
451
+ // exactly the confusing "Nothing to do" case we want to avoid.
452
+ if (!isModuleInstalled(mod, knowhowDir) || allowScripts) {
311
453
  const installTarget = await fetchLatestCompatibleVersion(mod);
312
454
  console.log(`📦 Installing ${installTarget}...`);
313
- npmInstallToKnowhow(installTarget, knowhowDir);
455
+ npmInstallToKnowhow(installTarget, knowhowDir, { allowScripts });
314
456
  console.log(`✅ Installed ${mod}`);
315
457
  anyChanges = true;
316
458
  }
@@ -346,9 +488,14 @@ export function addModulesCommand(program: Command): void {
346
488
  )
347
489
  .option("--global", "Use the global config (~/.knowhow/knowhow.json)")
348
490
  .option("--latest", "Force install the latest version (bypasses package-lock)")
491
+ .option(
492
+ "--allow-scripts",
493
+ "Allow the module's native install/build scripts to run (needed for native deps like isolated-vm under npm 11)"
494
+ )
349
495
  .action(async (moduleName: string | undefined, opts) => {
350
496
  try {
351
497
  const isGlobal: boolean = opts.global ?? false;
498
+ const allowScripts: boolean = opts.allowScripts ?? false;
352
499
  const cfg = isGlobal ? await getGlobalConfig() : await getConfig();
353
500
  const configLabel = isGlobal
354
501
  ? "~/.knowhow/knowhow.json"
@@ -374,7 +521,7 @@ export function addModulesCommand(program: Command): void {
374
521
  for (const mod of installable) {
375
522
  console.log(` 📦 Installing ${mod}...`);
376
523
  const installTarget = opts.latest ? await fetchLatestCompatibleVersion(mod) : mod;
377
- npmInstallToKnowhow(installTarget, knowhowDir);
524
+ npmInstallToKnowhow(installTarget, knowhowDir, { allowScripts });
378
525
  console.log(` ✅ Installed ${mod}`);
379
526
  }
380
527
  console.log(`\n🎉 All modules installed!`);
@@ -384,7 +531,7 @@ export function addModulesCommand(program: Command): void {
384
531
  // Install the specified module
385
532
  const installTarget = opts.latest ? await fetchLatestCompatibleVersion(moduleName) : moduleName;
386
533
  console.log(`📦 Installing ${installTarget} into ${knowhowDir}/node_modules...`);
387
- npmInstallToKnowhow(installTarget, knowhowDir);
534
+ npmInstallToKnowhow(installTarget, knowhowDir, { allowScripts });
388
535
  console.log(`✅ Installed ${moduleName}`);
389
536
 
390
537
  // Add to config if not already there
@@ -453,10 +600,15 @@ export function addModulesCommand(program: Command): void {
453
600
  )
454
601
  .option("--global", "Use the global config (~/.knowhow/knowhow.json)")
455
602
  .option("-y, --yes", "Skip confirmation prompt and update all outdated modules automatically")
603
+ .option(
604
+ "--allow-scripts",
605
+ "Allow modules' native install/build scripts to run (needed for native deps like isolated-vm under npm 11)"
606
+ )
456
607
  .action(async (opts) => {
457
608
  try {
458
609
  const isGlobal: boolean = opts.global ?? false;
459
610
  const skipConfirm: boolean = opts.yes ?? false;
611
+ const allowScripts: boolean = opts.allowScripts ?? false;
460
612
  const cfg = isGlobal ? await getGlobalConfig() : await getConfig();
461
613
  const configLabel = isGlobal
462
614
  ? "~/.knowhow/knowhow.json"
@@ -540,7 +692,7 @@ export function addModulesCommand(program: Command): void {
540
692
  console.log("");
541
693
  for (const { mod, compatibleVersion } of toUpdate) {
542
694
  console.log(` 📦 Updating ${compatibleVersion}...`);
543
- npmInstallToKnowhow(compatibleVersion, knowhowDir);
695
+ npmInstallToKnowhow(compatibleVersion, knowhowDir, { allowScripts });
544
696
  console.log(` ✅ Updated ${mod}`);
545
697
  }
546
698
  console.log(`\n🎉 Update complete! ${toUpdate.length} module(s) updated.`);
@@ -49,6 +49,20 @@ export async function setupServices() {
49
49
 
50
50
  Tools.addContext("Mcp", Mcp);
51
51
 
52
+ // The `agentCall` tool + function are registered by AgentService on the
53
+ // global ToolsService (AllTools), not on the LazyToolsService we build here.
54
+ // Copy it across so agents/scripts using this Tools instance can delegate to
55
+ // other agents via agentCall (matching startAgentTask availability).
56
+ try {
57
+ const agentCallDef = AllTools.getTool?.("agentCall");
58
+ const agentCallFn = AllTools.getFunction?.("agentCall");
59
+ if (agentCallDef) Tools.addTool(agentCallDef);
60
+ if (agentCallFn) Tools.setFunction("agentCall", agentCallFn);
61
+ } catch (_) {
62
+ // Non-fatal: if agentCall isn't registered yet, agents can still use
63
+ // startAgentTask for delegation.
64
+ }
65
+
52
66
  Agents.setAgentContext(agentContext);
53
67
 
54
68
  console.log("🔌 Connecting to MCP...");
package/src/generate.ts CHANGED
@@ -51,6 +51,12 @@ export interface GenerateOptions {
51
51
  * so they appear in `knowhow agents list`. Overrides the per-source syncFs flag.
52
52
  */
53
53
  syncFs?: boolean;
54
+ /**
55
+ * When true, agent-driven sources push their work to a remote Knowhow task
56
+ * (identified by the source's taskId). Overrides the per-source syncRemote
57
+ * flag. Requires the sources to run as agents with a known taskId.
58
+ */
59
+ syncRemote?: boolean;
54
60
  /** When true, skip hash checks and regenerate all matching sources regardless of changes */
55
61
  force?: boolean;
56
62
  /**
@@ -58,6 +64,14 @@ export interface GenerateOptions {
58
64
  * dependency wave. Defaults to 3. Set to 1 for fully sequential execution.
59
65
  */
60
66
  concurrency?: number;
67
+ /**
68
+ * In-memory generation sources. When provided, these are used INSTEAD of the
69
+ * sources defined in `.knowhow/knowhow.json`. This enables callers (e.g. an
70
+ * agent using the `runGenerate` tool) to define arbitrary pipelines at
71
+ * runtime without persisting them to config — supporting complex task
72
+ * orchestration with `dependsOn`, per-source `agent`, analysis, etc.
73
+ */
74
+ sources?: GenerationSource[];
61
75
  }
62
76
 
63
77
  // ---------------------------------------------------------------------------
@@ -416,12 +430,14 @@ async function runSource(
416
430
  console.log("Generating", source.input, "to", source.output);
417
431
 
418
432
  const effectiveSyncFs = options.syncFs ?? source.syncFs;
433
+ const effectiveSyncRemote = options.syncRemote ?? source.syncRemote;
419
434
  const agentOpts: AgentOptions | undefined = source.agent
420
435
  ? {
421
436
  syncFs: effectiveSyncFs,
437
+ syncRemote: effectiveSyncRemote,
422
438
  taskId:
423
439
  source.taskId ??
424
- (effectiveSyncFs
440
+ (effectiveSyncFs || effectiveSyncRemote
425
441
  ? `generate:${source.output.replace(/[^a-zA-Z0-9_.-]/g, "_")}`
426
442
  : undefined),
427
443
  maxTimeLimit: source.maxTimeLimit,
@@ -469,8 +485,14 @@ async function runSource(
469
485
  // ---------------------------------------------------------------------------
470
486
 
471
487
  export async function generate(options: GenerateOptions = {}): Promise<void> {
472
- const config = await getConfig();
473
- const allSources = config.sources ?? [];
488
+ let allSources: GenerationSource[];
489
+ if (options.sources && options.sources.length > 0) {
490
+ // In-memory pipeline definition — bypass config entirely.
491
+ allSources = options.sources;
492
+ } else {
493
+ const config = await getConfig();
494
+ allSources = config.sources ?? [];
495
+ }
474
496
  const sources = allSources.filter((s) => sourceMatchesFilter(s, options));
475
497
 
476
498
  if ((options.name || options.filter) && sources.length === 0) {
@@ -104,11 +104,29 @@ export class AgentService {
104
104
  }
105
105
  }
106
106
 
107
- public async callAgent(name: string, query: string): Promise<string> {
107
+ /**
108
+ * Run a registered agent and return both its final answer and the cost it
109
+ * incurred. Returning cost (from the agent's own `getTotalCostUsd()`) lets
110
+ * callers — notably the script sandbox — account subagent spend against a
111
+ * budget. The `answer` string preserves the previous plain-string behavior
112
+ * for consumers that only care about the response.
113
+ */
114
+ public async callAgent(
115
+ name: string,
116
+ query: string
117
+ ): Promise<{ answer: string; costUsd: number }> {
108
118
  const agent = this.agents.get(name);
109
119
  if (!agent) {
110
- return `Agent ${name} not found. Options are: ${this.listAgents()}`;
120
+ return {
121
+ answer: `Agent ${name} not found. Options are: ${this.listAgents()}`,
122
+ costUsd: 0,
123
+ };
111
124
  }
112
- return agent.call(query);
125
+ const answer = await agent.call(query);
126
+ const costUsd =
127
+ typeof (agent as any).getTotalCostUsd === "function"
128
+ ? (agent as any).getTotalCostUsd()
129
+ : 0;
130
+ return { answer, costUsd };
113
131
  }
114
132
  }
@@ -11,6 +11,8 @@ export interface FsSyncOptions {
11
11
  taskId: string;
12
12
  prompt: string;
13
13
  agentName?: string;
14
+ /** The taskId of the parent agent that spawned this task, if any. */
15
+ parentTaskId?: string;
14
16
  }
15
17
 
16
18
  /**
@@ -62,8 +64,15 @@ export class AgentSyncFs {
62
64
  await this.writeInput("");
63
65
  await this.writeMetadata({
64
66
  taskId: this.taskId,
67
+ // Record the OS process id of this agent subprocess so orchestrating
68
+ // tools (e.g. waitForAgentCompleted) can detect a crashed-but-not-
69
+ // finalized task via process-liveness. This is the child's own pid,
70
+ // so it needs no coordination with the spawner and never touches
71
+ // execCommand's child processes.
72
+ pid: process.pid,
65
73
  prompt: options.prompt,
66
74
  agentName: options.agentName || "unknown",
75
+ ...(options.parentTaskId ? { parentTaskId: options.parentTaskId } : {}),
67
76
  startTime: new Date().toISOString(),
68
77
  status: "running",
69
78
  });
@@ -492,11 +501,58 @@ export class AgentSyncFs {
492
501
  console.error(`❌ Error cleaning up ${entry.name}:`, error);
493
502
  }
494
503
  }
504
+
505
+ // Also clean up old execCommand background log files in .knowhow/processes/*.txt
506
+ await AgentSyncFs.cleanupOldProcessLogs(threeDaysMs);
495
507
  } catch (error) {
496
508
  console.error(`❌ Error during cleanup:`, error);
497
509
  }
498
510
  }
499
511
 
512
+ /**
513
+ * Clean up old execCommand background log files (.knowhow/processes/*.txt).
514
+ * These are created by the execCommand tool when running background/long-lived
515
+ * commands, and are never cleaned up otherwise.
516
+ */
517
+ private static async cleanupOldProcessLogs(maxAgeMs: number): Promise<void> {
518
+ try {
519
+ // execCommand writes logs to the parent of the agents dir: .knowhow/processes
520
+ const processesPath = path.dirname(AgentSyncFs.sharedBasePath);
521
+
522
+ try {
523
+ await fs.access(processesPath);
524
+ } catch {
525
+ return;
526
+ }
527
+
528
+ const entries = await fs.readdir(processesPath, { withFileTypes: true });
529
+ const now = Date.now();
530
+
531
+ for (const entry of entries) {
532
+ // Only target the .txt log files that execCommand creates; leave the
533
+ // agents/ directory (and any other subdirectories) alone.
534
+ if (!entry.isFile() || !entry.name.endsWith(".txt")) continue;
535
+
536
+ const logPath = path.join(processesPath, entry.name);
537
+
538
+ try {
539
+ const stats = await fs.stat(logPath);
540
+ const age = now - stats.mtimeMs;
541
+
542
+ if (age > maxAgeMs) {
543
+ console.log(`🧹 Cleaning up old process log file: ${entry.name}`);
544
+ await fs.rm(logPath, { force: true });
545
+ }
546
+ } catch (error) {
547
+ // Skip if we can't stat or delete
548
+ console.error(`❌ Error cleaning up log ${entry.name}:`, error);
549
+ }
550
+ }
551
+ } catch (error) {
552
+ console.error(`❌ Error during process log cleanup:`, error);
553
+ }
554
+ }
555
+
500
556
  /**
501
557
  * Start periodic cleanup process
502
558
  */
@@ -13,7 +13,7 @@ import { wait } from "../utils";
13
13
 
14
14
  export interface SyncOptions {
15
15
  messageId?: string;
16
- existingKnowhowTaskId?: string;
16
+ syncRemote?: boolean;
17
17
  prompt: string;
18
18
  }
19
19
 
@@ -255,7 +255,10 @@ export class EventService extends EventEmitter {
255
255
  this.emit(this.eventTypes.agentsRegister, { name: agent.name, agent });
256
256
  }
257
257
 
258
- callAgent(name: string, query: string): Promise<string> {
258
+ callAgent(
259
+ name: string,
260
+ query: string
261
+ ): Promise<{ answer: string; costUsd: number }> {
259
262
  return new Promise((resolve, reject) => {
260
263
  this.emit(this.eventTypes.agentsCall, { name, query, resolve, reject });
261
264
  });
@@ -122,6 +122,13 @@ export interface GitCredentialResponse {
122
122
  }
123
123
 
124
124
  export function loadKnowhowJwt(): string {
125
+ // Check env var first — this is how cloud workers/CI runners get auth
126
+ // (injected via KNOWHOW_JWT env var in the runner's .env file by GithubRunnerService).
127
+ // This avoids depending on a potentially stale .knowhow/.jwt file from the snapshot.
128
+ if (process.env.KNOWHOW_JWT) {
129
+ return process.env.KNOWHOW_JWT;
130
+ }
131
+
125
132
  const jwtFile = path.join(process.cwd(), ".knowhow", ".jwt");
126
133
  if (!fs.existsSync(jwtFile)) {
127
134
  return "";
@@ -1,5 +1,5 @@
1
1
  import { minimatch } from "minimatch";
2
- import { ToolsService, ToolContext } from "./Tools";
2
+ import { ToolsService, ToolContext, ToolCallContext } from "./Tools";
3
3
  import { Tool } from "../clients/types";
4
4
  import { ToolCall } from "../clients/types";
5
5
  import {
@@ -140,7 +140,11 @@ export class LazyToolsService extends ToolsService {
140
140
  }
141
141
 
142
142
  // Override callTool to auto-enable tools that are in allTools but not yet enabled
143
- async callTool(toolCall: ToolCall, enabledTools?: string[]) {
143
+ async callTool(
144
+ toolCall: ToolCall,
145
+ enabledTools?: string[],
146
+ callContext?: ToolCallContext
147
+ ) {
144
148
  const functionName = toolCall.function.name;
145
149
 
146
150
  // If the tool isn't currently visible but exists in allTools, auto-enable it.
@@ -160,7 +164,7 @@ export class LazyToolsService extends ToolsService {
160
164
 
161
165
  // Always use the current enabled tool names after any auto-enable above,
162
166
  // so the base class check sees the freshly-enabled tool in the allowed list.
163
- return super.callTool(toolCall, this.getToolNames());
167
+ return super.callTool(toolCall, this.getToolNames(), callContext);
164
168
  }
165
169
 
166
170
  // Internal: Update visible tools based on patterns